ExternalAccessory on iOS at Xamarin
Asked Answered
A

2

1

Anybody has a clue on how to use the ExternalAccessory API on Xamarin.iOS?

My Xamarin Studio version is 4.0.12(build 3), Xamarin.Android version 4.8.1, Xamarin.iOS version 6.4.5.0 and Xcode is Version 5.0 (5A1413) and I tried target both 6.1 and 7.0 iPad/iPhone.

I've walked the internet and there is not much documentation. Even the MonoTouch docs have broken links.

What I want is, list the connected bluetooth devices, get one of then by name and then connect to it so I can open a socket and start sending data to it. It is a device that uses Serial communication and yes, it has the Apple external accessory protocol ID.

I've tried this:

var am = EAAccessoryManager.SharedAccessoryManager;

It just throws me an exception an InvaidCastException.

Any clues?

Thanks! I really appreciate the help.

PS: Xamarin Details

Xamarin Studio
Version 4.0.12 (build 3)
Installation UUID: 7348d641-ed6d-4c8a-b59a-116674e06dfd
Runtime:
    Mono 3.2.0 ((no/7c7fcc7)
    GTK 2.24.20
    GTK# (2.12.0.0)
    Package version: 302000000

[...]

Apple Developer Tools
Xcode 5.0 (3332.25)
Build 5A1413

[...]

Xamarin.iOS
Version: 6.4.5.0 (Trial Edition)
Hash: 1336a36
Branch: 
Build date: 2013-10-09 11:14:45-0400

Build Information
Release ID: 400120003
Git revision: 593d7acb1cb78ceeeb482d5133cf1fe514467e39
Build date: 2013-08-07 20:30:53+0000
Xamarin addins: 25a0858b281923e666b09259ad4746b774e0a873

Operating System
Mac OS X 10.8.5
Darwin Gutembergs-MacBook-Pro.local 12.5.0 Darwin Kernel Version 12.5.0
    Mon Jul 29 16:33:49 PDT 2013
    root:xnu-2050.48.11~1/RELEASE_X86_64 x86_64
Adan answered 18/9, 2013 at 23:48 Comment(5)
Which version of Xamarin.iOS are you using ? and which version of iOS ? simulator or devices ? The above works fine with the just released 7.0 (and iOS 7.0). Documentation on the framework is sparse (it's not used by many people) even from Apple but it should apply easily to Xamarin.iOS.Sporulate
I'm using the latest Xamarin for iOS and targeting iOS 6.1(with the latest xcode that supports iOS 7 but we still on previous one yet).Adan
Sadly latest means next-to-nothing... even less in a few days, months, years where the question will still be on stackoverflow.com. The easiest way to get exact version information is to use the "Xamarin Studio" menu, "About Xamarin Studio" item, "Show Details" button and copy/paste the version informations (you can use the "Copy Information" button). You can edit your question to include this (it won't be readable as a comment).Sporulate
I cannot duplicate this exception. Can you file a bug report (bugzilla.xamarin.com) and attach a small, self-contained test case ? My only guess is that something in your options (e.g. build settings) might be causing an indirect issue.Sporulate
@Sporulate Thanks for the reply. I updated to the latest bits yesterday and the error stopped and did nothing new :)Adan
S
5

Although it seems like you've worked this out, I thought I'd show some code snippets that show the basics (in this case connecting to a Sphero and turning it green):

EAAccessoryManager mgr = EAAccessoryManager.SharedAccessoryManager;
var accessories = mgr.ConnectedAccessories;
foreach(var accessory in accessories)
{
    myLabel.Text = "Got me an accessory";
    Console.WriteLine(accessory.ToString());
    Console.WriteLine(accessory.Name);
    var protocol = "com.orbotix.robotprotocol";

    if(accessory.ProtocolStrings.Where(s => s == protocol).Any())
    {
        myLabel.Text = "Got me a Sphero";

        var session = new EASession(accessory, protocol);
        var outputStream = session.OutputStream;
        outputStream.Delegate = new MyOutputStreamDelegate(myLabel);
        outputStream.Schedule(NSRunLoop.Current, "kCFRunLoopDefaultMode");
        outputStream.Open();
    }
}

and

public class MyOutputStreamDelegate : NSStreamDelegate
{
    UILabel label;
    bool hasWritten = false;

    public MyOutputStreamDelegate(UILabel label)
    {
        this.label = label;
    }
    public override void HandleEvent(NSStream theStream, NSStreamEvent streamEvent)
    {
        if(streamEvent == NSStreamEvent.HasSpaceAvailable && ! hasWritten)
        {
            //Set the color of the Sphero
            var written = ((NSOutputStream)theStream).Write(new byte[] {0xFF, 0xFF, 0x02, 0x20, 0x0e, 0x05, 0x1F, 0xFF, 0x1B, 0x00, 0x91}, 11);
            if(written == 11)
            {
                label.Text = "Sphero should be green";
            }
            hasWritten = true;
        }
    }
}
Somato answered 20/9, 2013 at 21:14 Comment(0)
V
4

I know you specifically asked about writing data to the bluetooth device, but this is just expanding on reading data, as well as the general use of the External Accessory API for Xamarin.iOS because there isn't much documentation or Xamarin samples out there. This is a loose conversion from the Apple sample done with Objective-C. My accessory was a MFi certified microchip reader. I've only put in the "read" functionality since I only needed that for my app.

Create a SessionController class inheriting from NSStreamDelegate and this does a lot of the plumbing. Opens, closes sessions, handles events from the device and reads the data. You'd add your write methods here too I think.

public class EASessionController : NSStreamDelegate
{

    NSString SessionDataReceivedNotification = (NSString)"SessionDataReceivedNotification";

    public static EAAccessory _accessory;
    public static string _protocolString;

    EASession _session;
    NSMutableData _readData;

    public static EASessionController SharedController()
    {
        EASessionController sessionController = null;

        if (sessionController == null)
        {
            sessionController = new EASessionController();
        }

        return sessionController;

    }

    public void SetupController(EAAccessory accessory, string protocolString)
    {

        _accessory = accessory;
        _protocolString = protocolString;

    }

    public bool OpenSession()
    {

        Console.WriteLine("opening new session");

        _accessory.WeakDelegate = this;

        if (_session == null)
            _session = new EASession(_accessory, _protocolString);

        // Open both input and output streams even if the device only makes use of one of them

        _session.InputStream.Delegate = this;
        _session.InputStream.Schedule(NSRunLoop.Current, NSRunLoopMode.Default);
        _session.InputStream.Open();

        _session.OutputStream.Delegate = this;
        _session.OutputStream.Schedule(NSRunLoop.Current, NSRunLoopMode.Default);
        _session.OutputStream.Open();

        return (_session != null);

    }

    public void CloseSession()
    {
        _session.InputStream.Unschedule(NSRunLoop.Current, NSRunLoopMode.Default);
        _session.InputStream.Delegate = null;
        _session.InputStream.Close();

        _session.OutputStream.Unschedule(NSRunLoop.Current, NSRunLoopMode.Default);
        _session.OutputStream.Delegate = null;
        _session.OutputStream.Close();

        _session = null;

    }


    /// <summary>
    /// Get Number of bytes to read into local buffer
    /// </summary>
    /// <returns></returns>
    public nuint ReadBytesAvailable()
    {
        return _readData.Length;
    }



    /// <summary>
    /// High level read method
    /// </summary>
    /// <param name="bytesToRead"></param>
    /// <returns></returns>
    public NSData ReadData(nuint bytesToRead)
    {

        NSData data = null;

        if (_readData.Length >= bytesToRead)
        {
            NSRange range = new NSRange(0, (nint)bytesToRead);
            data = _readData.Subdata(range);
            _readData.ReplaceBytes(range, IntPtr.Zero, 0);
        }

        return data;

    }

    /// <summary>
    /// Low level read method - read data while there is data and space in input buffer, then post notification to observer
    /// </summary>
    void ReadData()
    {

        nuint bufferSize = 128;
        byte[] buffer = new byte[bufferSize];

        while (_session.InputStream.HasBytesAvailable())
        {
            nint bytesRead = _session.InputStream.Read(buffer, bufferSize);

            if (_readData == null)
            {
                _readData = new NSMutableData(); 
            }
            _readData.AppendBytes(buffer, 0, bytesRead);
            Console.WriteLine(buffer);


        }

        // We now have our data from the device (stored in _readData), so post the notification for an observer to do something with the data

        NSNotificationCenter.DefaultCenter.PostNotificationName(SessionDataReceivedNotification, this);

    }


    /// <summary>
    /// Handle the events occurring with the external accessory
    /// </summary>
    /// <param name="theStream"></param>
    /// <param name="streamEvent"></param>
    public override void HandleEvent(NSStream theStream, NSStreamEvent streamEvent)
    {

        switch (streamEvent)
        {

            case NSStreamEvent.None:
                Console.WriteLine("StreamEventNone");
                break;
            case NSStreamEvent.HasBytesAvailable:
                Console.WriteLine("StreamEventHasBytesAvailable");
                ReadData();
                break;
            case NSStreamEvent.HasSpaceAvailable:
                Console.WriteLine("StreamEventHasSpaceAvailable");
                // Do write operations to the device here
                break;
            case NSStreamEvent.OpenCompleted:
                Console.WriteLine("StreamEventOpenCompleted");
                break;
            case NSStreamEvent.ErrorOccurred:
                Console.WriteLine("StreamEventErroOccurred");
                break;
            case NSStreamEvent.EndEncountered:
                Console.WriteLine("StreamEventEndEncountered");
                break;
            default:
                Console.WriteLine("Stream present but no event");
                break;

        }
    }

}

In my ViewController that's going to display the data I've just read from the external accessory, we wire it all up. In ViewDidLoad, create observers so the view knows when an event has been fired by the device. Also check we're connected to the correct accessory and open a session.

  public EASessionController _EASessionController;
  EAAccessory[] _accessoryList;
  EAAccessory _selectedAccessory;
  NSString SessionDataReceivedNotification = (NSString)"SessionDataReceivedNotification";
  string myDeviceProtocol = "com.my-microchip-reader.1234";

  public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        NSNotificationCenter.DefaultCenter.AddObserver(EAAccessoryManager.DidConnectNotification, EADidConnect);
        NSNotificationCenter.DefaultCenter.AddObserver(EAAccessoryManager.DidDisconnectNotification, EADidDisconnect);
        NSNotificationCenter.DefaultCenter.AddObserver(SessionDataReceivedNotification, SessionDataReceived);
        EAAccessoryManager.SharedAccessoryManager.RegisterForLocalNotifications();



        _EASessionController = EASessionController.SharedController();
        _accessoryList = EAAccessoryManager.SharedAccessoryManager.ConnectedAccessories;

        foreach (EAAccessory acc in _accessoryList)
        {
            if (acc.ProtocolStrings.Contains(myDeviceProtocol))
            {
                // Connected to the correct accessory
                _selectedAccessory = acc;
                _EASessionController.SetupController(acc, myDeviceProtocol);
                _EASessionController.OpenSession();
                lblEAConnectionStatus.Text = acc.Name;

                Console.WriteLine("Already connected via bluetooth");

            }
            else
            {
                // Not connected
            }
        }

}

Create the DidConnect, DidDisconnect and SessionDataReceived methods. The device name is just updated on some labels when connected/disconnected and I'm displaying the data in text field.

void EADidConnect(NSNotification notification)
    {
        EAAccessory connectedAccessory = (EAAccessory)notification.UserInfo.ObjectForKey((NSString)"EAAccessoryKey");
        Console.WriteLine("I did connect!!");
        _accessoryList = EAAccessoryManager.SharedAccessoryManager.ConnectedAccessories;

        // Reconnect and open the session in case the device was disconnected
        foreach (EAAccessory acc in _accessoryList)
        {
            if (acc.ProtocolStrings.Contains(myDeviceProtocol))
            {
                // Connected to the correct accessory
                _selectedAccessory = acc;
                Console.WriteLine(_selectedAccessory.ProtocolStrings);

                _EASessionController.SetupController(acc, myDeviceProtocol);
                _EASessionController.OpenSession();

            }
            else
            {
                // Not connected
            }
        }

        Console.WriteLine(connectedAccessory.Name);

        // Update a label to show it's connected
        lblEAConnectionStatus.Text = connectedAccessory.Name;

    }

    void EADidDisconnect(NSNotification notification)
    {

        Console.WriteLine("Accessory disconnected");
        _EASessionController.CloseSession();
        lblEAConnectionStatus.Text = string.Empty;

    }


    /// <summary>
    /// Data receieved from accessory
    /// </summary>
    /// <param name="notification"></param>
    void SessionDataReceived(NSNotification notification)
    {

        EASessionController sessionController = (EASessionController)notification.Object;

        nuint bytesAvailable = 0;


        while ((bytesAvailable = sessionController.ReadBytesAvailable()) > 0)
        {

            // read the data as a string

            NSData data = sessionController.ReadData(bytesAvailable);
            NSString chipNumber = new NSString(data, NSStringEncoding.UTF8);

           // Displaying the data
            txtMircochipNumber.Text = chipNumber;
           }


        }
Voyeurism answered 13/7, 2017 at 0:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.