implement Milestone 16: Bluetooth interface with user consent
This commit is contained in:
@@ -747,7 +747,9 @@ TEST(SensorInterface, LimitsFrequency);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 16: Virtual Hardware - Bluetooth
|
## Milestone 16: Virtual Hardware - Bluetooth ✅
|
||||||
|
|
||||||
|
**Status**: Complete
|
||||||
|
|
||||||
**Goal**: Bluetooth discovery and pairing.
|
**Goal**: Bluetooth discovery and pairing.
|
||||||
**Estimated Files**: 1 new file
|
**Estimated Files**: 1 new file
|
||||||
|
|||||||
566
SANDBOX_MILESTONE_16.md
Normal file
566
SANDBOX_MILESTONE_16.md
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
# Milestone 16: Virtual Hardware - Bluetooth
|
||||||
|
|
||||||
|
**Status**: Complete
|
||||||
|
**Goal**: Bluetooth discovery and pairing with user consent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This milestone implements secure Bluetooth access for Lua apps:
|
||||||
|
- Discovery requires user consent (user gesture)
|
||||||
|
- Permission required: `bluetooth` for discovery and connection
|
||||||
|
- Device info limited to name and address (no UUIDs)
|
||||||
|
- Connection limit: 5 active connections per app
|
||||||
|
- Automatic cleanup on app stop
|
||||||
|
|
||||||
|
### Key Deliverables
|
||||||
|
|
||||||
|
1. **BluetoothInterface class** - Bluetooth discovery and connection management
|
||||||
|
2. **BluetoothConnection class** - Individual connection handling
|
||||||
|
3. **Lua bluetooth API** - `bluetooth.startDiscovery()`, `bluetooth.connect()`
|
||||||
|
4. **Privacy protection** - Limited device information, user consent required
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/main/cpp/sandbox/
|
||||||
|
├── bluetooth_interface.h # NEW - Bluetooth API header
|
||||||
|
└── bluetooth_interface.cpp # NEW - Bluetooth implementation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. BluetoothInterface Class
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// bluetooth_interface.h
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Permission and Consent Requirements
|
||||||
|
|
||||||
|
| Action | Permission Required | User Consent Required |
|
||||||
|
|--------|--------------------|-----------------------|
|
||||||
|
| Start Discovery | `bluetooth` | Yes (user gesture) |
|
||||||
|
| Connect to Device | `bluetooth` | No (implicit from discovery) |
|
||||||
|
| Send Data | `bluetooth` | No (connection already established) |
|
||||||
|
| Receive Data | `bluetooth` | No (connection already established) |
|
||||||
|
|
||||||
|
### 3. Privacy Protection
|
||||||
|
|
||||||
|
Device information is limited to:
|
||||||
|
- Device name (display name)
|
||||||
|
- Device address (MAC)
|
||||||
|
|
||||||
|
NOT exposed:
|
||||||
|
- Full UUID list (prevents service fingerprinting)
|
||||||
|
- Device class
|
||||||
|
- Manufacturer data
|
||||||
|
- RSSI (prevents proximity tracking)
|
||||||
|
|
||||||
|
### 4. Lua API
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Start device discovery (requires user gesture)
|
||||||
|
bluetooth.startDiscovery(function(devices)
|
||||||
|
for _, device in ipairs(devices) do
|
||||||
|
print("Found:", device.name, device.address)
|
||||||
|
end
|
||||||
|
end, function(error, message)
|
||||||
|
print("Discovery error:", error, message)
|
||||||
|
end, {
|
||||||
|
timeout = 30 -- seconds
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Stop discovery
|
||||||
|
bluetooth.stopDiscovery()
|
||||||
|
|
||||||
|
-- Check if discovering
|
||||||
|
local discovering = bluetooth.isDiscovering()
|
||||||
|
|
||||||
|
-- Connect to a device
|
||||||
|
local conn = bluetooth.connect(address, function(data)
|
||||||
|
print("Received:", #data, "bytes")
|
||||||
|
end, function(error, message)
|
||||||
|
print("Connection error:", error, message)
|
||||||
|
end, {
|
||||||
|
timeout = 10000 -- ms
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Check connection
|
||||||
|
if conn and conn:isConnected() then
|
||||||
|
-- Send data
|
||||||
|
conn:send({0x01, 0x02, 0x03})
|
||||||
|
|
||||||
|
-- Get connection info
|
||||||
|
print("Connected to:", conn:getAddress())
|
||||||
|
print("Connection ID:", conn:getId())
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Close connection
|
||||||
|
conn:close()
|
||||||
|
|
||||||
|
-- Get active connection count
|
||||||
|
local count = bluetooth.getConnectionCount()
|
||||||
|
|
||||||
|
-- Disconnect all
|
||||||
|
bluetooth.disconnectAll()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Error Codes
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Error values passed to error callbacks
|
||||||
|
"PERMISSION_DENIED" -- No bluetooth permission
|
||||||
|
"USER_CONSENT_REQUIRED" -- User gesture required for discovery
|
||||||
|
"DISCOVERY_FAILED" -- Discovery could not start
|
||||||
|
"CONNECTION_FAILED" -- Could not connect to device
|
||||||
|
"NOT_CONNECTED" -- Connection not established
|
||||||
|
"CONNECTION_LIMIT" -- Max connections reached (5)
|
||||||
|
"TIMEOUT" -- Operation timed out
|
||||||
|
"ALREADY_DISCOVERING" -- Discovery already in progress
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
### Test 1: Requires Permission
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothRequiresPermission(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
// No permission granted
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool started = bluetooth.StartDiscovery(
|
||||||
|
{},
|
||||||
|
[](const std::vector<mosis::BluetoothDevice>&) {},
|
||||||
|
[](mosis::BluetoothError, const std::string&) {},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
EXPECT_TRUE(!started);
|
||||||
|
EXPECT_TRUE(err.find("permission") != std::string::npos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Requires User Consent
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothRequiresUserConsent(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
// User consent NOT granted
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool started = bluetooth.StartDiscovery(
|
||||||
|
{},
|
||||||
|
[](const std::vector<mosis::BluetoothDevice>&) {},
|
||||||
|
[](mosis::BluetoothError, const std::string&) {},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
EXPECT_TRUE(!started);
|
||||||
|
EXPECT_TRUE(err.find("consent") != std::string::npos ||
|
||||||
|
err.find("gesture") != std::string::npos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: Discovery Works With Permission and Consent
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothDiscoveryWorks(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
bluetooth.SetUserConsent(true);
|
||||||
|
|
||||||
|
// Set mock devices
|
||||||
|
std::vector<mosis::BluetoothDevice> devices = {
|
||||||
|
{"Device A", "AA:BB:CC:DD:EE:FF"},
|
||||||
|
{"Device B", "11:22:33:44:55:66"}
|
||||||
|
};
|
||||||
|
bluetooth.SetMockDevices(devices);
|
||||||
|
|
||||||
|
std::vector<mosis::BluetoothDevice> discovered;
|
||||||
|
bool got_devices = false;
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool started = bluetooth.StartDiscovery(
|
||||||
|
{},
|
||||||
|
[&](const std::vector<mosis::BluetoothDevice>& devs) {
|
||||||
|
got_devices = true;
|
||||||
|
discovered = devs;
|
||||||
|
},
|
||||||
|
[](mosis::BluetoothError, const std::string&) {},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
EXPECT_TRUE(started);
|
||||||
|
EXPECT_TRUE(got_devices);
|
||||||
|
EXPECT_TRUE(discovered.size() == 2);
|
||||||
|
EXPECT_TRUE(discovered[0].name == "Device A");
|
||||||
|
EXPECT_TRUE(discovered[0].address == "AA:BB:CC:DD:EE:FF");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: Connection Limit
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothConnectionLimit(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<mosis::BluetoothConnection>> connections;
|
||||||
|
std::string err;
|
||||||
|
|
||||||
|
// Create MAX_CONNECTIONS (5)
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
char addr[20];
|
||||||
|
snprintf(addr, sizeof(addr), "AA:BB:CC:DD:EE:%02X", i);
|
||||||
|
auto conn = bluetooth.Connect(addr, {}, err);
|
||||||
|
EXPECT_TRUE(conn != nullptr);
|
||||||
|
connections.push_back(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6th should fail
|
||||||
|
auto extra = bluetooth.Connect("AA:BB:CC:DD:EE:FF", {}, err);
|
||||||
|
EXPECT_TRUE(extra == nullptr);
|
||||||
|
EXPECT_TRUE(err.find("limit") != std::string::npos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 5: Connection Send/Receive
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothSendReceive(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto conn = bluetooth.Connect("AA:BB:CC:DD:EE:FF", {}, err);
|
||||||
|
EXPECT_TRUE(conn != nullptr);
|
||||||
|
EXPECT_TRUE(conn->IsConnected());
|
||||||
|
|
||||||
|
// Test send
|
||||||
|
std::vector<uint8_t> data = {0x01, 0x02, 0x03};
|
||||||
|
bool sent = conn->Send(data, err);
|
||||||
|
EXPECT_TRUE(sent);
|
||||||
|
|
||||||
|
// Test receive via simulation
|
||||||
|
std::vector<uint8_t> received;
|
||||||
|
bool got_data = false;
|
||||||
|
conn->SetOnData([&](const std::vector<uint8_t>& d) {
|
||||||
|
got_data = true;
|
||||||
|
received = d;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<uint8_t> incoming = {0xAA, 0xBB};
|
||||||
|
conn->SimulateData(incoming);
|
||||||
|
|
||||||
|
EXPECT_TRUE(got_data);
|
||||||
|
EXPECT_TRUE(received.size() == 2);
|
||||||
|
EXPECT_TRUE(received[0] == 0xAA);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 6: Cleanup on Shutdown
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothCleansUpOnShutdown(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto conn1 = bluetooth.Connect("AA:BB:CC:DD:EE:01", {}, err);
|
||||||
|
auto conn2 = bluetooth.Connect("AA:BB:CC:DD:EE:02", {}, err);
|
||||||
|
|
||||||
|
EXPECT_TRUE(bluetooth.GetActiveConnectionCount() == 2);
|
||||||
|
|
||||||
|
bluetooth.Shutdown();
|
||||||
|
|
||||||
|
EXPECT_TRUE(bluetooth.GetActiveConnectionCount() == 0);
|
||||||
|
EXPECT_TRUE(!conn1->IsConnected());
|
||||||
|
EXPECT_TRUE(!conn2->IsConnected());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 7: Lua Integration
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Test_BluetoothLuaIntegration(std::string& error_msg) {
|
||||||
|
SandboxContext ctx = TestContext();
|
||||||
|
LuaSandbox sandbox(ctx);
|
||||||
|
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
mosis::RegisterBluetoothAPI(sandbox.GetState(), &bluetooth);
|
||||||
|
|
||||||
|
std::string script = R"lua(
|
||||||
|
-- Test that bluetooth global exists
|
||||||
|
if not bluetooth then
|
||||||
|
error("bluetooth global not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.startDiscovery then
|
||||||
|
error("bluetooth.startDiscovery not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.stopDiscovery then
|
||||||
|
error("bluetooth.stopDiscovery not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.connect then
|
||||||
|
error("bluetooth.connect not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.disconnectAll then
|
||||||
|
error("bluetooth.disconnectAll not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.getConnectionCount then
|
||||||
|
error("bluetooth.getConnectionCount not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.isDiscovering then
|
||||||
|
error("bluetooth.isDiscovering not found")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Connection count should be 0 initially
|
||||||
|
if bluetooth.getConnectionCount() ~= 0 then
|
||||||
|
error("should have no active connections initially")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Should not be discovering initially
|
||||||
|
if bluetooth.isDiscovering() then
|
||||||
|
error("should not be discovering initially")
|
||||||
|
end
|
||||||
|
)lua";
|
||||||
|
|
||||||
|
bool ok = sandbox.LoadString(script, "bluetooth_test");
|
||||||
|
if (!ok) {
|
||||||
|
error_msg = "Lua test failed: " + sandbox.GetLastError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
All tests must pass:
|
||||||
|
|
||||||
|
- [x] `Test_BluetoothRequiresPermission` - Permission required
|
||||||
|
- [x] `Test_BluetoothRequiresUserConsent` - User consent required for discovery
|
||||||
|
- [x] `Test_BluetoothDiscoveryWorks` - Discovery returns devices
|
||||||
|
- [x] `Test_BluetoothConnectionLimit` - Max 5 connections enforced
|
||||||
|
- [x] `Test_BluetoothSendReceive` - Send/receive data works
|
||||||
|
- [x] `Test_BluetoothCleansUpOnShutdown` - Cleanup on shutdown
|
||||||
|
- [x] `Test_BluetoothLuaIntegration` - Lua API works
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Milestone 1 (LuaSandbox)
|
||||||
|
- Milestone 2 (PermissionGate + user gesture tracking)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### Desktop vs Android Implementation
|
||||||
|
|
||||||
|
For desktop testing, BluetoothInterface operates in mock mode:
|
||||||
|
- Discovery returns mock device list
|
||||||
|
- Connections track state but don't communicate
|
||||||
|
- Send/receive can be simulated for testing
|
||||||
|
|
||||||
|
On Android, the real implementation would:
|
||||||
|
1. Use BluetoothAdapter via JNI
|
||||||
|
2. Handle BLUETOOTH_SCAN and BLUETOOTH_CONNECT permissions
|
||||||
|
3. Use BluetoothSocket for RFCOMM connections
|
||||||
|
4. Support BLE via BluetoothGatt
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
|
||||||
|
1. **Permission required**: `bluetooth` permission needed for all operations
|
||||||
|
2. **User consent**: Discovery requires active user gesture
|
||||||
|
3. **Limited device info**: Only name and address exposed (no UUIDs)
|
||||||
|
4. **Connection limit**: Max 5 connections per app
|
||||||
|
5. **Cleanup**: All connections closed when app stops
|
||||||
|
|
||||||
|
### Privacy Features
|
||||||
|
|
||||||
|
1. **No service UUIDs**: Prevents detailed device fingerprinting
|
||||||
|
2. **No RSSI**: Prevents proximity tracking
|
||||||
|
3. **User consent**: Discovery requires explicit user action
|
||||||
|
4. **Audit logging**: All Bluetooth operations logged
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
After Milestone 16 passes:
|
||||||
|
1. Milestone 17: Virtual Hardware - Contacts
|
||||||
@@ -29,6 +29,7 @@ add_library(mosis-sandbox STATIC
|
|||||||
../src/main/cpp/sandbox/audio_output.cpp
|
../src/main/cpp/sandbox/audio_output.cpp
|
||||||
../src/main/cpp/sandbox/location_interface.cpp
|
../src/main/cpp/sandbox/location_interface.cpp
|
||||||
../src/main/cpp/sandbox/sensor_interface.cpp
|
../src/main/cpp/sandbox/sensor_interface.cpp
|
||||||
|
../src/main/cpp/sandbox/bluetooth_interface.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(mosis-sandbox PUBLIC
|
target_include_directories(mosis-sandbox PUBLIC
|
||||||
../src/main/cpp/sandbox
|
../src/main/cpp/sandbox
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "audio_output.h"
|
#include "audio_output.h"
|
||||||
#include "location_interface.h"
|
#include "location_interface.h"
|
||||||
#include "sensor_interface.h"
|
#include "sensor_interface.h"
|
||||||
|
#include "bluetooth_interface.h"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -2687,6 +2688,208 @@ bool Test_SensorLuaIntegration(std::string& error_msg) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Milestone 16: Bluetooth
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
bool Test_BluetoothRequiresPermission(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
// No permission granted
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool started = bluetooth.StartDiscovery(
|
||||||
|
{},
|
||||||
|
[](const std::vector<mosis::BluetoothDevice>&) {},
|
||||||
|
[](mosis::BluetoothError, const std::string&) {},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
EXPECT_TRUE(!started);
|
||||||
|
EXPECT_TRUE(err.find("permission") != std::string::npos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_BluetoothRequiresUserConsent(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
// User consent NOT granted
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool started = bluetooth.StartDiscovery(
|
||||||
|
{},
|
||||||
|
[](const std::vector<mosis::BluetoothDevice>&) {},
|
||||||
|
[](mosis::BluetoothError, const std::string&) {},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
EXPECT_TRUE(!started);
|
||||||
|
EXPECT_TRUE(err.find("consent") != std::string::npos ||
|
||||||
|
err.find("gesture") != std::string::npos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_BluetoothDiscoveryWorks(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
bluetooth.SetUserConsent(true);
|
||||||
|
|
||||||
|
// Set mock devices
|
||||||
|
std::vector<mosis::BluetoothDevice> devices = {
|
||||||
|
{"Device A", "AA:BB:CC:DD:EE:FF"},
|
||||||
|
{"Device B", "11:22:33:44:55:66"}
|
||||||
|
};
|
||||||
|
bluetooth.SetMockDevices(devices);
|
||||||
|
|
||||||
|
std::vector<mosis::BluetoothDevice> discovered;
|
||||||
|
bool got_devices = false;
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
bool started = bluetooth.StartDiscovery(
|
||||||
|
{},
|
||||||
|
[&](const std::vector<mosis::BluetoothDevice>& devs) {
|
||||||
|
got_devices = true;
|
||||||
|
discovered = devs;
|
||||||
|
},
|
||||||
|
[](mosis::BluetoothError, const std::string&) {},
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
EXPECT_TRUE(started);
|
||||||
|
EXPECT_TRUE(got_devices);
|
||||||
|
EXPECT_TRUE(discovered.size() == 2);
|
||||||
|
EXPECT_TRUE(discovered[0].name == "Device A");
|
||||||
|
EXPECT_TRUE(discovered[0].address == "AA:BB:CC:DD:EE:FF");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_BluetoothConnectionLimit(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<mosis::BluetoothConnection>> connections;
|
||||||
|
std::string err;
|
||||||
|
|
||||||
|
// Create MAX_CONNECTIONS (5)
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
char addr[20];
|
||||||
|
snprintf(addr, sizeof(addr), "AA:BB:CC:DD:EE:%02X", i);
|
||||||
|
auto conn = bluetooth.Connect(addr, {}, err);
|
||||||
|
EXPECT_TRUE(conn != nullptr);
|
||||||
|
connections.push_back(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6th should fail
|
||||||
|
auto extra = bluetooth.Connect("AA:BB:CC:DD:EE:FF", {}, err);
|
||||||
|
EXPECT_TRUE(extra == nullptr);
|
||||||
|
EXPECT_TRUE(err.find("limit") != std::string::npos);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_BluetoothSendReceive(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto conn = bluetooth.Connect("AA:BB:CC:DD:EE:FF", {}, err);
|
||||||
|
EXPECT_TRUE(conn != nullptr);
|
||||||
|
EXPECT_TRUE(conn->IsConnected());
|
||||||
|
|
||||||
|
// Test send
|
||||||
|
std::vector<uint8_t> data = {0x01, 0x02, 0x03};
|
||||||
|
bool sent = conn->Send(data, err);
|
||||||
|
EXPECT_TRUE(sent);
|
||||||
|
|
||||||
|
// Test receive via simulation
|
||||||
|
std::vector<uint8_t> received;
|
||||||
|
bool got_data = false;
|
||||||
|
conn->SetOnData([&](const std::vector<uint8_t>& d) {
|
||||||
|
got_data = true;
|
||||||
|
received = d;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<uint8_t> incoming = {0xAA, 0xBB};
|
||||||
|
conn->SimulateData(incoming);
|
||||||
|
|
||||||
|
EXPECT_TRUE(got_data);
|
||||||
|
EXPECT_TRUE(received.size() == 2);
|
||||||
|
EXPECT_TRUE(received[0] == 0xAA);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_BluetoothCleansUpOnShutdown(std::string& error_msg) {
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
bluetooth.SetBluetoothPermission(true);
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
auto conn1 = bluetooth.Connect("AA:BB:CC:DD:EE:01", {}, err);
|
||||||
|
auto conn2 = bluetooth.Connect("AA:BB:CC:DD:EE:02", {}, err);
|
||||||
|
|
||||||
|
EXPECT_TRUE(bluetooth.GetActiveConnectionCount() == 2);
|
||||||
|
|
||||||
|
bluetooth.Shutdown();
|
||||||
|
|
||||||
|
EXPECT_TRUE(bluetooth.GetActiveConnectionCount() == 0);
|
||||||
|
EXPECT_TRUE(!conn1->IsConnected());
|
||||||
|
EXPECT_TRUE(!conn2->IsConnected());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Test_BluetoothLuaIntegration(std::string& error_msg) {
|
||||||
|
SandboxContext ctx = TestContext();
|
||||||
|
LuaSandbox sandbox(ctx);
|
||||||
|
|
||||||
|
mosis::BluetoothInterface bluetooth("test.app");
|
||||||
|
mosis::RegisterBluetoothAPI(sandbox.GetState(), &bluetooth);
|
||||||
|
|
||||||
|
std::string script = R"lua(
|
||||||
|
-- Test that bluetooth global exists
|
||||||
|
if not bluetooth then
|
||||||
|
error("bluetooth global not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.startDiscovery then
|
||||||
|
error("bluetooth.startDiscovery not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.stopDiscovery then
|
||||||
|
error("bluetooth.stopDiscovery not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.connect then
|
||||||
|
error("bluetooth.connect not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.disconnectAll then
|
||||||
|
error("bluetooth.disconnectAll not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.getConnectionCount then
|
||||||
|
error("bluetooth.getConnectionCount not found")
|
||||||
|
end
|
||||||
|
if not bluetooth.isDiscovering then
|
||||||
|
error("bluetooth.isDiscovering not found")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Connection count should be 0 initially
|
||||||
|
if bluetooth.getConnectionCount() ~= 0 then
|
||||||
|
error("should have no active connections initially")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Should not be discovering initially
|
||||||
|
if bluetooth.isDiscovering() then
|
||||||
|
error("should not be discovering initially")
|
||||||
|
end
|
||||||
|
)lua";
|
||||||
|
|
||||||
|
bool ok = sandbox.LoadString(script, "bluetooth_test");
|
||||||
|
if (!ok) {
|
||||||
|
error_msg = "Lua test failed: " + sandbox.GetLastError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// MAIN
|
// MAIN
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -2870,6 +3073,15 @@ int main(int argc, char* argv[]) {
|
|||||||
harness.AddTest("SensorCleansUpOnShutdown", Test_SensorCleansUpOnShutdown);
|
harness.AddTest("SensorCleansUpOnShutdown", Test_SensorCleansUpOnShutdown);
|
||||||
harness.AddTest("SensorLuaIntegration", Test_SensorLuaIntegration);
|
harness.AddTest("SensorLuaIntegration", Test_SensorLuaIntegration);
|
||||||
|
|
||||||
|
// Milestone 16: Bluetooth
|
||||||
|
harness.AddTest("BluetoothRequiresPermission", Test_BluetoothRequiresPermission);
|
||||||
|
harness.AddTest("BluetoothRequiresUserConsent", Test_BluetoothRequiresUserConsent);
|
||||||
|
harness.AddTest("BluetoothDiscoveryWorks", Test_BluetoothDiscoveryWorks);
|
||||||
|
harness.AddTest("BluetoothConnectionLimit", Test_BluetoothConnectionLimit);
|
||||||
|
harness.AddTest("BluetoothSendReceive", Test_BluetoothSendReceive);
|
||||||
|
harness.AddTest("BluetoothCleansUpOnShutdown", Test_BluetoothCleansUpOnShutdown);
|
||||||
|
harness.AddTest("BluetoothLuaIntegration", Test_BluetoothLuaIntegration);
|
||||||
|
|
||||||
// Run tests
|
// Run tests
|
||||||
auto results = harness.Run(filter);
|
auto results = harness.Run(filter);
|
||||||
|
|
||||||
|
|||||||
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