이전 튜토리얼에서는 Particle Xenon 애플리케이션에 블루투스를 추가하는 방법을 다뤘습니다. 이를 통해 nRF Connect나 Light Blue Explorer 같은 테스트 앱으로 온보드 RGB LED를 제어할 수 있었죠.
이번 글에서는 한 단계 더 나아가, Particle Mesh 보드의 RGB LED를 직접 제어하는 Swift 앱을 개발해 보겠습니다. 순서대로 따라오시면 약 20분 만에 동작하는 앱을 완성할 수 있습니다!
그럼 바로 시작하겠습니다.
준비 작업
- Xcode를 설치합니다. App Store에서 다운로드할 수 있습니다.
- Apple 계정도 필요합니다. 저는 iCloud 이메일을 사용했습니다. 아직 계정이 없다면 Xcode 내에서 새 계정을 만들 수 있습니다.
- Particle Mesh 보드에 RGB 예제 코드를 플래싱합니다.
프로젝트 생성
모든 준비가 끝났다면 이제 본격적으로 재미있는 부분으로 넘어가 볼까요?
Xcode를 열고 File → New Project로 이동합니다.
Single View App을 선택합니다.
그런 다음 Project Name을 원하는 이름으로 변경합니다. 저는 조직 식별자(organization identifier)를 com.jaredwolff로 수정했습니다. 여러분의 상황에 맞게 자유롭게 변경하세요!
프로젝트를 저장할 위치를 선택한 후, Info.plist 파일을 찾습니다.
Info.plist에 Privacy - Bluetooth Peripheral Usage Description 항목을 추가합니다.
저는 다음과 같은 설명을 사용했습니다: App uses Bluetooth to connect to the Particle Xenon RGB Example
이 설정 덕분에 나중에 앱을 출시하더라도 블루투스 기능을 문제없이 사용할 수 있습니다.
자, 이제 최소한으로 동작하는 앱을 만들어 보겠습니다!
최소 기능 구현
이제 연결 및 서비스 검색(service discovery)까지 수행하는 최소 기능의 앱을 만들어 보겠습니다. 대부분의 작업은 ViewController.swift에서 진행됩니다.
먼저 CoreBluetooth를 임포트합니다.
import CoreBluetooth이를 통해 iOS에서 BLE(Bluetooth Low Energy) 기능을 제어할 수 있습니다. 그다음 ViewController 클래스에 CBPeripheralDelegate와 CBCentralManagerDelegate 프로토콜을 모두 추가합니다.
class ViewController: UIViewController, CBPeripheralDelegate, CBCentralManagerDelegate {이제 중앙 관리자(central manager)와 주변 장치(peripheral) 인스턴스를 저장할 로컬 private 변수를 생성합니다. 잠시 후 자세히 설정하겠습니다.
// Properties
private var centralManager: CBCentralManager!
private var peripheral: CBPeripheral!viewDidLoad 함수 안에서 centralManager를 초기화합니다.
centralManager = CBCentralManager(delegate: self, queue: nil)delegate: self 설정이 매우 중요합니다. 이를 누락하면 시작 시 중앙 관리자 상태가 갱신되지 않습니다.
본격적으로 들어가기 전에 ParticlePeripheral.swift라는 새 파일을 하나 만들어 주세요. 위치는 어디든 상관없지만, 저는 추후 관리를 위해 Models라는 별도 그룹에 넣었습니다.
파일 안에는 Particle 보드의 UUID 값을 담은 public 변수들을 만들 것입니다. 이전 튜토리얼을 보셨다면 익숙한 값일 겁니다!
import UIKit
import CoreBluetooth
class ParticlePeripheral: NSObject {
/// MARK: - Particle LED services and characteristics Identifiers
public static let particleLEDServiceUUID = CBUUID.init(string: "b4250400-fb4b-4746-b2b0-93f0e61122c6")
public static let redLEDCharacteristicUUID = CBUUID.init(string: "b4250401-fb4b-4746-b2b0-93f0e61122c6")
public static let greenLEDCharacteristicUUID = CBUUID.init(string: "b4250402-fb4b-4746-b2b0-93f0e61122c6")
public static let blueLEDCharacteristicUUID = CBUUID.init(string: "b4250403-fb4b-4746-b2b0-93f0e61122c6")
}다시 ViewController.swift로 돌아가 블루투스 관련 코드를 조립해 봅시다.
블루투스 핵심 개념
블루투스와 관련된 모든 처리는 이벤트 기반으로 동작합니다. 우리는 이러한 이벤트를 처리하는 여러 함수를 정의하게 됩니다. 핵심적인 함수들은 다음과 같습니다:
centralManagerDidUpdateState는 블루투스 주변 장치가 켜지거나 꺼질 때 호출됩니다. 앱이 처음 실행될 때도 발생하여 현재 블루투스 상태를 알려줍니다. 스캔 시작 지점이기도 합니다.
centralManager의 didDiscover 이벤트는 스캔 결과를 수신했을 때 발생합니다. 이 시점에 연결을 시작합니다.
centralManager의 didConnect 이벤트는 장치가 성공적으로 연결되면 발생합니다. 여기서 서비스 검색(device discovery)을 시작합니다. 참고: 서비스 검색이란 어떤 서비스와 특성(characteristic)이 사용 가능한지 확인하는 과정입니다. 연결된 장치가 어떤 종류인지 판별하는 좋은 방법이기도 합니다.
peripheral의 didDiscoverServices 이벤트는 모든 서비스 검색이 완료되면 발생합니다. 연결된 시점부터는 centralManager가 아니라 peripheral을 사용한다는 점에 유의하세요. 여기서 특성 검색을 시작하며, RGB 서비스 UUID를 타깃으로 삼습니다.
peripheral의 didDiscoverCharacteristicsFor 이벤트는 지정된 서비스 UUID에 해당하는 모든 특성을 전달합니다. 이것이 전체 장치 검색 과정의 마지막 단계입니다. 다소 복잡해 보이지만 연결 단계에서 딱 한 번만 수행하면 됩니다!
블루투스 함수 정의하기
이제 어떤 이벤트 함수들이 트리거되는지 알았으니, 연결 사이클에서 실제로 발생하는 순서대로 논리적으로 정의해 보겠습니다.
먼저 centralManagerDidUpdateState를 정의하여, Particle RGB LED 서비스를 가진 장치 스캔을 시작합니다. 블루투스가 활성화되지 않았다면 아무 작업도 수행하지 않습니다.
// If we're powered on, start scanning
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("Central state update")
if central.state != .poweredOn {
print("Central is not powered on")
} else {
print("Central scanning for", ParticlePeripheral.particleLEDServiceUUID);
centralManager.scanForPeripherals(withServices: [ParticlePeripheral.particleLEDServiceUUID],
options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
}
}다음 단계는 centralManager의 didDiscover 정의입니다. 이 이벤트가 발생했다는 것은 장치를 찾았다는 의미입니다.
// Handles the result of the scan
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// We've found it so stop scan
self.centralManager.stopScan()
// Copy the peripheral instance
self.peripheral = peripheral
self.peripheral.delegate = self
// Connect!
self.centralManager.connect(self.peripheral, options: nil)
}self.centralManager.stopScan()으로 스캔을 중단하고, peripheral을 설정하여 앱 전체에서 해당 인스턴스가 유지되도록 한 뒤, self.centralManager.connect로 장치에 연결합니다.
연결이 완료되면 올바른 장치인지 다시 한번 확인해야 합니다.
// The handler if we do connect succesfully
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
if peripheral == self.peripheral {
print("Connected to your Particle Board")
peripheral.discoverServices([ParticlePeripheral.particleLEDServiceUUID])
}
}두 peripheral 객체를 비교함으로써 앞서 발견한 장치가 맞는지 확인할 수 있습니다. 그런 다음 peripheral.discoverServices로 서비스 검색을 시작합니다. 파라미터로 ParticlePeripheral.particleLEDServiceUUID를 전달하면 불필요한 서비스까지 검색하는 일을 막을 수 있습니다.
서비스 검색이 끝나면 didDiscoverServices 이벤트를 받게 됩니다. "사용 가능한" 서비스 목록을 순회합니다. (실제로는 하나뿐이지만!)
// Handles discovery event
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let services = peripheral.services {
for service in services {
if service.uuid == ParticlePeripheral.particleLEDServiceUUID {
print("LED service found")
//Now kick off discovery of characteristics
peripheral.discoverCharacteristics([ParticlePeripheral.redLEDCharacteristicUUID,
ParticlePeripheral.greenLEDCharacteristicUUID,
ParticlePeripheral.blueLEDCharacteristicUUID], for: service)
return
}
}
}
}이 시점이면 이미 세 번째로 올바른 서비스인지 확인하고 있는 셈입니다. 나중에 서비스와 특성이 많아질 때 큰 도움이 되는 습관입니다.
peripheral.discoverCharacteristics를 호출하며 찾고자 하는 특성들의 UUID 배열을 전달합니다. 모두 ParticlePeripheral.swift에서 정의한 UUID들입니다.
마지막으로 didDiscoverCharacteristicsFor 이벤트를 처리합니다. 사용 가능한 모든 특성을 순회하면서 찾고 있는 값과 비교합니다.
// Handling discovery of characteristics
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let characteristics = service.characteristics {
for characteristic in characteristics {
if characteristic.uuid == ParticlePeripheral.redLEDCharacteristicUUID {
print("Red LED characteristic found")
} else if characteristic.uuid == ParticlePeripheral.greenLEDCharacteristicUUID {
print("Green LED characteristic found")
} else if characteristic.uuid == ParticlePeripheral.blueLEDCharacteristicUUID {
print("Blue LED characteristic found");
}
}
}
}여기까지 완료하면 Particle Mesh 장치에 대한 전체 장치 검색을 수행할 준비가 된 것입니다. 다음 섹션에서 지금까지 작성한 코드가 정상 동작하는지 테스트해 보겠습니다.
최소 예제 테스트하기
시작하기 전에, 문제가 발생하면 하단의 문제 해결 단계를 참고하세요.
테스트하려면 BLE를 지원하는 iPhone이 필요합니다. 요즘 출시되는 대부분의 iPhone이 이를 지원합니다. BLE 미지원 마지막 모델은 아마 iPhone 4 또는 3Gs였던 것으로 기억합니다. (즉, 대부분의 분들은 걱정 없으실 겁니다.)
먼저 iPhone을 컴퓨터에 연결합니다.
상단의 재생/정지 버튼 근처에서 타깃 장치를 선택합니다. 저는 제 폰(Jared's iPhone)을 선택했지만, iPad를 사용해도 됩니다.
Command + R 또는 재생 버튼을 눌러 앱을 폰에 설치합니다.
로그 탭이 열려 있는지 확인하세요. 우측 상단의 하단 패널 버튼을 클릭하면 활성화할 수 있습니다.
메시 장치가 설정되어 예제 코드가 실행 중인지도 확인하세요. 관련 내용은 이전 포스트를 참조하시면 됩니다. 참고로 Particle Mesh 보드는 블루투스 기능을 위해 반드시 Device OS 1.3.0 이상이 실행되고 있어야 합니다!
펌웨어와 앱이 모두 로드되면 로그 출력을 확인합니다.
다음과 비슷하게 출력될 것입니다:
View loaded
Central state update
Central scanning for B4250400-FB4B-4746-B2B0-93F0E61122C6
Connected to your Particle Board
LED service found
Red LED characteristic found
Green LED characteristic found
Blue LED characteristic found이 출력은 폰이 성공적으로 연결되어 LED 서비스를 찾았다는 의미입니다! 특성(characteristic)까지 검색되었다는 점도 중요합니다. 특성 검색 없이는 메시 장치로 데이터를 전송할 수 없습니다.
다음 단계는 슬라이더를 추가하여 RGB 값을 실시간으로 업데이트할 수 있게 만드는 것입니다.
슬라이더 UI 추가하기
이제 Main.storyboard에 몇 가지 UI 요소를 추가하겠습니다. Main.storyboard를 열고 View Controller 아래의 View를 클릭합니다.
그런 다음 Library 버튼을 클릭합니다. (옛날 Apple 홈 버튼 아트처럼 생긴 버튼입니다)
앱에 삽입할 수 있는 모든 요소가 담긴 팝업이 나타납니다.
Label 세 개와 Slider 세 개를 뷰로 드래그합니다.
레이블을 더블클릭하면 원하는 이름으로 변경할 수 있습니다.
클릭한 상태로 유지하면 유용한 정렬 도구가 나타나고, 화면 중앙에 자동으로 스냅되기도 합니다!
모든 요소를 함께 선택해서 움직일 수도 있습니다. 세로와 가로로 정렬해 봅시다.
요소들이 화면 중앙에 고정되도록 autoresizing 속성을 제거합니다. 우측 상단의 Ruler 아이콘을 클릭한 뒤 각각의 빨간 막대(red bars)를 클릭하세요. 이렇게 하면 레이블과 슬라이더가 항상 화면 안에 유지됩니다!
다음으로 Show Assistant Editor 버튼을 클릭합니다. (벤 다이어그램처럼 생긴 버튼입니다)
참고: Assistant Editor에 ViewController.swift가 열려 있는지 꼭 확인하세요.
그런 다음 /properties 섹션 아래에서 Control 키를 누른 채 Red Slider를 드래그하여 코드에 연결합니다.
나머지 슬라이더도 같은 방식으로 반복합니다. 각각 다른 이름으로 지정해야 합니다. 완료하면 코드가 다음과 같아야 합니다:
// Properties
private var centralManager: CBCentralManager!
private var peripheral: CBPeripheral!
// Sliders
@IBOutlet weak var redSlider: UISlider!
@IBOutlet weak var greenSlider: UISlider!
@IBOutlet weak var blueSlider: UISlider!이렇게 하면 코드에서 슬라이더 값에 접근할 수 있게 됩니다.
다음으로 각 슬라이더에 Value Changed 이벤트를 연결합니다. 폴더 뷰에서 Red Slider를 우클릭합니다.
이벤트 옵션이 나타날 겁니다. Value Changed 이벤트를 클릭한 채 코드 영역으로 드래그하세요. 의미를 알 수 있는 이름을 붙이세요. 저는 Red Slider에 RedSliderChanged라고 명명했습니다.
두 번 더 반복합니다. 이 단계가 끝나면 코드는 다음과 같아야 합니다:
@IBAction func RedSliderChanged(_ sender: Any) {
}
@IBAction func GreenSliderChanged(_ sender: Any) {
}
@IBAction func BlueSliderChanged(_ sender: Any) {
}또한 저는 각 슬라이더를 선택하고 Enabled 체크박스를 해제했습니다. 그러면 연결 전에는 슬라이더를 조작할 수 없습니다. 나중에 코드로 활성화할 겁니다.
이참에 maximum value를 255로 변경하고, 기본 value를 0.5에서 0으로 설정하는 것도 좋습니다.
이제 파일 상단으로 돌아가 각 특성(characteristic)을 담을 로컬 변수를 생성합니다. 슬라이더 값을 Particle Mesh 보드에 전송할 때 이 변수들을 사용할 것입니다.
// Characteristics
private var redChar: CBCharacteristic?
private var greenChar: CBCharacteristic?
private var blueChar: CBCharacteristic?자, 이제 모든 것을 하나로 묶어 봅시다!
didDiscoverCharacteristicsFor 콜백 함수에서 각 특성을 할당합니다. 예를 들면 다음과 같습니다:
if characteristic.uuid == ParticlePeripheral.redLEDCharacteristicUUID {
print("Red LED characteristic found")
redChar = characteristic각 특성을 발견할 때마다 같은 위치에서 해당 슬라이더를 활성화할 수도 있습니다.
// Unmask red slider
redSlider.isEnabled = true완성된 didDiscoverCharacteristicsFor 함수는 다음과 같습니다:
// Handling discovery of characteristics
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let characteristics = service.characteristics {
for characteristic in characteristics {
if characteristic.uuid == ParticlePeripheral.redLEDCharacteristicUUID {
print("Red LED characteristic found")
redChar = characteristic
redSlider.isEnabled = true
} else if characteristic.uuid == ParticlePeripheral.greenLEDCharacteristicUUID {
print("Green LED characteristic found")
greenChar = characteristic
greenSlider.isEnabled = true
} else if characteristic.uuid == ParticlePeripheral.blueLEDCharacteristicUUID {
print("Blue LED characteristic found");
blueChar = characteristic
blueSlider.isEnabled = true
}
}
}
}이제 RedSliderChanged, GreenSliderChanged, BlueSliderChanged 함수를 업데이트하겠습니다. 여기서 하려는 작업은 각 Changed 함수에 연결된 특성을 업데이트하는 것입니다. 저는 writeLEDValueToChar라는 별도 함수를 만들었으며, 특성과 데이터를 파라미터로 전달합니다.
private func writeLEDValueToChar( withCharacteristic characteristic: CBCharacteristic, withValue value: Data) {
// Check if it has the write property
if characteristic.properties.contains(.writeWithoutResponse) && peripheral != nil {
peripheral.writeValue(value, for: characteristic, type: .withoutResponse)
}
}이제 각 Changed 함수에 writeLEDValueToChar 호출을 추가합니다. 값을 UInt8로 캐스팅해야 합니다. (Particle Mesh 장치는 부호 없는 8비트 정수를 기대합니다.)
@IBAction func RedSliderChanged(_ sender: Any) {
print("red:",redSlider.value);
let slider:UInt8 = UInt8(redSlider.value)
writeLEDValueToChar( withCharacteristic: redChar!, withValue: Data([slider]))
}GreenSliderChanged와 BlueSliderChanged도 동일하게 작성합니다. 각 함수에서 red를 green과 blue로 변경하는 것을 잊지 마세요!
마지막으로 코드를 깔끔하게 유지하기 위해, 블루투스 연결 해제를 처리하는 함수도 추가했습니다.
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {함수 안에서는 슬라이더 상태를 0으로 리셋하고 비활성화해야 합니다.
if peripheral == self.peripheral {
print("Disconnected")
redSlider.isEnabled = false
greenSlider.isEnabled = false
blueSlider.isEnabled = false
redSlider.value = 0
greenSlider.value = 0
blueSlider.value = 0self.peripheral을 nil로 초기화하는 것도 좋은 습관입니다. 그래야 존재하지 않는 장치에 데이터를 쓰려 시도하는 일이 없습니다.
self.peripheral = nil마지막으로 연결이 끊겼으므로 다시 스캔을 시작합니다!
// Start scanning again
print("Central scanning for", ParticlePeripheral.particleLEDServiceUUID);
centralManager.scanForPeripherals(withServices: [ParticlePeripheral.particleLEDServiceUUID],
options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])
}좋습니다! 이제 거의 테스트 준비가 끝났습니다. 다음(마지막) 단계로 넘어가겠습니다.
슬라이더 테스트하기
어려운 작업은 모두 끝났습니다. 이제 즐길 시간입니다!
가장 쉬운 테스트 방법은 좌측 상단의 재생 버튼을 클릭하거나 Command + R 단축키를 사용하는 것입니다. Xcode가 앱을 폰에 로드합니다. 흰 화면이 잠시 나타난 뒤 슬라이더가 있는 화면이 보일 겁니다!
Particle Mesh 보드에 연결될 때까지 슬라이더는 회색으로 비활성화 상태를 유지합니다. 연결이 수립되었는지는 로그 출력에서 확인할 수 있습니다.
View loaded
Central state update
Central scanning for B4250400-FB4B-4746-B2B0-93F0E61122C6
Connected to your Particle Board
LED service found
Red LED characteristic found
Green LED characteristic found
Blue LED characteristic found(익숙하죠? 연결 성공입니다!)
모든 과정을 잘 따라오셨다면 슬라이더를 움직일 수 있을 겁니다. 더 좋은 건, Particle Mesh 보드 위의 RGB LED 색상이 실시간으로 변한다는 점입니다!
마무리
이번 글에서는 Particle Mesh 보드와 iOS 장치를 블루투스로 연결하는 방법을 배웠습니다. 사용 가능한 각 특성에 연결하는 방법은 물론, 이 모든 것을 깔끔한 UI로 제어하는 인터페이스까지 만들어 보았습니다.
짐작하시겠지만, iOS의 블루투스 세계는 무궁무진합니다. 곧 출간될 가이드 The Ultimate Guide to Particle Mesh에서 더 많은 내용을 다룰 예정입니다. 구독자에게는 사전 공개 콘텐츠와 출간 시 할인 혜택이 제공됩니다. 지금 등록하세요!
소스 코드
전체 소스 코드는 Github에서 확인할 수 있습니다. 도움이 되셨다면 ⭐️ 스타 버튼을 눌러주세요.