1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | // // ViewController.swift // iBeacon // // Created by nina on 2025/4/15. Updated by ChatGPT // import UIKit import CoreLocation import CoreMotion class ViewController: UIViewController, CLLocationManagerDelegate { // MARK: - IBOutlet @IBOutlet weak var monitorResultTextView: UITextView! @IBOutlet weak var rangingResultTextView: UITextView! @IBOutlet weak var manualXField: UITextField! @IBOutlet weak var manualYField: UITextField! @IBOutlet weak var exportStatusLabel: UILabel! @IBOutlet weak var positionStatusTextView: UITextView! @IBOutlet weak var mapView: UIView! // 地圖底圖 var markerView: UIView = UIView() // 定位點 // MARK: - Beacon + Motion var locationManager: CLLocationManager = CLLocationManager() let motionManager = CMMotionManager() let uuid = "77C99B62-88FC-4C78-B39C-D6FE06E76372" let identifier = "esd region" var region: CLBeaconRegion! var beaconData: [[String: Any]] = [] var isCollecting = true override func viewDidLoad() { super.viewDidLoad() locationManager.delegate = self if CLLocationManager.isMonitoringAvailable(for: CLBeaconRegion.self) { if #available(iOS 14, *) { let currentStatus = locationManager.authorizationStatus if currentStatus != .authorizedAlways { locationManager.requestAlwaysAuthorization() } } else { let currentStatus = CLLocationManager.authorizationStatus() if currentStatus != .authorizedAlways { locationManager.requestAlwaysAuthorization() } } } region = CLBeaconRegion(uuid: UUID(uuidString: uuid)!, identifier: identifier) region.notifyEntryStateOnDisplay = true region.notifyOnEntry = true region.notifyOnExit = true locationManager.startMonitoring(for: region) if motionManager.isAccelerometerAvailable { motionManager.startAccelerometerUpdates() } if motionManager.isGyroAvailable { motionManager.startGyroUpdates() } // 初始化定位點視圖 markerView.frame = CGRect(x: 0, y: 0, width: 12, height: 12) markerView.layer.cornerRadius = 6 markerView.backgroundColor = .systemRed markerView.isHidden = true mapView.addSubview(markerView) } // MARK: - CLLocationManagerDelegate func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) { switch state { case .inside: monitorResultTextView.text = "state inside\n" + monitorResultTextView.text manager.startRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: UUID(uuidString: uuid)!)) case .outside: monitorResultTextView.text = "state outside\n" + monitorResultTextView.text manager.stopRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: UUID(uuidString: uuid)!)) default: break } } func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { if !isCollecting { return } rangingResultTextView.text = "" let ordered = beacons.filter { $0.rssi != 0 }.sorted { $0.rssi > $1.rssi } for beacon in ordered { var proximityString = "" switch beacon.proximity { case .far: proximityString = "far" case .near: proximityString = "near" case .immediate: proximityString = "immediate" default: proximityString = "unknown" } rangingResultTextView.text += """ Major: \(beacon.major) Minor: \(beacon.minor) RSSI: \(beacon.rssi) Proximity: \(proximityString) Accuracy: \(beacon.accuracy) """ let minor = beacon.minor.intValue let major = beacon.major.intValue let rssi = beacon.rssi let (mode, estX, estY) = estimatePosition(major: major, minor: minor, rssi: rssi) let manualX = Double(manualXField.text ?? "") ?? -1 let manualY = Double(manualYField.text ?? "") ?? -1 let timestamp = Date() let acc = motionManager.accelerometerData let gyro = motionManager.gyroData let logLine = "\(timestamp) [\(mode)] → (x: \(estX), y: \(estY)) RSSI: \(rssi) M: \(major)-\(minor)\n" positionStatusTextView.text = logLine + positionStatusTextView.text updateMarker(x: estX, y: estY) let record: [String: Any] = [ "timestamp": timestamp, "uuid": beacon.uuid.uuidString, "major": beacon.major, "minor": beacon.minor, "rssi": beacon.rssi, "accuracy": beacon.accuracy, "mode": mode, "est_x": estX, "est_y": estY, "manual_x": manualX, "manual_y": manualY, "acc_x": acc?.acceleration.x ?? 0, "acc_y": acc?.acceleration.y ?? 0, "acc_z": acc?.acceleration.z ?? 0, "gyro_x": gyro?.rotationRate.x ?? 0, "gyro_y": gyro?.rotationRate.y ?? 0, "gyro_z": gyro?.rotationRate.z ?? 0 ] beaconData.append(record) } } func updateMarker(x: Double, y: Double) { // 假設地圖比例:x = 0~20 對應畫面寬,y = 0~12 對應畫面高 let mapW = mapView.bounds.width let mapH = mapView.bounds.height let scaleX = mapW / 20.0 let scaleY = mapH / 12.0 let px = CGFloat(x) * scaleX let py = mapH - CGFloat(y) * scaleY // y 軸反過來顯示 markerView.center = CGPoint(x: px, y: py) markerView.isHidden = false } func estimatePosition(major: Int, minor: Int, rssi: Int) -> (String, Double, Double) { if major == 1 { let map: [Int: (Double, Double, String)] = [ 1: (2.21, 0, "C"), 2: (4.3, 5.7, "B"), 3: (4.3, 7.8, "A"), 4: (2.64, 11.75, "A") ] let val = map[minor] ?? (0, 0, "Unknown") return ("區域 \(val.2)", val.0, val.1) } else if major == 2 { let posMap: [Int: (Double, Double)] = [ 1: (0, 0), 2: (2.6, 2.67), 3: (5.71, 2.67), 4: (9.7, 2.67), 5: (13.5, 2.67), 6: (15.1, 0), 7: (16.9, 3.9), 8: (18.1, 0) ] let power = -59.0 let distance = pow(10.0, (power - Double(rssi)) / 20.0) let coord = posMap[minor] ?? (0, 0) return ("座標推估", coord.0, coord.1) } return ("未知模式", 0, 0) } @IBAction func stopCollection(_ sender: Any) { isCollecting = false exportCSV() } func exportCSV() { var csv = "timestamp,uuid,major,minor,rssi,accuracy,mode,est_x,est_y,manual_x,manual_y,acc_x,acc_y,acc_z,gyro_x,gyro_y,gyro_z\n" for row in beaconData { csv += "\(row["timestamp"]!),\(row["uuid"]!),\(row["major"]!),\(row["minor"]!),\(row["rssi"]!),\(row["accuracy"]!),\(row["mode"]!),\(row["est_x"]!),\(row["est_y"]!),\(row["manual_x"]!),\(row["manual_y"]!),\(row["acc_x"]!),\(row["acc_y"]!),\(row["acc_z"]!),\(row["gyro_x"]!),\(row["gyro_y"]!),\(row["gyro_z"]!)\n" } let fm = FileManager.default let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("beacon_data.csv") do { try csv.write(to: url, atomically: true, encoding: .utf8) exportStatusLabel.text = "✅ 匯出成功:\(url.lastPathComponent)" shareCSV(at: url) } catch { exportStatusLabel.text = "❌ 匯出失敗" } } func shareCSV(at url: URL) { let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) activityVC.popoverPresentationController?.sourceView = self.view present(activityVC, animated: true) } } |
Direct link: https://paste.plurk.com/show/fJrUNvLUHjMAR9zEVrjg