WIP: multi-connection advertising attempts (unsuccessful), iOS connection panel, app icon
This commit is contained in:
parent
596f641f7f
commit
7570510491
|
|
@ -9,6 +9,7 @@ enum Tab: String, CaseIterable, Identifiable {
|
|||
case chlorine = "Chlorine"
|
||||
case ph = "pH"
|
||||
case sessions = "Sessions"
|
||||
case connection = "Connection"
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
|
@ -360,7 +361,7 @@ final class AppState {
|
|||
case .amp: ampRef = nil; status = "Amp reference cleared"
|
||||
case .chlorine: clRef = nil; status = "Chlorine reference cleared"
|
||||
case .ph: phRef = nil; status = "pH reference cleared"
|
||||
case .sessions: break
|
||||
case .sessions, .connection: break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,7 +402,7 @@ final class AppState {
|
|||
case .amp: ampRef != nil
|
||||
case .chlorine: clRef != nil
|
||||
case .ph: phRef != nil
|
||||
case .sessions: false
|
||||
case .sessions, .connection: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +413,7 @@ final class AppState {
|
|||
case .amp: !ampPoints.isEmpty
|
||||
case .chlorine: clResult != nil
|
||||
case .ph: phResult != nil
|
||||
case .sessions: false
|
||||
case .sessions, .connection: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
|
|
@ -18,11 +18,20 @@ final class BLEManager: NSObject {
|
|||
case connected = "Connected"
|
||||
}
|
||||
|
||||
struct DiscoveredDevice: Identifiable {
|
||||
let id: UUID
|
||||
let peripheral: CBPeripheral
|
||||
let name: String
|
||||
let rssi: Int
|
||||
var serviceUUIDs: [CBUUID]
|
||||
}
|
||||
|
||||
var state: ConnectionState = .disconnected
|
||||
var lastMessage: EisMessage?
|
||||
var discoveredDevices: [DiscoveredDevice] = []
|
||||
|
||||
private var centralManager: CBCentralManager!
|
||||
private var peripheral: CBPeripheral?
|
||||
private(set) var peripheral: CBPeripheral?
|
||||
private var midiCharacteristic: CBCharacteristic?
|
||||
private var onMessage: ((EisMessage) -> Void)?
|
||||
|
||||
|
|
@ -36,14 +45,32 @@ final class BLEManager: NSObject {
|
|||
}
|
||||
|
||||
func startScanning() {
|
||||
guard centralManager.state == .poweredOn else { return }
|
||||
guard centralManager.state == .poweredOn else {
|
||||
print("[BLE] can't scan, state: \(centralManager.state.rawValue)")
|
||||
return
|
||||
}
|
||||
print("[BLE] starting scan (no filter)")
|
||||
state = .scanning
|
||||
discoveredDevices.removeAll()
|
||||
centralManager.scanForPeripherals(
|
||||
withServices: [Self.midiServiceUUID],
|
||||
options: nil
|
||||
withServices: nil,
|
||||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
||||
)
|
||||
}
|
||||
|
||||
func stopScanning() {
|
||||
centralManager.stopScan()
|
||||
if state == .scanning { state = .disconnected }
|
||||
}
|
||||
|
||||
func connectTo(_ device: DiscoveredDevice) {
|
||||
centralManager.stopScan()
|
||||
peripheral = device.peripheral
|
||||
state = .connecting
|
||||
print("[BLE] connecting to \(device.name)")
|
||||
centralManager.connect(device.peripheral, options: nil)
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
if let p = peripheral {
|
||||
centralManager.cancelPeripheralConnection(p)
|
||||
|
|
@ -105,6 +132,7 @@ final class BLEManager: NSObject {
|
|||
extension BLEManager: CBCentralManagerDelegate {
|
||||
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
print("[BLE] centralManager state: \(central.state.rawValue)")
|
||||
if central.state == .poweredOn {
|
||||
startScanning()
|
||||
}
|
||||
|
|
@ -116,11 +144,21 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
advertisementData: [String: Any],
|
||||
rssi RSSI: NSNumber
|
||||
) {
|
||||
guard peripheral.name == "EIS4" else { return }
|
||||
central.stopScan()
|
||||
self.peripheral = peripheral
|
||||
state = .connecting
|
||||
central.connect(peripheral, options: nil)
|
||||
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "Unknown"
|
||||
let svcUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
|
||||
|
||||
print("[BLE] found: \(name) rssi:\(RSSI) services:\(svcUUIDs)")
|
||||
|
||||
if discoveredDevices.contains(where: { $0.id == peripheral.identifier }) { return }
|
||||
|
||||
let device = DiscoveredDevice(
|
||||
id: peripheral.identifier,
|
||||
peripheral: peripheral,
|
||||
name: name,
|
||||
rssi: RSSI.intValue,
|
||||
serviceUUIDs: svcUUIDs
|
||||
)
|
||||
discoveredDevices.append(device)
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ConnectionView: View {
|
||||
var state: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
Divider()
|
||||
deviceList
|
||||
}
|
||||
.navigationTitle("Connection")
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(statusColor)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(state.ble.state.rawValue)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if state.ble.state == .connected {
|
||||
Button("Disconnect") { state.ble.disconnect() }
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
} else if state.ble.state == .scanning {
|
||||
Button("Stop") { state.ble.stopScanning() }
|
||||
.buttonStyle(.bordered)
|
||||
} else {
|
||||
Button("Scan") { state.ble.startScanning() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch state.ble.state {
|
||||
case .connected: .green
|
||||
case .scanning: .orange
|
||||
case .connecting: .yellow
|
||||
case .disconnected: .red
|
||||
}
|
||||
}
|
||||
|
||||
private var deviceList: some View {
|
||||
List {
|
||||
if state.ble.discoveredDevices.isEmpty && state.ble.state == .scanning {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Scanning for devices...")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(state.ble.discoveredDevices) { device in
|
||||
Button {
|
||||
state.ble.connectTo(device)
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(device.name)
|
||||
.font(.body.weight(.medium))
|
||||
if !device.serviceUUIDs.isEmpty {
|
||||
Text(device.serviceUUIDs.map(\.uuidString).joined(separator: ", "))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text("\(device.rssi) dBm")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if device.name == "EIS4" {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(state.ble.state == .connecting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ struct ContentView: View {
|
|||
}
|
||||
Section("Data") {
|
||||
sidebarButton(.sessions, "Sessions", "folder")
|
||||
sidebarButton(.connection, "Connection", "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
Section {
|
||||
cleanControls
|
||||
|
|
@ -126,6 +127,10 @@ struct ContentView: View {
|
|||
SessionView(state: state)
|
||||
.tabItem { Label("Sessions", systemImage: "folder") }
|
||||
.tag(Tab.sessions)
|
||||
|
||||
ConnectionView(state: state)
|
||||
.tabItem { Label("Connection", systemImage: "antenna.radiowaves.left.and.right") }
|
||||
.tag(Tab.connection)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +145,7 @@ struct ContentView: View {
|
|||
case .chlorine: ChlorineView(state: state)
|
||||
case .ph: PhView(state: state)
|
||||
case .sessions: SessionView(state: state)
|
||||
case .connection: ConnectionView(state: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
34
main/ble.c
34
main/ble.c
|
|
@ -30,7 +30,7 @@ static const ble_uuid128_t midi_chr_uuid = BLE_UUID128_INIT(
|
|||
0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1,
|
||||
0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77);
|
||||
|
||||
#define MAX_CONNECTIONS 2
|
||||
#define MAX_CONNECTIONS 4
|
||||
|
||||
static EventGroupHandle_t ble_events;
|
||||
static QueueHandle_t cmd_queue;
|
||||
|
|
@ -523,8 +523,13 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static void start_adv(void)
|
||||
static void adv_task(void *param)
|
||||
{
|
||||
(void)param;
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
ble_gap_adv_stop();
|
||||
|
||||
struct ble_hs_adv_fields fields = {0};
|
||||
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
fields.name = (uint8_t *)DEVICE_NAME;
|
||||
|
|
@ -539,23 +544,38 @@ static void start_adv(void)
|
|||
fields.uuids16 = adv_uuids;
|
||||
fields.num_uuids16 = 2;
|
||||
fields.uuids16_is_complete = 0;
|
||||
ble_gap_adv_set_fields(&fields);
|
||||
|
||||
int rc = ble_gap_adv_set_fields(&fields);
|
||||
if (rc) { printf("BLE: set_fields failed: %d\n", rc); goto done; }
|
||||
|
||||
struct ble_hs_adv_fields rsp = {0};
|
||||
rsp.uuids128 = (ble_uuid128_t *)&midi_svc_uuid;
|
||||
rsp.num_uuids128 = 1;
|
||||
rsp.uuids128_is_complete = 1;
|
||||
ble_gap_adv_rsp_set_fields(&rsp);
|
||||
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp);
|
||||
if (rc) { printf("BLE: set_rsp failed: %d\n", rc); goto done; }
|
||||
|
||||
struct ble_gap_adv_params params = {0};
|
||||
params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
int rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
|
||||
¶ms, gap_event_cb, NULL);
|
||||
params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MAX;
|
||||
|
||||
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
|
||||
¶ms, gap_event_cb, NULL);
|
||||
if (rc)
|
||||
printf("BLE: adv_start failed: %d\n", rc);
|
||||
else
|
||||
printf("BLE: advertising\n");
|
||||
printf("BLE: advertising (%d connected)\n", conn_count);
|
||||
|
||||
done:
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void start_adv(void)
|
||||
{
|
||||
xTaskCreate(adv_task, "adv", 2048, NULL, 5, NULL);
|
||||
}
|
||||
|
||||
static void on_sync(void)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=2
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=4
|
||||
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
|
||||
CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
|
||||
CONFIG_BT_NIMBLE_ROLE_OBSERVER=n
|
||||
|
|
|
|||
Loading…
Reference in New Issue