Skip to content

Commit

Permalink
Fix iOS17 bug where encoding buffer does not include newline character (
Browse files Browse the repository at this point in the history
#280)

* fix(bug): changes default encoding to fix bug in iOS17 where newline character is not stringified

* Update docs (#280)

---------

Co-authored-by: James Best <[email protected]>
  • Loading branch information
jim-at-jibba and jim-at-jibba authored Nov 21, 2023
1 parent 486298d commit 47e3113
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 46 deletions.
7 changes: 5 additions & 2 deletions docs/src/docs/ios/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Once installed [autolinking](https://github.com/react-native-community/cli/blob/

Remember to make sure you have your protocol strings provided within your application `plist` file - this is a requirement of the External Accessory framework. This is the top cause of:

> `Unhandled JS Exception: TypeError: Cannot read property 'xxx' of undefined`
> `Unhandled JS Exception: TypeError: Cannot read property 'xxx' of undefined`
while attempting to use the library. An example of what this looks like in your own `plist` file is:

Expand All @@ -42,6 +42,9 @@ There are also a number of other IOS security related items that are required:

```


> In iOS17 a seemingly silent change has occurred where `utf` encoded strings with newline characters are encoded in an unexpected way. This means that the default delimiter of a new line character does not get picked up in the `read` function and so no message is returned. To fix this the default encoding has been changed to `nonLossyASCII`. [Encoding Docs](https://developer.apple.com/documentation/swift/string/encoding/nonlossyascii).
## F.A.Q

#### Why isn't my protocol working?
Expand All @@ -52,7 +55,7 @@ Remember these are MFi protocols, they are not communication protocols. This is

You either need to find the protocols listed on the companies website (Zebra for example has some posted) but in most cases these are kept super secret; like Fight Club secret!

Getting devices MFi compliant costs companies a boat load of money, so they don't generally give these out for free. You'll need to work with a vendor constantly (as you'll find there are some super annoying things to do for releasing).
Getting devices MFi compliant costs companies a boat load of money, so they don't generally give these out for free. You'll need to work with a vendor constantly (as you'll find there are some super annoying things to do for releasing).

#### I Can See my Device in Bluetooth Screen

Expand Down
16 changes: 9 additions & 7 deletions docs/src/home/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@
- [https://github.com/rusel1989/react-native-bluetooth-serial](https://github.com/rusel1989/react-native-bluetooth-serial)
- [https://github.com/nuttawutmalee/react-native-bluetooth-serial-next](https://github.com/nuttawutmalee/react-native-bluetooth-serial-next)

are both fantastic implementations but they fall back to BLE on IOS.
are both fantastic implementations but they fall back to BLE on IOS.

## Environments

The following environments are supported with React Native and this library:

## IOS
## IOS

Devices using bluetooth classic and MFi requires communication through the [External Accessory](https://developer.apple.com/documentation/externalaccessory) framework. Please note that while working with this library on IOS, you'll need to become accustomed with the Apple and their [MFi program](https://en.wikipedia.org/wiki/MFi_Program).
Devices using bluetooth classic and MFi requires communication through the [External Accessory](https://developer.apple.com/documentation/externalaccessory) framework. Please note that while working with this library on IOS, you'll need to become accustomed with the Apple and their [MFi program](https://en.wikipedia.org/wiki/MFi_Program).

Here are some links to check out:

- [MFi_Program](https://en.wikipedia.org/wiki/MFi_Program)
- [EA Demo Introduction](https://developer.apple.com/library/archive/samplecode/EADemo/Introduction/Intro.html)
- [Streams](https://developer.apple.com/documentation/foundation/stream)

## Android
> In iOS17 a seemingly silent change has occurred where `utf` encoded strings with newline characters are encoded in an unexpected way. This means that the default delimiter of a new line character does not get picked up in the `read` function and so no message is returned. To fix this the default encoding has been changed to `nonLossyASCII`. [Encoding Docs](https://developer.apple.com/documentation/swift/string/encoding/nonlossyascii).
## Android

Android uses the standard BluetoothAdapter for communication:

Expand Down Expand Up @@ -53,8 +55,8 @@ The following tables contains the best version availability based on OS type and
| 1.70.x | 0.70.0 | 11 () | IOS 9 | main |

> With how crazy mobile development is, it may or may not be possible to use the library in lower or
> higher versions that those noted. I will always accept reasonable pull requests that bridge the gap
> between usable and required versions. This library can also be modified directly in your project and
> higher versions that those noted. I will always accept reasonable pull requests that bridge the gap
> between usable and required versions. This library can also be modified directly in your project and
> stored within your own project repository if needed.
## Changes
Expand All @@ -69,7 +71,7 @@ Installation, like almost everything, is done through `npm`:
$ npm install react-native-bluetooth-classic --save
```

Once installed [autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) will take over within your application.
Once installed [autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) will take over within your application.

> With the changes in v1.0.0 it's possible that Autolinking doesn't actually work (just be prepared for that). The goal is to have it 100% working and customizable as per the React Native documentation, but until then just beware.
Expand Down
74 changes: 37 additions & 37 deletions ios/conn/DelimitedStringDeviceConnectionImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import ExternalAccessory
* @author kendavidson
*/
class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDelegate {

private var _dataReceivedDelegate: DataReceivedDelegate?
var dataReceivedDelegate: DataReceivedDelegate? {
set(newDelegate) {
Expand All @@ -38,55 +38,55 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
return self._dataReceivedDelegate
}
}

private var session: EASession?
private var inBuffer: Data
private var outBuffer: Data

private(set) var accessory: EAAccessory
private(set) var properties: Dictionary<String,Any>

private var readSize: Int
private var delimiter: String
private var encoding: String.Encoding

init(
accessory: EAAccessory,
options: Dictionary<String,Any>
) {
self.accessory = accessory;
self.properties = Dictionary<String,Any>()
self.properties.merge(options) { $1 }

self.inBuffer = Data()
self.outBuffer = Data()

// For lack of knowing how to actually do this properly, Swift (I can't stand) doesn't like
// the enumeration method from Java. If someone can figure this one out, let me know.
if let value = self.properties["READ_SIZE"] { self.readSize = value as! Int }
if let value = self.properties["read_size"] { self.readSize = value as! Int }
else { self.readSize = 1024 }

if let value = self.properties["DELIMITER"] { self.delimiter = value as! String }
else if let value = self.properties["delimiter"] { self.delimiter = value as! String }
else { self.delimiter = "\n" }

if let value = self.properties["DEVICE_CHARSET"] { self.encoding = String.Encoding.from(value as! CFStringEncoding) }
else if let value = self.properties["device_charset"] { self.encoding = String.Encoding.from(value as! CFStringEncoding) }
else if let value = self.properties["charset"] { self.encoding = String.Encoding.from(value as! CFStringEncoding) }
else { self.encoding = String.Encoding.from(CFStringBuiltInEncodings.ASCII.rawValue) }
else { self.encoding = String.Encoding.from(CFStringBuiltInEncodings.nonLossyASCII.rawValue) }
}

/**
* This implementation attempts to open an EASession for the provided protocol string
*/
func connect() throws {
let protocolString: String = self.properties["PROTOCOL_STRING"] as! String

NSLog("(BluetoothDevice:connect) Attempting Bluetooth connection to %@ using protocol %@", accessory.serialNumber, protocolString)
if let connected = EASession(accessory: accessory, forProtocol: protocolString) {
self.session = connected

if let inStream = connected.inputStream,
let outStream = connected.outputStream {
inStream.delegate = self
Expand All @@ -97,10 +97,10 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
outStream.open()
}
} else {
throw BluetoothError.CONNECTION_FAILED
throw BluetoothError.CONNECTION_FAILED
}
}

/**
* Attempts to disconnect from the EAAccessory and EASession
*/
Expand All @@ -116,10 +116,10 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
outStream.remove(from: .main, forMode: .commonModes)
}
}

session = nil
}

/**
* Returns the number of mesages available. As this is a delmited string connection, the number of messages
* are the number of delimiters found.
Expand All @@ -129,7 +129,7 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
func available() -> Int {
var count = 0;
let content = String(data: inBuffer, encoding: self.encoding)!

// I really hate swift, apparently String.isEmpty is ambiguious??
if (delimiter.count == 0) {
count = content.count
Expand All @@ -138,10 +138,10 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
count += 1
}
}

return count;
}

/**
* Attempts to write to the out buffer. This intermedate step is required so that the StreamDelegate will find and read
* the available information when more space is available on the stream.
Expand All @@ -151,7 +151,7 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
if let sending = String(data: data, encoding: self.encoding) {
NSLog("(BluetoothDevice:writeToDevice) Writing %@ to device %@", sending, accessory.serialNumber)
outBuffer.append(data)

// If there is space available for writing then we want to kick off the process.
// If all the data cannot be fully written, then the hasSpaceAvailable will be
// fired and we can continue. In most cases, we shouldn't be sending that much
Expand All @@ -160,10 +160,10 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
} else {
return false
}

return true
}

/**
* Reads the next message from the inBuffer. This particular implementation converse the inBuffer into a
* String using the provided encoding, then search for the first instance of that delimiter, and finally
Expand All @@ -172,7 +172,7 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
func read() -> String? {
NSLog("(BluetoothDevice:readFromDevice) Reading device %@ until delimiter %@",
self.accessory.serialNumber, self.delimiter)

let content = String(data: inBuffer, encoding: self.encoding)!
var message:String?

Expand All @@ -186,18 +186,18 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
.data(using: self.encoding) ?? Data()
}
}

return message
}

/**
* Removed all data from the in/out buffers.
* Removed all data from the in/out buffers.
*/
func clear() {
inBuffer.removeAll()
outBuffer.removeAll()
}

/**
* Implements the StreamDelegate stream method.
*/
Expand Down Expand Up @@ -232,7 +232,7 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
NSLog("Stream %@ had some other event occur", aStream)
}
}

