Skip to content

Commit 97f1129

Browse files
committed
Updates: Apple Music controls are now working, some more segmenting and tidying of code and comments
1 parent 26b75a4 commit 97f1129

6 files changed

Lines changed: 180 additions & 41 deletions

File tree

Infini-iOS/BLEManager.swift

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,22 @@ let batCBUUID = CBUUID(string: "2A19")
2121
let timeCBUUID = CBUUID(string: "2A2B")
2222
let notifyCBUUID = CBUUID(string: "2A46")
2323
let musicControlCBUUID = CBUUID(string: "00000001-78FC-48FE-8E23-433B3A1942D0")
24+
let musicTrackCBUUID = CBUUID(string: "00000004-78FC-48FE-8E23-433B3A1942D0")
25+
let musicArtistCBUUID = CBUUID(string: "00000003-78FC-48FE-8E23-433B3A1942D0")
2426

2527
class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate {
2628

2729
var myCentral: CBCentralManager!
2830
var notifyCharacteristic: CBCharacteristic!
2931

32+
struct musicCharacteristics {
33+
var control: CBCharacteristic!
34+
var track: CBCharacteristic!
35+
var artist: CBCharacteristic!
36+
}
37+
38+
@Published var musicChars = musicCharacteristics()
39+
3040
// UI flag variables
3141
@Published var isSwitchedOn = false // for now this is used to display if bluetooth is on in the main app screen. maybe an alert in the future?
3242
@Published var isScanning = false // another UI flag. Probably not necessary for anything but debugging. I dunno maybe a little swirly animation or something could be triggered by this
@@ -86,7 +96,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate {
8696

8797
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
8898

89-
var peripheralName: String! // ** not necessary without below scan list thing
99+
var peripheralName: String!
90100

91101
if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
92102
peripheralName = name
@@ -98,23 +108,18 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate {
98108
let newPeripheral = Peripheral(id: peripheralDictionary.count, name: peripheralName, rssi: RSSI.intValue, peripheralHash: peripheral.hash)
99109

100110
// compare peripheral hashes to make sure we're only adding each device once -- super helpful if you have a very noisy BLE advertiser nearby!
111+
// this hash value is functional only for separating devices during this search, and is not at all guaranteed to be a persistent value. Probably not to be trusted for long-term autoconnect persistence. So far, I have gotten the same value for my PineTime every time I run the app, but based on the Apple docs this is not a guarantee.
101112
if !peripherals.contains(where: {$0.peripheralHash == newPeripheral.peripheralHash}) {
102113
// I think there's probably a way to get rid of this array someday, but for now it's useful for displaying the device names. You cant have a Peripheral struct as a key in the peripheralDictionary, so there has to be some way to pass the names to the UI, and the peripherals array seems like it.
103114
peripherals.append(newPeripheral)
104115
peripheralDictionary[newPeripheral.peripheralHash] = peripheral
105116

106117
print(newPeripheral, "added to list")
107-
/*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*
108-
TODO: hey alpha testers! Please send me a message/comment/email/toot/whatever with the peripheralHash of your PineTime! While the hash is valuable for weeding out repeat broadcasters, I'm not sure what exactly is hashed, and if that would be unique between two of the same device. If it's just a hash of "InfiniTime" + $INFINITIMEUUID, then there will be unintended collisions and this won't solve anything for people that have more than one InfiniTime watch to sync.
109-
110-
I'm getting the same value each time I run the app (138271609), let's hope yours is different!
111-
*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*/
112118
}
113119
}
114120

115121
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
116122
self.infiniTime.discoverServices(nil)
117-
sendNotification(notification: "iOS Connected!")
118123
}
119124

120125
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
@@ -148,15 +153,30 @@ extension BLEManager: CBPeripheralDelegate {
148153
peripheral.readValue(for: characteristic)
149154
}
150155

151-
// subscribe to values that can be subscribed to
156+
// subscribe to all values that can be subscribed to.
157+
// TODO: separate this out and subscribe individually for each service in a separate .swift document so this isn't so monolithic
152158
if characteristic.properties.contains(.notify) {
159+
switch characteristic.uuid {
160+
case musicControlCBUUID:
161+
peripheral.setNotifyValue(true, for: characteristic)
162+
print("subscribed to", characteristic.uuid)
163+
case hrmCBUUID:
164+
peripheral.setNotifyValue(true, for: characteristic)
165+
print("subscribed to", characteristic.uuid)
166+
case batCBUUID:
167+
peripheral.setNotifyValue(true, for: characteristic)
168+
print("subscribed to", characteristic.uuid)
169+
default:
170+
break
171+
}
153172
peripheral.setNotifyValue(true, for: characteristic)
154173
}
155174

156175
if characteristic.properties.contains(.write) {
157176
if characteristic.uuid == notifyCBUUID {
158-
// I'm sure there's a less clunky way to grab the full characteristic for the sendNotification() function, but this ad-hoc method works okay and allows it to be published as well.
177+
// I'm sure there's a less clunky way to grab the full characteristic for the sendNotification() function, but this works fine for now
159178
notifyCharacteristic = characteristic
179+
sendNotification(notification: "iOS Connected!")
160180
}
161181
}
162182
}
@@ -165,11 +185,18 @@ extension BLEManager: CBPeripheralDelegate {
165185
switch characteristic.uuid {
166186
case musicControlCBUUID:
167187
// listen for the music controller notifications
188+
musicChars.control = characteristic
168189
let musicControl = [UInt8](characteristic.value!)
169-
let musicNumber = String(musicControl[0])
170-
// for now just print to console, but I am getting the numbers as a string here, so hopefully I can use that to control music apps soon
171-
print(musicNumber) // debug
172-
190+
controlMusic(controlNumber: Int(musicControl[0]))
191+
192+
case musicTrackCBUUID:
193+
// select track characteristic for writing to music app
194+
musicChars.track = characteristic
195+
196+
case musicArtistCBUUID:
197+
// select artist characteristic for writing to music app
198+
musicChars.artist = characteristic
199+
173200
case hrmCBUUID:
174201
// read heart rate hex, convert to decimal
175202
let bpm = heartRate(from: characteristic)
@@ -188,18 +215,14 @@ extension BLEManager: CBPeripheralDelegate {
188215
break
189216
}
190217
}
191-
192-
func sendNotification(notification: String) {
193-
// I'm pretty sure this is due to a lack of understanding on my part of the notification protocol, but sending ascii text as a notification eats the first 3 characters seemingly no matter what they are, so add 3 spaces here to absorb that, then encode the string to ASCII Data
194-
let paddedNotification = " " + notification
195-
let notificationData = paddedNotification.data(using: .ascii)!
196-
197-
// this line prevents crashes when sending a notification before the app has finished establishing the notification write characteristic
198-
if notifyCharacteristic != nil {
199-
infiniTime.writeValue(notificationData, for: notifyCharacteristic, type: .withResponse)
200-
}
218+
219+
// this function converts string to ascii and writes to the selected characteristic. Used for notifications and music app
220+
func writeASCIIToPineTime(message: String, characteristic: CBCharacteristic) {
221+
let writeData = message.data(using: .ascii)!
222+
infiniTime.writeValue(writeData, for: characteristic, type: .withResponse)
201223
}
202224

225+
203226
// function to translate heart rate to decimal, copied straight up from this tut: https://www.raywenderlich.com/231-core-bluetooth-tutorial-for-ios-heart-rate-monitor#toc-anchor-014
204227
private func heartRate(from characteristic: CBCharacteristic) -> Int {
205228
guard let characteristicData = characteristic.value else { return -1 }

Infini-iOS/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<true/>
2323
<key>NSBluetoothAlwaysUsageDescription</key>
2424
<string>This app uses bluetooth to communicate with your PineTime.</string>
25+
<key>kTCCServiceMediaLibrary</key>
26+
<string>This app needs permission to allow your PineTime to control your music.</string>
27+
<key>NSAppleMusicUsageDescription</key>
28+
<string>This app needs permission to allow your PineTime to control your music.</string>
2529
<key>UIApplicationSceneManifest</key>
2630
<dict>
2731
<key>UIApplicationSupportsMultipleScenes</key>

Infini-iOS/Music/BLEMusic.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// BLEMusic.swift
3+
// Infini-iOS
4+
//
5+
// Created by Alex Emry on 8/7/21.
6+
//
7+
8+
import Foundation
9+
10+
extension BLEManager{
11+
func controlMusic(controlNumber: Int) {
12+
13+
let musicController = MusicController()
14+
15+
// when CoreBluetooth gets an update from the music control characteristic, parse that number and take an action, and in any case, make sure the track and artist are relatively up to date
16+
switch controlNumber {
17+
case 0:
18+
if musicController.getPlaybackStatus() == 1 {
19+
musicController.pause()
20+
} else {
21+
musicController.play()
22+
}
23+
case 2:
24+
print("volUp") // system volume controls are not accessible from an app
25+
case 3:
26+
musicController.nextTrack()
27+
case 4:
28+
musicController.prevTrack()
29+
case 5:
30+
print("volDown") // system volume controls are not accessible from an app
31+
default:
32+
break
33+
}
34+
updateMusicInformation(songInfo: musicController.getCurrentSongInfo())
35+
}
36+
func updateMusicInformation(songInfo: MusicController.songInfo) {
37+
let musicController = MusicController()
38+
let songInfo = musicController.getCurrentSongInfo()
39+
40+
writeASCIIToPineTime(message: songInfo.trackName, characteristic: musicChars.track)
41+
writeASCIIToPineTime(message: songInfo.artistName, characteristic: musicChars.artist)
42+
}
43+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//
2+
// MusicController.swift
3+
// Infini-iOS
4+
//
5+
// Created by Alex Emry on 8/7/21.
6+
//
7+
8+
import Foundation
9+
import MediaPlayer
10+
11+
class MusicController: NSObject, ObservableObject{
12+
/*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*
13+
For now, this is a rudimentary implementation of apple's MediaPlayer framework, which unfortunately only works with Apple Music. Apple does not allow control of system volume levels at from the app level, so the volume controls do not work currently. Control of the "Now Playing" media on the device is also not supported at the app level, so we have to specifically work with Apple Music through the existing framework.
14+
15+
In the future, if Apple's proprietary AMS (Apple Media Service) is implemented in InfiniTime, these controls should work on their own, and the track/artist/elapsed time/total time should automatically populate. Not sure how much work that would be to implement, so this may be the best we can do for a while.
16+
17+
TODO: figure out the formatting that PineTime expects for time elapsed/total time. Hex value of 0101 = 12:32, 0102 = 04:48. Writing decimal does nothing. ASCII also gives wacky results
18+
*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*/
19+
20+
var musicPlayer = MPMusicPlayerController.systemMusicPlayer
21+
22+
let volumeView = MPVolumeView()
23+
24+
struct songInfo {
25+
var trackName: String!
26+
var artistName: String!
27+
//var currentDuration: Int
28+
}
29+
30+
override init() {
31+
super.init()
32+
}
33+
34+
func getPlaybackStatus() -> Int {
35+
musicPlayer.playbackState.rawValue
36+
}
37+
38+
func pause() {
39+
musicPlayer.pause()
40+
}
41+
42+
func play() {
43+
musicPlayer.play()
44+
}
45+
46+
func nextTrack() {
47+
musicPlayer.skipToNextItem()
48+
}
49+
50+
func prevTrack() {
51+
musicPlayer.skipToPreviousItem()
52+
}
53+
54+
func getCurrentSongInfo() -> songInfo {
55+
let currentTrack = musicPlayer.nowPlayingItem
56+
let currentSongInfo = songInfo(trackName: currentTrack?.title ?? "Not Playing", artistName: currentTrack?.artist ?? "Not Playing")
57+
return currentSongInfo
58+
}
59+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// Notifications.swift
3+
// Infini-iOS
4+
//
5+
// Created by Alex Emry on 8/8/21.
6+
//
7+
8+
import Foundation
9+
10+
extension BLEManager {
11+
func sendNotification(notification: String) {
12+
// I'm pretty sure this is due to a lack of understanding on my part of the notification protocol, but sending ascii text as a notification eats the first 3 characters seemingly no matter what they are, so add 3 spaces here to absorb that, then encode the string to ASCII Data
13+
let paddedNotification = ("123" + notification) //.data(using: .ascii)!
14+
writeASCIIToPineTime(message: paddedNotification, characteristic: notifyCharacteristic)
15+
}
16+
}

README.md

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,35 @@
33
This is a proof-of-concept, barely-functional iOS application to interact with your PineTime running (at least) InfiniTime 1.3.0 (and maybe more, I haven't tested it against other watches or OSes).
44

55
### What works:
6-
- Scan nearby devices and allow the user to select a device to connect to
7-
- so far this is card coded to be a device named 'InfiniTime' because I don't have anything else to test it against and I don't want to fat-finger the wrong device and have it break
6+
- Scan nearby devices and allow the user to select an InfiniTime device to connect to
87
- Connect to a PineTime running InfiniTime 1.3.0
98
- Set time and date immediately after connection
109
- Read heart rate, and subscribe to HRM's notifier for updated values
1110
- Read battery level, and subscribe to battery level's notifier for updated values
1211
- Display heart rate, battery level, and connection/bluetooth/scanning status to app main page
12+
- Music controls on InfiniTime can control Apple Music. I can't access system-level music controls or system volume from within an app, so the controls literally only work on Apple Music.
1313

1414
### What sort of works but mostly just sucks:
1515
- The UI: It's just a proof of concept so far, so I put as little effort as possible into the UI.
16-
- Notifications: I can send a test notification to the PineTime, but can't send phone notifications to the watch yet.
17-
- Music controls: I have subscribed to the InfiniTime music app notifier, but so far have only implemented printing music control button presses to console. They do print though, so it should be doable if I can access the phone's music controls somehow!
16+
- Notifications: I can send a test notification to the PineTime, but can't send phone notifications to the watch yet. Apple requires the ANCS protocol and bonding to be implemented on the peripheral device, so there's some big hills to climb before notifications are functional.
1817

1918
### What's next:
20-
- Figure out how to send phone notifications to watch
21-
- Figure out how to control music from watch
22-
- Optional auto-connect: save some device-specific characteristic (MAC address? Serial number?) to the app, and allow users to automatically connect to their device. I know I probably won't want to connect to anything other than my own pinetime with very few exceptions, so it'd save me a few taps if it just snagged my watch automatically when I open the app.
23-
- Learn anything whatsoever about making an app design in SwiftUI that isn't a horrific clusterwhoops
24-
- Send notifications to the phone, probably. Might be nice to get a buzz on your phone if the watch disconnects for some reason
25-
- User-configurable settings:
26-
- select device for autoconnect
27-
- enable/disable notifications
28-
- taking suggestions
29-
- Clean everything up. Still tons of commented lines and code blocks from debugging and trial and error stuff that I should really remove. Probably should add a few more explanatory comments here and there too, mostly for my own benefit...
30-
- I mean, there's the navigation thing? I guess poke at that. I'm not super sure that's a priority for me at all, but if that's something that people want I can definitely look into it sooner.
19+
- Learn anything whatsoever about making an app design in SwiftUI that isn't an awful mess
20+
- Send notifications to the phone, probably. Might be nice to get a buzz on your phone if the watch disconnects for some reason or if the watch battery is running low
21+
- User-configurable settings, like enabling or disabling Apple Music controls, notifications, etc
22+
- Clean everything up. Being my first major foray into larger-scale coding projects, I have not done a great job of compartmentalizing my code, so the BLEManager.swift file is pretty monolithic.
23+
- Watch navigation app. This is a lower priority to me personally, but I'll definitely give it a shot eventually. Based on how the music control and notifications have gone, I'm guessing there's another proprietary Apple service that will need to be implemented to make this work.
3124

32-
### How to try it out
25+
### How to try it out:
3326
- Snag this repo and open it in XCode on a Mac
3427
- Plug an iPhone into your computer and select it as the build target in XCode
3528
- Make sure you're signed into your appleID in XCode and that you've done whatever it wants you to do to flag yourself as a code signer
3629
- Change the code signing information in the Infini-iOS properties:
3730
- Click the main project in the files sidebar
3831
- Navigate to the 'Signing and Capabilities' tab
3932
- Change the 'Team' pulldown to reflect your appleID that you used to sign into XCode
33+
- Change the Bundle Identifier to match your team
4034
- Build and run!
4135

42-
### Disclaimers
43-
**This is the first time I've worked with Swift, SwiftUI, XCode, BLE, or anything else in this application. I take no responsibility for what happens if you interact with this repository in any way. If it breaks your phone or your watch or your brain,** ***that's on you buddy!***
36+
### Disclaimer
37+
**This is the first time I've worked with Swift, SwiftUI, XCode, BLE, or anything else in this application. I take no responsibility for what happens if you interact with this repository in any way. If it breaks your phone, your watch, or your brain,** ***that's on you buddy!***

0 commit comments

Comments
 (0)