Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ORSSerial and SwiftUI/Combine #162

Open
janhendry opened this issue Oct 1, 2020 · 3 comments
Open

ORSSerial and SwiftUI/Combine #162

janhendry opened this issue Oct 1, 2020 · 3 comments

Comments

@janhendry
Copy link

janhendry commented Oct 1, 2020

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 update SerialPortCombine and 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
Copy link
Owner

armadsen commented Oct 1, 2020

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
Copy link
Author

pull request it out :)

@ronnyandre
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants