ev3ios icon indicating copy to clipboard operation
ev3ios copied to clipboard

wifi connection

Open konsdor opened this issue 5 years ago • 8 comments

Hi @andiikaa,

Thank you for this nice work! Can you give any clue how to extend the SDK to a wifi connection feature?

Thanks, Konstantin

konsdor avatar Mar 28 '19 15:03 konsdor

Hi, its been a while since i have worked with the EV3 and iOS. There is a Ev3Connection class, which handles the communication. If you are able to get the EASession over Wifi, it should work. But if i look at the EAAccessory or ExternalAccessory i´m not really sure, if the EV3 is supported as external accessory over Wifi. I don´t own a EV3 myself, so i can´t test it.

andiikaa avatar Mar 28 '19 16:03 andiikaa

EV3 does not support EAAccessory over WiFi. But I was able to connect to EV3 though simple terminal program https://github.com/anoop4real/SocketSwiftSample. Now I don't know how to replace EAAssesory with DataSocket.

konsdor avatar Mar 28 '19 17:03 konsdor

Ah ok. On the open() method in Ev3Connection class, i obtain the in- and output streams from the EASession. So instead getting the streams from the EASession, you could get them from the Socket. In your link posted, they do this within the connectWith() method in SocketDataManager class.

andiikaa avatar Mar 29 '19 07:03 andiikaa

I tried this, but it doesn't work:

import Foundation import ExternalAccessory

public protocol Ev3ConnectionChangedDelegate: class { func ev3ConnectionChanged(connected: Bool) }

public class Ev3Connection : NSObject, StreamDelegate { //var accessory: EAAccessory var session: EASession? var socket: DataSocket? var readStream: Unmanaged<CFReadStream>? var writeStream: Unmanaged<CFWriteStream>? var inputStream: InputStream? var outputStream: OutputStream? var messages = AnyHashable weak var uiPresenter :PresenterProtocol!

init(with presenter:PresenterProtocol){
    
    self.uiPresenter = presenter
}
func connectWith(socket: DataSocket) {
    
    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (socket.ipAddress! as CFString), UInt32(socket.port), &readStream, &writeStream)
    messages = [AnyHashable]()
    open()
}

func disconnect(){
    
    close()
}


/// max command buffer size
let maxBufferSize = 2

/// sleeping time after each command was send to ev3
let connSleepTime = 0.125

/// indicating if the connection is closed
var isClosed = true

/// informs the delegates if a report was received
var reportReceivedDelegates = [Ev3ReportDelegate]()

/// delegate to informate the brick, that the connection hase changed 
/// (true -> connected, false -> disconnected)
var connectionChangedDelegates = [Ev3ConnectionChangedDelegate]()

/// flag which is indicating, that spase is available on the output stream, 
/// but there was no data to write.
private var canWrite = true

/// trying to handle all messages in a another queue
private let queue = DispatchQueue(label: "com.ev3ios.connection.queue")


/// buffer for the input stream size, with size of 2
private var sizeBuffer = [UInt8](repeating: 0x00, count: 2)

/// buffer for appending data to write e.g. if currently no space
/// is available on the stream
private var writeBuffer = Array<NSData>()

// init(accessory: EAAccessory){ // self.accessory = accessory // }

/// checks if the given accessory supports the ev3 protocol
public static func supportsEv3Protocol(accessory: EAAccessory) -> Bool {
    return accessory.protocolStrings.contains(Ev3Constants.supportedProtocol)
}

/// open the connection before using
func open(){
    print("Opening streams.")
    outputStream = writeStream?.takeRetainedValue()
    inputStream = readStream?.takeRetainedValue()
    outputStream?.delegate = self
    inputStream?.delegate = self
    outputStream?.schedule(in: RunLoop.main, forMode: RunLoop.Mode.common)
    inputStream?.schedule(in: RunLoop.main, forMode: RunLoop.Mode.common)
    outputStream?.open()
    inputStream?.open()
}

/// delegate for receiving reports received from the input stream
func addEv3ReportDelegate(_ delegate: Ev3ReportDelegate) {
    reportReceivedDelegates.append(delegate)
}

/// delegate for receiving updates, if the connection has changed
public func addEv3ConnectionChangedDelegate(delegate: Ev3ConnectionChangedDelegate){
    connectionChangedDelegates.append(delegate)
}

