Compare commits

...

3 Commits

11 changed files with 507 additions and 36 deletions

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import Observation import Observation
import Combine
enum Tab: String, CaseIterable, Identifiable { enum Tab: String, CaseIterable, Identifiable {
case eis = "EIS" case eis = "EIS"
@ -8,6 +9,7 @@ enum Tab: String, CaseIterable, Identifiable {
case chlorine = "Chlorine" case chlorine = "Chlorine"
case ph = "pH" case ph = "pH"
case sessions = "Sessions" case sessions = "Sessions"
case connection = "Connection"
var id: String { rawValue } var id: String { rawValue }
} }
@ -16,6 +18,7 @@ enum Tab: String, CaseIterable, Identifiable {
@Observable @Observable
final class AppState { final class AppState {
let ble: BLEManager
var tab: Tab = .eis var tab: Tab = .eis
var status: String = "Disconnected" var status: String = "Disconnected"
var bleConnected: Bool = false var bleConnected: Bool = false
@ -74,52 +77,255 @@ final class AppState {
// Device reference collection // Device reference collection
var collectingRefs: Bool = false var collectingRefs: Bool = false
var hasDeviceRefs: Bool = false var hasDeviceRefs: Bool = false
private var eisRefs: [Int: [EisPoint]] = [:]
private var refMode: UInt8?
private var refRtia: UInt8?
// Session
var currentSessionId: Int64? = nil
// Clean // Clean
var cleanV: String = "1200" var cleanV: String = "1200"
var cleanDur: String = "30" var cleanDur: String = "30"
// Temperature polling
private var tempTimer: Timer?
init() {
ble = BLEManager()
ble.setMessageHandler { [weak self] msg in
self?.handleMessage(msg)
}
startTempPolling()
}
// MARK: - BLE connection tracking
func updateConnectionState() {
let connected = ble.state == .connected
if connected != bleConnected {
bleConnected = connected
status = ble.state.rawValue
}
}
// MARK: - Temperature polling
private func startTempPolling() {
tempTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
guard let self, self.bleConnected else { return }
self.send(buildSysexGetTemp())
}
}
// MARK: - Send helper
func send(_ sysex: [UInt8]) {
ble.sendCommand(sysex)
}
// MARK: - Message handler
private func handleMessage(_ msg: EisMessage) {
switch msg {
case .sweepStart(let numPoints, let freqStart, let freqStop):
if collectingRefs {
eisPoints.removeAll()
sweepTotal = numPoints
} else {
eisPoints.removeAll()
sweepTotal = numPoints
status = String(format: "Sweep: %d pts, %.0f--%.0f Hz", numPoints, freqStart, freqStop)
}
case .dataPoint(_, let point):
eisPoints.append(point)
if !collectingRefs {
status = "Receiving: \(eisPoints.count)/\(sweepTotal)"
}
case .sweepEnd:
if collectingRefs {
if let r = refRtia, r < 8 {
eisRefs[Int(r)] = eisPoints
}
eisPoints.removeAll()
} else {
saveEis()
status = "Sweep complete: \(eisPoints.count) points"
}
case .config(let cfg):
freqStart = String(format: "%.0f", cfg.freqStart)
freqStop = String(format: "%.0f", cfg.freqStop)
ppd = "\(cfg.ppd)"
rtia = cfg.rtia
rcal = cfg.rcal
electrode = cfg.electrode
status = "Config received"
case .lsvStart(let numPoints, let vStart, let vStop):
lsvPoints.removeAll()
lsvTotal = numPoints
status = String(format: "LSV: %d pts, %.0f--%.0f mV", numPoints, vStart, vStop)
case .lsvPoint(_, let point):
lsvPoints.append(point)
status = "LSV: \(lsvPoints.count)/\(lsvTotal)"
case .lsvEnd:
saveLsv()
status = "LSV complete: \(lsvPoints.count) points"
case .ampStart(let vHold):
ampPoints.removeAll()
ampRunning = true
status = String(format: "Amp: %.0f mV", vHold)
case .ampPoint(let index, let point):
ampPoints.append(point)
ampTotal = index + 1
status = "Amp: \(ampPoints.count) pts"
case .ampEnd:
ampRunning = false
saveAmp()
status = "Amp complete: \(ampPoints.count) points"
case .clStart(let numPoints):
clPoints.removeAll()
clResult = nil
clTotal = numPoints
status = "Chlorine: \(numPoints) pts"
case .clPoint(_, let point):
clPoints.append(point)
status = "Chlorine: \(clPoints.count)/\(clTotal)"
case .clResult(let r):
clResult = r
status = String(format: "Chlorine: free=%.3f uA, total=%.3f uA", r.iFreeUa, r.iTotalUa)
case .clEnd:
saveCl()
status = "Chlorine complete: \(clPoints.count) points"
case .phResult(let r):
if collectingRefs {
phRef = r
} else {
savePh(r)
status = String(format: "pH: %.2f (OCP=%.1f mV, T=%.1fC)", r.ph, r.vOcpMv, r.tempC)
phResult = r
}
case .temperature(let t):
tempC = t
case .refFrame(let mode, let rtiaIdx):
refMode = mode
refRtia = rtiaIdx
let modeName: String
switch mode {
case 0: modeName = "EIS"
case 1: modeName = "LSV"
case 2: modeName = "Amp"
case 3: modeName = "Cl"
case 4: modeName = "pH"
default: modeName = "?"
}
if mode == 0 {
status = "Ref: \(modeName) RTIA \(rtiaIdx + 1)/8"
} else {
status = "Ref: \(modeName) range search"
}
case .refLpRange:
break
case .refsDone:
collectingRefs = false
hasDeviceRefs = true
refMode = nil
refRtia = nil
let idx = Int(rtia.rawValue)
if idx < 8, let pts = eisRefs[idx] {
eisRef = pts
}
status = "Reference collection complete"
case .refStatus(let hasRefs):
hasDeviceRefs = hasRefs
if !hasRefs {
status = "No device refs"
}
}
}
// MARK: - Actions // MARK: - Actions
func applyEISSettings() { func applyEISSettings() {
let fs = Float(freqStart) ?? 1000 let fs = Float(freqStart) ?? 1000
let fe = Float(freqStop) ?? 200000 let fe = Float(freqStop) ?? 200000
let p = UInt16(ppd) ?? 10 let p = UInt16(ppd) ?? 10
status = "Applying: \(fs)-\(fe) Hz, \(p) PPD" send(buildSysexSetSweep(freqStart: fs, freqStop: fe, ppd: p))
send(buildSysexSetRtia(rtia))
send(buildSysexSetRcal(rcal))
send(buildSysexSetElectrode(electrode))
send(buildSysexGetConfig())
} }
func startSweep() { func startSweep() {
eisPoints.removeAll() eisPoints.removeAll()
status = "Starting sweep..." send(buildSysexGetTemp())
send(buildSysexStartSweep())
} }
func startLSV() { func startLSV() {
lsvPoints.removeAll() lsvPoints.removeAll()
let vs = Float(lsvStartV) ?? 0 let vs = Float(lsvStartV) ?? 0
let ve = Float(lsvStopV) ?? 500 let ve = Float(lsvStopV) ?? 500
status = "Starting LSV: \(vs)-\(ve) mV" let sr = Float(lsvScanRate) ?? 50
send(buildSysexGetTemp())
send(buildSysexStartLsv(vStart: vs, vStop: ve, scanRate: sr, lpRtia: lsvRtia))
} }
func startAmp() { func startAmp() {
ampPoints.removeAll() ampPoints.removeAll()
ampRunning = true ampRunning = true
status = "Starting amperometry..." let vh = Float(ampVHold) ?? 200
let iv = Float(ampInterval) ?? 100
let dur = Float(ampDuration) ?? 60
send(buildSysexGetTemp())
send(buildSysexStartAmp(vHold: vh, intervalMs: iv, durationS: dur, lpRtia: ampRtia))
} }
func stopAmp() { func stopAmp() {
ampRunning = false send(buildSysexStopAmp())
status = "Stopping amperometry..."
} }
func startChlorine() { func startChlorine() {
clPoints.removeAll() clPoints.removeAll()
clResult = nil clResult = nil
status = "Starting chlorine measurement..." let vCond = Float(clCondV) ?? 800
let tCond = Float(clCondT) ?? 2000
let vFree = Float(clFreeV) ?? 100
let vTotal = Float(clTotalV) ?? -200
let tDep = Float(clDepT) ?? 5000
let tMeas = Float(clMeasT) ?? 5000
send(buildSysexGetTemp())
send(buildSysexStartCl(
vCond: vCond, tCondMs: tCond, vFree: vFree, vTotal: vTotal,
tDepMs: tDep, tMeasMs: tMeas, lpRtia: clRtia
))
} }
func startPh() { func startPh() {
phResult = nil phResult = nil
status = "Starting pH measurement..." let stab = Float(phStabilize) ?? 30
send(buildSysexGetTemp())
send(buildSysexStartPh(stabilizeS: stab))
} }
func setReference() { func setReference() {
@ -155,29 +361,37 @@ final class AppState {
case .amp: ampRef = nil; status = "Amp reference cleared" case .amp: ampRef = nil; status = "Amp reference cleared"
case .chlorine: clRef = nil; status = "Chlorine reference cleared" case .chlorine: clRef = nil; status = "Chlorine reference cleared"
case .ph: phRef = nil; status = "pH reference cleared" case .ph: phRef = nil; status = "pH reference cleared"
case .sessions: break case .sessions, .connection: break
} }
} }
func collectRefs() { func collectRefs() {
collectingRefs = true collectingRefs = true
eisRefs.removeAll()
status = "Starting reference collection..." status = "Starting reference collection..."
send(buildSysexStartRefs())
}
func getRefs() {
collectingRefs = true
eisRefs.removeAll()
send(buildSysexGetRefs())
} }
func clearRefs() { func clearRefs() {
collectingRefs = false collectingRefs = false
hasDeviceRefs = false hasDeviceRefs = false
eisRefs.removeAll()
eisRef = nil eisRef = nil
lsvRef = nil
ampRef = nil
clRef = nil
phRef = nil phRef = nil
send(buildSysexClearRefs())
status = "Refs cleared" status = "Refs cleared"
} }
func startClean() { func startClean() {
let v = Float(cleanV) ?? 1200 let v = Float(cleanV) ?? 1200
let d = Float(cleanDur) ?? 30 let d = Float(cleanDur) ?? 30
send(buildSysexStartClean(vMv: v, durationS: d))
status = String(format: "Cleaning: %.0f mV for %.0fs", v, d) status = String(format: "Cleaning: %.0f mV for %.0fs", v, d)
} }
@ -188,7 +402,7 @@ final class AppState {
case .amp: ampRef != nil case .amp: ampRef != nil
case .chlorine: clRef != nil case .chlorine: clRef != nil
case .ph: phRef != nil case .ph: phRef != nil
case .sessions: false case .sessions, .connection: false
} }
} }
@ -199,10 +413,97 @@ final class AppState {
case .amp: !ampPoints.isEmpty case .amp: !ampPoints.isEmpty
case .chlorine: clResult != nil case .chlorine: clResult != nil
case .ph: phResult != nil case .ph: phResult != nil
case .sessions: false case .sessions, .connection: false
} }
} }
// MARK: - Auto-save
private func saveEis() {
guard let sid = currentSessionId else { return }
let params: [String: String] = [
"freq_start": freqStart,
"freq_stop": freqStop,
"ppd": ppd,
"rtia": rtia.label,
"rcal": rcal.label,
"electrode": electrode.label,
]
guard let configData = try? JSONEncoder().encode(params) else { return }
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .eis) else { return }
meas.config = configData
guard let mid = meas.id else { return }
let indexed = eisPoints.enumerated().map { (index: $0.offset, value: $0.element) }
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
}
private func saveLsv() {
guard let sid = currentSessionId else { return }
let params: [String: String] = [
"v_start": lsvStartV,
"v_stop": lsvStopV,
"scan_rate": lsvScanRate,
"rtia": lsvRtia.label,
]
guard let configData = try? JSONEncoder().encode(params) else { return }
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .lsv) else { return }
meas.config = configData
guard let mid = meas.id else { return }
let indexed = lsvPoints.enumerated().map { (index: $0.offset, value: $0.element) }
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
}
private func saveAmp() {
guard let sid = currentSessionId else { return }
let params: [String: String] = [
"v_hold": ampVHold,
"interval_ms": ampInterval,
"duration_s": ampDuration,
"rtia": ampRtia.label,
]
guard let configData = try? JSONEncoder().encode(params) else { return }
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .amp) else { return }
meas.config = configData
guard let mid = meas.id else { return }
let indexed = ampPoints.enumerated().map { (index: $0.offset, value: $0.element) }
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
}
private func saveCl() {
guard let sid = currentSessionId else { return }
let params: [String: String] = [
"cond_v": clCondV,
"cond_t": clCondT,
"free_v": clFreeV,
"total_v": clTotalV,
"dep_t": clDepT,
"meas_t": clMeasT,
"rtia": clRtia.label,
]
guard let configData = try? JSONEncoder().encode(params) else { return }
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .chlorine) else { return }
meas.config = configData
guard let mid = meas.id else { return }
let indexed = clPoints.enumerated().map { (index: $0.offset, value: $0.element) }
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
if let r = clResult {
try? Storage.shared.setMeasurementResult(mid, result: r)
}
}
private func savePh(_ result: PhResult) {
guard let sid = currentSessionId else { return }
let params: [String: String] = [
"stabilize_s": phStabilize,
]
guard let configData = try? JSONEncoder().encode(params) else { return }
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .ph) else { return }
meas.config = configData
guard let mid = meas.id else { return }
try? Storage.shared.addDataPoint(measurementId: mid, index: 0, point: result)
try? Storage.shared.setMeasurementResult(mid, result: result)
}
// MARK: - Measurement loading // MARK: - Measurement loading
func loadMeasurement(_ measurement: Measurement) { func loadMeasurement(_ measurement: Measurement) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@ -8,8 +8,8 @@ import Foundation
@Observable @Observable
final class BLEManager: NSObject { final class BLEManager: NSObject {
static let midiServiceUUID = CBUUID(string: "03B80E5A-EDE8-4B33-A751-6CE34EC4C700") nonisolated(unsafe) static let midiServiceUUID = CBUUID(string: "03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
static let midiCharUUID = CBUUID(string: "7772E5DB-3868-4112-A1A9-F2669D106BF3") nonisolated(unsafe) static let midiCharUUID = CBUUID(string: "7772E5DB-3868-4112-A1A9-F2669D106BF3")
enum ConnectionState: String { enum ConnectionState: String {
case disconnected = "Disconnected" case disconnected = "Disconnected"
@ -18,11 +18,20 @@ final class BLEManager: NSObject {
case connected = "Connected" 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 state: ConnectionState = .disconnected
var lastMessage: EisMessage? var lastMessage: EisMessage?
var discoveredDevices: [DiscoveredDevice] = []
private var centralManager: CBCentralManager! private var centralManager: CBCentralManager!
private var peripheral: CBPeripheral? private(set) var peripheral: CBPeripheral?
private var midiCharacteristic: CBCharacteristic? private var midiCharacteristic: CBCharacteristic?
private var onMessage: ((EisMessage) -> Void)? private var onMessage: ((EisMessage) -> Void)?
@ -36,14 +45,32 @@ final class BLEManager: NSObject {
} }
func startScanning() { 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 state = .scanning
discoveredDevices.removeAll()
centralManager.scanForPeripherals( centralManager.scanForPeripherals(
withServices: [Self.midiServiceUUID], withServices: nil,
options: 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() { func disconnect() {
if let p = peripheral { if let p = peripheral {
centralManager.cancelPeripheralConnection(p) centralManager.cancelPeripheralConnection(p)
@ -105,6 +132,7 @@ final class BLEManager: NSObject {
extension BLEManager: CBCentralManagerDelegate { extension BLEManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) { func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("[BLE] centralManager state: \(central.state.rawValue)")
if central.state == .poweredOn { if central.state == .poweredOn {
startScanning() startScanning()
} }
@ -116,11 +144,21 @@ extension BLEManager: CBCentralManagerDelegate {
advertisementData: [String: Any], advertisementData: [String: Any],
rssi RSSI: NSNumber rssi RSSI: NSNumber
) { ) {
guard peripheral.name == "EIS4" else { return } let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "Unknown"
central.stopScan() let svcUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
self.peripheral = peripheral
state = .connecting print("[BLE] found: \(name) rssi:\(RSSI) services:\(svcUUIDs)")
central.connect(peripheral, options: nil)
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) { func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {

View File

@ -2,11 +2,14 @@ import SwiftUI
@main @main
struct CueIOSApp: App { struct CueIOSApp: App {
@State private var ble = BLEManager() @State private var state = AppState()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView(state: state)
.onReceive(Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()) { _ in
state.updateConnectionState()
}
} }
} }
} }

View File

@ -2,6 +2,18 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>
<string>EIS4 uses Bluetooth to communicate with the impedance analyzer</string> <string>EIS4 uses Bluetooth to communicate with the impedance analyzer</string>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

View File

@ -43,7 +43,7 @@ struct DataPoint: Codable, FetchableRecord, MutablePersistableRecord {
// MARK: - Database manager // MARK: - Database manager
final class Storage { final class Storage: @unchecked Sendable {
static let shared = Storage() static let shared = Storage()
private let dbQueue: DatabaseQueue private let dbQueue: DatabaseQueue

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

@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
struct ContentView: View { struct ContentView: View {
@State private var state = AppState() @Bindable var state: AppState
@Environment(\.horizontalSizeClass) private var sizeClass @Environment(\.horizontalSizeClass) private var sizeClass
var body: some View { var body: some View {
@ -40,6 +40,7 @@ struct ContentView: View {
} }
Section("Data") { Section("Data") {
sidebarButton(.sessions, "Sessions", "folder") sidebarButton(.sessions, "Sessions", "folder")
sidebarButton(.connection, "Connection", "antenna.radiowaves.left.and.right")
} }
Section { Section {
cleanControls cleanControls
@ -126,6 +127,10 @@ struct ContentView: View {
SessionView(state: state) SessionView(state: state)
.tabItem { Label("Sessions", systemImage: "folder") } .tabItem { Label("Sessions", systemImage: "folder") }
.tag(Tab.sessions) .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 .chlorine: ChlorineView(state: state)
case .ph: PhView(state: state) case .ph: PhView(state: state)
case .sessions: SessionView(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, 0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1,
0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77); 0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77);
#define MAX_CONNECTIONS 2 #define MAX_CONNECTIONS 4
static EventGroupHandle_t ble_events; static EventGroupHandle_t ble_events;
static QueueHandle_t cmd_queue; static QueueHandle_t cmd_queue;
@ -523,8 +523,13 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg)
return 0; 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}; struct ble_hs_adv_fields fields = {0};
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.name = (uint8_t *)DEVICE_NAME; fields.name = (uint8_t *)DEVICE_NAME;
@ -539,19 +544,38 @@ static void start_adv(void)
fields.uuids16 = adv_uuids; fields.uuids16 = adv_uuids;
fields.num_uuids16 = 2; fields.num_uuids16 = 2;
fields.uuids16_is_complete = 0; 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}; struct ble_hs_adv_fields rsp = {0};
rsp.uuids128 = (ble_uuid128_t *)&midi_svc_uuid; rsp.uuids128 = (ble_uuid128_t *)&midi_svc_uuid;
rsp.num_uuids128 = 1; rsp.num_uuids128 = 1;
rsp.uuids128_is_complete = 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}; struct ble_gap_adv_params params = {0};
params.conn_mode = BLE_GAP_CONN_MODE_UND; params.conn_mode = BLE_GAP_CONN_MODE_UND;
params.disc_mode = BLE_GAP_DISC_MODE_GEN; params.disc_mode = BLE_GAP_DISC_MODE_GEN;
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); &params, gap_event_cb, NULL);
if (rc)
printf("BLE: adv_start failed: %d\n", rc);
else
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) static void on_sync(void)

View File

@ -1,7 +1,7 @@
CONFIG_IDF_TARGET="esp32s3" CONFIG_IDF_TARGET="esp32s3"
CONFIG_BT_ENABLED=y CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_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_PERIPHERAL=y
CONFIG_BT_NIMBLE_ROLE_CENTRAL=y CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
CONFIG_BT_NIMBLE_ROLE_OBSERVER=n CONFIG_BT_NIMBLE_ROLE_OBSERVER=n