Skip to content

Commit

Permalink
Merge pull request flipperdevices#5 from evnik/InitialBluetooth
Browse files Browse the repository at this point in the history
Initial support for Bluetooth (devices scanning). First set of changes for flipperdevices#7
  • Loading branch information
evnik authored Oct 30, 2020
2 parents 5b122e7 + 9ab40bc commit d4e19d1
Show file tree
Hide file tree
Showing 21 changed files with 841 additions and 26 deletions.
6 changes: 5 additions & 1 deletion FlipperZero/.xcccr.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
FilterTargets = ["CoreTests"]
FilterPaths = ["Core/View"]
FilterPaths = [
"Core/Container/ObservableResolver.swift",
"Core/Service/Platform",
"Core/View"
]
ZeroWarnOnly = true
57 changes: 57 additions & 0 deletions FlipperZero/Core/Container/Container.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Container.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/23/20.
//

// TODO: Replace with well-known DI container or extend to support resolving with dependencies
class Container: Resolver {
private struct Key: Hashable {
private let type: Any.Type

init(_ type: Any.Type) {
self.type = type
}

func hash(into hasher: inout Hasher) {
ObjectIdentifier(self.type).hash(into: &hasher)
}

static func == (lhs: Container.Key, rhs: Container.Key) -> Bool {
lhs.type == rhs.type
}
}

private var factories = [Key: ServiceFactory]()

func register<Service>(_ builder: @escaping () -> Service, as type: Service.Type, isSingleton: Bool = false) {
self.factories[Key(type)] = isSingleton ? SingletonFactory(builder) : SingleUseFactory(builder)
}

func resolve<Service>(_ type: Service.Type) -> Service {
guard let factory = self.factories[Key(type)] else {
fatalError("Factory service for [\(type)] is not registered")
}

guard let service = factory.create() as? Service else {
fatalError("Service created by factory resolved for [\(type)] cannot be casted")
}

return service
}
}

extension Container {
func register<Service>(instance: Service) {
self.register(instance: instance, as: Service.self)
}

func register<Service>(instance: Service, as type: Service.Type) {
self.register({ instance }, as: type, isSingleton: true)
}

func register<Service>(_ builder: @escaping () -> Service, isSingleton: Bool = false) {
self.register(builder, as: Service.self, isSingleton: isSingleton)
}
}
21 changes: 21 additions & 0 deletions FlipperZero/Core/Container/ObservableResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// ObservableResolver.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/23/20.
//

import Combine

public class ObservableResolver: Resolver, ObservableObject {
private let container: Container

public init() {
self.container = Container()
self.container.register(BluetoothService.init, as: BluetoothConnector.self, isSingleton: true)
}

public func resolve<Service>(_ type: Service.Type) -> Service {
self.container.resolve(type)
}
}
10 changes: 10 additions & 0 deletions FlipperZero/Core/Container/Resolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// Resolver.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/23/20.
//

public protocol Resolver {
func resolve<Service>(_ type: Service.Type) -> Service
}
40 changes: 40 additions & 0 deletions FlipperZero/Core/Container/ServiceFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// ServiceFactory.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 9/3/20.
//

import Foundation

protocol ServiceFactory {
func create() -> Any
}

class SingletonFactory: ServiceFactory {
private let builder: () -> Any

init(_ builder: @escaping () -> Any) {
self.builder = builder
}

private lazy var value: Any = {
self.builder()
}()

func create() -> Any {
self.value
}
}

class SingleUseFactory: ServiceFactory {
private let builder: () -> Any

init(_ builder: @escaping () -> Any) {
self.builder = builder
}

func create() -> Any {
self.builder()
}
}
18 changes: 18 additions & 0 deletions FlipperZero/Core/Model/Bluetooth/BluetoothStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// BluetoothStatus.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/22/20.
//

enum BluetoothStatus: Equatable {
enum NotReadyReason: String {
case poweredOff
case preparing
case unauthorized
case unsupported
}

case ready
case notReady(NotReadyReason)
}
11 changes: 11 additions & 0 deletions FlipperZero/Core/Model/Bluetooth/Peripheral.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Peripheral.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/22/20.
//

struct Peripheral: EquatableById, Identifiable {
let id: UUID
let name: String
}
15 changes: 15 additions & 0 deletions FlipperZero/Core/Model/EquatableById.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// EquatableById.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/22/20.
//

protocol EquatableById: Equatable {
}

extension EquatableById where Self: Identifiable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}
14 changes: 14 additions & 0 deletions FlipperZero/Core/Service/BluetoothConnector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// BluetoothConnector.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/22/20.
//

protocol BluetoothConnector {
var peripherals: SafePublisher<[Peripheral]> { get }
var status: SafePublisher<BluetoothStatus> { get }

func startScanForPeripherals()
func stopScanForPeripherals()
}
100 changes: 100 additions & 0 deletions FlipperZero/Core/Service/Platform/BluetoothService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// BluetoothService.swift
// FlipperZero
//
// Created by Eugene Berdnikov on 8/22/20.
//

import CoreBluetooth

class BluetoothService: NSObject, BluetoothConnector {
private let manager: CBCentralManager
private let peripheralsSubject = SafeSubject([Peripheral]())
private let statusSubject = SafeSubject(BluetoothStatus.notReady(.preparing))

var peripherals: SafePublisher<[Peripheral]> {
self.peripheralsSubject.eraseToAnyPublisher()
}

private var peripheralsMap = [UUID: CBPeripheral]() {
didSet {
self.peripheralsSubject.value =
self.peripheralsMap.values.compactMap(Peripheral.init).sorted { $0.name < $1.name }
}
}

var status: SafePublisher<BluetoothStatus> {
self.statusSubject.eraseToAnyPublisher()
}

override init() {
self.manager = CBCentralManager()
super.init()
self.manager.delegate = self
}

func startScanForPeripherals() {
if self.statusSubject.value == .ready {
// TODO: Provide CBUUID relevant to Flipper devices
self.manager.scanForPeripherals(withServices: nil)
}
}

func stopScanForPeripherals() {
if self.manager.isScanning {
self.peripheralsMap.removeAll()
self.manager.stopScan()
}
}
}

extension BluetoothService: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ manager: CBCentralManager) {
let status = BluetoothStatus(manager.state)
self.statusSubject.value = status
if status != .ready {
self.peripheralsMap.removeAll()
}
}

func centralManager(
_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi: NSNumber
) {
if self.peripheralsMap[peripheral.identifier] == nil,
let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool,
isConnectable {

self.peripheralsMap[peripheral.identifier] = peripheral
}
}
}

fileprivate extension BluetoothStatus {
init(_ source: CBManagerState) {
switch source {
case .resetting, .unknown:
self = .notReady(.preparing)
case .unsupported:
self = .notReady(.unsupported)
case .unauthorized:
self = .notReady(.unauthorized)
case .poweredOff:
self = .notReady(.poweredOff)
case .poweredOn:
self = .ready
@unknown default:
self = .notReady(.unsupported)
}
}
}

fileprivate extension Peripheral {
init?(_ source: CBPeripheral) {
guard let name = source.name else {
return nil
}

self.id = source.identifier
self.name = name
}
}
Loading

0 comments on commit d4e19d1

Please sign in to comment.