ในบทเรียนก่อน คุณเรียนวิธีเพิ่ม Bluetooth ให้ Particle Xenon Application เพื่อควบคุม Onboard RGB LED จาก Test App อย่าง nRF Connect หรือ Light Blue Explorer
ในโพสต์นี้ เราจะก้าวต่อไป เราจะพัฒนาแอป Swift ควบคุม Particle Mesh RGB LED หาก一切顺利 คุณควรได้แอปที่ทำงานภายใน 20 นาที!
เริ่มกันเลย
ไม่มีเวลาอ่านบทความเต็มตอนนี้?
ดาวน์โหลด PDF Version ได้ที่นี่
การตั้งค่า (Getting Set Up)
- ติดตั้ง Xcode ดาวน์โหลดจาก App Store ได้ที่นี่
- ต้องมี Apple Login ผมใช้ iCloud Email สร้างบัญชีใหม่ใน Xcode ได้ถ้ายังไม่มี
- ติดตั้ง RGB Example Code บน Particle Mesh Board
สร้างโปรเจกต์
ติดตั้งเสร็จแล้ว มาส่วนสนุก!
เปิด Xcode ไป File → New Project

เลือก Single View App

อัปเดต Project Name ตามใจชอบ ผมเปลี่ยน Organization Identifier เป็น com.jaredwolff แก้ได้ตามต้องการ
เลือก Location บันทึก
หา Info.plist

อัปเดต info.plist เพิ่ม Privacy - Bluetooth Peripheral Usage Description
Description ที่ผมใช้: App uses Bluetooth to connect to the Particle Xenon RGB Example
นี้อนุญาตให้ใช้ Bluetooth ในแอป ถ้าอยาก Release
ตอนนี้ ทำให้ทำงานขั้นพื้นฐานได้!
ทำงานขั้นพื้นฐาน (Minimally Functional)

ต่อไป สร้างแอปขั้นพื้นฐาน Connect และทำ Services Discovery ส่วนใหญ่จะอยู่ใน ViewController.swift
Import CoreBluetooth ก่อน
import CoreBluetooth
นี้อนุญาตควบคุม Bluetooth Low Energy ใน iOS เพิ่ม CBPeripheralDelegate และ CBCentralManagerDelegate ให้ Class ViewController
class ViewController: UIViewController, CBPeripheralDelegate, CBCentralManagerDelegate {
สร้าง Private Variables เก็บ Central Manager และ Peripheral ตั้งค่าเพิ่มเติมทีละครั้ง
// Properties
private var centralManager: CBCentralManager!
private var peripheral: CBPeripheral!
ใน viewDidLoad Init centralManager
centralManager = CBCentralManager(delegate: self, queue: nil)
delegate: self สำคัญมาก ไม่งั้น Central State จะไม่เปลี่ยนตอน Startup
ก่อนไปต่อ สร้างไฟล์แยกชื่อ ParticlePeripheral.swift วางที่ไหนก็ได้ ผมวางใน Group Models ไว้ใช้ภายหลัง
ภายใน สร้าง Public Variables เก็บ UUIDs ของ Particle Board ควรคุ้นเคย!
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 รวม Bluetooth Bits
Bluetooth Bits

ทุกอย่างเกี่ยว Bluetooth เป็น Event-based เราจะ Define ฟังก์ชันจัดการ Events สำคัญคือ:
centralManagerDidUpdateStateอัปเดตตอน Bluetooth Peripheral เปิด/ปิด ยิงตอน App เริ่มต้น ทราบ State ของ Bluetooth เริ่ม Scan ที่นี่centralManager didDiscoverเกิดตอนได้ Scan Results ใช้เริ่ม ConnectioncentralManager didConnectยิงตอน Device Connected เริ่ม Device Discovery ที่นี่ Note: Device Discovery คือวิธีทราบ Services และ Characteristics ที่มี วิธีดียืนยัน Device Type ที่ Connectedperipheral didDiscoverServicesยิงครั้งแรกตอน Services Discovered ทั้งหมด สังเกต Switch จาก CentralManager เป็น Peripheral ตอน Connected เริ่ม Characteristic Discovery ที่นี่ ใช้ RGB Service UUID เป็น Targetperipheral didDiscoverCharacteristicsForให้ Characteristics ทั้งหมดด้วย Service UUID ที่ให้ นี่คือขั้นสุดท้ายของ Full Device Discovery ซับซ้อนแต่อยากทำแค่ครั้งเดียวตอน Connect!
Define ฟังก์ชัน Bluetooth ทั้งหมด
ตอนรู้ Functions/Events ที่ยิง เราจะ Define ตามลำดับ Logic ตอน Connection Cycle
ก่อนอื่น Define centralManagerDidUpdateState เริ่ม Scan หา Device ด้วย Particle RGB LED Service ถ้า Bluetooth ไม่เปิด จะไม่ทำอะไร
// If 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])
}
}
Define centralManager didDiscover ขั้นต่อไป ถ้า Event นี้เกิด = เจอ Device
// Handles scan result
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
// Found it, stop scan
self.centralManager.stopScan()
// Copy peripheral instance
self.peripheral = peripheral
self.peripheral.delegate = self
// Connect!
self.centralManager.connect(self.peripheral, options: nil)
}
Stop Scan ด้วย self.centralManager.stopScan() Set peripheral ให้คงอยู่ตลอด App แล้ว Connect ด้วย self.centralManager.connect
ตอน Connected ต้อง Double Check ว่า Device ถูกต้อง
// Handler ถ้า Connect สำเร็จ
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
if peripheral == self.peripheral {
print("Connected to your Particle Board")
peripheral.discoverServices([ParticlePeripheral.particleLEDServiceUUID])
}
}
เปรียบเทียบ 2 Peripherals จะรู้ว่า Device เดียวกับที่เจอ กด Start Services Discovery ด้วย peripheral.discoverService ใช้ ParticlePeripheral.particleLEDServiceUUID เป็น Parameter เพื่อไม่ดึง Services ที่ไม่สนใจ
ตอน Discover Services เสร็จ จะได้ Event didDiscoverServices Iterate ทุก "Available" Services (แม้จะมีแค่ 1!)
// 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")
// Kick off characteristic discovery
peripheral.discoverCharacteristics([ParticlePeripheral.redLEDCharacteristicUUID, ParticlePeripheral.greenLEDCharacteristicUUID, ParticlePeripheral.blueLEDCharacteristicUUID], for: service)
return
}
}
}
}
นี่ครั้งที่ 3 เช็ค Service ถูกต้อง มีประโยชน์ตอนมี Characteristics และ Services เยอะ
เรียก peripheral.discoverCharacteristics ด้วย Array UUIDs ที่หา มาจาก ParticlePeripheral.swift
สุดท้าย Handle Event didDiscoverCharacteristicsFor Iterate Characteristics ทั้งหมด เปรียบเทียบกับที่หา
// 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")
}
}
}
}
ตอนนี้พร้อม Full Device Discovery ของ Particle Mesh Device ส่วนต่อไป Test ว่าทำงานได้
Test ตัวอย่างขั้นพื้นฐาน

