WIP: multi-connection advertising attempts (unsuccessful), iOS connection panel, app icon

This commit is contained in:
jess 2026-03-31 19:46:23 -07:00
parent 596f641f7f
commit 7570510491
8 changed files with 172 additions and 20 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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)
}
}
}
}

View File

@ -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.

View File

@ -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,
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,
&params, 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)

View File

@ -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