/// close the connection after use
func close(){
    print("Closing streams.")
    uiPresenter?.resetUIWithConnection(status: false)
    inputStream?.close()
    outputStream?.close()
    inputStream?.remove(from: RunLoop.main, forMode: RunLoop.Mode.default)
    outputStream?.remove(from: RunLoop.main, forMode: RunLoop.Mode.default)
    inputStream?.delegate = nil
    outputStream?.delegate = nil
    inputStream = nil
    outputStream = nil
}

// Dispatch stuff to read
// https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/

/// writes the data to the outputstream
private func write(){
    self.queue.async {
        if self.writeBuffer.count < 1 {
            return
        }

        self.canWrite = false
        
        if self.session?.outputStream?.hasSpaceAvailable == false {
            print("error: stream has no space available")
            return
        }
        
        let mData = self.writeBuffer.remove(at: 0)
        
        print("Writing data: ")
        print(ByteTools.asHexString(data: mData))
        
        var bytes = mData.bytes.bindMemory(to: UInt8.self, capacity: mData.length)

        var bytesLeftToWrite: NSInteger = mData.length
        
        let bytesWritten = self.session?.outputStream?.write(bytes, maxLength: bytesLeftToWrite) ?? -1
        if bytesWritten == -1 {
            print("error while writing data to bt output stream")
            self.canWrite = true
            return // Some error occurred ...
        }
        
        bytesLeftToWrite -= bytesWritten
        bytes = bytes.advanced(by: bytesWritten)
        
        if bytesLeftToWrite > 0 {
            print("error: not enough space in stream")
            self.queue.async {
                self.writeBuffer.insert(NSData(bytes: &bytes, length: bytesLeftToWrite), at: 0)
            }
        }
        
        print("bytes written \(bytesWritten)")
        print("write buffer size: \(self.writeBuffer.count)")
        Thread.sleep(forTimeInterval: self.connSleepTime) //give the ev3 time - too much traffic will disconnect the bt connection
    }
}

/// writes data to the output stream of a accessory. the operation is handled on a own serial queue, 
/// so that no concurrent write ops should happen
private func write(data: NSData) {
    self.queue.async {
        self.dismissCommandsIfNeeded()
        self.writeBuffer.append(data)
        if self.canWrite {
            self.write()
        }
    }
}

/// write a command to the outputstream
func write(command: Ev3Command) {
    write(data: command.toBytes())
}

/// cleares the writebuffer if it exceeds a given maximum
private func dismissCommandsIfNeeded(){
    if(writeBuffer.count > maxBufferSize){
        self.queue.async {
            self.writeBuffer.removeAll()
        }
        print("cleared write buffer")
    }
}

public func dismissAllCommands(){
self.queue.async {
self.writeBuffer.removeAll()
}
print("cleared all commands")

}

/// reads the data from the inputstream if bytes are available. calls the delegates,
/// with the received data
private func readInBackground(){
    
    let result = session?.inputStream?.read(&sizeBuffer, maxLength: sizeBuffer.count) ?? 0
    
    if(result > 0) {
        // buffer contains result bytes of data to be handled
        let size: Int16 = Int16(sizeBuffer[1]) << 8 | Int16(sizeBuffer[0])
        
        if size > 0 {
            var buffer = [UInt8](repeating: 0x00, count: Int(size))
            let result = session?.inputStream?.read(&buffer, maxLength: buffer.count) ?? 0
            
            if result < 1 {
                print("error reading the input data with size: \(size)")
                return
            }
            
            print("read data:")
            print(ByteTools.asHexString(data: NSData(bytes: buffer, length: buffer.count)))

            reportReceived(report: buffer)

        }
        else{
            print("error on input stream: reply size < 1")
        }
        
    } else {
        print("error on input stream, while reading reply size")
    }    
}

/// delegates for receiving the events for the input and outputstream
/// reading and writing ops are dispatched to a serial queue.
public func stream(_ aStream: Stream, handle eventCode: Stream.Event){
    switch eventCode {
   
    case Stream.Event.hasBytesAvailable:
        self.queue.async {
            self.readInBackground()
        }
        break
        
    case Stream.Event.hasSpaceAvailable:
        self.queue.async {
            self.canWrite = true
            self.write()
        }

        break
        
    case Stream.Event.openCompleted:
        print("stream opened")
        break
        
    case Stream.Event.errorOccurred:
        print("error on stream")
        break
        
    default:
        print("connection event: \(eventCode.rawValue)")
        break
    }

}

//if running in background, this must be dispatched to the main queue
private func reportReceived(report: [UInt8]){
    DispatchQueue.main.async {
        for delegate in self.reportReceivedDelegates {
            delegate.reportReceived(report: report)
        }
    }
}

}

