├── .bundle
└── config
├── .gitignore
├── bluetility_screenshot.png
├── fastlane
├── Pluginfile
├── Appfile
├── Gymfile
└── Fastfile
├── Bluetility
├── Assets.xcassets
│ ├── Contents.json
│ ├── Logs.imageset
│ │ ├── logs.pdf
│ │ └── Contents.json
│ ├── Refresh.imageset
│ │ ├── refresh.pdf
│ │ └── Contents.json
│ ├── SortBySignal.imageset
│ │ ├── low21.pdf
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ ├── bluetooth128.png
│ │ ├── bluetooth16.png
│ │ ├── bluetooth256.png
│ │ ├── bluetooth32.png
│ │ ├── bluetooth512.png
│ │ ├── bluetooth64.png
│ │ ├── bluetooth1024.png
│ │ ├── bluetooth256-1.png
│ │ ├── bluetooth32-1.png
│ │ ├── bluetooth512-1.png
│ │ └── Contents.json
├── Credits.html
├── NSTextField+Shake.swift
├── AppDelegate.swift
├── Info.plist
├── Swift+Extensions.swift
├── LogViewController.swift
├── BluetilityWindowController.swift
├── PasteboardBrowser.swift
├── Scanner.swift
├── BluetilityLogHandler.swift
├── Device.swift
├── ViewController.swift
└── Base.lproj
│ └── Main.storyboard
├── Gemfile
├── Bluetility.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcshareddata
│ └── xcschemes
│ │ ├── Bluetility Release.xcscheme
│ │ └── Bluetility Debug.xcscheme
└── project.pbxproj
├── Config
├── Release.xcconfig
├── Debug.xcconfig
├── Shared.xcconfig
└── Warnings.xcconfig
├── LICENSE.md
├── README.md
└── Gemfile.lock
/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_PATH: "vendor/bundle"
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata
2 | fastlane/report.xml
3 | build
4 | vendor
5 | fastlane/README.md
6 |
--------------------------------------------------------------------------------
/bluetility_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/bluetility_screenshot.png
--------------------------------------------------------------------------------
/fastlane/Pluginfile:
--------------------------------------------------------------------------------
1 | # Autogenerated by fastlane
2 | #
3 | # Ensure this file is checked in to source control!
4 |
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/Logs.imageset/logs.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/Logs.imageset/logs.pdf
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/Refresh.imageset/refresh.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/Refresh.imageset/refresh.pdf
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/SortBySignal.imageset/low21.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/SortBySignal.imageset/low21.pdf
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth128.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth16.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth256.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth32.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth512.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth64.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth1024.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth256-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth256-1.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth32-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth32-1.png
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth512-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jnross/Bluetility/HEAD/Bluetility/Assets.xcassets/AppIcon.appiconset/bluetooth512-1.png
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
6 | eval_gemfile(plugins_path) if File.exist?(plugins_path)
7 |
--------------------------------------------------------------------------------
/Bluetility.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/Logs.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "logs.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/Refresh.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "refresh.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/SortBySignal.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "low21.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/Bluetility.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Bluetility/Credits.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | app_identifier("com.rossible.Bluetility") # The bundle identifier of your app
2 | apple_id("jnross@gmail.com") # Your Apple email address
3 |
4 |
5 | # For more information about the Appfile, see:
6 | # https://docs.fastlane.tools/advanced/#appfile
7 |
8 | team_id "PTE4TYXPW2" # Developer Portal Team ID
9 | itc_team_id "PTE4TYXPW2" # Developer Portal Team ID
10 |
--------------------------------------------------------------------------------
/fastlane/Gymfile:
--------------------------------------------------------------------------------
1 | # For more information about this configuration visit
2 | # https://docs.fastlane.tools/actions/gym/#gymfile
3 |
4 | # In general, you can use the options available
5 | # fastlane gym --help
6 |
7 | # Remove the # in front of the line to enable the option
8 |
9 | # scheme("")
10 |
11 | sdk("macosx")
12 |
13 | clean(true)
14 |
15 | output_directory("./build")
16 |
--------------------------------------------------------------------------------
/Bluetility.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-log",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-log.git",
7 | "state" : {
8 | "revision" : "32e8d724467f8fe623624570367e3d50c5638e46",
9 | "version" : "1.5.2"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Config/Release.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // Release.xcconfig
3 | //
4 | // Settings for Release build
5 | //
6 |
7 | #include "Shared.xcconfig"
8 |
9 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym
10 | ENABLE_NS_ASSERTIONS = NO
11 | MTL_ENABLE_DEBUG_INFO = NO
12 | SWIFT_COMPILATION_MODE = wholemodule
13 |
14 | CODE_SIGN_IDENTITY = Developer ID Application
15 | CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO
16 | DEVELOPMENT_TEAM = PTE4TYXPW2
17 | OTHER_CODE_SIGN_FLAGS = --timestamp
18 |
--------------------------------------------------------------------------------
/Config/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // Debug.xcconfig
3 | //
4 | // Settings for Debug build
5 | //
6 |
7 | #include "Shared.xcconfig"
8 |
9 | DEBUG_INFORMATION_FORMAT = dwarf
10 | ENABLE_TESTABILITY = YES
11 | GCC_DYNAMIC_NO_PIC = NO
12 | GCC_OPTIMIZATION_LEVEL = 0
13 | GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
14 | MTL_ENABLE_DEBUG_INFO = YES
15 | ONLY_ACTIVE_ARCH = YES
16 | SWIFT_OPTIMIZATION_LEVEL = -Onone
17 |
18 | DEVELOPMENT_TEAM =
19 | CODE_SIGN_IDENTITY = -
20 |
--------------------------------------------------------------------------------
/Config/Shared.xcconfig:
--------------------------------------------------------------------------------
1 |
2 | #include "Warnings.xcconfig"
3 |
4 | // Project
5 | MACOSX_DEPLOYMENT_TARGET = 10.13
6 | SDKROOT = macosx
7 | SWIFT_VERSION = 5.0
8 | VERSIONING_SYSTEM = apple-generic
9 | COPY_PHASE_STRIP = NO
10 |
11 | // Target
12 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
13 | CODE_SIGN_STYLE = Manual
14 | COMBINE_HIDPI_IMAGES = YES
15 | ENABLE_HARDENED_RUNTIME = YES
16 | INFOPLIST_FILE = Bluetility/Info.plist
17 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks
18 | PRODUCT_BUNDLE_IDENTIFIER = com.rossible.Bluetility
19 | PRODUCT_NAME = $(TARGET_NAME)
20 |
21 | // Version
22 | CURRENT_PROJECT_VERSION = 9
23 | MARKETING_VERSION = 1.5.1
24 |
--------------------------------------------------------------------------------
/Bluetility/NSTextField+Shake.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSTextField+Shake.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 9/28/19.
6 | // Copyright © 2019 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | extension NSTextField {
12 | func shake() {
13 | let animation = CABasicAnimation(keyPath: "position")
14 | animation.duration = 0.05
15 | animation.repeatCount = 5
16 | animation.autoreverses = true
17 | animation.fromValue = CGPoint(x: self.frame.origin.x - 4.0, y: self.frame.origin.y)
18 | animation.toValue = CGPoint(x: self.frame.origin.x + 4.0, y: self.frame.origin.y)
19 | layer?.add(animation, forKey: "position")
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Bluetility/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 11/9/15.
6 | // Copyright © 2015 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Logging
11 |
12 | @NSApplicationMain
13 | class AppDelegate: NSObject, NSApplicationDelegate {
14 |
15 |
16 |
17 | func applicationDidFinishLaunching(_ aNotification: Notification) {
18 | // Insert code here to initialize your application
19 | let defaults = ["NSToolTipAutoWrappingDisabled" : NSNumber(value: true)]
20 | UserDefaults.standard.register(defaults: defaults)
21 | }
22 |
23 | func applicationWillTerminate(_ aNotification: Notification) {
24 | // Insert code here to tear down your application
25 | }
26 |
27 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
28 | return true
29 | }
30 |
31 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
32 | true
33 | }
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015-2016 Joseph Ross
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:mac)
17 |
18 | platform :mac do
19 | desc "Increments the build number, builds, and notarizes the Release configuration of the app."
20 | lane :build_release do
21 | ensure_git_status_clean
22 | gym(
23 | scheme: "Bluetility Release",
24 | skip_package_pkg: true,
25 | )
26 | app_path = "build/Bluetility.app"
27 | notarize(
28 | package: app_path,
29 | try_early_stapling: true,
30 | verbose: true,
31 | api_key_path: ENV['API_KEY_PATH'],
32 | )
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/Bluetility/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | NSHumanReadableCopyright
28 | Copyright © 2015 Joseph Ross. All rights reserved.
29 | NSMainStoryboardFile
30 | Main
31 | NSPrincipalClass
32 | NSApplication
33 | NSBluetoothAlwaysUsageDescription
34 | Please allow Bluetooth access to connect to devices.
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Bluetility/Swift+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Swift+Extensions.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 7/1/20.
6 | // Copyright © 2020 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // MARK: Data
12 |
13 | extension Data {
14 | var hexString: String {
15 | var hex:String = ""
16 | for byte in self {
17 | hex += String(format: "%02X", byte)
18 | }
19 | return hex
20 | }
21 | }
22 |
23 | extension Array {
24 | var hexString: String {
25 | var hex:String = ""
26 | for byte in self {
27 | hex += String(format: "%02X", byte)
28 | }
29 | return hex
30 | }
31 | }
32 |
33 | // MARK: Dictionary
34 |
35 | func += (left: inout Dictionary, right: Dictionary) {
36 | for (k, v) in right {
37 | left.updateValue(v, forKey: k)
38 | }
39 | }
40 |
41 | infix operator ??? : NilCoalescingPrecedence
42 |
43 | func ??? (left: Any?, right: String) -> String {
44 | if let left = left {
45 | return String(describing: left)
46 | } else {
47 | return right
48 | }
49 | }
50 |
51 | extension Collection {
52 | /// Returns the element at the specified index if it is within bounds, otherwise nil.
53 | subscript (safe index: Index) -> Element? {
54 | return indices.contains(index) ? self[index] : nil
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Config/Warnings.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // Warnings.xcconfig
3 | //
4 | // Contains common warning and error settings for the whole project
5 | //
6 |
7 | ALWAYS_SEARCH_USER_PATHS = NO
8 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES
9 | CLANG_CXX_LANGUAGE_STANDARD = gnu++0x
10 | CLANG_CXX_LIBRARY = libc++
11 | CLANG_ENABLE_MODULES = YES
12 | CLANG_ENABLE_OBJC_ARC = YES
13 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
14 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES
15 | CLANG_WARN_BOOL_CONVERSION = YES
16 | CLANG_WARN_COMMA = YES
17 | CLANG_WARN_CONSTANT_CONVERSION = YES
18 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES
19 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR
20 | CLANG_WARN_EMPTY_BODY = YES
21 | CLANG_WARN_ENUM_CONVERSION = YES
22 | CLANG_WARN_INFINITE_RECURSION = YES
23 | CLANG_WARN_INT_CONVERSION = YES
24 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES
25 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
26 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES
27 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR
28 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES
29 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES
30 | CLANG_WARN_STRICT_PROTOTYPES = YES
31 | CLANG_WARN_SUSPICIOUS_MOVE = YES
32 | CLANG_WARN_UNREACHABLE_CODE = YES
33 | ENABLE_STRICT_OBJC_MSGSEND = YES
34 | GCC_C_LANGUAGE_STANDARD = gnu99
35 | GCC_NO_COMMON_BLOCKS = YES
36 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES
37 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR
38 | GCC_WARN_UNDECLARED_SELECTOR = YES
39 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE
40 | GCC_WARN_UNUSED_FUNCTION = YES
41 | GCC_WARN_UNUSED_VARIABLE = YES
42 |
--------------------------------------------------------------------------------
/Bluetility/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "bluetooth16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "bluetooth32-1.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "bluetooth32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "bluetooth64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "bluetooth128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "bluetooth256-1.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "bluetooth256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "bluetooth512-1.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "bluetooth512.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "bluetooth1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bluetility
2 |
3 | Bluetility is a general-purpose Bluetooth Low-Energy utility for Mac OS X. It scans for advertising peripherals, provides a interface to browse a connected peripheral's services and characteristics, and allows characteristic values to be read, written, and subscribed.
4 |
5 |
6 | ## Installation
7 |
8 | ### Manual
9 | 1. Download the latest release: https://github.com/jnross/Bluetility/releases/latest/download/Bluetility.app.zip
10 | 2. Extract the downloaded archive.
11 | 3. Move Bluetility.app into your /Applications folder. Or don't!
12 | 4. Open Bluetility.app.
13 |
14 | ### Using Homebrew
15 | ```
16 | brew install --cask bluetility
17 | ```
18 |
19 | ## Features
20 |
21 | * Scan for nearby advertising peripherals
22 | * Sort peripherals by received signal strength
23 | * View advertising data via tooltip on Devices list
24 | * Browse services and characteristics of connected peripheral
25 | * Subscribe to characteristic notifications
26 | * Read/Write characteristic values
27 | * View log of characteristic read/writes, logs may be saved as CSV
28 |
29 | ## Motivation
30 | Bluetility is inspired by [LightBlue](https://itunes.apple.com/us/app/lightblue/id639944780?mt=12), a free bluetooth utility published by [Punch Through Design](https://punchthrough.com/). Bluetility was created to resolve issues in this tool, and add missing features:
31 |
32 | * Support copy/paste via Cmd+C and Cmd+V
33 | * Sort peripherals by received signal strength
34 | * View advertising data
35 | * Automatically reconnect to disconnected peripherals when needed
36 |
37 | Bluetility is published as open-source so that anyone can tweak or improve its functionality to meet their own needs.
38 |
39 | ## Release Build
40 |
41 | 1. Set notarization password as environment variable `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`
42 | 2. Execute command ```bundle exec fastlane mac build_release```
43 |
--------------------------------------------------------------------------------
/Bluetility/LogViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogViewController.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 12/29/15.
6 | // Copyright © 2015 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import CoreBluetooth
11 |
12 | class LogViewController: NSViewController, LogRecorderDelegate {
13 |
14 | var recorder: LogRecorder? = nil {
15 | didSet {
16 | recorder?.delegate = self
17 | logText.string = recorder?.lines.joined(separator: "\n") ?? ""
18 | }
19 | }
20 | @IBOutlet var logText:NSTextView! = nil
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 | // Do view setup here.
25 | }
26 |
27 | func appendLogText(_ message:String) {
28 | logText.string.append(message + "\n")
29 | logText.scrollToEndOfDocument(self)
30 | }
31 |
32 | @IBAction func clearLogPressed(_ sender:NSButton) {
33 | recorder?.reset()
34 | }
35 |
36 | @IBAction func saveCSVPressed(_ sender:NSButton) {
37 | let savePanel = NSSavePanel()
38 | savePanel.allowedFileTypes = ["txt"]
39 | savePanel.beginSheetModal(for: self.view.window!) { (result) -> Void in
40 | if result == NSApplication.ModalResponse.OK {
41 | savePanel.orderOut(self)
42 | if let selectedUrl = savePanel.url {
43 | let contents:String = self.logText.string
44 | do {
45 | try contents.write(to: selectedUrl, atomically: true, encoding: String.Encoding.utf8)
46 | } catch {
47 | //TODO: Display error to user
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
54 |
55 | func recorder(_ recorder: LogRecorder, appendedLine: String) {
56 | appendLogText(appendedLine)
57 | }
58 |
59 | func recorderDidReset(_ recorder: LogRecorder) {
60 | logText.string = ""
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Bluetility/BluetilityWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BluetilityWindowController.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 11/9/15.
6 | // Copyright © 2015 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import Logging
11 |
12 | class BluetilityWindowController: NSWindowController {
13 |
14 | let recorder: LogRecorder = LogRecorder()
15 | var viewController:ViewController?
16 | @IBOutlet var refreshItem: NSToolbarItem!
17 | @IBOutlet var sortItem: NSToolbarItem!
18 | @IBOutlet var logItem: NSToolbarItem!
19 | @IBOutlet var searchItemField: NSSearchField!
20 |
21 | required init?(coder: NSCoder) {
22 | super.init(coder: coder)
23 | LoggingSystem.bootstrap { label in
24 | return BluetilityLogHandler(label: label, recorder: self.recorder)
25 | }
26 | }
27 |
28 | override func windowDidLoad() {
29 | super.windowDidLoad()
30 | self.shouldCascadeWindows = false
31 | window?.setFrameAutosaveName("bluetility")
32 |
33 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
34 | viewController = self.contentViewController as? ViewController
35 | viewController?.recorder = recorder
36 | logItem.target = viewController
37 | sortItem.target = viewController
38 | refreshItem.target = viewController
39 | logItem.action = #selector(ViewController.logPressed(_:))
40 | sortItem.action = #selector(ViewController.sortPressed(_:))
41 | refreshItem.action = #selector(ViewController.refreshPressed(_:))
42 | searchItemField.delegate = self
43 | }
44 | }
45 |
46 | extension BluetilityWindowController: NSSearchFieldDelegate {
47 | func controlTextDidChange(_ obj: Notification) {
48 | guard let searchField = obj.object as? NSSearchField, searchField == searchItemField else {
49 | return
50 | }
51 |
52 | viewController?.searchTextDidChange(value: searchField.stringValue)
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/Bluetility/PasteboardBrowser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasteboardBrowser.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 12/16/15.
6 | // Copyright © 2015 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | protocol PasteboardBrowserDelegate {
12 | func browser(_ browser: PasteboardBrowser, pasteboardStringFor indexPath: IndexPath) -> String?
13 | func browser(_ browser: PasteboardBrowser, menuFor cell: NSBrowserCell, atRow row: Int, column: Int) -> NSMenu?
14 | }
15 |
16 | class PasteboardBrowser: NSBrowser {
17 |
18 | @IBAction
19 | func copy(_ sender:AnyObject) {
20 | var string:String? = nil
21 | if let pasteboardDelegate = self.delegate as? PasteboardBrowserDelegate,
22 | let indexPath = self.selectionIndexPath
23 | {
24 | string = pasteboardDelegate.browser(self, pasteboardStringFor: indexPath)
25 | }
26 | if string == nil {
27 | string = (self.selectedCell() as? NSBrowserCell)?.title
28 | }
29 | guard let pasteString = string else {return}
30 | let pb = NSPasteboard.general
31 | pb.declareTypes([.string], owner: self)
32 | pb.setString(pasteString, forType: .string)
33 | }
34 |
35 | override func reloadData(forRowIndexes rowIndexes: IndexSet, inColumn column: Int) {
36 | for rowIndex in rowIndexes {
37 | guard let cell = loadedCell(atRow: rowIndex, column: column) else { continue }
38 | delegate?.browser?(self, willDisplayCell: cell, atRow: rowIndex, column: column)
39 | }
40 | }
41 |
42 | override func menu(for event: NSEvent) -> NSMenu? {
43 | let windowPoint = event.locationInWindow
44 | let viewPoint = self.convert(windowPoint, from: window?.contentView)
45 | var row: Int = 0
46 | var column: Int = 0
47 | guard getRow(&row, column: &column, for: viewPoint) else { return nil }
48 | selectRow(row, inColumn: column)
49 | self.sendAction()
50 |
51 | guard
52 | let cell = selectedCell() as? NSBrowserCell,
53 | let delegate = delegate as? PasteboardBrowserDelegate
54 | else {
55 | return nil
56 | }
57 | return delegate.browser(self, menuFor: cell, atRow: row, column: column)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Bluetility/Scanner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Scanner.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 11/9/15.
6 | // Copyright © 2015 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import CoreBluetooth
10 |
11 | protocol ScannerDelegate: AnyObject {
12 | func scanner(_ scanner: Scanner, didUpdateDevices: [Device])
13 | }
14 |
15 | class Scanner: NSObject {
16 |
17 | var central:CBCentralManager
18 | weak var delegate:ScannerDelegate? = nil
19 | var devices:[Device] = []
20 | var started:Bool = false
21 |
22 | override init() {
23 | central = CBCentralManager(delegate: nil, queue: nil)
24 | super.init()
25 | central.delegate = self
26 | }
27 |
28 | func start() {
29 | started = true
30 | devices = []
31 | startIfReady()
32 | }
33 |
34 | private func startIfReady() {
35 | if central.state == .poweredOn && started {
36 | central.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:false])
37 | }
38 | }
39 |
40 | func stop() {
41 | started = false
42 | central.stopScan()
43 | }
44 |
45 | func restart() {
46 | stop()
47 | start()
48 | }
49 |
50 | }
51 |
52 | extension Scanner : CBCentralManagerDelegate {
53 |
54 | func centralManagerDidUpdateState(_ central: CBCentralManager) {
55 | startIfReady()
56 | }
57 |
58 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
59 | guard let existingDevice = devices.first(where: { $0.peripheral == peripheral } ) else {
60 | let newDevice = Device(scanner: self, peripheral: peripheral, advertisingData: advertisementData, rssi: RSSI.intValue)
61 | devices.append(newDevice)
62 | return
63 | }
64 |
65 | existingDevice.rssi = RSSI.intValue
66 | existingDevice.updateAdvertisingData(advertisementData)
67 |
68 | delegate?.scanner(self, didUpdateDevices: devices)
69 | }
70 |
71 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
72 | guard let device = devices.first(where:{ $0.peripheral == peripheral }) else { return }
73 | device.peripheralDidConnect()
74 | }
75 |
76 | func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
77 | guard let device = devices.first(where:{ $0.peripheral == peripheral }) else { return }
78 | device.peripheralDidDisconnect(error: error)
79 | }
80 | }
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/Bluetility.xcodeproj/xcshareddata/xcschemes/Bluetility Release.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/Bluetility.xcodeproj/xcshareddata/xcschemes/Bluetility Debug.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Bluetility/BluetilityLogHandler.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Logging
3 | import struct Logging.Logger
4 | import os
5 |
6 | public struct BluetilityLogHandler: LogHandler {
7 | public var logLevel: Logger.Level = .debug
8 | public var recordedLogLevel: Logger.Level = .info
9 | public let label: String
10 | private let oslogger: OSLog
11 | private let recorder: LogRecorder?
12 |
13 | public init(label: String) {
14 | self.label = label
15 | self.oslogger = OSLog(subsystem: label, category: "")
16 | self.recorder = nil
17 | }
18 |
19 | public init(label: String, recorder: LogRecorder) {
20 | self.label = label
21 | self.oslogger = OSLog(subsystem: label, category: "")
22 | self.recorder = recorder
23 | }
24 |
25 | public init(label: String, log: OSLog, recorder: LogRecorder) {
26 | self.label = label
27 | self.oslogger = log
28 | self.recorder = recorder
29 | }
30 |
31 | public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) {
32 | var combinedPrettyMetadata = self.prettyMetadata
33 | if let metadataOverride = metadata, !metadataOverride.isEmpty {
34 | combinedPrettyMetadata = self.prettify(
35 | self.metadata.merging(metadataOverride) {
36 | return $1
37 | }
38 | )
39 | }
40 |
41 | var formedMessage = message.description
42 | if combinedPrettyMetadata != nil {
43 | formedMessage += " -- " + combinedPrettyMetadata!
44 | }
45 | os_log("%{public}@", log: self.oslogger, type: OSLogType.from(loggerLevel: level), formedMessage as NSString)
46 |
47 | if level >= recordedLogLevel {
48 | recorder?.append(message.description)
49 | }
50 | }
51 |
52 | private var prettyMetadata: String?
53 | public var metadata = Logger.Metadata() {
54 | didSet {
55 | self.prettyMetadata = self.prettify(self.metadata)
56 | }
57 | }
58 |
59 | /// Add, remove, or change the logging metadata.
60 | /// - parameters:
61 | /// - metadataKey: the key for the metadata item.
62 | public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
63 | get {
64 | return self.metadata[metadataKey]
65 | }
66 | set {
67 | self.metadata[metadataKey] = newValue
68 | }
69 | }
70 |
71 | private func prettify(_ metadata: Logger.Metadata) -> String? {
72 | if metadata.isEmpty {
73 | return nil
74 | }
75 | return metadata.map {
76 | "\($0)=\($1)"
77 | }.joined(separator: " ")
78 | }
79 | }
80 |
81 | extension OSLogType {
82 | static func from(loggerLevel: Logger.Level) -> Self {
83 | switch loggerLevel {
84 | case .trace:
85 | /// `OSLog` doesn't have `trace`, so use `debug`
86 | return .debug
87 | case .debug:
88 | return .debug
89 | case .info:
90 | return .info
91 | case .notice:
92 | // https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code
93 | // According to the documentation, `default` is `notice`.
94 | return .default
95 | case .warning:
96 | /// `OSLog` doesn't have `warning`, so use `info`
97 | return .info
98 | case .error:
99 | return .error
100 | case .critical:
101 | return .fault
102 | }
103 | }
104 | }
105 |
106 | public class LogRecorder {
107 | var lines: [String] = []
108 |
109 | var delegate: LogRecorderDelegate? = nil
110 |
111 | func append(_ line: String) {
112 | lines.append(line)
113 | delegate?.recorder(self, appendedLine: line)
114 | }
115 |
116 | func reset() {
117 | lines = []
118 | delegate?.recorderDidReset(self)
119 | }
120 | }
121 |
122 | public protocol LogRecorderDelegate: AnyObject {
123 | func recorder(_ recorder: LogRecorder, appendedLine: String)
124 | func recorderDidReset(_ recorder: LogRecorder)
125 | }
126 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | addressable (2.8.7)
9 | public_suffix (>= 2.0.2, < 7.0)
10 | artifactory (3.0.17)
11 | atomos (0.1.3)
12 | aws-eventstream (1.3.0)
13 | aws-partitions (1.1044.0)
14 | aws-sdk-core (3.217.1)
15 | aws-eventstream (~> 1, >= 1.3.0)
16 | aws-partitions (~> 1, >= 1.992.0)
17 | aws-sigv4 (~> 1.9)
18 | jmespath (~> 1, >= 1.6.1)
19 | aws-sdk-kms (1.97.0)
20 | aws-sdk-core (~> 3, >= 3.216.0)
21 | aws-sigv4 (~> 1.5)
22 | aws-sdk-s3 (1.179.0)
23 | aws-sdk-core (~> 3, >= 3.216.0)
24 | aws-sdk-kms (~> 1)
25 | aws-sigv4 (~> 1.5)
26 | aws-sigv4 (1.11.0)
27 | aws-eventstream (~> 1, >= 1.0.2)
28 | babosa (1.0.4)
29 | base64 (0.2.0)
30 | claide (1.1.0)
31 | colored (1.2)
32 | colored2 (3.1.2)
33 | commander (4.6.0)
34 | highline (~> 2.0.0)
35 | declarative (0.0.20)
36 | digest-crc (0.7.0)
37 | rake (>= 12.0.0, < 14.0.0)
38 | domain_name (0.6.20240107)
39 | dotenv (2.8.1)
40 | emoji_regex (3.2.3)
41 | excon (0.112.0)
42 | faraday (1.10.4)
43 | faraday-em_http (~> 1.0)
44 | faraday-em_synchrony (~> 1.0)
45 | faraday-excon (~> 1.1)
46 | faraday-httpclient (~> 1.0)
47 | faraday-multipart (~> 1.0)
48 | faraday-net_http (~> 1.0)
49 | faraday-net_http_persistent (~> 1.0)
50 | faraday-patron (~> 1.0)
51 | faraday-rack (~> 1.0)
52 | faraday-retry (~> 1.0)
53 | ruby2_keywords (>= 0.0.4)
54 | faraday-cookie_jar (0.0.7)
55 | faraday (>= 0.8.0)
56 | http-cookie (~> 1.0.0)
57 | faraday-em_http (1.0.0)
58 | faraday-em_synchrony (1.0.0)
59 | faraday-excon (1.1.0)
60 | faraday-httpclient (1.0.1)
61 | faraday-multipart (1.1.0)
62 | multipart-post (~> 2.0)
63 | faraday-net_http (1.0.2)
64 | faraday-net_http_persistent (1.2.0)
65 | faraday-patron (1.0.0)
66 | faraday-rack (1.0.0)
67 | faraday-retry (1.0.3)
68 | faraday_middleware (1.2.1)
69 | faraday (~> 1.0)
70 | fastimage (2.4.0)
71 | fastlane (2.226.0)
72 | CFPropertyList (>= 2.3, < 4.0.0)
73 | addressable (>= 2.8, < 3.0.0)
74 | artifactory (~> 3.0)
75 | aws-sdk-s3 (~> 1.0)
76 | babosa (>= 1.0.3, < 2.0.0)
77 | bundler (>= 1.12.0, < 3.0.0)
78 | colored (~> 1.2)
79 | commander (~> 4.6)
80 | dotenv (>= 2.1.1, < 3.0.0)
81 | emoji_regex (>= 0.1, < 4.0)
82 | excon (>= 0.71.0, < 1.0.0)
83 | faraday (~> 1.0)
84 | faraday-cookie_jar (~> 0.0.6)
85 | faraday_middleware (~> 1.0)
86 | fastimage (>= 2.1.0, < 3.0.0)
87 | fastlane-sirp (>= 1.0.0)
88 | gh_inspector (>= 1.1.2, < 2.0.0)
89 | google-apis-androidpublisher_v3 (~> 0.3)
90 | google-apis-playcustomapp_v1 (~> 0.1)
91 | google-cloud-env (>= 1.6.0, < 2.0.0)
92 | google-cloud-storage (~> 1.31)
93 | highline (~> 2.0)
94 | http-cookie (~> 1.0.5)
95 | json (< 3.0.0)
96 | jwt (>= 2.1.0, < 3)
97 | mini_magick (>= 4.9.4, < 5.0.0)
98 | multipart-post (>= 2.0.0, < 3.0.0)
99 | naturally (~> 2.2)
100 | optparse (>= 0.1.1, < 1.0.0)
101 | plist (>= 3.1.0, < 4.0.0)
102 | rubyzip (>= 2.0.0, < 3.0.0)
103 | security (= 0.1.5)
104 | simctl (~> 1.6.3)
105 | terminal-notifier (>= 2.0.0, < 3.0.0)
106 | terminal-table (~> 3)
107 | tty-screen (>= 0.6.3, < 1.0.0)
108 | tty-spinner (>= 0.8.0, < 1.0.0)
109 | word_wrap (~> 1.0.0)
110 | xcodeproj (>= 1.13.0, < 2.0.0)
111 | xcpretty (~> 0.4.0)
112 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
113 | fastlane-sirp (1.0.0)
114 | sysrandom (~> 1.0)
115 | gh_inspector (1.1.3)
116 | google-apis-androidpublisher_v3 (0.54.0)
117 | google-apis-core (>= 0.11.0, < 2.a)
118 | google-apis-core (0.11.3)
119 | addressable (~> 2.5, >= 2.5.1)
120 | googleauth (>= 0.16.2, < 2.a)
121 | httpclient (>= 2.8.1, < 3.a)
122 | mini_mime (~> 1.0)
123 | representable (~> 3.0)
124 | retriable (>= 2.0, < 4.a)
125 | rexml
126 | google-apis-iamcredentials_v1 (0.17.0)
127 | google-apis-core (>= 0.11.0, < 2.a)
128 | google-apis-playcustomapp_v1 (0.13.0)
129 | google-apis-core (>= 0.11.0, < 2.a)
130 | google-apis-storage_v1 (0.31.0)
131 | google-apis-core (>= 0.11.0, < 2.a)
132 | google-cloud-core (1.7.1)
133 | google-cloud-env (>= 1.0, < 3.a)
134 | google-cloud-errors (~> 1.0)
135 | google-cloud-env (1.6.0)
136 | faraday (>= 0.17.3, < 3.0)
137 | google-cloud-errors (1.4.0)
138 | google-cloud-storage (1.47.0)
139 | addressable (~> 2.8)
140 | digest-crc (~> 0.4)
141 | google-apis-iamcredentials_v1 (~> 0.1)
142 | google-apis-storage_v1 (~> 0.31.0)
143 | google-cloud-core (~> 1.6)
144 | googleauth (>= 0.16.2, < 2.a)
145 | mini_mime (~> 1.0)
146 | googleauth (1.8.1)
147 | faraday (>= 0.17.3, < 3.a)
148 | jwt (>= 1.4, < 3.0)
149 | multi_json (~> 1.11)
150 | os (>= 0.9, < 2.0)
151 | signet (>= 0.16, < 2.a)
152 | highline (2.0.3)
153 | http-cookie (1.0.8)
154 | domain_name (~> 0.5)
155 | httpclient (2.8.3)
156 | jmespath (1.6.2)
157 | json (2.9.1)
158 | jwt (2.10.1)
159 | base64
160 | mini_magick (4.13.2)
161 | mini_mime (1.1.5)
162 | multi_json (1.15.0)
163 | multipart-post (2.4.1)
164 | nanaimo (0.4.0)
165 | naturally (2.2.1)
166 | nkf (0.2.0)
167 | optparse (0.6.0)
168 | os (1.1.4)
169 | plist (3.7.2)
170 | public_suffix (6.0.1)
171 | rake (13.2.1)
172 | representable (3.2.0)
173 | declarative (< 0.1.0)
174 | trailblazer-option (>= 0.1.1, < 0.2.0)
175 | uber (< 0.2.0)
176 | retriable (3.1.2)
177 | rexml (3.4.0)
178 | rouge (3.28.0)
179 | ruby2_keywords (0.0.5)
180 | rubyzip (2.4.1)
181 | security (0.1.5)
182 | signet (0.19.0)
183 | addressable (~> 2.8)
184 | faraday (>= 0.17.5, < 3.a)
185 | jwt (>= 1.5, < 3.0)
186 | multi_json (~> 1.10)
187 | simctl (1.6.10)
188 | CFPropertyList
189 | naturally
190 | sysrandom (1.0.5)
191 | terminal-notifier (2.0.0)
192 | terminal-table (3.0.2)
193 | unicode-display_width (>= 1.1.1, < 3)
194 | trailblazer-option (0.1.2)
195 | tty-cursor (0.7.1)
196 | tty-screen (0.8.2)
197 | tty-spinner (0.9.3)
198 | tty-cursor (~> 0.7)
199 | uber (0.1.0)
200 | unicode-display_width (2.6.0)
201 | word_wrap (1.0.0)
202 | xcodeproj (1.27.0)
203 | CFPropertyList (>= 2.3.3, < 4.0)
204 | atomos (~> 0.1.3)
205 | claide (>= 1.0.2, < 2.0)
206 | colored2 (~> 3.1)
207 | nanaimo (~> 0.4.0)
208 | rexml (>= 3.3.6, < 4.0)
209 | xcpretty (0.4.0)
210 | rouge (~> 3.28.0)
211 | xcpretty-travis-formatter (1.0.1)
212 | xcpretty (~> 0.2, >= 0.0.7)
213 |
214 | PLATFORMS
215 | ruby
216 |
217 | DEPENDENCIES
218 | fastlane
219 |
220 | BUNDLED WITH
221 | 2.5.23
222 |
--------------------------------------------------------------------------------
/Bluetility/Device.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Device.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 7/1/20.
6 | // Copyright © 2020 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import CoreBluetooth
10 | import Logging
11 |
12 | protocol DeviceDelegate: AnyObject {
13 | func deviceDidConnect(_ device: Device)
14 | func deviceDidDisconnect(_ device: Device)
15 | func deviceDidUpdateName(_ device: Device)
16 | func device(_ device: Device, updated services: [CBService])
17 | func device(_ device: Device, updated characteristics: [CBCharacteristic], for service: CBService)
18 | func device(_ device: Device, updatedValueFor characteristic: CBCharacteristic)
19 | }
20 |
21 | class Device : NSObject {
22 | let peripheral: CBPeripheral
23 | unowned var scanner: Scanner
24 | var advertisingData: [String:Any]
25 | var rssi: Int
26 | let logger = Logger(label: "com.rossible.Bluetility.Device")
27 |
28 | weak var delegate: DeviceDelegate? = nil
29 |
30 | // Transient data
31 | var manufacturerName: String? = nil
32 | var modelName: String? = nil
33 | var companyIdentifier: String? = nil
34 | var company: String? = nil
35 |
36 | init(scanner: Scanner, peripheral: CBPeripheral, advertisingData: [String: Any], rssi: Int) {
37 | self.scanner = scanner
38 | self.peripheral = peripheral
39 | self.advertisingData = advertisingData
40 | self.rssi = rssi
41 |
42 | super.init()
43 |
44 | extractCompanyIdentifier()
45 | peripheral.delegate = self
46 | }
47 |
48 | deinit {
49 | peripheral.delegate = nil
50 | }
51 |
52 | func extractCompanyIdentifier() {
53 | if let manufacturerData = self.advertisingData[CBAdvertisementDataManufacturerDataKey] as? Data {
54 | if manufacturerData.count >= 2 {
55 | let companyIdentifier = manufacturerData[0..<2].reversed().hexString
56 | self.companyIdentifier = companyIdentifier
57 | self.company = COMPANY_IDENTIFIERS[companyIdentifier]
58 | }
59 | }
60 | }
61 |
62 | func updateAdvertisingData(_ newData:[String: Any]) {
63 | self.advertisingData += newData
64 | extractCompanyIdentifier()
65 | }
66 |
67 | var friendlyName : String {
68 | if let advertisedName = advertisingData[CBAdvertisementDataLocalNameKey] as? String {
69 | return advertisedName
70 | }
71 | if let peripheralName = peripheral.name {
72 | return peripheralName
73 | }
74 | let infoFields = [manufacturerName, modelName].compactMap({$0})
75 | if infoFields.count > 0 {
76 | return infoFields.joined(separator: " ")
77 | }
78 |
79 | return "Untitled"
80 | }
81 |
82 | var services: [CBService] {
83 | return peripheral.services ?? []
84 | }
85 |
86 | func connect() {
87 | scanner.central.connect(self.peripheral, options: [:])
88 | }
89 |
90 | func disconnect() {
91 | scanner.central.cancelPeripheralConnection(self.peripheral)
92 | }
93 |
94 | func discoverCharacteristics(for service: CBService) {
95 | peripheral.discoverCharacteristics(nil, for: service)
96 | }
97 |
98 | func read(characteristic: CBCharacteristic) {
99 | peripheral.readValue(for: characteristic)
100 | }
101 |
102 | func write(data: Data, for characteristic: CBCharacteristic, type:CBCharacteristicWriteType) {
103 | peripheral.writeValue(data, for: characteristic, type: type)
104 | }
105 |
106 | func setNotify(_ enabled: Bool, for characteristic: CBCharacteristic) {
107 | peripheral.setNotifyValue(enabled, for: characteristic)
108 | }
109 | }
110 |
111 | extension Device : CBPeripheralDelegate {
112 | func peripheralDidConnect() {
113 | peripheral.discoverServices(nil)
114 | delegate?.deviceDidConnect(self)
115 | }
116 |
117 | func peripheralDidDisconnect(error: Error?) {
118 | delegate?.deviceDidDisconnect(self)
119 | }
120 |
121 | func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
122 | delegate?.deviceDidUpdateName(self)
123 | }
124 |
125 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
126 | if let error = error {
127 | // TODO: report an error?
128 | logger.error("Peripheral: \(peripheral.name ?? "-") didDiscoverServices encountered error: \(error)")
129 | return
130 | }
131 | let services = peripheral.services ?? []
132 |
133 | logger.info("Peripheral: \(peripheral.name ?? "-") didDiscoverServices: \(services.map({ $0.uuid }))")
134 |
135 | handleSpecialServices(services)
136 |
137 | delegate?.device(self, updated: services)
138 | }
139 |
140 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
141 | if let error = error {
142 | // TODO: report an error?
143 | logger.error("Peripheral: \(peripheral.name ?? "-") didDiscoverCharacteristicsFor: \(service) encountered error: \(error)")
144 | return
145 | }
146 | let characteristics = service.characteristics ?? []
147 |
148 | logger.info("Peripheral: \(peripheral.name ?? "-") didDiscoverCharacteristics: \(characteristics.map({ $0.uuid })) for: \(service.uuid) ")
149 | handleSpecialCharacteristics(characteristics)
150 |
151 | delegate?.device(self, updated: characteristics, for: service)
152 | }
153 |
154 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
155 | if let error = error {
156 | // TODO: report an error?
157 | logger.error("Peripheral: \(peripheral.name ?? "-") didUpdateValueFor: \(characteristic) encountered error: \(error)")
158 | return
159 | }
160 | logger.info("Peripheral: \(peripheral.name ?? "-") didUpdateValueFor: \(characteristic.uuid) to: \(characteristic.value?.hexString ?? "nil")")
161 |
162 | handleSpecialCharacteristic(characteristic)
163 | delegate?.device(self, updatedValueFor: characteristic)
164 | }
165 |
166 | func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
167 | if let error = error {
168 | // TODO: report an error?
169 | logger.error("Peripheral: \(peripheral.name ?? "-") didWriteValueFor: \(characteristic) encountered error: \(error)")
170 | }
171 | // TODO: report successful write?
172 | logger.info("Peripheral: \(peripheral.name ?? "-") didWriteValueFor: \(characteristic) successfully")
173 | }
174 |
175 | }
176 |
177 | // MARK: Handle Special Characteristics
178 |
179 | fileprivate let specialServiceUUIDs = [
180 | CBUUID(string: "180A"), // Device Information Service
181 | ]
182 |
183 | fileprivate let manufacturerNameUUID = CBUUID(string: "2A29")
184 | fileprivate let modelNumberUUID = CBUUID(string: "2A24")
185 |
186 | fileprivate let specialCharacteristicUUIDs = [
187 | manufacturerNameUUID, // Manufacturer Name
188 | modelNumberUUID, // Model Number
189 | ]
190 |
191 | extension Device {
192 | func handleSpecialServices(_ services: [CBService]) {
193 | for service in services {
194 | if specialServiceUUIDs.contains(service.uuid) {
195 | peripheral.discoverCharacteristics(specialCharacteristicUUIDs, for: service)
196 | }
197 | }
198 | }
199 |
200 | func handleSpecialCharacteristics(_ characteristics: [CBCharacteristic]) {
201 | for characteristic in characteristics {
202 | if specialCharacteristicUUIDs.contains(characteristic.uuid) {
203 | peripheral.readValue(for: characteristic)
204 | handleSpecialCharacteristic(characteristic)
205 | }
206 | }
207 | }
208 |
209 | func handleSpecialCharacteristic(_ characteristic: CBCharacteristic) {
210 | guard let value = characteristic.value else { return }
211 |
212 | if specialCharacteristicUUIDs.contains(characteristic.uuid) {
213 | switch characteristic.uuid {
214 | case manufacturerNameUUID:
215 | manufacturerName = String(bytes: value, encoding: .utf8)
216 | case modelNumberUUID:
217 | modelName = String(bytes: value, encoding: .utf8)
218 | default:
219 | assertionFailure("Forgot to handle one of the UUIDs in specialCharacteristicUUIDs: \(characteristic.uuid)")
220 | logger.critical("Forgot to handle one of the UUIDs in specialCharacteristicUUIDs: \(characteristic.uuid)")
221 | }
222 | delegate?.deviceDidUpdateName(self)
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/Bluetility.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 3749318B1BF169C90087E39F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3749318A1BF169C90087E39F /* AppDelegate.swift */; };
11 | 3749318D1BF169C90087E39F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3749318C1BF169C90087E39F /* ViewController.swift */; };
12 | 3749318F1BF169C90087E39F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3749318E1BF169C90087E39F /* Assets.xcassets */; };
13 | 374931921BF169C90087E39F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 374931901BF169C90087E39F /* Main.storyboard */; };
14 | 3749319A1BF16D980087E39F /* BluetilityWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374931991BF16D980087E39F /* BluetilityWindowController.swift */; };
15 | 3749319C1BF173470087E39F /* Scanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3749319B1BF173470087E39F /* Scanner.swift */; };
16 | 3769BDE01C308814004C089D /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = 3769BDDF1C308814004C089D /* Credits.html */; };
17 | 3769BDE41C32FB0D004C089D /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3769BDE31C32FB0D004C089D /* LogViewController.swift */; };
18 | 37F9CCD51C21D856003580CB /* PasteboardBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9CCD41C21D856003580CB /* PasteboardBrowser.swift */; };
19 | 505C6E80233FC8C400ABC0A3 /* NSTextField+Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505C6E7F233FC8C400ABC0A3 /* NSTextField+Shake.swift */; };
20 | D89F265029A1B5DF00061958 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = D89F264F29A1B5DF00061958 /* Logging */; };
21 | D89F265529A1C56300061958 /* BluetilityLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89F265429A1C56300061958 /* BluetilityLogHandler.swift */; };
22 | D89F71D324ACF02800861217 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89F71D224ACF02700861217 /* Device.swift */; };
23 | D89F71D724ACF67500861217 /* Swift+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89F71D624ACF67500861217 /* Swift+Extensions.swift */; };
24 | D8F204122D5088D900AAB8F8 /* CompanyIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F204112D5088D200AAB8F8 /* CompanyIdentifiers.swift */; };
25 | /* End PBXBuildFile section */
26 |
27 | /* Begin PBXFileReference section */
28 | 374931871BF169C90087E39F /* Bluetility.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bluetility.app; sourceTree = BUILT_PRODUCTS_DIR; };
29 | 3749318A1BF169C90087E39F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
30 | 3749318C1BF169C90087E39F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
31 | 3749318E1BF169C90087E39F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
32 | 374931911BF169C90087E39F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
33 | 374931931BF169C90087E39F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
34 | 374931991BF16D980087E39F /* BluetilityWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetilityWindowController.swift; sourceTree = ""; };
35 | 3749319B1BF173470087E39F /* Scanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scanner.swift; sourceTree = ""; };
36 | 3769BDDF1C308814004C089D /* Credits.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = Credits.html; sourceTree = ""; };
37 | 3769BDE31C32FB0D004C089D /* LogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = ""; };
38 | 37F9CCD41C21D856003580CB /* PasteboardBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasteboardBrowser.swift; sourceTree = ""; };
39 | 505C6E7F233FC8C400ABC0A3 /* NSTextField+Shake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+Shake.swift"; sourceTree = ""; };
40 | D82AF517222AF834005D80D1 /* fastlane */ = {isa = PBXFileReference; lastKnownFileType = folder; path = fastlane; sourceTree = ""; };
41 | D85908F12427E34700CBECC9 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; };
42 | D85908F22427E34700CBECC9 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
43 | D85908F32427E34700CBECC9 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
44 | D85908F42427E34700CBECC9 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
45 | D8789F0E24390B7900B83AA4 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
46 | D89F265429A1C56300061958 /* BluetilityLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetilityLogHandler.swift; sourceTree = ""; };
47 | D89F71D024AC084B00861217 /* Gemfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile; sourceTree = ""; };
48 | D89F71D124AC084B00861217 /* LICENSE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; };
49 | D89F71D224ACF02700861217 /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; };
50 | D89F71D624ACF67500861217 /* Swift+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Swift+Extensions.swift"; sourceTree = ""; };
51 | D8F204112D5088D200AAB8F8 /* CompanyIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyIdentifiers.swift; sourceTree = ""; };
52 | /* End PBXFileReference section */
53 |
54 | /* Begin PBXFrameworksBuildPhase section */
55 | 374931841BF169C90087E39F /* Frameworks */ = {
56 | isa = PBXFrameworksBuildPhase;
57 | buildActionMask = 2147483647;
58 | files = (
59 | D89F265029A1B5DF00061958 /* Logging in Frameworks */,
60 | );
61 | runOnlyForDeploymentPostprocessing = 0;
62 | };
63 | /* End PBXFrameworksBuildPhase section */
64 |
65 | /* Begin PBXGroup section */
66 | 3749317E1BF169C90087E39F = {
67 | isa = PBXGroup;
68 | children = (
69 | D8789F0E24390B7900B83AA4 /* README.md */,
70 | 374931891BF169C90087E39F /* Bluetility */,
71 | D85908F02427E34700CBECC9 /* Config */,
72 | 374931881BF169C90087E39F /* Products */,
73 | D82AF517222AF834005D80D1 /* fastlane */,
74 | D89F71D024AC084B00861217 /* Gemfile */,
75 | D89F71D124AC084B00861217 /* LICENSE.md */,
76 | );
77 | sourceTree = "";
78 | };
79 | 374931881BF169C90087E39F /* Products */ = {
80 | isa = PBXGroup;
81 | children = (
82 | 374931871BF169C90087E39F /* Bluetility.app */,
83 | );
84 | name = Products;
85 | sourceTree = "";
86 | };
87 | 374931891BF169C90087E39F /* Bluetility */ = {
88 | isa = PBXGroup;
89 | children = (
90 | D8F204112D5088D200AAB8F8 /* CompanyIdentifiers.swift */,
91 | 3749318A1BF169C90087E39F /* AppDelegate.swift */,
92 | 374931991BF16D980087E39F /* BluetilityWindowController.swift */,
93 | D89F71D224ACF02700861217 /* Device.swift */,
94 | 3749319B1BF173470087E39F /* Scanner.swift */,
95 | 3749318C1BF169C90087E39F /* ViewController.swift */,
96 | 3749318E1BF169C90087E39F /* Assets.xcassets */,
97 | 374931901BF169C90087E39F /* Main.storyboard */,
98 | 374931931BF169C90087E39F /* Info.plist */,
99 | 37F9CCD41C21D856003580CB /* PasteboardBrowser.swift */,
100 | 3769BDDF1C308814004C089D /* Credits.html */,
101 | 3769BDE31C32FB0D004C089D /* LogViewController.swift */,
102 | D89F71D624ACF67500861217 /* Swift+Extensions.swift */,
103 | 505C6E7F233FC8C400ABC0A3 /* NSTextField+Shake.swift */,
104 | D89F265429A1C56300061958 /* BluetilityLogHandler.swift */,
105 | );
106 | path = Bluetility;
107 | sourceTree = "";
108 | };
109 | D85908F02427E34700CBECC9 /* Config */ = {
110 | isa = PBXGroup;
111 | children = (
112 | D85908F12427E34700CBECC9 /* Shared.xcconfig */,
113 | D85908F22427E34700CBECC9 /* Debug.xcconfig */,
114 | D85908F32427E34700CBECC9 /* Release.xcconfig */,
115 | D85908F42427E34700CBECC9 /* Warnings.xcconfig */,
116 | );
117 | path = Config;
118 | sourceTree = "";
119 | };
120 | /* End PBXGroup section */
121 |
122 | /* Begin PBXNativeTarget section */
123 | 374931861BF169C90087E39F /* Bluetility */ = {
124 | isa = PBXNativeTarget;
125 | buildConfigurationList = 374931961BF169C90087E39F /* Build configuration list for PBXNativeTarget "Bluetility" */;
126 | buildPhases = (
127 | 374931831BF169C90087E39F /* Sources */,
128 | 374931841BF169C90087E39F /* Frameworks */,
129 | 374931851BF169C90087E39F /* Resources */,
130 | );
131 | buildRules = (
132 | );
133 | dependencies = (
134 | );
135 | name = Bluetility;
136 | packageProductDependencies = (
137 | D89F264F29A1B5DF00061958 /* Logging */,
138 | );
139 | productName = Bluetility;
140 | productReference = 374931871BF169C90087E39F /* Bluetility.app */;
141 | productType = "com.apple.product-type.application";
142 | };
143 | /* End PBXNativeTarget section */
144 |
145 | /* Begin PBXProject section */
146 | 3749317F1BF169C90087E39F /* Project object */ = {
147 | isa = PBXProject;
148 | attributes = {
149 | LastSwiftUpdateCheck = 0710;
150 | LastUpgradeCheck = 1320;
151 | ORGANIZATIONNAME = "Joseph Ross";
152 | TargetAttributes = {
153 | 374931861BF169C90087E39F = {
154 | CreatedOnToolsVersion = 7.1;
155 | LastSwiftMigration = 1130;
156 | ProvisioningStyle = Manual;
157 | SystemCapabilities = {
158 | com.apple.HardenedRuntime = {
159 | enabled = 1;
160 | };
161 | };
162 | };
163 | };
164 | };
165 | buildConfigurationList = 374931821BF169C90087E39F /* Build configuration list for PBXProject "Bluetility" */;
166 | compatibilityVersion = "Xcode 3.2";
167 | developmentRegion = en;
168 | hasScannedForEncodings = 0;
169 | knownRegions = (
170 | en,
171 | Base,
172 | );
173 | mainGroup = 3749317E1BF169C90087E39F;
174 | packageReferences = (
175 | D89F264E29A1B5DF00061958 /* XCRemoteSwiftPackageReference "swift-log" */,
176 | );
177 | productRefGroup = 374931881BF169C90087E39F /* Products */;
178 | projectDirPath = "";
179 | projectRoot = "";
180 | targets = (
181 | 374931861BF169C90087E39F /* Bluetility */,
182 | );
183 | };
184 | /* End PBXProject section */
185 |
186 | /* Begin PBXResourcesBuildPhase section */
187 | 374931851BF169C90087E39F /* Resources */ = {
188 | isa = PBXResourcesBuildPhase;
189 | buildActionMask = 2147483647;
190 | files = (
191 | 3749318F1BF169C90087E39F /* Assets.xcassets in Resources */,
192 | 3769BDE01C308814004C089D /* Credits.html in Resources */,
193 | 374931921BF169C90087E39F /* Main.storyboard in Resources */,
194 | );
195 | runOnlyForDeploymentPostprocessing = 0;
196 | };
197 | /* End PBXResourcesBuildPhase section */
198 |
199 | /* Begin PBXSourcesBuildPhase section */
200 | 374931831BF169C90087E39F /* Sources */ = {
201 | isa = PBXSourcesBuildPhase;
202 | buildActionMask = 2147483647;
203 | files = (
204 | 3749319A1BF16D980087E39F /* BluetilityWindowController.swift in Sources */,
205 | 3749318D1BF169C90087E39F /* ViewController.swift in Sources */,
206 | 37F9CCD51C21D856003580CB /* PasteboardBrowser.swift in Sources */,
207 | 505C6E80233FC8C400ABC0A3 /* NSTextField+Shake.swift in Sources */,
208 | D89F71D724ACF67500861217 /* Swift+Extensions.swift in Sources */,
209 | 3769BDE41C32FB0D004C089D /* LogViewController.swift in Sources */,
210 | D8F204122D5088D900AAB8F8 /* CompanyIdentifiers.swift in Sources */,
211 | D89F265529A1C56300061958 /* BluetilityLogHandler.swift in Sources */,
212 | 3749319C1BF173470087E39F /* Scanner.swift in Sources */,
213 | 3749318B1BF169C90087E39F /* AppDelegate.swift in Sources */,
214 | D89F71D324ACF02800861217 /* Device.swift in Sources */,
215 | );
216 | runOnlyForDeploymentPostprocessing = 0;
217 | };
218 | /* End PBXSourcesBuildPhase section */
219 |
220 | /* Begin PBXVariantGroup section */
221 | 374931901BF169C90087E39F /* Main.storyboard */ = {
222 | isa = PBXVariantGroup;
223 | children = (
224 | 374931911BF169C90087E39F /* Base */,
225 | );
226 | name = Main.storyboard;
227 | sourceTree = "";
228 | };
229 | /* End PBXVariantGroup section */
230 |
231 | /* Begin XCBuildConfiguration section */
232 | 374931941BF169C90087E39F /* Debug */ = {
233 | isa = XCBuildConfiguration;
234 | baseConfigurationReference = D85908F22427E34700CBECC9 /* Debug.xcconfig */;
235 | buildSettings = {
236 | };
237 | name = Debug;
238 | };
239 | 374931951BF169C90087E39F /* Release */ = {
240 | isa = XCBuildConfiguration;
241 | baseConfigurationReference = D85908F32427E34700CBECC9 /* Release.xcconfig */;
242 | buildSettings = {
243 | };
244 | name = Release;
245 | };
246 | 374931971BF169C90087E39F /* Debug */ = {
247 | isa = XCBuildConfiguration;
248 | baseConfigurationReference = D85908F22427E34700CBECC9 /* Debug.xcconfig */;
249 | buildSettings = {
250 | CURRENT_PROJECT_VERSION = 11;
251 | MARKETING_VERSION = 1.6.0;
252 | };
253 | name = Debug;
254 | };
255 | 374931981BF169C90087E39F /* Release */ = {
256 | isa = XCBuildConfiguration;
257 | baseConfigurationReference = D85908F32427E34700CBECC9 /* Release.xcconfig */;
258 | buildSettings = {
259 | CURRENT_PROJECT_VERSION = 11;
260 | MARKETING_VERSION = 1.6.0;
261 | };
262 | name = Release;
263 | };
264 | /* End XCBuildConfiguration section */
265 |
266 | /* Begin XCConfigurationList section */
267 | 374931821BF169C90087E39F /* Build configuration list for PBXProject "Bluetility" */ = {
268 | isa = XCConfigurationList;
269 | buildConfigurations = (
270 | 374931941BF169C90087E39F /* Debug */,
271 | 374931951BF169C90087E39F /* Release */,
272 | );
273 | defaultConfigurationIsVisible = 0;
274 | defaultConfigurationName = Release;
275 | };
276 | 374931961BF169C90087E39F /* Build configuration list for PBXNativeTarget "Bluetility" */ = {
277 | isa = XCConfigurationList;
278 | buildConfigurations = (
279 | 374931971BF169C90087E39F /* Debug */,
280 | 374931981BF169C90087E39F /* Release */,
281 | );
282 | defaultConfigurationIsVisible = 0;
283 | defaultConfigurationName = Release;
284 | };
285 | /* End XCConfigurationList section */
286 |
287 | /* Begin XCRemoteSwiftPackageReference section */
288 | D89F264E29A1B5DF00061958 /* XCRemoteSwiftPackageReference "swift-log" */ = {
289 | isa = XCRemoteSwiftPackageReference;
290 | repositoryURL = "https://github.com/apple/swift-log.git";
291 | requirement = {
292 | kind = upToNextMajorVersion;
293 | minimumVersion = 1.0.0;
294 | };
295 | };
296 | /* End XCRemoteSwiftPackageReference section */
297 |
298 | /* Begin XCSwiftPackageProductDependency section */
299 | D89F264F29A1B5DF00061958 /* Logging */ = {
300 | isa = XCSwiftPackageProductDependency;
301 | package = D89F264E29A1B5DF00061958 /* XCRemoteSwiftPackageReference "swift-log" */;
302 | productName = Logging;
303 | };
304 | /* End XCSwiftPackageProductDependency section */
305 | };
306 | rootObject = 3749317F1BF169C90087E39F /* Project object */;
307 | }
308 |
--------------------------------------------------------------------------------
/Bluetility/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Bluetility
4 | //
5 | // Created by Joseph Ross on 11/9/15.
6 | // Copyright © 2015 Joseph Ross. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import CoreBluetooth
11 | import Logging
12 |
13 | class ViewController: NSViewController {
14 |
15 | let scanner = Scanner()
16 |
17 | var recorder: LogRecorder? = nil
18 | let logger: Logger = Logger(label:"VC")
19 |
20 | var selectedDevice: Device? = nil
21 | var selectedService: CBService? = nil
22 | var selectedCharacteristic: CBCharacteristic? = nil
23 | var characteristicUpdatedDate: Date? = nil
24 | let dateFormatter = DateFormatter()
25 | var searchValue = ""
26 |
27 | @IBOutlet var browser: NSBrowser!
28 | @IBOutlet var writeAscii: NSTextField!
29 | @IBOutlet var writeHex: NSTextField!
30 | @IBOutlet var readButton: NSButton!
31 | @IBOutlet var subscribeButton: NSButton!
32 | @IBOutlet var statusLabel: NSTextField!
33 |
34 | var logWindowController: NSWindowController? = nil
35 | var logViewController: LogViewController? = nil
36 | var tooltipTagForRow: [Int:NSView.ToolTipTag] = [:]
37 | var rowForTooltipTag: [NSView.ToolTipTag:Int] = [:]
38 |
39 |
40 | override func viewDidLoad() {
41 | super.viewDidLoad()
42 | // Do any additional setup after loading the view.
43 | scanner.delegate = self
44 | scanner.start()
45 | dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss.SSS"
46 | browser.separatesColumns = false
47 | }
48 |
49 | override func viewWillAppear() {
50 | super.viewWillAppear()
51 |
52 | }
53 |
54 | override func viewDidLayout() {
55 | super.viewDidLayout()
56 | setupCharacteristicControls()
57 | }
58 |
59 | override func viewDidAppear() {
60 | super.viewDidAppear()
61 | }
62 |
63 | @IBAction func refreshPressed(_ sender: AnyObject?) {
64 | if let device = selectedDevice {
65 | device.disconnect()
66 | }
67 | selectedDevice = nil
68 | selectedService = nil
69 | scanner.restart()
70 | resetTooltips()
71 | }
72 |
73 | @IBAction func sortPressed(_ sender: AnyObject?) {
74 | scanner.devices.sort { return $0.rssi > $1.rssi }
75 | reloadColumn(0)
76 | }
77 |
78 | @IBAction func logPressed(_ sender: NSToolbarItem) {
79 | if let window = logWindowController?.window, window.isVisible || window.isMiniaturized {
80 | window.makeKeyAndOrderFront(self)
81 | } else {
82 | logWindowController?.window?.close()
83 | if let logWindowController = self.storyboard?.instantiateController(withIdentifier: "LogWindow") as? NSWindowController {
84 | logWindowController.shouldCascadeWindows = false
85 | logWindowController.window?.setFrameAutosaveName("bluetility_log")
86 | logWindowController.showWindow(sender)
87 | self.logWindowController = logWindowController
88 | self.logViewController = logWindowController.contentViewController as? LogViewController
89 | self.logViewController?.recorder = self.recorder
90 | }
91 | }
92 | }
93 |
94 | func searchTextDidChange(value: String) {
95 | searchValue = value.lowercased().trimmingCharacters(in: .whitespaces)
96 | resetTooltips()
97 | }
98 |
99 | func resetTooltips() {
100 | browser.removeAllToolTips()
101 | rowForTooltipTag = [:]
102 | tooltipTagForRow = [:]
103 | }
104 | }
105 |
106 | extension ViewController : NSBrowserDelegate {
107 | func filterBySearch(_ device: Device) -> Bool {
108 | if device.peripheral.identifier.uuidString.contains(searchValue)
109 | || device.friendlyName.lowercased().contains(searchValue)
110 | || device.companyIdentifier?.lowercased().contains(searchValue) ?? false
111 | || device.company?.lowercased().contains(searchValue) ?? false
112 |
113 | {
114 | return true
115 | }
116 |
117 | let advData = device.advertisingData
118 |
119 | if let serviceUUIDs = advData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID], containsInArrayOfCBUUIDs(searchFor: searchValue, inArray: serviceUUIDs){
120 | return true
121 | }
122 |
123 | if let serviceUUIDS = advData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID], containsInArrayOfCBUUIDs(searchFor: searchValue, inArray: serviceUUIDS) {
124 | return true
125 | }
126 |
127 | return false
128 | }
129 |
130 | func containsInArrayOfCBUUIDs(searchFor: String, inArray: [CBUUID]) -> Bool {
131 | let asString = inArray.map {$0.uuidString}
132 |
133 | return asString.contains(where: {
134 | return $0.lowercased().contains(searchFor)
135 | })
136 | }
137 |
138 |
139 | var visibleDevices: [Device] {
140 | if (searchValue == "") {
141 | return scanner.devices
142 | }
143 |
144 | return scanner.devices.filter(filterBySearch)
145 | }
146 |
147 | func browser(_ sender: NSBrowser, numberOfRowsInColumn column: Int) -> Int {
148 | switch column {
149 | case 0: return visibleDevices.count
150 | case 1: return selectedDevice?.services.count ?? 0
151 | case 2: return selectedService?.characteristics?.count ?? 0
152 | case 3: return 4
153 | default: return 0
154 | }
155 | }
156 |
157 | func browser(_ browser: NSBrowser, sizeToFitWidthOfColumn columnIndex: Int) -> CGFloat {
158 | return browser.width(ofColumn: columnIndex)
159 | }
160 |
161 | func browser(_ sender: NSBrowser, willDisplayCell cell: Any, atRow row: Int, column: Int) {
162 | guard let cell = cell as? NSBrowserCell else { return }
163 | switch column {
164 | case 0:
165 | let device = visibleDevices[row]
166 | cell.title = (device.friendlyName) + "(\(device.rssi))"
167 | let rect = browser.frame(ofRow: row, inColumn: column)
168 | if tooltipTagForRow[row] == nil {
169 | let tag = browser.addToolTip(rect, owner: self, userData: nil)
170 | tooltipTagForRow[row] = tag
171 | rowForTooltipTag[tag] = row
172 | }
173 | case 1:
174 | if let service = selectedDevice?.services[row] {
175 | cell.title = titleForUUID(service.uuid)
176 | }
177 | case 2:
178 | if let characteristic = selectedService?.characteristics?[row] {
179 | cell.title = titleForUUID(characteristic.uuid)
180 | }
181 | case 3:
182 | setupCharacteristicDetailCell(cell, forRow:row)
183 | default:
184 | break
185 | }
186 | }
187 |
188 | func titleForUUID(_ uuid:CBUUID) -> String {
189 | var title = uuid.description
190 | if (title.hasPrefix("Unknown")) {
191 | title = uuid.uuidString
192 | }
193 | return title
194 | }
195 |
196 | func browser(_ sender: NSBrowser, titleOfColumn column: Int) -> String? {
197 | switch column {
198 | case 0: return "Devices"
199 | case 1: return "Services"
200 | case 2: return "Characteristics"
201 | case 3: return "Detail"
202 | default: return ""
203 | }
204 | }
205 |
206 | func browser(_ browser: NSBrowser, shouldSizeColumn columnIndex: Int, forUserResize: Bool, toWidth suggestedWidth: CGFloat) -> CGFloat {
207 | if !forUserResize {
208 | if columnIndex == 0 {
209 | return 200
210 | }
211 | }
212 | return suggestedWidth
213 | }
214 |
215 | func selectDevice(_ device: Device) {
216 | if device != selectedDevice {
217 | selectedDevice?.disconnect()
218 | selectedDevice?.delegate = nil
219 | device.delegate = self
220 | device.connect()
221 | selectedDevice = device
222 | }
223 | }
224 |
225 |
226 | @IBAction
227 | func browserAction(_ sender:NSBrowser) {
228 | guard let indexPath = browser.selectionIndexPath else { return }
229 | let column = indexPath.count
230 | browser.setTitle(self.browser(browser,titleOfColumn:column)!, ofColumn: column)
231 | // Automatically reconnect if a service or characteristic is selected.
232 | if [2,3].contains(column) {
233 | reconnectPeripheral()
234 | }
235 | if column == 1 {
236 | let device = visibleDevices[indexPath[0]]
237 | if device != selectedDevice {
238 | updateStatusLabel(for: device)
239 | }
240 | selectDevice(device)
241 | reloadColumn(1)
242 | } else if column == 2 {
243 | if let service = selectedDevice?.services[safe: indexPath[1]] {
244 | selectedService = service
245 | selectedDevice?.discoverCharacteristics(for: service)
246 | selectedCharacteristic = nil
247 | reloadColumn(2)
248 | }
249 | } else if column == 3 {
250 | if let characteristic = selectedService?.characteristics?[indexPath[2]] {
251 | selectedCharacteristic = characteristic
252 | if characteristic.properties.contains(.read) {
253 | readCharacteristic()
254 |
255 | }
256 | refreshCharacteristicDetail()
257 | }
258 | }
259 | setupCharacteristicControls()
260 | }
261 |
262 | func reconnectPeripheral() {
263 | if let device = selectedDevice, device.peripheral.state != .connected {
264 | device.connect()
265 | }
266 | }
267 |
268 | func setupCharacteristicControls() {
269 | let minimumWidth = CGFloat(150)
270 | let frameWidth = view.frame.size.width
271 | var otherWidth = browser.width(ofColumn: 0)
272 | otherWidth += browser.width(ofColumn: 1)
273 | otherWidth += browser.width(ofColumn: 2)
274 |
275 | let fitWidth = frameWidth - otherWidth - 6
276 | if fitWidth >= minimumWidth {
277 | browser.setWidth(fitWidth, ofColumn: 3)
278 | browser.scrollColumnToVisible(0)
279 | } else {
280 | browser.setWidth(minimumWidth, ofColumn: 3)
281 | browser.scrollColumnToVisible(0)
282 | browser.scrollColumnToVisible(3)
283 | }
284 |
285 | readButton.removeFromSuperview()
286 | subscribeButton.removeFromSuperview()
287 | writeAscii.removeFromSuperview()
288 | writeHex.removeFromSuperview()
289 | if let characteristic = selectedCharacteristic {
290 | let frame = browser.frame(ofColumn: 3)
291 | if characteristic.properties.contains(.read) {
292 | browser.addSubview(readButton)
293 | readButton.frame = CGRect(x: frame.origin.x + (frame.size.width/2 - 42),y: frame.origin.y + 120,width: 84,height: 32)
294 | }
295 | if !characteristic.properties.intersection([.write, .writeWithoutResponse]).isEmpty {
296 | browser.addSubview(writeAscii)
297 | browser.addSubview(writeHex)
298 | writeHex.frame = CGRect(x: frame.origin.x + (frame.size.width/2 - 75),y: frame.origin.y + 80,width: 150,height: 22)
299 | writeAscii.frame = CGRect(x: frame.origin.x + (frame.size.width/2 - 75),y: frame.origin.y + 40,width: 150,height: 22)
300 | }
301 | if !characteristic.properties.intersection([.indicate, .notify]).isEmpty {
302 | browser.addSubview(subscribeButton)
303 | subscribeButton.frame = CGRect(x: frame.origin.x + (frame.size.width/2 - 45),y: frame.origin.y + 150,width: 90,height: 32)
304 | }
305 | }
306 | }
307 |
308 | func refreshCharacteristicDetail() {
309 | reloadColumn(3)
310 | }
311 |
312 | func reloadColumn(_ column: Int) {
313 | for row in 0.. String {
431 | if let row = rowForTooltipTag[tag] {
432 | let device = visibleDevices[row]
433 | return tooltip(for: device)
434 | }
435 | return ""
436 | }
437 |
438 | func tooltip(for device: Device) -> String {
439 | var tooltipParts:[String] = []
440 | tooltipParts.append("\t\(device.friendlyName)")
441 | tooltipParts.append("identifier:\t\t\(device.peripheral.identifier)")
442 | if let company = device.company {
443 | tooltipParts.append("company:\t\t\(company)")
444 | }
445 | else if let companyIdentifier = device.companyIdentifier {
446 | tooltipParts.append("companyID:\t\(companyIdentifier)")
447 | }
448 | if let mfgData = device.advertisingData[CBAdvertisementDataManufacturerDataKey] as? Data {
449 | tooltipParts.append("Mfg Data:\t\t0x\(mfgData.hexString)")
450 | if mfgData[0] == 0xD9 && mfgData[1] == 0x01 {
451 | let uidData = mfgData[6..<14]
452 | tooltipParts.append("UID:\t\t\t\(uidData.hexString)")
453 | }
454 | }
455 | var allServiceUUIDs:[CBUUID] = []
456 | if let serviceUUIDs = device.advertisingData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
457 | allServiceUUIDs += serviceUUIDs
458 | }
459 | if let serviceUUIDs = device.advertisingData[CBAdvertisementDataOverflowServiceUUIDsKey] as? [CBUUID] {
460 | allServiceUUIDs += serviceUUIDs
461 | }
462 | if allServiceUUIDs.count > 0 {
463 | tooltipParts.append("Service UUIDs:\t\(allServiceUUIDs.map({return $0.uuidString}).joined(separator: ", "))")
464 | }
465 | if let txPower = device.advertisingData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
466 | tooltipParts.append("Tx Power:\t\t\(txPower)")
467 | }
468 |
469 | return tooltipParts.joined(separator: "\n")
470 | }
471 | }
472 |
473 | extension ViewController : PasteboardBrowserDelegate {
474 | func browser(_ browser: PasteboardBrowser, pasteboardStringFor indexPath: IndexPath) -> String? {
475 | if indexPath.count == 1 {
476 | let row = indexPath[0]
477 | guard let tag = tooltipTagForRow[row] else {return nil}
478 | let peripheralName = visibleDevices[row].friendlyName
479 | return "Name:\t\t\t\(peripheralName)\n" + self.view(browser, stringForToolTip: tag, point: NSPoint(), userData: nil)
480 | }
481 | return nil
482 | }
483 |
484 | func browser(_ browser: PasteboardBrowser, menuFor cell: NSBrowserCell, atRow row: Int, column: Int) -> NSMenu? {
485 | let menu = NSMenu()
486 | let copyItem = NSMenuItem(title: "Copy", action: #selector(copyCellContents(sender:)), keyEquivalent: "")
487 | menu.addItem(copyItem)
488 |
489 | if column == 0 {
490 | menu.addItem(NSMenuItem(title: "Copy Device Identifier", action: #selector(copyDeviceIdentifier(sender:)), keyEquivalent: ""))
491 | menu.addItem(NSMenuItem(title: "Copy Device Tooltip", action: #selector(copyDeviceTooltip(sender:)), keyEquivalent: ""))
492 | } else if column == 1 {
493 | menu.addItem(NSMenuItem(title: "Copy Service UUID", action: #selector(copyServiceUUID(sender:)), keyEquivalent: ""))
494 | } else if column == 2 {
495 | menu.addItem(NSMenuItem(title: "Copy Characteristic UUID", action: #selector(copyCharacteristicUUID(sender:)), keyEquivalent: ""))
496 | }
497 |
498 | return menu
499 | }
500 |
501 | @IBAction
502 | func copyCellContents(sender: Any) {
503 | guard let cell = browser.selectedCell() as? NSBrowserCell else { return }
504 |
505 | putPasteboardString(cell.title)
506 | }
507 |
508 | @IBAction
509 | func copyDeviceIdentifier(sender: Any) {
510 | guard let deviceIdentifier = selectedDevice?.peripheral.identifier else { return }
511 |
512 | putPasteboardString(deviceIdentifier.uuidString)
513 | }
514 |
515 | @IBAction
516 | func copyDeviceTooltip(sender: Any) {
517 | guard let device = selectedDevice else { return }
518 |
519 | putPasteboardString(tooltip(for: device))
520 | }
521 |
522 | @IBAction
523 | func copyServiceUUID(sender: Any) {
524 | guard let serviceUUID = selectedService?.uuid else { return }
525 |
526 | putPasteboardString(serviceUUID.uuidString)
527 | }
528 |
529 | @IBAction
530 | func copyCharacteristicUUID(sender: Any) {
531 | guard let characteristicUUID = selectedCharacteristic?.uuid else { return }
532 |
533 | putPasteboardString(characteristicUUID.uuidString)
534 | }
535 |
536 |
537 | private func putPasteboardString(_ string: String) {
538 | let pb = NSPasteboard.general
539 | pb.declareTypes([.string], owner: self)
540 | pb.setString(string, forType: .string)
541 | }
542 | }
543 |
544 | extension ViewController : ScannerDelegate {
545 | func scanner(_ scanner: Scanner, didUpdateDevices: [Device]) {
546 | reloadColumn(0)
547 | }
548 | }
549 |
550 | extension ViewController : DeviceDelegate {
551 | func deviceDidConnect(_ device: Device) {
552 | updateStatusLabel(for: device)
553 | }
554 |
555 | func deviceDidDisconnect(_ device: Device) {
556 | updateStatusLabel(for: device)
557 | }
558 |
559 | func deviceDidUpdateName(_ device: Device) {
560 | updateStatusLabel(for: device)
561 | guard let deviceIndex = visibleDevices.firstIndex(of: device) else { return }
562 | browser.reloadData(forRowIndexes: IndexSet(integer: deviceIndex), inColumn: 0)
563 | }
564 |
565 | func device(_ device: Device, updated services: [CBService]) {
566 | reloadColumn(1)
567 | if browser.selectionIndexPath?.count == 2 {
568 | browserAction(browser)
569 | } else {
570 | for service in services {
571 | if service.uuid == selectedService?.uuid {
572 | selectedService = service
573 | device.discoverCharacteristics(for: service)
574 | }
575 | }
576 | }
577 | }
578 |
579 | func device(_ device: Device, updated characteristics: [CBCharacteristic], for service: CBService) {
580 | reloadColumn(2)
581 | if browser.selectionIndexPath?.count == 3 {
582 | browserAction(browser)
583 | } else {
584 | for characteristic in characteristics {
585 | if characteristic.uuid == selectedCharacteristic?.uuid {
586 | selectedCharacteristic = characteristic
587 | if characteristic.properties.contains(.read) {
588 | device.read(characteristic: characteristic)
589 |
590 | }
591 | refreshCharacteristicDetail()
592 | }
593 | }
594 | }
595 | }
596 |
597 | func device(_ device: Device, updatedValueFor characteristic: CBCharacteristic) {
598 | if characteristic == selectedCharacteristic {
599 | characteristicUpdatedDate = Date()
600 | refreshCharacteristicDetail()
601 | }
602 | }
603 | }
604 |
--------------------------------------------------------------------------------
/Bluetility/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
461 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
--------------------------------------------------------------------------------