ก่อนเริ่ม ถ้าเจอปัญญา ดู Troubleshooting ใน Footnotes
ต้องมี iPhone ที่รองรับ Bluetooth Low Energy iPhone สมัยใหม่เกือบทุกเครื่องมี iPhone ตัวสุดท้ายที่ไม่มีคือ iPhone 4 หรือ 3Gs (คุณน่าจะโอเค)
เสียบ iPhone เข้า Computer
ไปด้านบนปุ่ม Play/Stop เลือก Target Device กรณีผมเลือกโทรศัพท์ (Jared's iPhone) ใช้ iPad ก็ได้

กด Command + R หรือปุ่ม Play โหลดแอปลงโทรศัพท์
เปิด Log Tab คลิกปุ่ม Bottom Pane มุมขวาบน

ต้องมี Mesh Device รัน Example Code ไปที่โพสต์นี้เพื่อดึง Firmware จำ: Particle Mesh Board ต้องรัน Device OS 1.3.0 ขึ้นไปสำห Bluetooth!
โหลด Firmware และ App เสร็จ เช็ค Log Output
ควรเป็นแบบนี้:
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
หมายถึง Phone Connected, เจอ LED Service Characteristics ก็ถูก Discover สำคัญมาก เพราะไม่มีเหล่านี้ ไม่ส่ง Data ไป Mesh Device ได้
ขั้นตอนต่อไป สร้าง Sliders อัปเดต RGB Values แบบ Real-time
Slide to the Left, Slide to the Right
เพิ่ม Elements ใน Main.storyboard เปิด Main.storyboard คลิก View ใต้ View Controller

คลิกปุ่ม Library (Icon เหมือน Home Button เก่า)

ได้ Popup ตัวเลือก Insert ได้ทั้งหมด

Drag 3 Labels และ 3 Sliders ลง View

Double Click Labels เปลี่ยนชื่อ

Click และ Hold จะมี Alignment Tools Pop Up Snap to Center!

Select ทั้งหมด Move พร้อมกัน Align Vertically และ Horizontally
ให้อยู่กลาง ลบ Autoresizing Property คลิก Ruler Icon มุมขวาบน คลิก Red Bars ทั้งหมด ให้ Labels และ Sliders อยู่บนจอ!

คลิก Show Assistant Editor (Icon เหมือน Venn Diagram)

Note: ต้องแน่ใจว่า ViewController.swift เปิดอยู่ใน Assistant Editor

Control-Click และ Drag Red Slider ลง Code ใต้ Section /properties

ทำซ้ำ Sliders อื่นๆ ตั้งชื่อต่างกัน Code ควรเป็นแบบนี้:
// 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!
เข้าถึงค่า Sliders ได้แล้ว
ต่อไป Attach Event Value Changed ให้แต่ละ Slider Right Click Red Slider ใน Folder View

ได้ Options Events Click Drag Value Changed ลง Code ตั้งชื่อเข้าใจง่าย ผมใช้ RedSliderChanged สำห Red Slider
ทำซ้ำอีก 2 ครั้ง Code ตอนจบขั้นตอน:
@IBAction func RedSliderChanged(_ sender: Any) { }
@IBAction func GreenSliderChanged(_ sender: Any) { }
@IBAction func BlueSliderChanged(_ sender: Any) { }
Uncheck Enabled ทุก Slider ไม่ให้เลื่อนได้ จะ Enable ใน Code ภายหลัง

เปลี่ยน Maximum Value เป็น 255 และ Default Value จาก 0.5 เป็น 0

บนสุดไฟล์ สร้าง Local Variables สำห Characteristics แต่ละตัว ใช้เขียนค่า Slider ไป Particle Mesh Board
// Characteristics
private var redChar: CBCharacteristic?
private var greenChar: CBCharacteristic?
private var blueChar: CBCharacteristic?
รวมทั้งหมดเข้าด้วยกัน!
ใน Callback didDiscoverCharacteristicsFor Assign Characteristics เช่น
if characteristic.uuid == ParticlePeripheral.redLEDCharacteristicUUID {
print("Red LED characteristic found")
redChar = characteristic
}
ตอนเจอแต่ละ Characteristic Enable Slider ในจุดเดียวกัน
// 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 ต้องการ Update Characteristic ที่เกี่ยวข้อง สร้าง Function แยก writeLEDValueToChar ส่ง Characteristic และ Data
private func writeLEDValueToChar(withCharacteristic characteristic: CBCharacteristic, withValue value: Data) {
// Check write property
if characteristic.properties.contains(.writeWithoutResponse) && peripheral != nil {
peripheral.writeValue(value, for: characteristic, type: .withoutResponse)
}
}
เพิ่ม Call writeLEDValueToChar ในแต่ละ Changed Function Cast Value เป็น UInt8 (Particle Mesh คาดหวัง Unsigned 8-bit)
@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 ให้ถูกต้อง!
สุดท้าย เพิ่ม Function จัดการ Bluetooth Disconnects
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
ภายใน Reset Slider State เป็น 0 และ Disable
if peripheral == self.peripheral {
print("Disconnected")
redSlider.isEnabled = false
greenSlider.isEnabled = false
blueSlider.isEnabled = false
redSlider.value = 0
greenSlider.value = 0
blueSlider.value = 0
}
Reset self.peripheral เป็น nil ดี ไม่งั้นอาจเขียนไป Phantom Device
self.peripheral = nil
สุดท้าย เนื่องจาก Disconnected เริ่ม Scan ใหม่!
// Start scanning again
print("Central scanning for", ParticlePeripheral.particleLEDServiceUUID)
centralManager.scanForPeripherals(withServices: [ParticlePeripheral.particleLEDServiceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
}
เกือบพร้อม Test ไปขั้นสุดท้าย!
Test Sliders

Hard Work เสร็จแล้ว มาเล่น!
วิธี Test ง่ายสุด: กด Play Button มุมซ้ายบน หรือ Command + R Xcode โหลดแอปลงโทรศัพท์ ควรเห็นหน้าจอขาว แล้วหน้าจอ Sliders
Sliders ควรเป็นสีเทา (Disabled) จนกว่าจะ Connected Particle Mesh Board เช็ค Log Output ว่า Connected แล้ว
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
(คุ้นเคยไหม? Connected แล้ว!)
ถ้าทำตามทุกขั้นตอน ควรเลื่อน Sliders ได้ และ RGB LED บน Particle Mesh Board ควรเปลี่ยนสี

สรุป
ในบทความนี้เรียนรู้เชื่อม Particle Mesh Board และ iOS ผ่าน Bluetooth เรียนรู้เชื่อม Characteristics แต่ละตัว และทำ Clean Interface ควบคุมทั้งหมด
จินตนาการได้ว่า Bluetooth บน iOS ลึกกว่านี้ มาอีกใน Guide: The Ultimate Guide to Particle Mesh Subscriber ได้ Pre-launch Content และ Discount คลิกสมัครที่นี่
Code
Full Source Code บน GitHub หากมีประโยชน์ กด Star ⭐️