armadsen / ORSSerialPort

Serial port library for Objective-C and Swift macOS apps
MIT License
751 stars 183 forks source link

ORSSerial and SwiftUI/Combine #162

Open janhendry opened 3 years ago

janhendry commented 3 years ago

I want to implement this view Bildschirmfoto 2020-10-01 um 15 41 11

I found a good way to integrate ORSSerialPort into a SwiftUI view. I would like to show them here. Maybe it will help someone, or maybe someone has an even better idea.

I have a View struct, a ViewModel Class and then SerialPortCombine class. The SerialPortCombine wrapper the ORSSerialPort.

Start with a simple View Bildschirmfoto 2020-10-01 um 14 47 07

struct SettingsView: View {
    @Binding var settings: PortSettings
    var body: some View{
        HStack{
            VStack(alignment: .trailing){
                Text("path")
                Text("name")
                Text("baudRate")
                Text("Stopbits")
                Text("parity")
                Text("rts/cts")
            }
            VStack(alignment: .leading){
                Text(settings.path)
                Text(settings.name)
                Text(settings.baudRate.description)
                Text(String(settings.numberOfStopBits))
                Text(settings.parity.description())
                Text(settings.rtscts ? "on" : "off")
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var viewM: ViewModel = ViewModel()
    var body: some View {
        SettingsView(settings: $viewM.settings)

    }
}

We have the PortSettings they have all values of the ORSSerialPort.

struct PortSettings{
    var path: String = "/dev/cu.usb12"
    var name: String = "cu.usb12"
    var isOpen: Bool = false
    var isConnect: Bool = false
    var cts: Bool = false
    var dsr: Bool = false
    var dcdOut: Bool = false
    var baudRate: Int = 9600
    var numberOfStopBits: Int = 1
    var rtscts: Bool = false
    var dtrdsr: Bool = false
    var rts: Bool = false
    var dtr: Bool = false
    var dcdIn: Bool = false
    var echo: Bool = false
    var parity: ORSSerialPortParity = .none
    var numberOfDataBits: Int = 0

    init(){}
}

I add the SerialPortCombine the var portSettings: CurrentValueSubject<PortSettings,Never> Publisher. They will trigger a event when one of the values from the ORSSerialPort should change.

class SerialPortCombine:NSObject, ObservableObject {

    var portSettings: CurrentValueSubject<PortSettings,Never>

}

And finally we have the ViewModel.

class ViewModel: ObservableObject{

    @Published var settings = PortSettings()
    @Published var serialPort: SerialPortCombine
    private var subSet = Set<AnyCancellable>(

    init(){
        serialPort = SerialPortCombine(path)
        serialPort?.portSettings
            .assign(to: \.settings, on: self)
            .store(in: &subSet)
    }
}

The model will subscribes the portSettings publisher and loads the data into the setting variable. This is an @observerbal, so the view can @binding this struct.

Now it is possible to display all variables in the view. Now we want to control the variables via the view Control like this. Bildschirmfoto 2020-10-02 um 05 49 16

struct DemoView1: View {
    @ObservedObject var serialP = SerialPortCombine("/dev/cu.usbmodem143201")!

    var body: some View {

        VStack(alignment: .leading){
            Picker("baudrate", selection: $serialP.baudRate){
                ForEach(BaudRate.allCases, id: \.value) {
                    Text(String($0.value))
                }
            }
            Picker("stopBits", selection: $serialP.numberOfStopBits){
                ForEach([1,2], id: \.self) {
                    Text(String($0))
                }
            }
            .frame(width: 130)
            .pickerStyle(SegmentedPickerStyle())

        }.frame(width: 200)
    }

}

For this we need @Published properties. We add an @Published variable to SerialPortCombine for every variable that we can change on ORSSerialPort. And I add the ObservableObject protocol, that we can observe the @Published values in the View.


class SerialPortCombine: ObservableObject {

    @Published var baudRate: Int
    @Published var allowsNonStandardBaudRates: Bool
    @Published var numberOfStopBits: Int
    @Published var parity: ORSSerialPortParity
    @Published var usesRTSCTSFlowControl: Bool
    @Published var usesDTRDSRFlowControl: Bool
    @Published var usesDCDOutputFlowControl: Bool
    @Published var shouldEchoReceivedData: Bool
    @Published var rts: Bool
    @Published var dtr: Bool
    @Published var numberOfDataBits: Int

    var portSettings: CurrentValueSubject<PortSettings,Never>

    private var port: ORSSerialPort
}

That is all what we need. Now comes the big question about how we are implementing SerialPortCombine.

Let's start :)

At first we track the KVO from ORSSerialPort, If someone change we trigger the portSettings publisher. And update the Values in SerialPortCombine.

     func initORSSerialPortSub(){

        port.publisher(for: \.cts)
            .sink{ _ in self.portSettings.send(PortSettings(self.port,isConnect: self.isConnect.value))}
            .store(in: &subSet)
        port.publisher(for: \.dsr)
            .sink{ _ in self.portSettings.send(PortSettings(self.port,isConnect: self.isConnect.value))}
            .store(in: &subSet)
        port.publisher(for: \.dcd)
            .sink{ value in self.portSettings.send(PortSettings(self.port,isConnect: self.isConnect.value))}
            .store(in: &subSet)
        port.publisher(for: \.rts)
            .sink{value in
                if (self.rts != value){
                    self.rts = value
                    self.portSettings.send(PortSettings(self.port,isConnect: self.isConnect.value))
                }}
            .store(in: &subSet)
        port.publisher(for: \.dtr)
            .sink{ value in
                if(self.dtr != value){
                    self.portSettings.send(PortSettings(self.port,isConnect: self.isConnect.value))
                    self.dtr = value }
            }
            .store(in: &subSet)

        port.publisher(for: \.pendingRequest)
            .sink{ value in self.pendingRequest = value }
            .store(in: &subSet)
        port.publisher(for: \.queuedRequests)
            .sink{ value in self.queuedRequests = value }
            .store(in: &subSet)
    }

}

The next step is, we need update the values in ORSSerialPort if the @Published values in SerialPortCombine will change.

By the values kts and dtr Its Important that we check, that we cancel the Cycle. That not ORSSerialPort updateSerialPortCombineand that SerialPortCombineupdateORSSerialPort` and so on. I always check whether the value has really changed.

    func initSub(){
        $baudRate
            .removeDuplicates()
            .sink{value in
                self.port.baudRate = NSNumber(value: value)
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $numberOfStopBits
            .removeDuplicates()
            .map{UInt($0)}
            .sink{value in
                self.port.numberOfStopBits = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $parity
            .removeDuplicates()
            .sink{value in
                self.port.parity = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $usesRTSCTSFlowControl
            .removeDuplicates()
            .sink{value in
                self.port.usesRTSCTSFlowControl = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $usesDTRDSRFlowControl
            .removeDuplicates()
            .sink{value in
                self.port.usesDTRDSRFlowControl = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $usesDCDOutputFlowControl
            .removeDuplicates()
            .sink{value in
                self.port.usesDCDOutputFlowControl = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $shouldEchoReceivedData
            .removeDuplicates()
            .sink{value in
                self.port.shouldEchoReceivedData = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $rts
            .removeDuplicates()
            .sink{value in
                self.port.rts = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $dtr
            .removeDuplicates()
            .sink{value in
                self.port.dtr = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)
        $numberOfDataBits
            .removeDuplicates()
            .map{UInt($0)}
            .sink{value in
                self.port.numberOfDataBits = value
                self.portSettings.send(PortSettings(self.port, isConnect: self.isConnect.value)) }
            .store(in: &subSet)

        $allowsNonStandardBaudRates
            .removeDuplicates()
            .sink{ value in
                self.port.allowsNonStandardBaudRates = value
                self.portSettings.send(PortSettings(self.port,isConnect: self.isConnect.value))}
            .store(in: &subSet)
}

So now we are finish with the connection between ORSSerialPort and SerialPortCombine.

What we are now missing is the isOpen and isConnection value. For that we need add the ORSSerialPortDelegate protocol and add this variable to SerialPortCombine.

    var isConnect: CurrentValueSubject<Bool,Never>
    var isOpen: CurrentValueSubject<Bool,Never>
    var receiveData = PassthroughSubject<Data,Never>()
    var receivePacket = PassthroughSubject<(Data,ORSSerialPacketDescriptor),Never>()
    var error = PassthroughSubject<Error,Never>()
    var responseData = PassthroughSubject<(Data,ORSSerialRequest),Never>()
    var requestTimeout = PassthroughSubject<ORSSerialRequest,Never>()

extension SerialPortCombine: ORSSerialPortDelegate{

    func serialPortWasRemovedFromSystem(_ serialPort: ORSSerialPort){
        isConnect.send(false)
        isOpen.send(false)
    }

    func serialPort(_ serialPort: ORSSerialPort, didReceive data: Data){
        receiveData.send(data)
    }

    func serialPort(_ serialPort: ORSSerialPort, didReceivePacket packetData: Data, matching descriptor: ORSSerialPacketDescriptor){
        receivePacket.send((packetData,descriptor))
    }

    func serialPort(_ serialPort: ORSSerialPort, didReceiveResponse responseData: Data, to request: ORSSerialRequest){
        self.responseData.send((responseData, request))
    }

    func serialPort(_ serialPort: ORSSerialPort, requestDidTimeout request: ORSSerialRequest){
        requestTimeout.send(request)
    }

    func serialPort(_ serialPort: ORSSerialPort, didEncounterError error: Error){
        self.error.send(error)
    }

    func serialPortWasOpened(_ serialPort: ORSSerialPort){
        isOpen.send(true)
    }

    func serialPortWasClosed(_ serialPort: ORSSerialPort){
        isOpen.send(false)
    }

}
 func initNotificationSub(){
        isConnect
            .removeDuplicates()
            .sink{ self.portSettings.send(PortSettings(self.port, isConnect: $0)) }
            .store(in: &subSet)

        NotificationCenter.default
            .publisher(for: NSNotification.Name.ORSSerialPortsWereConnected)
            .sink() { notification in
                if let userInfo = notification.userInfo {
                    let connectedPorts = userInfo[ORSConnectedSerialPortsKey] as! [ORSSerialPort]
                    if  let  _ = connectedPorts.first(where: { x in x.path.elementsEqual(self.port.path) }){
                        self.isConnect.send(true)
                    }
                }
            }
            .store(in: &self.subSet)

        NotificationCenter.default
            .publisher(for: NSNotification.Name.ORSSerialPortsWereDisconnected)
            .sink() { notification in
                if let userInfo = notification.userInfo {
                    let disconnectedPorts: [ORSSerialPort] = userInfo[ORSDisconnectedSerialPortsKey] as! [ORSSerialPort]
                    if let _ = disconnectedPorts.first(where: { x in x.path.elementsEqual(self.port.path) }){
                        self.isConnect.send(false)
                        self.isOpen.send(false)
                    }
                }
            }
            .store(in: &self.subSet)
    }

That's all. I will upload the full code in example. If anyone has better ideas, please feel free to comment.

armadsen commented 3 years ago

Thank you, this is great. I'd be happy to consider a pull request that adds your demo example to the examples folder in the repo.

janhendry commented 3 years ago

pull request it out :)

ronnyandre commented 3 years ago

I could really use this example app, so please add it in a pull request!