如何通过蓝牙使用 Xamarin.Android/iOS/Mono 和 SPP?

How to work with Xamarin.Android/iOS/Mono and SPP over Bluetooth?

我需要在 Xamarin 上通过蓝牙使用 SPP。Android/iOS/Mono。这是我正在为 Xamarin.Android 尝试的代码,但如果我转到 iOS 和 Linux/Mac 上的 Mono,行为是相同的:

    using System;
using System.Collections.Generic;
using System.Text;
using Android.Bluetooth;
using Java.Util;
using System.IO;
using System.Threading;
using PI.SDK.Devices.BC.Responses;
using System.Threading.Tasks;

namespace PI.SDK.Devices.BC
{
    public class BluetoothDeviceConnectionChannel : IBCDeviceConnectionChannel
    {
        private Queue<ResponseBase> _dispatcher;
        private bool _abort = false;

        private BluetoothAdapter _adapter;
        private BluetoothSocket _socket;
        private BluetoothDevice _device;
        private static UUID _uuid = UUID.FromString("00001101-0000-1000-8000-00805F9B34FB");
        private StreamReader _reader;
        private StreamWriter _writer;
        private string _deviceAddress;

        public event Action<string> Notify;
        public bool IsOpen { get { return _socket.IsConnected; } }

        public BluetoothDeviceConnectionChannel(string deviceAddress)
        {
            _adapter = BluetoothAdapter.DefaultAdapter;
            if (_adapter == null)
                throw new PIDeviceManagerException("Bluetooth is not supported on this Android device");

            _deviceAddress = deviceAddress;
        }

        public BluetoothDeviceConnectionChannel(BluetoothDevice device) : this(device.Address) { }

        public void Close()
        {
            _socket.Close();
        }

        public bool Open()
        {
            if (!_adapter.IsEnabled)
            {
                throw new PIDeviceManagerException("Bluetooth is not enabled");
            }

            _adapter.CancelDiscovery();

            _device = _adapter.GetRemoteDevice(_deviceAddress);
            _socket = _device.CreateRfcommSocketToServiceRecord(_uuid);
            _socket.Connect();

            if (_socket.IsConnected)
            {
                _reader = new StreamReader(_socket.InputStream, Encoding.GetEncoding("Windows-1252"));
                _writer = new StreamWriter(_socket.OutputStream, Encoding.GetEncoding("Windows-1252"));
                _dispatcher = new Queue<ResponseBase>();
                Task.Factory.StartNew(() => ReceiveData());
                return true;
            }

            return false;
        }

        public void ReceiveData()
        {
            while (_socket != null && _socket.IsConnected)
            {
                var data = _reader.ReadToEnd();

                if (string.IsNullOrWhiteSpace(data))
                    continue;

                var dataBuffer = data.ToCharArray();
                var synBuilder = new StringBuilder();

                foreach (var c in dataBuffer)
                {
                    switch (c)
                    {
                        case ControlChars.NACK:
                        case ControlChars.EOT:
#if DEBUG
                            System.Diagnostics.Debug.WriteLine($"[PINPAD -> APP] {c.ToString().Dump()}");
#endif
                            _abort = true;
                            return;
                        case ControlChars.ACK:
#if DEBUG
                            System.Diagnostics.Debug.WriteLine($"[PINPAD -> APP] {c.ToString().Dump()}");
#endif
                            continue;
                        case ControlChars.SYN:
                            synBuilder.Append(c);
                            break;
                        case ControlChars.ETB:
                            synBuilder.Append(c);
                            var cmdResponse = synBuilder.ToString();
#if DEBUG
                            System.Diagnostics.Debug.WriteLine($"[PINPAD -> APP] {cmdResponse.Dump()}");
#endif
                            var response = CommandResponseParser.Parse(cmdResponse);
                            if (response != null)
                            {
                                _dispatcher.Enqueue(response);
                            }
                            return;
                        default:
                            synBuilder.Append(c);
                            break;
                    }
                }
            }
        }

        public ResponseBase SendData(string data)
        {
            _abort = false;
            try
            {
                _writer.Write(data);
            }
            catch
            {
                throw new PIException("Unable to send data to device");
            }
#if DEBUG
            System.Diagnostics.Debug.WriteLine($"[APP -> PINPAD] {data.Dump()}");
#endif

            if (data[0] == ControlChars.CAN)
            {
                Thread.Sleep(100);
                return null;
            }

            while (!_abort)
            {
                if (_dispatcher.Count > 0)
                {
                    var response = _dispatcher.Dequeue();
                    if (response != null)
                    {
                        if (response is PPNotifyResponse)
                        {
                            if (Notify != null && Notify.GetInvocationList().Length > 0)
                                Notify(response.Message);

                            continue;
                        }

                        return response;
                    }
                }
            }
            throw new InvalidOperationException("invalidData");
        }

        public ResponseBase SendData(CommandBase data)
        {
            var cmd = data.ToBCCommandString();
            return SendData(cmd);
        }
    }
}

我想使用 SerialPort class 和 COMxxx 端口为 Windows 实现与下面代码相同的行为,其中这个端口只不过是一个串行蓝牙 COM 与目标设备。

using PI.SDK.Devices.BC.Responses;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
using System.Threading;

namespace PI.SDK.Devices.BC
{
    public class SerialDeviceConnectionChannel : IBCDeviceConnectionChannel
    {
        private SerialPort _port;
        private Queue<ResponseBase> _dispatcher;
        private bool _abort = false;

        public event Action<string> Notify;
        public bool IsOpen { get { return _port.IsOpen; } }

        public SerialDeviceConnectionChannel(string port)
        {
            _port = new SerialPort(port, 19200, Parity.None, 8, StopBits.One);
            _port.ReadTimeout = 3 * 1000;
            _port.WriteTimeout = 3 * 1000;
            _port.Encoding = Encoding.GetEncoding(1252);
            _port.DataReceived += DataReceived;
        }
        public void Close()
        {
            _port.Close();
        }

        public bool Open()
        {
            while (true)
            {
                try
                {
                    _port.Open();
                    _port.DiscardInBuffer();
                    _port.DiscardInBuffer();

                    break;
                }
                catch { Console.WriteLine($"Trying to connect to  {_port}"); }
            }
            _dispatcher = new Queue<ResponseBase>();
            return _port.IsOpen;
        }

        private void DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            var data = _port.ReadExisting();

            var dataBuffer = data.ToCharArray();
            var synBuilder = new StringBuilder();
            foreach (var c in dataBuffer)
            {
                switch (c)
                {
                    case ControlChars.NACK:
                    case ControlChars.EOT:
#if DEBUG
                        Console.WriteLine($"[PINPAD -> APP] {c.ToString().Dump()}");
#endif
                        _abort = true;
                        return;
                    case ControlChars.ACK:
#if DEBUG
                        Console.WriteLine($"[PINPAD -> APP] {c.ToString().Dump()}");
#endif
                        continue;
                    case ControlChars.SYN:
                        synBuilder.Append(c);
                        break;
                    case ControlChars.ETB:
                        synBuilder.Append(c);
                        var cmdResponse = synBuilder.ToString();
#if DEBUG
                        Console.WriteLine($"[PINPAD -> APP] {cmdResponse.Dump()}");
#endif
                        var response = CommandResponseParser.Parse(cmdResponse);
                        if (response != null)
                        {
                            _dispatcher.Enqueue(response);
                        }
                        return;
                    default:
                        synBuilder.Append(c);
                        break;
                }
            }    
        }

        public ResponseBase SendData(string data)
        {
            _abort = false;
            try
            {
                _port.Write(data);
            }
            catch
            {
                throw new PIException("Unable to send data to device");
            }
#if DEBUG
            Console.WriteLine($"[APP -> PINPAD] {data.Dump()}");
#endif

            if (data[0] == ControlChars.CAN)
            {
                Thread.Sleep(100);
                return null;
            }

            while (!_abort)
            {
                if (_dispatcher.Count > 0)
                {
                    var response = _dispatcher.Dequeue();
                    if (response != null)
                    {
                        if (response is PPNotifyResponse)
                        {
                            if (Notify != null && Notify.GetInvocationList().Length > 0)
                                Notify(response.Message);

                            continue;
                        }

                        return response;
                    }
                }
            }
            throw new InvalidOperationException("invalidData");
        }

        public ResponseBase SendData(CommandBase data)
        {
            var cmd = data.ToBCCommandString();
            return SendData(cmd);
        }
    }
}

此代码在调用 _reader.ReadToEnd() 时挂起;在除 Windows 以外的所有其他平台上。看来我没有收到回复。

请注意,Android/iOS/Mono 版本必须遵守 Windows classe 的构造函数中所述的串行连接配置,以及消息和串行通信的编码, 必须是 Windows-1252.

任何指出错误或如何让它以与 Windows 相同的方式工作的帮助将不胜感激,因为没有 SerialPort class,我有点迷失在那些设备上并且在谈论 xamarin/mobile 设备时,蓝牙通讯似乎有些晦涩难懂。

谢谢!

此致, 古腾堡

找到问题了。在 Windows 中,对 serialPort.Read() 的调用可以是异步的,而在其他线程上,在 Android/iOS/Mono 中则不能。

如果我在 _writer.Write() 之后开始阅读,换句话说,在同一个线程上,我可以让它正常工作。