├── 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 |
16 | 3. Edit the entitlements and add `com.apple.security.device.serial`
17 |
18 |
19 | ## Demo
20 |
21 | Serial Communication Demo App for Arduino is in this Project.
22 |
23 |
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 |
--------------------------------------------------------------------------------