diff --git a/content/additional-resources/swiftui.md b/content/additional-resources/swiftui.md index 09090e1..50e400c 100644 --- a/content/additional-resources/swiftui.md +++ b/content/additional-resources/swiftui.md @@ -32,4 +32,4 @@ You can download XCode [here](https://developer.apple.com/xcode/). It is common for teams to want to develop an iOS app that communicates with their device via BLE. -An old EnVision tutorial for creating a simple RGB lamp can be found [here](https://github.com/AdinAck/EnVision-Tutorial-Lamp). This tutorial goes over setting up a Swift app to use BLE and writing Arduino BLE code for the device. +To learn more, do the [Full Stack](/assignments/full-stack) assignment. diff --git a/content/assignments/_index.md b/content/assignments/_index.md index 98dfd9e..28dd79d 100644 --- a/content/assignments/_index.md +++ b/content/assignments/_index.md @@ -11,4 +11,5 @@ All of the ECE 196 assignment instructions can be found here. {{< card link="dev-board" title="DevBoard" icon="chip" >}} {{< card link="vu-meter" title="VU Meter" icon="chart-square-bar" >}} {{< card link="spinning-and-blinking" title="Spinning and Blinking" icon="code" >}} + {{< card link="full-stack" title="Full Stack" icon="wifi" >}} {{< /cards >}} diff --git a/content/assignments/full-stack/BLE/_index.md b/content/assignments/full-stack/BLE/_index.md index ad21b57..c45e600 100644 --- a/content/assignments/full-stack/BLE/_index.md +++ b/content/assignments/full-stack/BLE/_index.md @@ -62,7 +62,7 @@ A service is like an Object, it contains states, and represents some kind of dat Services contain **characteristics**, which are the states of the service. You can configure characteristics to be readable and writable. -In the case of our lamp, we will have one service *Lamp* with three characteristics: *R*, *G*, and *B* (The red, green, and blue values). +In this case, we will have one service with one characteristic: *LED*. Here is a flowchart of how our BLE system will work: diff --git a/content/assignments/full-stack/BLE/app.md b/content/assignments/full-stack/BLE/app.md new file mode 100644 index 0000000..c36ddbc --- /dev/null +++ b/content/assignments/full-stack/BLE/app.md @@ -0,0 +1,352 @@ +--- +title: App +type: docs +prev: assignments/full-stack/ble/firmware +next: assignments/full-stack/what-just-happened +weight: 2 +--- + +Rather than using NRF Connect, let's make an app to interact with our device. + +Open XCode and create a new iOS project. + +![image](images/xcode_ios.png) + +Before we get started, we need to enable the BLE access specifiers in the info pane. + +![image](images/xcode_info.png) +![image](images/xcode_properties.png) +![image](images/xcode_ble_privacy.png) +> The operating system grants resource permissions on a per-request basis. Our app can't +> use these protected resources without asking. + +## Model + +Create a new file called `BLE.swift`. + +![image](images/xcode_ble_file.jpg) + +This is where we will create the model for BLE. + +To start, define an observable class to delegate central manager and peripheral +events: + +```swift +import CoreBluetooth + +@Observable +class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { + // more to come... +} +``` + +Add a static `Logger` instance so we can log events to help with debugging: + +```swift +import os + +class BLE: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { + // logger for this class + private static let LOGGER = Logger(subsystem: Bundle.main.bundleIdentifier!, category: BLE.description()) +} +``` + +Let's create a type to represent different kinds of data we expect to read or write: + +```swift +enum Endpoint { + case led +} +``` + +For now there is only one piece of data we care about: the state of the LED, but in the future you may +add more. + +Let's store the service and characteristic UUIDs: + +```swift +// expected service UUID +static let SERVICE_UUID: CBUUID = CBUUID(string: "937312e0-2354-11eb-9f10-fbc30a62cf38") +// expected characteristic UUIDs +static let CHARACTERISTIC_MAP: [CBUUID: Endpoint] = [ + CBUUID(string: "957312e0-2354-11eb-9f10-fbc30a62cf38"): .led, +] +``` + +and add our first member, the central manager: + +```swift +// the central manager store +var centralManager: CBCentralManager! +``` +> The `!` indicates that this value can be `nil` but we guarantee it to be +> filled before the first time it is read. + +Acting as a delegate for the central manager begins in the constructor: + +```swift +override init() { + super.init() + + // attach to the central manager + centralManager = CBCentralManager(delegate: self, queue: nil) +} +``` + +Now we can define the behavior for when the central manager's state changes: + +```swift +func centralManagerDidUpdateState(_ central: CBCentralManager) { + switch central.state { + // if the central manager is on... + case .poweredOn: + // ...start scanning + startScanning() + default: + Self.LOGGER.error("Central manager state is not powered on.") + } +} +``` + +We only care for the central manager to be powered on, so let's log an error if that is not the case. + +We haven't defined the primitive `startScanning` yet, so let's do that: + +```swift +func startScanning() { + Self.LOGGER.info("Scanning...") + // start scanning for devices advertising the expected service + centralManager.scanForPeripherals(withServices: [Self.SERVICE_UUID]) +} +``` + +and for later, `stopScanning`: + +```swift +func stopScanning() { + centralManager.stopScan() +} +``` + +To connect to a peripheral and delegate its events, we need to store it +as a member (similarly to the central manager): + +```swift +// the peripheral store +var peripheral: CBPeripheral? +``` +> The `?` indicates that `nil` is a possible value for this member. This is called an *optional*. + +We are eventually going to start keeping track of handles to discovered characteristics, +so let's add a member for that: + +```swift +// discovered characteristics +var characteristics: [Endpoint: CBCharacteristic] = [:] +``` + +Let's implement the event handler for when the central manager discovers a viable peripheral: + +```swift +// when the central manager discovers a peripheral... +func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { + Self.LOGGER.info("Discovered peripheral: \(peripheral).") + + // ...store the peripheral... + self.peripheral = peripheral + // ...and connect to it + centralManager.connect(peripheral) +} +``` + +And now when a connection is established: + +```swift +// when the central manager connects to a peripheral... +func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + Self.LOGGER.info("Connected to peripheral: \(peripheral).") + + // ...assign this instance as the peripheral delegate... + self.peripheral!.delegate = self + // ...and discover the expected services of the peripheral + self.peripheral!.discoverServices([Self.SERVICE_UUID]) + + // no longer need to scan since a connection has been made + stopScanning() +} +``` + +And naturally undo this when a peripheral disconnects: + +```swift +// when the peripheral disconnects... +func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { + Self.LOGGER.info("Disconnected from peripheral: \(peripheral).") + + // ...invalidate peripheral and characterstic handles + self.peripheral = nil + characteristics = [:] + + // resume scanning to potentially connect again + startScanning() +} +``` + +When services are discovered, we want to begin discovering the expected characteristics: + +```swift +// when all services have been discovered... +func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: (any Error)?) { + let services = peripheral.services! + + Self.LOGGER.info("Discovered services: \(services).") + + // ...for each discovered service... + for service in services { + // ...if the service is the service we are looking for... + if service.uuid == Self.SERVICE_UUID { + // ...discover all expected characteristics from that service + peripheral.discoverCharacteristics(Array(Self.CHARACTERISTIC_MAP.keys), for: service) + break // don't care about other services + } + } +} +``` + +And for each characteristic, we want to store its handle and read its value: + +```swift +// when all characteristics of a service have been discovered... +func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: (any Error)?) { + let characteristics = service.characteristics! + + Self.LOGGER.info("Discovered characteristics: \(characteristics).") + + // ...for each discovered characteristic... + for characteristic in characteristics { + // ...store into the charactersistics dict... + // NOTE: all discovered characteristics should be expected so this will never panic + self.characteristics[Self.CHARACTERISTIC_MAP[characteristic.uuid]!] = characteristic + // ...and read the value + peripheral.readValue(for: characteristic) + } +} +``` + +Now let's add an internal state for tracking the LED state: + +```swift +// internal led state +private var led_state = false +``` + +The last event to handle is when the characteristic corresponding to the led state updates: + +```swift +// when a characteristic value has updated... +func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: (any Error)?) { + Self.LOGGER.info("Value updated for characteristic: \(characteristic).") + + // ...if the characteristic is the LED characteristic... + if characteristic.uuid == characteristics[.led]!.uuid { + // ...update the LED state with the new value + led_state = characteristic.value!.withUnsafeBytes({ ptr in + ptr.load(as: Bool.self) + }) + + Self.LOGGER.trace("LED state updated: \(self.led_state).") + } +} +``` +> Can you think of how to make this generic for any number of endpoints? + +Now let's create a computed property to expose the led state to views: + +```swift +// exposed led state +var led: Bool { + // when this property is read, return the internal state + get { + led_state + } + + // when this property is written to, send the new value to the BLE peripheral + // and update the internal state + set(newValue) { + peripheral?.writeValue(Data([newValue ? 1 : 0]), for: characteristics[.led]!, type: .withResponse) + led_state = newValue + + Self.LOGGER.trace("LED state written: \(newValue).") + } +} +``` + +Finally, let's create two more computed properties for convenience: + +```swift +// if the peripheral is not null, it must be connected +var connected: Bool { + peripheral != nil +} +// if all expected characteristics have been found, discovery is complete +var loaded: Bool { + characteristics.count == Self.CHARACTERISTIC_MAP.count +} +``` + +## View + +With the model complete, we can design our view. + +In `ContentView`, declare the model as a state of the view: + +```swift +@State var ble = BLE() +``` + +Create a `NavigationView` to house the LED toggle: + +```swift +NavigationView { + List { + Toggle(isOn: $ble.led) { + Text("LED") + } + } + .navigationTitle("FullStack") + .disabled(!ble.loaded) +} +``` + +To make this look a little better, let's create an overlay view +for when the peripheral is not connected: + +```swift +VStack { + ProgressView() + .padding() + + Text(ble.connected ? "Connected. Loading..." : "Looking for device...") + .font(.caption) + .foregroundStyle(.secondary) +} +.frame(maxWidth: .infinity, maxHeight: .infinity) +.background(.background) +.opacity(ble.loaded ? 0 : 1) +``` + +Combine these two views and enable animations for the `loaded` property of the model: + +```swift +ZStack { + // navigation view + // overlay +} +.animation(.default, value: ble.loaded) +``` + +And that's it! Run it on your phone or your Mac with Mac Catalyst and watch it work! We think you're gonna love it \:) + +{{< callout type="warning" >}} + To learn how to deploy the app to your device, google it! +{{< /callout >}} diff --git a/content/assignments/full-stack/BLE/firmware.md b/content/assignments/full-stack/BLE/firmware.md index e71f936..9a509af 100644 --- a/content/assignments/full-stack/BLE/firmware.md +++ b/content/assignments/full-stack/BLE/firmware.md @@ -2,7 +2,7 @@ title: Firmware type: docs prev: assignments/full-stack/ble -next: assignments/full-stack/ble/client +next: assignments/full-stack/ble/app weight: 1 --- @@ -22,7 +22,7 @@ cargo add esp-wifi --features esp32s3,ble,async,phy-enable-usb to add the `esp-wifi` crate with BLE capability enabled. -And add the following dependency: +And add the following dependency in `Cargo.toml` under `[dependencies]`: ```toml bleps = { git = "https://github.com/bjoernQ/bleps", package = "bleps", branch = "main", features = [ @@ -157,7 +157,7 @@ It makes sense to advertise the UUID of an available service. In this case, I randomly created a UUID with [this](https://www.uuidgenerator.net) tool. -Now let's create a GATT server with one service with one characteristic: +Now let's create a GATT server with a single service that has one characteristic: ```rust gatt!([service { @@ -290,9 +290,9 @@ let mut wf = |_offset: usize, data: &[u8]| { }); }; ``` -> `borrow()` and `borrow_mut()` will panic of the resource is already borrowed, but we know +> `borrow()` and `borrow_mut()` will panic if the resource is already borrowed, but we know > the lifetimes of these closures will not overlap. -> The compiler may be able to determine this fact, and will optimize out the runtime check +> The compiler may be able to determine this fact, and could optimize out the runtime check > entirely! Finally, we can start the server: diff --git a/content/assignments/full-stack/BLE/images/xcode_ble_file.jpg b/content/assignments/full-stack/BLE/images/xcode_ble_file.jpg new file mode 100644 index 0000000..5816976 Binary files /dev/null and b/content/assignments/full-stack/BLE/images/xcode_ble_file.jpg differ diff --git a/content/assignments/full-stack/BLE/images/xcode_ble_privacy.png b/content/assignments/full-stack/BLE/images/xcode_ble_privacy.png new file mode 100644 index 0000000..81241a8 Binary files /dev/null and b/content/assignments/full-stack/BLE/images/xcode_ble_privacy.png differ diff --git a/content/assignments/full-stack/BLE/images/xcode_info.png b/content/assignments/full-stack/BLE/images/xcode_info.png new file mode 100644 index 0000000..5aeddfc Binary files /dev/null and b/content/assignments/full-stack/BLE/images/xcode_info.png differ diff --git a/content/assignments/full-stack/BLE/images/xcode_ios.png b/content/assignments/full-stack/BLE/images/xcode_ios.png new file mode 100644 index 0000000..7ea9775 Binary files /dev/null and b/content/assignments/full-stack/BLE/images/xcode_ios.png differ diff --git a/content/assignments/full-stack/BLE/images/xcode_properties.png b/content/assignments/full-stack/BLE/images/xcode_properties.png new file mode 100644 index 0000000..449161c Binary files /dev/null and b/content/assignments/full-stack/BLE/images/xcode_properties.png differ diff --git a/content/assignments/full-stack/BLE/what-just-happened.md b/content/assignments/full-stack/BLE/what-just-happened.md new file mode 100644 index 0000000..f106528 --- /dev/null +++ b/content/assignments/full-stack/BLE/what-just-happened.md @@ -0,0 +1,17 @@ +--- +title: What Just Happened +type: docs +prev: assignments/full-stack/ble/app +next: assignments/full-stack/submission +weight: 3 +--- + +Ok! That was a lot! But i'm glad you made it :\) + +You now know how to: + +- write asynchronous firmware +- work with the BLE stack +- create user interfaces with SwiftUI + - interaction between views and models +- handle events with delegates diff --git a/content/assignments/full-stack/USB/what-just-happened.md b/content/assignments/full-stack/USB/what-just-happened.md index 6026201..101d3f5 100644 --- a/content/assignments/full-stack/USB/what-just-happened.md +++ b/content/assignments/full-stack/USB/what-just-happened.md @@ -2,6 +2,7 @@ title: What Just Happened type: docs prev: assignments/full-stack/usb/client +next: assignments/full-stack/submission weight: 3 --- @@ -11,9 +12,9 @@ You now know how to: - write interrupt driven firmware - use the serial peripheral -- digital communication between two devices +- communicate between two devices over serial - validate data -- create user interfaces with Python +- build user interfaces with Python - interaction between front and back end -- threading +- create thread-safe structures - locks diff --git a/content/assignments/full-stack/_index.md b/content/assignments/full-stack/_index.md index 7da8306..32929e0 100644 --- a/content/assignments/full-stack/_index.md +++ b/content/assignments/full-stack/_index.md @@ -4,8 +4,6 @@ type: docs prev: assignments/spinning-and-blinking/ next: assignments/full-stack/tutorial weight: 5 -sidebar: - exclude: true --- ## Preamble @@ -21,7 +19,7 @@ For us, the full stack is: 1. Communication 1. User Interface -You're half way there! In one fell swoop, we're going to finish off the last two. +You're halfway there! In one fell swoop, we're going to finish off the last two. ## Assignment @@ -32,11 +30,11 @@ You will create a **client** (the GUI running on your computer/phone) which comm **You are once again faced with an important decision** -You have **two*** pathways to choose from: (kind of four pathways) +You have **two** pathways to choose from: {{< cards >}} - {{< card link="usb" title="USB: Python & Arduino or Rust" >}} - {{< card link="ble" title="BLE: Swift & Arduino or Rust" >}} + {{< card link="usb" title="USB: Python & Arduino" >}} + {{< card link="ble" title="BLE: Swift & Rust" >}} {{< /cards >}} {{< callout type="warning" >}} @@ -50,14 +48,12 @@ You have **two*** pathways to choose from: (kind of four pathways) ## Your Options ### USB -The first two options will teach you how to conduct serial communication over USB[^1]. +This option will teach you how to conduct serial communication over USB[^1]. On your computer you will use **Python** to create the client. -You have probably heard of Python before, as it is hugely popular. It's also very easy to learn. - ### BLE -The last two options will teach you how to communicate with BLE[^2]. You will develop an app +This option will teach you how to communicate with BLE[^2]. You will develop an app for your phone using **Swift** and **SwiftUI**. Swift is a programming language made by Apple[^3] primarily for app development. SwiftUI is diff --git a/content/assignments/full-stack/quiz.md b/content/assignments/full-stack/quiz.md deleted file mode 100644 index 6a333b8..0000000 --- a/content/assignments/full-stack/quiz.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Quiz -type: docs -prev: assignments/full-stack/tutorial -weight: 2 ---- - -This assignment is the first to have a quiz! - -You can find the quiz in the root directory of the assignment repo, named `QUIZ.md`. The questions are there, you just need to fill in the answers. - -If you need hints, feel free to talk to the TAs. - -{{< callout type="warning" >}} - You may discuss with your peers, but **do not** copy each other. -{{< /callout >}} diff --git a/content/assignments/full-stack/submission.md b/content/assignments/full-stack/submission.md index 00f41e4..9a57fd3 100644 --- a/content/assignments/full-stack/submission.md +++ b/content/assignments/full-stack/submission.md @@ -4,6 +4,5 @@ type: docs weight: 3 --- -1. Completed code (in `app.py`). -1. Answers in `QUIZ.md`. -1. Video of LED being controlled by the Python GUI named `submission.*` placed in root of this repository. +1. Completed code. +1. Video of LED being controlled named `submission.*` placed in root of this repository.