├── Screenshots ├── demo.png ├── sandbox.png └── entitlements.png ├── Demo ├── SerialGateDemo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── SerialGateDemo.entitlements │ ├── SerialGateDemoApp.swift │ ├── ContentView.swift │ └── ContentViewModel.swift ├── Package.swift └── SerialGateDemo.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── Sources ├── SerialGate │ ├── SGParity.swift │ ├── SGPortState.swift │ ├── Combine+Extension.swift │ ├── termios+Extension.swift │ ├── SGError.swift │ ├── SGSerialDeviceDetector.swift │ ├── SGPortManager.swift │ └── SGPort.swift └── Logput │ └── logput.swift ├── .gitignore ├── Arduino └── TestForSerialGate.ino ├── Package.swift ├── LICENSE └── README.md /Screenshots/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/SerialGate/HEAD/Screenshots/demo.png -------------------------------------------------------------------------------- /Screenshots/sandbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/SerialGate/HEAD/Screenshots/sandbox.png -------------------------------------------------------------------------------- /Screenshots/entitlements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyome22/SerialGate/HEAD/Screenshots/entitlements.png -------------------------------------------------------------------------------- /Demo/SerialGateDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/SerialGate/SGParity.swift: -------------------------------------------------------------------------------- 1 | public enum SGParity: Int, Sendable { 2 | case none 3 | case even 4 | case odd 5 | } 6 | -------------------------------------------------------------------------------- /Sources/SerialGate/SGPortState.swift: -------------------------------------------------------------------------------- 1 | public enum SGPortState: Sendable { 2 | case open 3 | case closed 4 | case sleeping 5 | case removed 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # Xcode 5 | xcuserdata/ 6 | *.xcuserstate 7 | 8 | # Swift Package Manager 9 | Packages.resolved 10 | .swiftpm/ 11 | .build/ 12 | -------------------------------------------------------------------------------- /Demo/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "", 6 | products: [], 7 | targets: [] 8 | ) 9 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/SerialGate/Combine+Extension.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension AnyCancellable: @retroactive @unchecked Sendable {} 4 | extension PassthroughSubject: @retroactive @unchecked Sendable {} 5 | extension CurrentValueSubject: @retroactive @unchecked Sendable {} 6 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Logput/logput.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func logput( 4 | _ items: Any..., 5 | file: String = #file, 6 | line: Int = #line, 7 | function: String = #function 8 | ) { 9 | #if DEBUG 10 | let fileName = URL(fileURLWithPath: file).lastPathComponent 11 | var array: [Any] = ["💫Log: \(fileName)", "Line:\(line)", function] 12 | array.append(contentsOf: items) 13 | Swift.print(array) 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo/SerialGateDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.device.usb 8 | 9 | com.apple.security.device.serial 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo/SerialGateDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SerialGateDemoApp.swift 3 | // SerialGateDemo 4 | // 5 | // Created by Takuto Nakamura on 2024/02/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct SerialGateDemoApp: App { 12 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView() 17 | } 18 | .defaultSize(width: 400, height: 400) 19 | } 20 | } 21 | 22 | final class AppDelegate: NSObject, NSApplicationDelegate { 23 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } 24 | } 25 | -------------------------------------------------------------------------------- /Arduino/TestForSerialGate.ino: -------------------------------------------------------------------------------- 1 | const int LED_PIN = 13; 2 | boolean flag = false; 3 | 4 | void setup() { 5 | Serial.begin(9600); 6 | Serial.setTimeout(1000); 7 | pinMode(LED_PIN, OUTPUT); 8 | digitalWrite(LED_PIN, LOW); 9 | } 10 | 11 | void loop() { 12 | String input = Serial.readString(); 13 | if (input.compareTo("ON") == 0) { 14 | flag = true; 15 | digitalWrite(LED_PIN, HIGH); 16 | } else if (input.compareTo("OFF") == 0) { 17 | flag = false; 18 | digitalWrite(LED_PIN, LOW); 19 | } else if (input.length() > 0) { 20 | Serial.println(input); 21 | } else { 22 | if (flag) { 23 | Serial.println("ON"); 24 | } else { 25 | Serial.println("OFF"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let swiftSettings: [SwiftSetting] = [ 6 | .enableUpcomingFeature("ExistentialAny"), 7 | ] 8 | 9 | let package = Package( 10 | name: "SerialGate", 11 | platforms: [ 12 | .macOS(.v13) 13 | ], 14 | products: [ 15 | .library( 16 | name: "Logput", 17 | targets: ["Logput"] 18 | ), 19 | .library( 20 | name: "SerialGate", 21 | targets: ["SerialGate"] 22 | ), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "Logput", 27 | swiftSettings: swiftSettings 28 | ), 29 | .target( 30 | name: "SerialGate", 31 | dependencies: ["Logput"], 32 | swiftSettings: swiftSettings 33 | ) 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Sources/SerialGate/termios+Extension.swift: -------------------------------------------------------------------------------- 1 | import Darwin 2 | 3 | extension termios { 4 | mutating func updateC_CC(_ n: Int32, v: UInt8) { 5 | switch n { 6 | case 0_: c_cc.0 = v 7 | case 1_: c_cc.1 = v 8 | case 2_: c_cc.2 = v 9 | case 3_: c_cc.3 = v 10 | case 4_: c_cc.4 = v 11 | case 5_: c_cc.5 = v 12 | case 6_: c_cc.6 = v 13 | case 7_: c_cc.7 = v 14 | case 8_: c_cc.8 = v 15 | case 9_: c_cc.9 = v 16 | case 10: c_cc.10 = v 17 | case 11: c_cc.11 = v 18 | case 12: c_cc.12 = v 19 | case 13: c_cc.13 = v 20 | case 14: c_cc.14 = v 21 | case 15: c_cc.15 = v 22 | case 16: c_cc.16 = v 23 | case 17: c_cc.17 = v 24 | case 18: c_cc.18 = v 25 | case 19: c_cc.19 = v 26 | default: break 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TakutoNakamura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SerialGate/SGError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum SGError: LocalizedError, Sendable { 4 | case failedToOpenPort(String) 5 | case portIsNotOpen(String) 6 | case failedToClosePort(String) 7 | case failedToSetOptions(String) 8 | case failedToEncodeText(String) 9 | case failedToEncodeData(String) 10 | case failedToWriteData(String) 11 | 12 | public var errorDescription: String? { 13 | switch self { 14 | case let .failedToOpenPort(portName): 15 | "Failed to open port (\(portName))." 16 | case let .portIsNotOpen(portName): 17 | "Port (\(portName)) is not open." 18 | case let .failedToClosePort(portName): 19 | "Failed to close port (\(portName))." 20 | case let .failedToSetOptions(portName): 21 | "Failed to set options (port \(portName))." 22 | case let .failedToEncodeText(portName): 23 | "Failed to encode received data to string (port \(portName))." 24 | case let .failedToEncodeData(portName): 25 | "Failed to encode received string to data (port \(portName))." 26 | case let .failedToWriteData(portName): 27 | "Failed to write data (port \(portName))." 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SerialGate 2 | 3 | Serial Communication Library for macOS written in Swift. 4 | 5 | ## Requirements 6 | 7 | - Development with Xcode 16.0+ 8 | - Written in Swift 6.0 9 | - Compatible with macOS 13.0+ 10 | 11 | ## Installation 12 | 13 | 1. DependencyList is available through [Swift Package Manager](https://github.com/apple/swift-package-manager). 14 | 2. Put a check mark for "USB" in Capabilities of Targets (SandBox) 15 | sandbox 16 | 3. Edit the entitlements and add `com.apple.security.device.serial` 17 | entitlements 18 | 19 | ## Demo 20 | 21 | Serial Communication Demo App for Arduino is in this Project. 22 | 23 | demo 24 | 25 | Sample Arduino code is [here](Arduino/TestForSerialGate.ino). 26 | 27 | ## Usage 28 | 29 | - Get serial ports 30 | 31 | ```swift 32 | import SerialGate 33 | 34 | Task { 35 | for await ports in SGPortManager.shared.availablePortsStream { 36 | // get ports 37 | } 38 | } 39 | ``` 40 | 41 | - Open a serial port 42 | 43 | ```swift 44 | try? port.set(baudRate: B9600) 45 | try? port.open() 46 | ``` 47 | 48 | - Close a serial port 49 | 50 | ```swift 51 | try? port.close() 52 | ``` 53 | 54 | - Send a message 55 | 56 | ```swift 57 | try? port.send(text: "Hello World") 58 | // or 59 | try? port.send(data: Data()) 60 | ``` 61 | 62 | - Read messages 63 | 64 | ```swift 65 | Task { 66 | for await result in port.textStream { 67 | switch result { 68 | case let .success(text): 69 | Swift.print(text) 70 | case let .failure(error): 71 | Swift.print(error.localizedDescription) 72 | } 73 | } 74 | } 75 | // or 76 | Task { 77 | for await result in port.dataStream { 78 | switch result { 79 | case let .success(data): 80 | // use data 81 | case let .failure(error): 82 | Swift.print(error.localizedDescription) 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | - Notifications about Port State 89 | 90 | ```swift 91 | Task { 92 | for await portState in port.portStateStream { 93 | Swift.print(String(describing: portState)) 94 | } 95 | } 96 | ``` 97 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SerialGateDemo 4 | // 5 | // Created by Takuto Nakamura on 2024/02/29. 6 | // 7 | 8 | import SwiftUI 9 | import SerialGate 10 | 11 | struct ContentView: View { 12 | @StateObject var viewModel = ContentViewModel() 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | HStack { 17 | Picker(selection: Binding( 18 | get: { viewModel.port }, 19 | set: { viewModel.select(port: $0) } 20 | )) { 21 | Text("Select port") 22 | .tag(SGPort?.none) 23 | ForEach(viewModel.portList) { port in 24 | Text(port.name) 25 | .tag(SGPort?.some(port)) 26 | } 27 | } label: { 28 | Text("Available ports:") 29 | } 30 | .disabled(viewModel.portIsOpening) 31 | Button { 32 | viewModel.openPort() 33 | } label: { 34 | Text("Open") 35 | } 36 | .disabled(viewModel.portIsOpening) 37 | Button { 38 | viewModel.closePort() 39 | } label: { 40 | Text("Close") 41 | } 42 | .disabled(!viewModel.portIsOpening) 43 | } 44 | HStack { 45 | TextField(text: $viewModel.inputText) { 46 | Text("Text you want to send to the port") 47 | } 48 | Button { 49 | viewModel.sendPort() 50 | } label: { 51 | Text("Send") 52 | } 53 | .disabled(!viewModel.portIsOpening) 54 | } 55 | LabeledContent { 56 | Text(viewModel.stateText) 57 | .foregroundStyle(Color.secondary) 58 | } label: { 59 | Text(verbatim: "Port state:") 60 | } 61 | Divider() 62 | ScrollViewReader { proxy in 63 | ScrollView(.vertical) { 64 | Text(verbatim: viewModel.receivedText) 65 | .multilineTextAlignment(.leading) 66 | .frame(maxWidth: .infinity, alignment: .leading) 67 | .padding(4) 68 | Spacer() 69 | .frame(maxWidth: .infinity) 70 | .id("bottomSpacer") 71 | } 72 | .border(Color.gray) 73 | .onChange(of: viewModel.receivedText) { _ in 74 | withAnimation { 75 | proxy.scrollTo("bottomSpacer") 76 | } 77 | } 78 | } 79 | } 80 | .frame(minWidth: 400, minHeight: 400) 81 | .padding() 82 | .onAppear { 83 | viewModel.onAppear() 84 | } 85 | .onDisappear { 86 | viewModel.onDisappear() 87 | } 88 | } 89 | } 90 | 91 | #Preview { 92 | ContentView() 93 | } 94 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo/ContentViewModel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | ContentViewModel.swift 3 | SerialGateDemo 4 | 5 | Created by Takuto Nakamura on 2024/02/29. 6 | 7 | */ 8 | 9 | import Logput 10 | import SwiftUI 11 | import SerialGate 12 | 13 | @MainActor 14 | final class ContentViewModel: ObservableObject { 15 | private let portManager = SGPortManager.shared 16 | private var availablePortsTask: Task? 17 | private var currentPortTask: Task? 18 | 19 | @Published var portList = [SGPort]() 20 | @Published var port: SGPort? 21 | @Published var portIsOpening = false 22 | @Published var receivedText = "" 23 | @Published var inputText = "" 24 | @Published var stateText = "stand-by" 25 | 26 | init() {} 27 | 28 | func onAppear() { 29 | availablePortsTask = Task { 30 | for await portList in portManager.availablePortsStream { 31 | update(portList: portList) 32 | } 33 | } 34 | } 35 | 36 | func onDisappear() { 37 | closePort() 38 | availablePortsTask?.cancel() 39 | currentPortTask?.cancel() 40 | } 41 | 42 | private func update(portList: [SGPort]) { 43 | self.portList = portList 44 | if let port, !portList.contains(port) { 45 | select(port: nil) 46 | } 47 | } 48 | 49 | func select(port: SGPort?) { 50 | self.port = port 51 | if let port { 52 | currentPortTask = Task { 53 | await withTaskGroup(of: Void.self) { group in 54 | group.addTask { [weak self] in 55 | for await portState in port.portStateStream { 56 | guard let self else { return } 57 | await MainActor.run { 58 | stateText = "Port: \(port.name) - \(String(describing: portState))" 59 | } 60 | } 61 | } 62 | group.addTask { [weak self] in 63 | for await result in port.textStream { 64 | switch result { 65 | case let .success(text): 66 | guard let self else { return } 67 | await MainActor.run { 68 | receivedText += text 69 | } 70 | case let .failure(error): 71 | logput(error.localizedDescription) 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } else { 78 | currentPortTask?.cancel() 79 | } 80 | } 81 | 82 | func openPort() { 83 | guard let port, !portIsOpening else { return } 84 | do { 85 | try port.set(baudRate: B9600) 86 | try port.open() 87 | portIsOpening = true 88 | receivedText = "" 89 | } catch { 90 | logput(error.localizedDescription) 91 | } 92 | } 93 | 94 | func closePort() { 95 | guard let port, portIsOpening else { return } 96 | do { 97 | try port.close() 98 | portIsOpening = false 99 | } catch { 100 | logput(error.localizedDescription) 101 | } 102 | } 103 | 104 | func sendPort() { 105 | guard let port, portIsOpening else { return } 106 | do { 107 | try port.send(text: inputText) 108 | } catch { 109 | logput(error.localizedDescription) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/SerialGate/SGSerialDeviceDetector.swift: -------------------------------------------------------------------------------- 1 | import IOKit.usb 2 | import os 3 | 4 | final class SGSerialDeviceDetector: Sendable { 5 | private let protectedRunLoop = OSAllocatedUnfairLock(uncheckedState: nil) 6 | private let protectedNotificationPort = OSAllocatedUnfairLock(uncheckedState: nil) 7 | private let protectedRunLoopSource = OSAllocatedUnfairLock(uncheckedState: nil) 8 | private let protectedAddedIterator = OSAllocatedUnfairLock(initialState: .zero) 9 | private let protectedRemovedIterator = OSAllocatedUnfairLock(initialState: .zero) 10 | private let protectedDevicesHandler = OSAllocatedUnfairLock<(@Sendable () -> Void)?>(initialState: nil) 11 | 12 | var devicesStream: AsyncStream { 13 | AsyncStream { continuation in 14 | protectedDevicesHandler.withLock { 15 | $0 = { continuation.yield() } 16 | } 17 | continuation.onTermination = { [weak self] _ in 18 | self?.protectedDevicesHandler.withLock { $0 = nil } 19 | } 20 | } 21 | } 22 | 23 | func start() { 24 | let runLoop = CFRunLoopGetCurrent() 25 | protectedRunLoop.withLockUnchecked { $0 = runLoop } 26 | let notificationPort = IONotificationPortCreate(kIOMainPortDefault) 27 | protectedNotificationPort.withLockUnchecked { $0 = notificationPort } 28 | let runLoopSource = IONotificationPortGetRunLoopSource(notificationPort)!.takeRetainedValue() 29 | protectedRunLoopSource.withLockUnchecked { $0 = runLoopSource } 30 | CFRunLoopAddSource(runLoop, runLoopSource, .defaultMode) 31 | 32 | let matchingDict = IOServiceMatching(kIOSerialBSDServiceValue) 33 | let opaqueSelf = Unmanaged.passUnretained(self).toOpaque() 34 | 35 | // MARK: Added Notification 36 | let addedCallback: IOServiceMatchingCallback = { (pointer, iterator) in 37 | let detector = Unmanaged.fromOpaque(pointer!).takeUnretainedValue() 38 | detector.protectedDevicesHandler.withLock { $0?() } 39 | while case let device = IOIteratorNext(iterator), device != IO_OBJECT_NULL { 40 | IOObjectRelease(device) 41 | } 42 | } 43 | var addedIterator = protectedAddedIterator.withLock(\.self) 44 | IOServiceAddMatchingNotification(notificationPort, 45 | kIOMatchedNotification, 46 | matchingDict, 47 | addedCallback, 48 | opaqueSelf, 49 | &addedIterator) 50 | while case let device = IOIteratorNext(addedIterator), device != IO_OBJECT_NULL { 51 | IOObjectRelease(device) 52 | } 53 | 54 | // MARK: Removed Notification 55 | let removedCallback: IOServiceMatchingCallback = { (pointer, iterator) in 56 | let detector = Unmanaged.fromOpaque(pointer!).takeUnretainedValue() 57 | detector.protectedDevicesHandler.withLock { $0?() } 58 | while case let device = IOIteratorNext(iterator), device != IO_OBJECT_NULL { 59 | IOObjectRelease(device) 60 | } 61 | } 62 | var removedIterator = protectedRemovedIterator.withLock(\.self) 63 | IOServiceAddMatchingNotification(notificationPort, 64 | kIOTerminatedNotification, 65 | matchingDict, 66 | removedCallback, 67 | opaqueSelf, 68 | &removedIterator) 69 | while case let device = IOIteratorNext(removedIterator), device != IO_OBJECT_NULL { 70 | IOObjectRelease(device) 71 | } 72 | } 73 | 74 | func stop() { 75 | guard let runLoop = protectedRunLoop.withLockUnchecked(\.self) else { return } 76 | CFRunLoopStop(runLoop) 77 | 78 | guard let runLoopSource = protectedRunLoopSource.withLockUnchecked(\.self) else { return } 79 | CFRunLoopRemoveSource(runLoop, runLoopSource, .defaultMode) 80 | 81 | protectedAddedIterator.withLock { _ = IOObjectRelease($0) } 82 | protectedRemovedIterator.withLock { _ = IOObjectRelease($0) } 83 | 84 | guard let notificationPort = protectedNotificationPort.withLockUnchecked(\.self) else { return } 85 | IONotificationPortDestroy(notificationPort) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/SerialGate/SGPortManager.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import IOKit 4 | import IOKit.serial 5 | import Logput 6 | import os 7 | 8 | public final class SGPortManager: Sendable { 9 | public static let shared = SGPortManager() 10 | 11 | private let detector = SGSerialDeviceDetector() 12 | private let protectedTask = OSAllocatedUnfairLock?>(initialState: nil) 13 | 14 | private let availablePortsSubject = CurrentValueSubject<[SGPort], Never>([]) 15 | public var availablePortsStream: AsyncStream<[SGPort]> { 16 | AsyncStream { continuation in 17 | let cancellable = availablePortsSubject.sink { value in 18 | continuation.yield(value) 19 | } 20 | continuation.onTermination = { _ in 21 | cancellable.cancel() 22 | } 23 | } 24 | } 25 | 26 | private init() { 27 | registerNotifications() 28 | setAvailablePorts() 29 | } 30 | 31 | // MARK: Notifications 32 | private func registerNotifications() { 33 | let task = Task { 34 | await withTaskGroup(of: Void.self) { group in 35 | // MARK: USB Detector 36 | group.addTask { [weak self, detector] in 37 | for await _ in detector.devicesStream { 38 | self?.updatePorts() 39 | } 40 | } 41 | // MARK: Sleep/WakeUp 42 | group.addTask { [weak self] in 43 | let notification = NSWorkspace.willSleepNotification 44 | for await _ in NSWorkspace.shared.notificationCenter.publisher(for: notification).values { 45 | self?.sleepPorts() 46 | } 47 | } 48 | group.addTask { [weak self] in 49 | let notification = NSWorkspace.didWakeNotification 50 | for await _ in NSWorkspace.shared.notificationCenter.publisher(for: notification).values { 51 | self?.wakeUpPorts() 52 | } 53 | } 54 | // MARK: Terminate 55 | group.addTask { [weak self] in 56 | let notification = NSApplication.willTerminateNotification 57 | for await _ in NotificationCenter.default.publisher(for: notification).values { 58 | self?.terminate() 59 | } 60 | } 61 | } 62 | } 63 | protectedTask.withLock { $0 = task } 64 | detector.start() 65 | } 66 | 67 | // MARK: Serial Ports 68 | private func setAvailablePorts() { 69 | let device = findDevice() 70 | let portList = getPortList(device) 71 | availablePortsSubject.value.append(contentsOf: portList.map(SGPort.init)) 72 | } 73 | 74 | private func updatePorts() { 75 | let device = findDevice() 76 | let portList = getPortList(device) 77 | var ports = availablePortsSubject.value 78 | let removedPorts = ports.filter { !portList.contains($0.name) } // [0] 79 | removedPorts.forEach { $0.removed() } 80 | ports.removeAll { removedPorts.contains($0) } 81 | let addedPorts = portList.compactMap { portName in 82 | ports.contains(where: { $0.name == portName }) ? nil : SGPort(portName) 83 | } 84 | if !addedPorts.isEmpty { 85 | ports.insert(contentsOf: addedPorts, at: .zero) 86 | } 87 | availablePortsSubject.send(ports) 88 | } 89 | 90 | private func sleepPorts() { 91 | availablePortsSubject.value.forEach { $0.fallSleep() } 92 | } 93 | 94 | private func wakeUpPorts() { 95 | availablePortsSubject.value.forEach { $0.wakeUp() } 96 | } 97 | 98 | private func terminate() { 99 | availablePortsSubject.value.removeAll() 100 | protectedTask.withLock { $0?.cancel() } 101 | detector.stop() 102 | } 103 | 104 | private func findDevice() -> io_iterator_t { 105 | var portIterator: io_iterator_t = .zero 106 | let matchingDict: CFMutableDictionary = IOServiceMatching(kIOSerialBSDServiceValue) 107 | let typeKey_cf: CFString = kIOSerialBSDTypeKey as NSString 108 | let allTypes_cf: CFString = kIOSerialBSDAllTypes as NSString 109 | let typeKey = Unmanaged.passRetained(typeKey_cf).autorelease().toOpaque() 110 | let allTypes = Unmanaged.passRetained(allTypes_cf).autorelease().toOpaque() 111 | CFDictionarySetValue(matchingDict, typeKey, allTypes) 112 | let result = IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &portIterator) 113 | guard result == KERN_SUCCESS else { 114 | logput("Error: IOServiceGetMatchingServices") 115 | return .zero 116 | } 117 | return portIterator 118 | } 119 | 120 | private func getPortList(_ iterator: io_iterator_t) -> [String] { 121 | var ports = [String]() 122 | while case let object = IOIteratorNext(iterator), object != IO_OBJECT_NULL { 123 | let cfKey: CFString = kIOCalloutDeviceKey as NSString 124 | if let cfStr = IORegistryEntryCreateCFProperty(object, cfKey, kCFAllocatorDefault, .zero) { 125 | ports.append(cfStr.takeUnretainedValue() as! String) 126 | } 127 | IOObjectRelease(object) 128 | } 129 | IOObjectRelease(iterator) 130 | return ports.reversed() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/SerialGate/SGPort.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import os 4 | 5 | public final class SGPort: Hashable, Identifiable, Sendable { 6 | private let protectedFileDescriptor = OSAllocatedUnfairLock(initialState: .zero) 7 | private let protectedReadTimer = OSAllocatedUnfairLock<(any DispatchSourceTimer)?>(uncheckedState: nil) 8 | private let protectedName: OSAllocatedUnfairLock 9 | private let protectedPortState = OSAllocatedUnfairLock(initialState: .closed) 10 | private let protectedBaudRate = OSAllocatedUnfairLock(initialState: B9600) 11 | private let protectedParity = OSAllocatedUnfairLock(initialState: .none) 12 | private let protectedStopBits = OSAllocatedUnfairLock(initialState: 1) 13 | private let portStateSubject = PassthroughSubject() 14 | private let dataSubject = PassthroughSubject, Never>() 15 | private let textSubject = PassthroughSubject, Never>() 16 | 17 | public var id: String { name } 18 | public var name: String { protectedName.withLock(\.self) } 19 | public var portState: SGPortState { protectedPortState.withLock(\.self) } 20 | public var baudRate: Int32 { protectedBaudRate.withLock(\.self) } 21 | public var parity: SGParity { protectedParity.withLock(\.self) } 22 | public var stopBits: UInt32 { protectedStopBits.withLock(\.self) } 23 | 24 | public var portStateStream: AsyncStream { 25 | AsyncStream { continuation in 26 | let cancellable = portStateSubject.sink { value in 27 | continuation.yield(value) 28 | } 29 | continuation.onTermination = { _ in 30 | cancellable.cancel() 31 | } 32 | } 33 | } 34 | 35 | public var dataStream: AsyncStream> { 36 | AsyncStream { continuation in 37 | let cancellable = dataSubject.sink { value in 38 | continuation.yield(value) 39 | } 40 | continuation.onTermination = { _ in 41 | cancellable.cancel() 42 | } 43 | } 44 | } 45 | 46 | public var textStream: AsyncStream> { 47 | AsyncStream { continuation in 48 | let cancellable = textSubject.sink { value in 49 | continuation.yield(value) 50 | } 51 | continuation.onTermination = { _ in 52 | cancellable.cancel() 53 | } 54 | } 55 | } 56 | 57 | init(_ portName: String) { 58 | protectedName = .init(initialState: portName) 59 | } 60 | 61 | deinit { 62 | try? close() 63 | } 64 | 65 | // MARK: Public Function 66 | public func open() throws { 67 | let name = protectedName.withLock(\.self) 68 | guard protectedPortState.withLock({ $0 == .closed }) else { 69 | throw SGError.failedToOpenPort(name) 70 | } 71 | let fd = Darwin.open(name.cString(using: .ascii)!, O_RDWR | O_NOCTTY | O_NONBLOCK) 72 | guard fd != -1, fcntl(fd, F_SETFL, .zero) != -1 else { 73 | throw SGError.failedToOpenPort(name) 74 | } 75 | 76 | // ★★★ Start Communication ★★★ // 77 | protectedFileDescriptor.withLock { [fd] in $0 = fd } 78 | try setOptions() 79 | let readTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.global()) 80 | readTimer.schedule( 81 | deadline: DispatchTime.now(), 82 | repeating: DispatchTimeInterval.nanoseconds(Int(10 * NSEC_PER_MSEC)), 83 | leeway: DispatchTimeInterval.nanoseconds(Int(5 * NSEC_PER_MSEC)) 84 | ) 85 | readTimer.setEventHandler { [weak self] in 86 | self?.read() 87 | } 88 | readTimer.resume() 89 | protectedReadTimer.withLockUnchecked { $0 = readTimer } 90 | protectedPortState.withLock { $0 = .open } 91 | portStateSubject.send(.open) 92 | } 93 | 94 | public func close() throws { 95 | let name = protectedName.withLock(\.self) 96 | guard protectedPortState.withLock({ [.open, .sleeping].contains($0) }) else { 97 | throw SGError.portIsNotOpen(name) 98 | } 99 | protectedReadTimer.withLockUnchecked { 100 | $0?.cancel() 101 | $0 = nil 102 | } 103 | let fileDescriptor = protectedFileDescriptor.withLock(\.self) 104 | var options = termios() 105 | guard tcdrain(fileDescriptor) != -1, 106 | tcsetattr(fileDescriptor, TCSADRAIN, &options) != -1 else { 107 | throw SGError.failedToClosePort(name) 108 | } 109 | Darwin.close(fileDescriptor) 110 | protectedFileDescriptor.withLock { $0 = -1 } 111 | protectedPortState.withLock { $0 = .closed } 112 | portStateSubject.send(.closed) 113 | } 114 | 115 | public func send(data: Data) throws { 116 | let name = protectedName.withLock(\.self) 117 | guard protectedPortState.withLock({ $0 == .open }) else { 118 | throw SGError.portIsNotOpen(name) 119 | } 120 | let fileDescriptor = protectedFileDescriptor.withLock(\.self) 121 | try data.withUnsafeBytes { bufferPointer in 122 | guard let pointer = bufferPointer.baseAddress, 123 | Darwin.write(fileDescriptor, pointer, data.count) != -1 else { 124 | throw SGError.failedToWriteData(name) 125 | } 126 | } 127 | } 128 | 129 | public func send(text: String, encoding: String.Encoding = .ascii) throws { 130 | guard let data = text.data(using: encoding) else { 131 | let name = protectedName.withLock(\.self) 132 | throw SGError.failedToEncodeData(name) 133 | } 134 | try send(data: data) 135 | } 136 | 137 | // MARK: Set Options 138 | private func setOptions() throws { 139 | let name = protectedName.withLock(\.self) 140 | let fileDescriptor = protectedFileDescriptor.withLock(\.self) 141 | guard fileDescriptor > 0 else { return } 142 | var options = termios() 143 | guard tcgetattr(fileDescriptor, &options) != -1 else { 144 | throw SGError.failedToSetOptions(name) 145 | } 146 | cfmakeraw(&options) 147 | options.updateC_CC(VMIN, v: 1) 148 | options.updateC_CC(VTIME, v: 2) 149 | 150 | // DataBits 151 | options.c_cflag &= ~UInt(CSIZE) 152 | options.c_cflag |= UInt(CS8) 153 | 154 | // StopBits 155 | if protectedStopBits.withLock({ $0 > 1 }) { 156 | options.c_cflag |= UInt(CSTOPB) 157 | } else { 158 | options.c_cflag &= ~UInt(CSTOPB) 159 | } 160 | 161 | // Parity 162 | switch protectedParity.withLock(\.self) { 163 | case .none: 164 | options.c_cflag &= ~UInt(PARENB) 165 | case .even: 166 | options.c_cflag |= UInt(PARENB) 167 | options.c_cflag &= ~UInt(PARODD) 168 | case .odd: 169 | options.c_cflag |= UInt(PARENB) 170 | options.c_cflag |= UInt(PARODD) 171 | } 172 | 173 | // EchoReceivedData 174 | options.c_cflag &= ~UInt(ECHO) 175 | // RTS CTS FlowControl 176 | options.c_cflag &= ~UInt(CRTSCTS) 177 | // DTR DSR FlowControl 178 | options.c_cflag &= ~UInt(CDTR_IFLOW | CDSR_OFLOW) 179 | // DCD OutputFlowControl 180 | options.c_cflag &= ~UInt(CCAR_OFLOW) 181 | 182 | options.c_cflag |= UInt(HUPCL) 183 | options.c_cflag |= UInt(CLOCAL) 184 | options.c_cflag |= UInt(CREAD) 185 | options.c_lflag &= ~UInt(ICANON | ISIG) 186 | 187 | let baudRate = protectedBaudRate.withLock(\.self) 188 | guard cfsetspeed(&options, speed_t(baudRate)) != -1, 189 | tcsetattr(fileDescriptor, TCSANOW, &options) != -1 else { 190 | throw SGError.failedToSetOptions(name) 191 | } 192 | } 193 | 194 | public func set(baudRate: Int32) throws { 195 | let previousBaudRate = protectedBaudRate.withLock(\.self) 196 | protectedBaudRate.withLock { $0 = baudRate } 197 | do { 198 | try setOptions() 199 | } catch { 200 | protectedBaudRate.withLock { $0 = previousBaudRate } 201 | throw error 202 | } 203 | } 204 | 205 | public func set(parity: SGParity) throws { 206 | let previousParity = protectedParity.withLock(\.self) 207 | protectedParity.withLock { $0 = parity } 208 | do { 209 | try setOptions() 210 | } catch { 211 | protectedParity.withLock { $0 = previousParity } 212 | throw error 213 | } 214 | } 215 | 216 | public func set(stopBits: UInt32) throws { 217 | let previousStopBits = protectedStopBits.withLock(\.self) 218 | protectedStopBits.withLock { $0 = stopBits } 219 | do { 220 | try setOptions() 221 | } catch { 222 | protectedStopBits.withLock { $0 = previousStopBits } 223 | throw error 224 | } 225 | } 226 | 227 | // MARK: Internal Function 228 | func removed() { 229 | protectedReadTimer.withLockUnchecked { 230 | $0?.cancel() 231 | $0 = nil 232 | } 233 | let fileDescriptor = protectedFileDescriptor.withLock(\.self) 234 | var options = termios() 235 | if tcdrain(fileDescriptor) != -1, 236 | tcsetattr(fileDescriptor, TCSADRAIN, &options) != -1 { 237 | Darwin.close(fileDescriptor) 238 | } 239 | protectedPortState.withLock { $0 = .removed } 240 | portStateSubject.send(.removed) 241 | } 242 | 243 | func fallSleep() { 244 | guard protectedPortState.withLock({ $0 == .open }) else { return } 245 | protectedReadTimer.withLockUnchecked { $0?.suspend() } 246 | protectedPortState.withLock { $0 = .sleeping } 247 | } 248 | 249 | func wakeUp() { 250 | guard protectedPortState.withLock({ $0 == .sleeping }) else { return } 251 | protectedReadTimer.withLockUnchecked { $0?.resume() } 252 | protectedPortState.withLock { $0 = .open } 253 | } 254 | 255 | // MARK: Private Function 256 | private func read(bufferSize: Int = 1024, encoding: String.Encoding = .ascii) { 257 | let name = protectedName.withLock(\.self) 258 | guard protectedPortState.withLock({ $0 == .open }) else { 259 | dataSubject.send(.failure(.portIsNotOpen(name))) 260 | textSubject.send(.failure(.portIsNotOpen(name))) 261 | return 262 | } 263 | var buffer = [UInt8](repeating: .zero, count: bufferSize) 264 | let fileDescriptor = protectedFileDescriptor.withLock(\.self) 265 | let readLength = Darwin.read(fileDescriptor, &buffer, buffer.count) 266 | guard readLength > 0 else { return } 267 | let data = Data(bytes: buffer, count: readLength) 268 | dataSubject.send(.success(data)) 269 | guard let text = String(data: data, encoding: encoding) else { 270 | textSubject.send(.failure(.failedToEncodeText(name))) 271 | return 272 | } 273 | textSubject.send(.success(text)) 274 | } 275 | 276 | // MARK: Equatable 277 | public static func == (lhs: SGPort, rhs: SGPort) -> Bool { 278 | lhs === rhs 279 | } 280 | 281 | // MARK: Hashable 282 | public func hash(into hasher: inout Hasher) { 283 | ObjectIdentifier(self).hash(into: &hasher) 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /Demo/SerialGateDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1CD5D7D12D32D8A10088B889 /* Logput in Frameworks */ = {isa = PBXBuildFile; productRef = 1CD5D7D02D32D8A10088B889 /* Logput */; }; 11 | 1CF4145C2B90077100A4DB17 /* SerialGateDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF4145B2B90077100A4DB17 /* SerialGateDemoApp.swift */; }; 12 | 1CF4145E2B90077100A4DB17 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF4145D2B90077100A4DB17 /* ContentView.swift */; }; 13 | 1CF414602B90077200A4DB17 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1CF4145F2B90077200A4DB17 /* Assets.xcassets */; }; 14 | 1CF4146D2B900A9A00A4DB17 /* SerialGate in Frameworks */ = {isa = PBXBuildFile; productRef = 1CF4146C2B900A9A00A4DB17 /* SerialGate */; }; 15 | 1CF4146F2B9015C200A4DB17 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF4146E2B9015C200A4DB17 /* ContentViewModel.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 1CF414582B90077100A4DB17 /* SerialGateDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SerialGateDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 1CF4145B2B90077100A4DB17 /* SerialGateDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialGateDemoApp.swift; sourceTree = ""; }; 21 | 1CF4145D2B90077100A4DB17 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 1CF4145F2B90077200A4DB17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | 1CF414642B90077200A4DB17 /* SerialGateDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SerialGateDemo.entitlements; sourceTree = ""; }; 24 | 1CF4146A2B900A9000A4DB17 /* SerialGate */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SerialGate; path = ..; sourceTree = ""; }; 25 | 1CF4146E2B9015C200A4DB17 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | 1CF414552B90077100A4DB17 /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | 1CF4146D2B900A9A00A4DB17 /* SerialGate in Frameworks */, 34 | 1CD5D7D12D32D8A10088B889 /* Logput in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 1CF4144F2B90077100A4DB17 = { 42 | isa = PBXGroup; 43 | children = ( 44 | 1CF4146A2B900A9000A4DB17 /* SerialGate */, 45 | 1CF4145A2B90077100A4DB17 /* SerialGateDemo */, 46 | 1CF414592B90077100A4DB17 /* Products */, 47 | 1CF4146B2B900A9A00A4DB17 /* Frameworks */, 48 | ); 49 | sourceTree = ""; 50 | }; 51 | 1CF414592B90077100A4DB17 /* Products */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | 1CF414582B90077100A4DB17 /* SerialGateDemo.app */, 55 | ); 56 | name = Products; 57 | sourceTree = ""; 58 | }; 59 | 1CF4145A2B90077100A4DB17 /* SerialGateDemo */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 1CF414642B90077200A4DB17 /* SerialGateDemo.entitlements */, 63 | 1CF4145F2B90077200A4DB17 /* Assets.xcassets */, 64 | 1CF4145B2B90077100A4DB17 /* SerialGateDemoApp.swift */, 65 | 1CF4145D2B90077100A4DB17 /* ContentView.swift */, 66 | 1CF4146E2B9015C200A4DB17 /* ContentViewModel.swift */, 67 | ); 68 | path = SerialGateDemo; 69 | sourceTree = ""; 70 | }; 71 | 1CF4146B2B900A9A00A4DB17 /* Frameworks */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | ); 75 | name = Frameworks; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | 1CF414572B90077100A4DB17 /* SerialGateDemo */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = 1CF414672B90077200A4DB17 /* Build configuration list for PBXNativeTarget "SerialGateDemo" */; 84 | buildPhases = ( 85 | 1CF414542B90077100A4DB17 /* Sources */, 86 | 1CF414552B90077100A4DB17 /* Frameworks */, 87 | 1CF414562B90077100A4DB17 /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = SerialGateDemo; 94 | packageProductDependencies = ( 95 | 1CF4146C2B900A9A00A4DB17 /* SerialGate */, 96 | 1CD5D7D02D32D8A10088B889 /* Logput */, 97 | ); 98 | productName = SerialGateDemo; 99 | productReference = 1CF414582B90077100A4DB17 /* SerialGateDemo.app */; 100 | productType = "com.apple.product-type.application"; 101 | }; 102 | /* End PBXNativeTarget section */ 103 | 104 | /* Begin PBXProject section */ 105 | 1CF414502B90077100A4DB17 /* Project object */ = { 106 | isa = PBXProject; 107 | attributes = { 108 | BuildIndependentTargetsInParallel = 1; 109 | LastSwiftUpdateCheck = 1520; 110 | LastUpgradeCheck = 1620; 111 | TargetAttributes = { 112 | 1CF414572B90077100A4DB17 = { 113 | CreatedOnToolsVersion = 15.2; 114 | }; 115 | }; 116 | }; 117 | buildConfigurationList = 1CF414532B90077100A4DB17 /* Build configuration list for PBXProject "SerialGateDemo" */; 118 | compatibilityVersion = "Xcode 14.0"; 119 | developmentRegion = en; 120 | hasScannedForEncodings = 0; 121 | knownRegions = ( 122 | en, 123 | Base, 124 | ); 125 | mainGroup = 1CF4144F2B90077100A4DB17; 126 | productRefGroup = 1CF414592B90077100A4DB17 /* Products */; 127 | projectDirPath = ""; 128 | projectRoot = ""; 129 | targets = ( 130 | 1CF414572B90077100A4DB17 /* SerialGateDemo */, 131 | ); 132 | }; 133 | /* End PBXProject section */ 134 | 135 | /* Begin PBXResourcesBuildPhase section */ 136 | 1CF414562B90077100A4DB17 /* Resources */ = { 137 | isa = PBXResourcesBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | 1CF414602B90077200A4DB17 /* Assets.xcassets in Resources */, 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXResourcesBuildPhase section */ 145 | 146 | /* Begin PBXSourcesBuildPhase section */ 147 | 1CF414542B90077100A4DB17 /* Sources */ = { 148 | isa = PBXSourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 1CF4145E2B90077100A4DB17 /* ContentView.swift in Sources */, 152 | 1CF4146F2B9015C200A4DB17 /* ContentViewModel.swift in Sources */, 153 | 1CF4145C2B90077100A4DB17 /* SerialGateDemoApp.swift in Sources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXSourcesBuildPhase section */ 158 | 159 | /* Begin XCBuildConfiguration section */ 160 | 1CF414652B90077200A4DB17 /* Debug */ = { 161 | isa = XCBuildConfiguration; 162 | buildSettings = { 163 | ALWAYS_SEARCH_USER_PATHS = NO; 164 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 165 | CLANG_ANALYZER_NONNULL = YES; 166 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 167 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_ENABLE_OBJC_WEAK = YES; 171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 172 | CLANG_WARN_BOOL_CONVERSION = YES; 173 | CLANG_WARN_COMMA = YES; 174 | CLANG_WARN_CONSTANT_CONVERSION = YES; 175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 178 | CLANG_WARN_EMPTY_BODY = YES; 179 | CLANG_WARN_ENUM_CONVERSION = YES; 180 | CLANG_WARN_INFINITE_RECURSION = YES; 181 | CLANG_WARN_INT_CONVERSION = YES; 182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 186 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 187 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 188 | CLANG_WARN_STRICT_PROTOTYPES = YES; 189 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 190 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | COPY_PHASE_STRIP = NO; 194 | DEAD_CODE_STRIPPING = YES; 195 | DEBUG_INFORMATION_FORMAT = dwarf; 196 | ENABLE_STRICT_OBJC_MSGSEND = YES; 197 | ENABLE_TESTABILITY = YES; 198 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 199 | GCC_C_LANGUAGE_STANDARD = gnu17; 200 | GCC_DYNAMIC_NO_PIC = NO; 201 | GCC_NO_COMMON_BLOCKS = YES; 202 | GCC_OPTIMIZATION_LEVEL = 0; 203 | GCC_PREPROCESSOR_DEFINITIONS = ( 204 | "DEBUG=1", 205 | "$(inherited)", 206 | ); 207 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 208 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 209 | GCC_WARN_UNDECLARED_SELECTOR = YES; 210 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 211 | GCC_WARN_UNUSED_FUNCTION = YES; 212 | GCC_WARN_UNUSED_VARIABLE = YES; 213 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 214 | MACOSX_DEPLOYMENT_TARGET = 13.0; 215 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 216 | MTL_FAST_MATH = YES; 217 | ONLY_ACTIVE_ARCH = YES; 218 | SDKROOT = macosx; 219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 221 | }; 222 | name = Debug; 223 | }; 224 | 1CF414662B90077200A4DB17 /* Release */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 252 | CLANG_WARN_STRICT_PROTOTYPES = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | COPY_PHASE_STRIP = NO; 258 | DEAD_CODE_STRIPPING = YES; 259 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 260 | ENABLE_NS_ASSERTIONS = NO; 261 | ENABLE_STRICT_OBJC_MSGSEND = YES; 262 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu17; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 267 | GCC_WARN_UNDECLARED_SELECTOR = YES; 268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 269 | GCC_WARN_UNUSED_FUNCTION = YES; 270 | GCC_WARN_UNUSED_VARIABLE = YES; 271 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 272 | MACOSX_DEPLOYMENT_TARGET = 13.0; 273 | MTL_ENABLE_DEBUG_INFO = NO; 274 | MTL_FAST_MATH = YES; 275 | SDKROOT = macosx; 276 | SWIFT_COMPILATION_MODE = wholemodule; 277 | }; 278 | name = Release; 279 | }; 280 | 1CF414682B90077200A4DB17 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 284 | CODE_SIGN_ENTITLEMENTS = SerialGateDemo/SerialGateDemo.entitlements; 285 | CODE_SIGN_STYLE = Automatic; 286 | COMBINE_HIDPI_IMAGES = YES; 287 | CURRENT_PROJECT_VERSION = 1; 288 | DEAD_CODE_STRIPPING = YES; 289 | DEVELOPMENT_TEAM = VJ5N2X84K8; 290 | ENABLE_HARDENED_RUNTIME = YES; 291 | ENABLE_PREVIEWS = YES; 292 | GENERATE_INFOPLIST_FILE = YES; 293 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 294 | LD_RUNPATH_SEARCH_PATHS = ( 295 | "$(inherited)", 296 | "@executable_path/../Frameworks", 297 | ); 298 | MARKETING_VERSION = 1.0; 299 | PRODUCT_BUNDLE_IDENTIFIER = com.kyome.SerialGateDemo; 300 | PRODUCT_NAME = "$(TARGET_NAME)"; 301 | SWIFT_EMIT_LOC_STRINGS = YES; 302 | SWIFT_VERSION = 6.0; 303 | }; 304 | name = Debug; 305 | }; 306 | 1CF414692B90077200A4DB17 /* Release */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CODE_SIGN_ENTITLEMENTS = SerialGateDemo/SerialGateDemo.entitlements; 311 | CODE_SIGN_STYLE = Automatic; 312 | COMBINE_HIDPI_IMAGES = YES; 313 | CURRENT_PROJECT_VERSION = 1; 314 | DEAD_CODE_STRIPPING = YES; 315 | DEVELOPMENT_TEAM = VJ5N2X84K8; 316 | ENABLE_HARDENED_RUNTIME = YES; 317 | ENABLE_PREVIEWS = YES; 318 | GENERATE_INFOPLIST_FILE = YES; 319 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/../Frameworks", 323 | ); 324 | MARKETING_VERSION = 1.0; 325 | PRODUCT_BUNDLE_IDENTIFIER = com.kyome.SerialGateDemo; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | SWIFT_EMIT_LOC_STRINGS = YES; 328 | SWIFT_VERSION = 6.0; 329 | }; 330 | name = Release; 331 | }; 332 | /* End XCBuildConfiguration section */ 333 | 334 | /* Begin XCConfigurationList section */ 335 | 1CF414532B90077100A4DB17 /* Build configuration list for PBXProject "SerialGateDemo" */ = { 336 | isa = XCConfigurationList; 337 | buildConfigurations = ( 338 | 1CF414652B90077200A4DB17 /* Debug */, 339 | 1CF414662B90077200A4DB17 /* Release */, 340 | ); 341 | defaultConfigurationIsVisible = 0; 342 | defaultConfigurationName = Release; 343 | }; 344 | 1CF414672B90077200A4DB17 /* Build configuration list for PBXNativeTarget "SerialGateDemo" */ = { 345 | isa = XCConfigurationList; 346 | buildConfigurations = ( 347 | 1CF414682B90077200A4DB17 /* Debug */, 348 | 1CF414692B90077200A4DB17 /* Release */, 349 | ); 350 | defaultConfigurationIsVisible = 0; 351 | defaultConfigurationName = Release; 352 | }; 353 | /* End XCConfigurationList section */ 354 | 355 | /* Begin XCSwiftPackageProductDependency section */ 356 | 1CD5D7D02D32D8A10088B889 /* Logput */ = { 357 | isa = XCSwiftPackageProductDependency; 358 | productName = Logput; 359 | }; 360 | 1CF4146C2B900A9A00A4DB17 /* SerialGate */ = { 361 | isa = XCSwiftPackageProductDependency; 362 | productName = SerialGate; 363 | }; 364 | /* End XCSwiftPackageProductDependency section */ 365 | }; 366 | rootObject = 1CF414502B90077100A4DB17 /* Project object */; 367 | } 368 | --------------------------------------------------------------------------------