An iOS SwiftUI app that discovers ESP32-based devices via Bluetooth LE, writes Wi‑Fi credentials to the device, and controls them over MQTT in real time. Built as a personal learning project by a university student.
- Bluetooth LE scan and connect
- Write Wi‑Fi SSID/password/topic to ESP32 via BLE characteristic
- MQTT (TLS) subscribe/publish for live device status and control
- Category-based device organization and navigation (NavigationStack + custom Router)
- Persistent device list (UserDefaults + JSON encoding)
- MVVM + Services
- View: SwiftUI screens (UI only)
- Model: Entities (e.g.,
Device) and simple value types - ViewModel: Screen state holders (optional/thin)
- Services: External dependencies (Bluetooth, MQTT)
- Helpers: Routing, small UI components, resources
esp32/View/ContentView.swift: Main grid + category bar; routes to connect/deviceConnectDeviceView.swift: BLE scanning, device selection, Wi‑Fi sheet, connect flowDeviceView.swift: Device control UI (publishes MQTT messages)
esp32/Model/Devices.swift:Device(entity), device store(s) for static definitions and user list, persistence helpersCategory.swift: Category list and selection
esp32/Services/BluetoothManager.swift: BLE scan/connect; discovers characteristic; writes Wi‑Fi JSONMQTTManager.swift: TLS MQTT setup, auto-reconnect, subscribe/publish, message handling
esp32/Helpers/HeaderView.swift: Reusable header UIRouter.swift,RouterViewModifier.swift: Navigation routing and destinationsAssets.xcassets,devices.json: Resources
esp32/esp32App.swift: App entry, provides shared services and stores via environment
BluetoothManager.startScan()begins scanning; peripherals are deduplicated by UUID on the main thread- In
ConnectDeviceView, user selects a peripheral; the app finds the matchingDevicedefinition and sets itstopic - A sheet collects Wi‑Fi SSID/password;
BluetoothManager.connect()→ services/characteristics →writeWiFiInfo(to:)writes JSON - On success, the device is added to the user’s list and the app returns to the main screen
Wi‑Fi JSON example:
{
"topic": "<categoryTopic>",
"ssid": "<wifi-ssid>",
"pass": "<wifi-password>",
"id": "<device-uuid>"
}MQTTManager.setupMQTT()configures TLS, credentials,autoReconnect, and a delegate, then connects- On
didConnectAck(.accept), the app subscribes (default:#, QoS 1) - On
didReceiveMessage, the app parses JSON, finds the device byid, and updatesisActiveon the main thread DeviceViewpublishes commands like:
{
"uuid": "<device-uuid>",
"status": true,
"mode": 2
}Topic: <device.topic>/<device.uuid>
Note: MQTTManager is a shared, app-level instance. It holds a direct reference to the device list store (not via @EnvironmentObject) so it keeps processing messages regardless of which view is visible.
Routerprovides:navigateToConnect(with topic: String)navigateToDevice(with device: Device)popToRoot()
RouterViewModifierwrapsNavigationStack(path:)and resolvesRoutedestinations- The app injects
Routervia.withRouter(router)so all screens can navigate