/**
* Reads data from the session.inputStream when there are available bytes. Data is
* appended to the receivedData string for access later. If there is a
Expand All @@ -241,24 +241,24 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
*/
private func readDataFromStream(_ stream: InputStream) {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: readSize)

while (stream.hasBytesAvailable) {
let numBytesRead = stream.read(buffer, maxLength: readSize)

if (numBytesRead < 0) {
break;
}

inBuffer.append(buffer, count: numBytesRead)
}

if let delegate = self.dataReceivedDelegate {
while let data = read() {
delegate.onReceivedData(fromDevice: accessory, receivedData: data)
}
}
}

/**
* Attempts to write as much data to the OutputStream as possible - currently this is maxed out at 512
* bytes per attempt. If all the data can't be written at one time, then the remaining will be written
Expand All @@ -272,14 +272,14 @@ class DelimitedStringDeviceConnectionImpl : NSObject, DeviceConnection, StreamDe
NSLog("(BluetoothDevice:writeData) No buffer data scheduled for deliver")
return
}

let len:Int = (outBuffer.count > self.readSize) ? self.readSize : outBuffer.count
NSLog("(BluetoothDevice:writeData) Attempting to send %d bytes to the device", len)

let buffer:UnsafeMutablePointer<UInt8> = UnsafeMutablePointer.allocate(capacity: len)
outBuffer.copyBytes(to: buffer, count: len)
outBuffer.removeFirst(len)

let bytesWritten = stream.write(buffer, maxLength: len)
NSLog("(BluetoothDevice:writeData) Sent %d bytes to the device", bytesWritten)
}
Expand Down

0 comments on commit 47e3113

Please sign in to comment.