Machineboy空

아이폰 2대 간 거리 탐지하기, iBeacon 본문

언어/iOS

아이폰 2대 간 거리 탐지하기, iBeacon

안녕도라 2024. 10. 31. 17:34

 

아이폰 2대를 가지고 A는 iBeacon으로 사용하고, B는 그 비콘을 탐지하는 기기로 사용하고 싶다.

A에는 전파 느낌의 UI를 띄우고, B에는 A와의 실시간 거리 상태를 표시해보겠다.


시도한 방법 1: CoreBlueTooth 사용

튜토리얼

https://www.youtube.com/watch?v=WFl4tnNWXP0

 

주변에 블루투스가 감지된 기기들을 띄워준다.

테스트 코드

우선 Info.plist에 Privacy-Bluetooth Always Usage Description 을 추가해 준다.

import SwiftUI
import CoreBluetooth

struct BeaconScanner: View {
    @StateObject private var beaconScannerClass = BeaconScannerClass()
    var body: some View {
        Text("List of scanned devices")
            .font(.largeTitle)
            .underline(color: .red)
        
        List(beaconScannerClass.discoveredBeacons, id: \.identifier){ beacon in
            Text(/*beacon.name ?? */beacon.identifier.uuidString)
        }
        Text("thisDevice " + beaconScannerClass.getDeviceUUID())
    }
}

class BeaconScannerClass: NSObject, ObservableObject, CBCentralManagerDelegate{
    var centralManager: CBCentralManager!
    @Published var discoveredBeacons: [CBPeripheral] = []
    
    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            startScanning()
        }else{
            stopScanning()
        }
    }
    
    func startScanning(){
        guard let centralManager = centralManager else {return}
        
        if centralManager.state == .poweredOn {
            let uuids: [CBUUID] = []
            let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
            centralManager.scanForPeripherals(withServices: uuids, options: options)
        }
    }
    
    func stopScanning(){
        centralManager.stopScan()
        
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if !discoveredBeacons.contains(peripheral){
            if RSSI.intValue > -40 {
                discoveredBeacons.append(peripheral)
            }
        }
    }
    
    func getDeviceUUID() -> String {
        print(UIDevice.current.identifierForVendor!.uuidString)
        return UIDevice.current.identifierForVendor!.uuidString
    }
}

#Preview {
    BeaconScanner()
}

 

결과 :

이 방법으로 기기간 거리를 감지하려면, 블루투스의 신호 세기로 거리를 가늠해야 한다.

이때 사용되는 개념이 RSSI인데, 아래 블로그에 개념이 잘 설명되어 있다.

뭔가 특정 기기를 판별해내는 방법으로는 적절하지 않은듯 하고, beacon을 사용해보고 싶었던 거라 탈락!

 

 

* RSSI (Received Signal Strength Indicator)는 블루투스 통신에서 신호의 강도를 측정하는 값입니다. 일반적으로 데시벨 밀리와트(dBm) 단위로 측정되며, 음수 값으로 표시된다. RSSI 값을 이용하여 두 블루투스 장치 간의 거리를 추정할 수 있다. 이는 주로 실내 위치 추적 시스템에서 많이 사용된다. 다만, RSSI 값은 주변 환경(벽, 장애물 등)에 영향을 받기 때문에, 정확한 거리 측정을 위해 보정이 필요하다.

 

https://velog.io/@maddie/iOS-CoreBluetooth-2-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%BD%94%EB%93%9C-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EC%99%80-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC

 

[iOS] CoreBluetooth (2) 키워드, 코드 프로퍼티와 메서드 정리

https://velog.io/@maddie/iOS-CoreBluetooth-1-%EA%B0%9C%EC%9A%94저번엔 CoreBluetooth 이론적인 내용에 대해 다뤘다고 한다면, 오늘은 진짜 써야 되는 실무적인 내용과 알아야 할 키워드를 다뤄보려고 합니다

velog.io


시도한 방법 2:  CoreLocation 사용


튜토리얼


내 기기를 비콘으로 만들고, UUID를 설정하는 방법은 아래 블로그,

해당 UUID를 가진 비콘을 감지하고, 테스트를 하기 위한 UI 등은 아래 영상을 참고했다.

 

https://onve.tistory.com/entry/Swift-Swift%EC%97%90%EC%84%9C-Beacon%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

 

[Swift] Swift에서 Beacon을 사용하는 방법

실내에서 위치를 파악할 수 있는 방법에는 여러가지가 있습니다. 그 중iBeacon을 사용하는 것은 굉장히 간단합니다.최근, 프로젝트 개발을 위하여 SwiftUI를 기반으로 iBeacon을 사용하게 되었습니다.

onve.tistory.com

https://www.youtube.com/watch?v=lCNpEaZiKqU

 

 


 

관련 개념

1) UUID 란?

UUID, universally unique Identifier로 각 기기에 부여되는 고유한 디바이스 식별값.

 

2) iBeacon이 방출하는 정보

  • UUID 
  • major: 128bit
  • minor: 16bit unsigned integer

UUID는 Top-level의 정보이고, major, minor는 더 세부적인 값이라고 보면 된다.