konsdor avatar Apr 09 '19 03:04 konsdor

What does not work? Could you write to the socket?

andiikaa avatar Apr 09 '19 09:04 andiikaa

For testing I created ViewControllerWiFi and BT class and I was able to send commands to ev3 using socketConnector.sendu(message: byteArray) where byteArray is a direct command written as [UInt8]. But when I try to do this with BT.shared.brick?.directCommand.playTone(volume: 30, frequency: 500, duration: 500) nothing happens. May be I defined self.brick incorrectly.

One more thing: in order to open port 5555 on ev3 I have to send "GET /target?sn=" first and after getting "Accept:EV340" respond from ev3, I can communicate with the brick.

//BT.swift import Foundation import UIKit

class BT { var brick: Ev3Brick? static let shared = BT() var socketConnector:SocketDataManager! weak var uiPresenter :PresenterProtocol!

func connect(){ let connection = Ev3Connection(with: uiPresenter)

self.brick = Ev3Brick(connection: connection)
connection.open()
}

private init() {
    
}

}

//ViewControllerWiFi: import UIKit

class ViewControllerWiFi: UIViewController {

var socketConnector:SocketDataManager!
@IBOutlet weak var ipAddressField: UITextField!
@IBOutlet weak var portField: UITextField!
@IBOutlet weak var messageField: UITextField!
@IBOutlet weak var messageHistoryView: UITextView!
@IBOutlet weak var connectBtn: UIButton!
@IBOutlet weak var sendBtn: UIButton!
@IBOutlet weak var statusView: UIView!
@IBOutlet weak var statusLabl: UILabel!
var brick: Ev3Brick?

override func viewDidLoad() {
    super.viewDidLoad()
    socketConnector = SocketDataManager(with: self)
    resetUIWithConnection(status: false)
    // Do any additional setup after loading the view, typically from a nib.
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


@IBAction func connect(){
    //http://localhost:50694/
    guard let ipAddr = ipAddressField.text, let portVal = portField.text  else {
        return
    }
    let soc = DataSocket(ip: ipAddr, port: portVal)
    socketConnector.connectWith(socket: soc)
    //
  
    
}
@IBAction func send(){

    send(message: "GET /target?sn=")
    messageField.text = ""
}
func send(message: String){
    
    socketConnector.send(message: message)
    update(message: "me:\(message)")
}


let byteArray: [UInt8] = [0x0E, 0x00, 0x2A, 0x00, 0x80, 0x00, 0x00, 0x94, 0x01, 0x01, 0x82, 0xB8, 0x01, 0x82, 0xE8, 0x03]




@IBAction func sendc() {
 //   socketConnector.sendu(message: byteArray)
 
 BT.shared.brick?.directCommand.playTone(volume: 30, frequency: 500, duration: 500)
}

}

extension ViewControllerWiFi: PresenterProtocol{

func resetUIWithConnection(status: Bool){
    
    ipAddressField.isEnabled = !status
    portField.isEnabled = !status
    messageField.isEnabled = status
    connectBtn.isEnabled = !status
    sendBtn.isEnabled = status
    
    if (status){
        updateStatusViewWith(status: "Connected")
    }else{
        updateStatusViewWith(status: "Disconnected")
    }
}
func updateStatusViewWith(status: String){
    
    statusLabl.text = status
}

func update(message: String){
    
    if let text = messageHistoryView.text{
        let newText = """
        \(text)            
        \(message)
        """
        messageHistoryView.text = newText
    }else{
        let newText = """
        \(message)
        """
        messageHistoryView.text = newText
    }

    let myRange=NSMakeRange(messageHistoryView.text.count-1, 0);
    messageHistoryView.scrollRangeToVisible(myRange)

    
}

}

konsdor avatar Apr 09 '19 16:04 konsdor

You could make an interface for the connection class, which contains the important public methods. And then have two connection implementation. Bluetooth (which already exists) and a Wifi connection class. Depending on which method you choose, you create a Ev3Brick instance with the specific connection. The Ev3Brick class should be changed, so that it only knows the interface, not the specific implementation class.

Now you could implement the Wifi class and use the SocketDataManager like in your example.

andiikaa avatar Apr 09 '19 16:04 andiikaa

Thank you! I thought I can just change EV3Connection and leave Ev3Brick as it is.

konsdor avatar Apr 09 '19 17:04 konsdor