UUID가 같아도 major, minor값으로 세부 건물을 분류할 수 있다.

 

튜토리얼을 참고하니 이런 관계성을 가지는 느낌이다.

UUID 홈플러스
major 홈플러스_포항점, 홈플러스_서울점 
minor 홈플러스_포항점_베스킨라빈스, 홈플러스_포항점_롯데시네마

 

 

UUID 관련 정보는 아래 티스토리, iBeacon관련 개념은 kodeco 튜토리얼을 참고하였다.

https://hilily.tistory.com/80

 

[iOS] UUID와 UDID

디바이스의 고유한 값을 통해 무언가를 식별하기위한 고유한 값이 필요할 수 있다. 이때 사용되는 개념으로 UUID와 UDID가 있다. 1. UDID (Unique Device Identifier) 각 기기에 부여되는 고유한 디바이스 식

hilily.tistory.com

 

https://www.kodeco.com/632-ibeacon-tutorial-with-ios-and-swift

 

iBeacon Tutorial with iOS and Swift

Learn how you can find an iBeacon around you, determine its proximity, and send notifications when it moves away from you.

www.kodeco.com


구조

  1. 비콘용 A 기기에는 BeaconTransmitter을 빌드
  2. 탐지용 B 기기에서는 BeaconDetector를 빌드

 

 

테스트 코드 

블루투스 권한과 위치 권한을 허용해줘야 한다!

Beacon용 스크립트

import SwiftUI
import CoreBluetooth
import CoreLocation

class BeaconTransmitter: NSObject, ObservableObject, CBPeripheralManagerDelegate {
    var peripheralManager: CBPeripheralManager?
    var beaconRegion: CLBeaconRegion?
    var beaconIdentityConstraint: CLBeaconIdentityConstraint?
    
    override init() {
        super.init()
        
        let uuid = UUID(uuidString: "DA1D2EFE-A565-5BD8-B301-6397766AAF26")!
        let major: CLBeaconMajorValue = 1000
        let minor: CLBeaconMinorValue = 1
        let beaconID = "Beacon"
        
        beaconIdentityConstraint = CLBeaconIdentityConstraint(uuid: uuid, major: major, minor: minor)
        beaconRegion = CLBeaconRegion(beaconIdentityConstraint: beaconIdentityConstraint!, identifier: beaconID)
        peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: nil)
    }
    
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        if peripheral.state == .poweredOn {
            startAdvertising()
        } else {
            stopAdvertising()
        }
    }
    
    func startAdvertising() {
        print("startAdverting")
        guard let beaconRegion = beaconRegion else { return }
        
        let peripheralData = beaconRegion.peripheralData(withMeasuredPower: nil)
        peripheralManager?.startAdvertising(((peripheralData as NSDictionary) as! [String: Any]))
    }
    
    func stopAdvertising() {
        peripheralManager?.stopAdvertising()
    }
}

struct BeaconView: View {
    @ObservedObject var transmitter = BeaconTransmitter()
    
    var body: some View {
        ZStack{
            WaveEffectView()
                .frame(width: 300, height: 300)
            Text("비콘 신호 전송중~")
        }
        
    }
}


struct WaveEffectView: View {
    @State private var animate = false
    
    var body: some View {
        ZStack {
            ForEach(0..<4) { index in
                Circle()
                    .stroke(lineWidth: 3)
                    .foregroundColor(Color.blue.opacity(0.4))
                    .frame(width: animate ? 200 : 50, height: animate ? 200 : 50)
                    .scaleEffect(animate ? 2.0 : 0.5)
                    .opacity(animate ? 0 : 1)
                    .animation(
                        Animation.easeOut(duration: 2.0)
                            .repeatForever(autoreverses: false)
                            .delay(Double(index) * 0.3)
                    )
            }
        }
        .onAppear {
            animate = true
        }
    }
}

탐지기용 스크립트

  • Ranging: Ranging is the process of reading the characteristics of a beacon region, such as signal strength, advertising interval, and measured power.

공식 문서에 나와있는 주석!

let beaconRegion = CLBeaconRegion(uuid: uuid, identifier: uuidString)
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: uuid))
  • Monitoring:정확한 정의는 나와있지 않지만, 좀 더 넓은 개념에서 beacon을 탐색하고 그다음 ranging으로 비컨별 세부 정보를 받아내는 프로세스인가? 싶다.

import Combine
import CoreLocation
import SwiftUI

class BeaconDetector: NSObject, ObservableObject, CLLocationManagerDelegate {
    var didChange = PassthroughSubject<Void, Never>()
    var locationManager: CLLocationManager?
    @Published var lastDistance = CLProximity.unknown
    
    override init() {
        super.init()
        locationManager = CLLocationManager()
        locationManager?.delegate = self
        locationManager?.requestWhenInUseAuthorization()
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if status == .authorizedWhenInUse {
            if CLLocationManager.isMonitoringAvailable(for:  CLBeaconRegion.self){
                print("isMonitoringAvailable")
                
                if(CLLocationManager.isRangingAvailable()){
                    
                    print("isRangingAvailable")
                    startScanning()
                }
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
        
        print("탐지된 비콘의 수는: \(beacons.count)" )
        if let beacon = beacons.first {
            update(distance: beacon.proximity)
        }else{
            update(distance: .unknown)
        }
    }
    
    func startScanning() {
        print("startScanning")
        
        // Transmitter에서 설정한 값 그대로 넣어주기!
        let uuid = UUID(uuidString: "DA1D2EFE-A565-5BD8-B301-6397766AAF26")!// 테스트 UUID  
        let constraint = CLBeaconIdentityConstraint(uuid: uuid, major: 1000, minor: 1)
        let beaconRegion = CLBeaconRegion(beaconIdentityConstraint: constraint, identifier: "MacBook")
        
        locationManager?.startMonitoring(for: beaconRegion)
        locationManager?.startRangingBeacons(satisfying: constraint)
    }
    
    
    func update(distance: CLProximity){
        lastDistance = distance
        didChange.send(())
    }
}

struct BigText: ViewModifier{
    func body(content: Content) -> some View {
        content
        .font(Font.system(size: 72, design: .rounded))
        .frame(minWidth: 0,maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
    }
}

struct ContentView: View {
    @ObservedObject var detector = BeaconDetector()
    
    var body: some View {
        VStack {
            if detector.lastDistance == .immediate {
                Text("Right here")
                    .modifier(BigText())
                    .background(Color.green)
                    .edgesIgnoringSafeArea(.all)
            }else if detector.lastDistance == .near {
                Text("Near")
                    .modifier(BigText())
                    .background(Color.yellow)
                    .edgesIgnoringSafeArea(.all)
            }else if detector.lastDistance == .far {
                Text("far")
                    .modifier(BigText())
                    .background(Color.orange)
                    .edgesIgnoringSafeArea(.all)
            }else{
                Text("UNKNOWN")
                    .modifier(BigText())
                    .background(Color.gray)
                    .edgesIgnoringSafeArea(.all)
            }      
        }       
    }
}


#Preview {
    ContentView()
}
//ver.2
import Combine
import CoreLocation
import SwiftUI

class BeaconDetector: NSObject, ObservableObject, CLLocationManagerDelegate {
    private var locationManager: CLLocationManager
    @Published var detectedBeacons: [CLBeacon] = []
    @Published var lastDistance: CLProximity = .unknown

    override init() {
        self.locationManager = CLLocationManager()
        super.init()
        self.locationManager.delegate = self
        self.locationManager.requestWhenInUseAuthorization()
    }

    func startScanning() {
        
        let beaconRegion = CLBeaconRegion(uuid: UUID(uuidString: "DA1D2EFE-A565-5BD8-B301-6397766AAF26")!,
                                          identifier: "동일하지 않아도 인식합니다.")
        self.locationManager.startMonitoring(for: beaconRegion)
        self.locationManager.startRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconRegion.uuid))
    }

    func stopScanning() {
        let beaconRegion = CLBeaconRegion(uuid: UUID(uuidString: "당신의 UUID")!,
                                          identifier: "동일하지 않아도 인식합니다.")
        self.locationManager.stopMonitoring(for: beaconRegion)
        self.locationManager.stopRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconRegion.uuid))
    }
    
    func locationManager(_ manager: CLLocationManager, didRange beacons: [CLBeacon], satisfying beaconConstraint: CLBeaconIdentityConstraint) {
            if let beacon = beacons.first {
                print("비콘 감지 완")
                lastDistance = beacon.proximity // 비콘의 거리 정보를 업데이트
            } else {
                lastDistance = .unknown // 비콘이 감지되지 않으면 거리 정보를 unknown으로 설정
            }
        }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if status == .authorizedAlways || status == .authorizedWhenInUse {
            print("권한 인정")
            startScanning()
        } else {
            stopScanning()
        }
    }
}

struct BigText: ViewModifier{
    func body(content: Content) -> some View {
        content
        .font(Font.system(size: 72, design: .rounded))
        .frame(minWidth: 0,maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
    }
}

struct ContentView: View {
    @ObservedObject var detector = BeaconDetector()
    
    var body: some View {
        VStack {
            if detector.lastDistance == .immediate {
                Text("Right here")
                    .modifier(BigText())
                    .background(Color.green)
                    .edgesIgnoringSafeArea(.all)
            }else if detector.lastDistance == .near {
                Text("Near")
                    .modifier(BigText())
                    .background(Color.yellow)
                    .edgesIgnoringSafeArea(.all)
            }else if detector.lastDistance == .far {
                Text("far")
                    .modifier(BigText())
                    .background(Color.orange)
                    .edgesIgnoringSafeArea(.all)
            }else{
                Text("UNKNOWN")
                    .modifier(BigText())
                    .background(Color.gray)
                    .edgesIgnoringSafeArea(.all)
            }      
        }       
    }
}


#Preview {
    ContentView()
}

결과 :

enum 형으로 반환되다보니, 정확한 m값을 알 수는 없지만,

near이면 이벤트가 발생하도록 구성하면 될 것 같다.