react-native-bluetooth-escpos-printer-fork 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -5
- package/android/.classpath +6 -0
- package/android/.gradle/6.1.1/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/6.1.1/fileChanges/last-build.bin +0 -0
- package/android/.gradle/6.1.1/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/6.1.1/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/checksums/checksums.lock +0 -0
- package/android/.gradle/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/.project +17 -0
- package/android/.settings/org.eclipse.buildship.core.prefs +11 -0
- package/android/.settings/org.eclipse.jdt.core.prefs +4 -0
- package/ios/ImageUtils.m +42 -17
- package/ios/PrintImageBleWriteDelegate.m +14 -14
- package/ios/RNBluetoothEscposPrinter.m +14 -1
- package/ios/RNBluetoothManager.h +1 -0
- package/ios/RNBluetoothManager.m +393 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,21 @@ Link the plugin to your RN project
|
|
|
28
28
|
react-native link react-native-bluetooth-escpos-printer
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
### Step3 (iOS only) ###
|
|
32
|
+
Add Bluetooth permissions to your `Info.plist`:
|
|
33
|
+
|
|
34
|
+
```xml
|
|
35
|
+
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
36
|
+
<string>This app needs Bluetooth to discover and connect to printers</string>
|
|
37
|
+
<key>NSBluetoothPeripheralUsageDescription</key>
|
|
38
|
+
<string>This app needs Bluetooth to connect to printer peripherals</string>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Note:**
|
|
42
|
+
- For iOS 13+, `NSBluetoothAlwaysUsageDescription` is required.
|
|
43
|
+
- For iOS 12 and below, `NSBluetoothPeripheralUsageDescription` is required.
|
|
44
|
+
- It's recommended to include both for maximum compatibility.
|
|
45
|
+
|
|
31
46
|
### Manual linking (Android) ###
|
|
32
47
|
Ensure your build files match the following requirements:
|
|
33
48
|
|
|
@@ -73,8 +88,12 @@ async function, checks whether Bluetooth service is enabled.
|
|
|
73
88
|
});
|
|
74
89
|
```
|
|
75
90
|
|
|
76
|
-
* enableBluetooth ==>
|
|
77
|
-
async function, enables the bluetooth service, returns the devices information already bound and paired.
|
|
91
|
+
* enableBluetooth ==>
|
|
92
|
+
async function, enables the bluetooth service, returns the devices information already bound and paired.
|
|
93
|
+
|
|
94
|
+
**Platform differences:**
|
|
95
|
+
- **Android:** If Bluetooth is off, shows system dialog to enable it. Returns array of paired devices after enabling (or immediately if already enabled).
|
|
96
|
+
- **iOS:** Cannot programmatically enable Bluetooth (must be done in Settings by user). Returns empty array `[]` since iOS Core Bluetooth doesn't have a "paired devices" concept. Use `scanDevices()` to discover available devices.
|
|
78
97
|
|
|
79
98
|
```javascript
|
|
80
99
|
BluetoothManager.enableBluetooth().then((r)=>{
|
|
@@ -94,8 +113,12 @@ BluetoothManager.enableBluetooth().then((r)=>{
|
|
|
94
113
|
});
|
|
95
114
|
```
|
|
96
115
|
|
|
97
|
-
* disableBluetooth ==>
|
|
98
|
-
async function
|
|
116
|
+
* disableBluetooth ==>
|
|
117
|
+
async function, disables the bluetooth service.
|
|
118
|
+
|
|
119
|
+
**Platform differences:**
|
|
120
|
+
- **Android:** Programmatically disables Bluetooth and stops any active connections.
|
|
121
|
+
- **iOS:** Cannot programmatically disable Bluetooth (must be done in Settings or Control Center by user). This method resolves successfully but does nothing on iOS.
|
|
99
122
|
|
|
100
123
|
```javascript
|
|
101
124
|
BluetoothManager.disableBluetooth().then(()=>{
|
|
@@ -139,6 +162,36 @@ BluetoothManager.scanDevices()
|
|
|
139
162
|
});
|
|
140
163
|
```
|
|
141
164
|
|
|
165
|
+
**Device Information Structure:**
|
|
166
|
+
|
|
167
|
+
Each device object returned contains:
|
|
168
|
+
- `name` (string): Device name
|
|
169
|
+
- `address` (string): Device MAC address (Android) or UUID (iOS)
|
|
170
|
+
- `deviceClass` (number): Bluetooth device class
|
|
171
|
+
- `majorDeviceClass` (number): Major device class category
|
|
172
|
+
- `rssi` (number, iOS only): Signal strength indicator
|
|
173
|
+
|
|
174
|
+
**Filtering for Printers:**
|
|
175
|
+
|
|
176
|
+
To identify printer devices, check the `majorDeviceClass`:
|
|
177
|
+
```javascript
|
|
178
|
+
const MAJOR_CLASS_IMAGING = 1536; // 0x0600 - Printers, scanners, cameras
|
|
179
|
+
const DEVICE_CLASS_PRINTER = 1664; // 0x0680 - Specifically printers
|
|
180
|
+
|
|
181
|
+
// Filter found devices to show only printers
|
|
182
|
+
const printers = ss.found.filter(device =>
|
|
183
|
+
device.majorDeviceClass === MAJOR_CLASS_IMAGING ||
|
|
184
|
+
device.deviceClass === DEVICE_CLASS_PRINTER
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Platform differences:**
|
|
189
|
+
- **Android:** Device classes are provided by the OS based on the device's Bluetooth profile
|
|
190
|
+
- **iOS:** Device classes are inferred from:
|
|
191
|
+
1. Advertised service UUIDs (SPP, ESC/POS services)
|
|
192
|
+
2. Device name patterns (contains "print", "pos", "thermal", "receipt", etc.)
|
|
193
|
+
3. If no printer indicators are found, device is marked as "unknown" (class 7936)
|
|
194
|
+
|
|
142
195
|
* connect ==>
|
|
143
196
|
async function, connects the specified device, if not bound, bound dailog prompts.
|
|
144
197
|
|
|
@@ -162,8 +215,10 @@ async function, connects the specified device, if not bound, bound dailog prompt
|
|
|
162
215
|
* unpair ==>
|
|
163
216
|
async function, disconnects and unpairs the specified devices
|
|
164
217
|
|
|
218
|
+
**Note for iOS:** Due to iOS security restrictions, this method can only disconnect the device but cannot programmatically unpair it. The device will remain paired at the iOS system level. Users must unpair manually from iOS Settings > Bluetooth if needed. On Android, full unpair functionality is supported.
|
|
219
|
+
|
|
165
220
|
```javascript
|
|
166
|
-
BluetoothManager.
|
|
221
|
+
BluetoothManager.unpaire(rowData.address)
|
|
167
222
|
.then((s)=>{
|
|
168
223
|
//success here
|
|
169
224
|
},
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<classpath>
|
|
3
|
+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
|
|
4
|
+
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
|
5
|
+
<classpathentry kind="output" path="bin/default"/>
|
|
6
|
+
</classpath>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
package/android/.project
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
<projects>
|
|
6
6
|
</projects>
|
|
7
7
|
<buildSpec>
|
|
8
|
+
<buildCommand>
|
|
9
|
+
<name>org.eclipse.jdt.core.javabuilder</name>
|
|
10
|
+
<arguments>
|
|
11
|
+
</arguments>
|
|
12
|
+
</buildCommand>
|
|
8
13
|
<buildCommand>
|
|
9
14
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
10
15
|
<arguments>
|
|
@@ -12,6 +17,18 @@
|
|
|
12
17
|
</buildCommand>
|
|
13
18
|
</buildSpec>
|
|
14
19
|
<natures>
|
|
20
|
+
<nature>org.eclipse.jdt.core.javanature</nature>
|
|
15
21
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
|
16
22
|
</natures>
|
|
23
|
+
<filteredResources>
|
|
24
|
+
<filter>
|
|
25
|
+
<id>1769891269456</id>
|
|
26
|
+
<name></name>
|
|
27
|
+
<type>30</type>
|
|
28
|
+
<matcher>
|
|
29
|
+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
|
30
|
+
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
|
31
|
+
</matcher>
|
|
32
|
+
</filter>
|
|
33
|
+
</filteredResources>
|
|
17
34
|
</projectDescription>
|
|
@@ -1,2 +1,13 @@
|
|
|
1
|
+
arguments=--init-script /Users/lukaszprivate/.local/share/opencode/bin/jdtls/config_mac/org.eclipse.osgi/59/0/.cp/gradle/init/init.gradle
|
|
2
|
+
auto.sync=false
|
|
3
|
+
build.scans.enabled=false
|
|
4
|
+
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
|
1
5
|
connection.project.dir=
|
|
2
6
|
eclipse.preferences.version=1
|
|
7
|
+
gradle.user.home=
|
|
8
|
+
java.home=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
|
|
9
|
+
jvm.arguments=
|
|
10
|
+
offline.mode=false
|
|
11
|
+
override.workspace.settings=true
|
|
12
|
+
show.console.view=true
|
|
13
|
+
show.executions.view=true
|
package/ios/ImageUtils.m
CHANGED
|
@@ -190,12 +190,24 @@ int p6[] = { 0, 0x02 };
|
|
|
190
190
|
**/
|
|
191
191
|
+ (NSData *)eachLinePixToCmd:(unsigned char *)src nWidth:(NSInteger) nWidth nHeight:(NSInteger) nHeight nMode:(NSInteger) nMode
|
|
192
192
|
{
|
|
193
|
-
NSLog(@"SIZE OF SRC: %lu",sizeof(&src));
|
|
194
193
|
NSInteger nBytesPerLine = (int)nWidth/8;
|
|
195
194
|
unsigned char * data = malloc(nHeight*(8+nBytesPerLine));
|
|
196
|
-
// const char* srcData = (const char*)[src bytes];
|
|
197
195
|
NSInteger k = 0;
|
|
198
|
-
|
|
196
|
+
|
|
197
|
+
// Log first few lines to see actual pixel distribution
|
|
198
|
+
int totalBlackPixelsInFirstLines = 0;
|
|
199
|
+
for(int line = 0; line < MIN(10, nHeight); line++) {
|
|
200
|
+
int blackInLine = 0;
|
|
201
|
+
for(int x = 0; x < nWidth; x++) {
|
|
202
|
+
if(src[line * nWidth + x] == 1) blackInLine++;
|
|
203
|
+
}
|
|
204
|
+
if(blackInLine > 0) {
|
|
205
|
+
NSLog(@"Line %d: %d/%d pixels are black", line, blackInLine, (int)nWidth);
|
|
206
|
+
}
|
|
207
|
+
totalBlackPixelsInFirstLines += blackInLine;
|
|
208
|
+
}
|
|
209
|
+
NSLog(@"First 10 lines: %d total black pixels", totalBlackPixelsInFirstLines);
|
|
210
|
+
|
|
199
211
|
for(int i=0;i<nHeight;i++){
|
|
200
212
|
NSInteger var10 = i*(8+nBytesPerLine);
|
|
201
213
|
//GS v 0 m xL xH yL yH d1....dk 打印光栅位图
|
|
@@ -207,19 +219,31 @@ int p6[] = { 0, 0x02 };
|
|
|
207
219
|
data[var10 + 5] = (unsigned char)(nBytesPerLine / 256);//xH
|
|
208
220
|
data[var10 + 6] = 1;//yL
|
|
209
221
|
data[var10 + 7] = 0;//yH
|
|
210
|
-
// for(int l=0;l<8;l++){
|
|
211
|
-
// NSInteger d =data[var10 + l];
|
|
212
|
-
// [toLog appendFormat:@"%ld,",(long)d];
|
|
213
|
-
// }
|
|
214
222
|
|
|
215
223
|
for (int j = 0; j < nBytesPerLine; ++j) {
|
|
216
224
|
data[var10 + 8 + j] = (int) (p0[src[k]] + p1[src[k + 1]] + p2[src[k + 2]] + p3[src[k + 3]] + p4[src[k + 4]] + p5[src[k + 5]] + p6[src[k + 6]] + src[k + 7]);
|
|
217
225
|
k =k+8;
|
|
218
|
-
// [toLog appendFormat:@"%ld,",(long)data[var10+8+j]];
|
|
219
226
|
}
|
|
220
|
-
// [toLog appendString:@"\n\r"];
|
|
221
227
|
}
|
|
222
|
-
|
|
228
|
+
|
|
229
|
+
// Find and log first line with actual black pixels
|
|
230
|
+
for(int i = 0; i < nHeight; i++) {
|
|
231
|
+
NSInteger var10 = i*(8+nBytesPerLine);
|
|
232
|
+
BOOL hasBlack = NO;
|
|
233
|
+
for(int j = 0; j < MIN(20, nBytesPerLine); j++) {
|
|
234
|
+
if(data[var10 + 8 + j] != 0) {
|
|
235
|
+
hasBlack = YES;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if(hasBlack) {
|
|
240
|
+
NSLog(@"First line with black pixels is line %d, first 10 bytes: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
|
|
241
|
+
i, data[var10+8], data[var10+9], data[var10+10], data[var10+11], data[var10+12],
|
|
242
|
+
data[var10+13], data[var10+14], data[var10+15], data[var10+16], data[var10+17]);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
223
247
|
return [NSData dataWithBytes:data length:nHeight*(8+nBytesPerLine)];
|
|
224
248
|
}
|
|
225
249
|
|
|
@@ -242,24 +266,25 @@ int p6[] = { 0, 0x02 };
|
|
|
242
266
|
}
|
|
243
267
|
|
|
244
268
|
int grayave = graytotal / ysize / xsize;
|
|
269
|
+
NSLog(@"Threshold: average gray = %d (pixels lighter than this become white)", grayave);
|
|
270
|
+
|
|
245
271
|
k = 0;
|
|
246
|
-
|
|
247
|
-
// int oneCount = 0;
|
|
272
|
+
int oneCount = 0;
|
|
248
273
|
for(i = 0; i < ysize; ++i) {
|
|
249
274
|
for(j = 0; j < xsize; ++j) {
|
|
250
275
|
gray = orgpixels[k] & 255;
|
|
251
276
|
if(gray > grayave) {
|
|
252
|
-
despixels[k] = 0;
|
|
277
|
+
despixels[k] = 0; // White (don't print)
|
|
253
278
|
} else {
|
|
254
|
-
despixels[k] = 1;
|
|
255
|
-
|
|
279
|
+
despixels[k] = 1; // Black (print)
|
|
280
|
+
oneCount++;
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
++k;
|
|
259
|
-
// [logStr appendFormat:@"%d,",despixels[k]];
|
|
260
284
|
}
|
|
261
285
|
}
|
|
262
|
-
|
|
286
|
+
NSLog(@"Thresholding complete: %d/%d pixels are black (%.1f%%)",
|
|
287
|
+
oneCount, (int)(xsize*ysize), (oneCount * 100.0 / (xsize*ysize)));
|
|
263
288
|
return despixels;
|
|
264
289
|
}
|
|
265
290
|
+(NSData *)pixToTscCmd:(uint8_t *)src width:(NSInteger) width
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
- (void) didWriteDataToBle: (BOOL)success
|
|
15
|
-
{
|
|
15
|
+
{
|
|
16
16
|
if(success){
|
|
17
17
|
if(_now == -1){
|
|
18
18
|
if(_pendingResolve) {_pendingResolve(nil); _pendingResolve=nil;}
|
|
@@ -42,20 +42,20 @@
|
|
|
42
42
|
-(void) print
|
|
43
43
|
{
|
|
44
44
|
@synchronized (self) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// sizePerLine = [_toPrint length] - _now;
|
|
49
|
-
// }
|
|
50
|
-
// if(sizePerLine>0){
|
|
51
|
-
NSData *subData = [_toPrint subdataWithRange:NSMakeRange(_now, sizePerLine)];
|
|
52
|
-
NSLog(@"Write data:%@",subData);
|
|
53
|
-
[RNBluetoothManager writeValue:subData withDelegate:self];
|
|
54
|
-
//}
|
|
55
|
-
_now = _now+sizePerLine;
|
|
56
|
-
[NSThread sleepForTimeInterval:0.01f];
|
|
45
|
+
// Each line contains: 8 bytes (ESC/POS raster command header) + (width/8) bytes (pixel data)
|
|
46
|
+
NSInteger sizePerLine = 8 + (int)(_width/8);
|
|
47
|
+
NSData *subData = [_toPrint subdataWithRange:NSMakeRange(_now, sizePerLine)];
|
|
57
48
|
|
|
49
|
+
// Log first line to debug
|
|
50
|
+
if(_now == 0) {
|
|
51
|
+
NSUInteger logLen = MIN(20, [subData length]);
|
|
52
|
+
NSData *logData = [subData subdataWithRange:NSMakeRange(0, logLen)];
|
|
53
|
+
NSLog(@"First image line (%lu bytes): %@", (unsigned long)[subData length], logData);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[RNBluetoothManager writeValue:subData withDelegate:self];
|
|
57
|
+
_now = _now+sizePerLine;
|
|
58
|
+
[NSThread sleepForTimeInterval:0.05f]; // 50ms delay between lines to avoid overwhelming printer buffer
|
|
58
59
|
}
|
|
59
|
-
//}while(_now<[_toPrint length]);
|
|
60
60
|
}
|
|
61
61
|
@end
|
|
@@ -488,7 +488,8 @@ RCT_EXPORT_METHOD(printPic:(NSString *) base64encodeStr withOptions:(NSDictionar
|
|
|
488
488
|
//mBitmap.getHeight() * width / mBitmap.getWidth();
|
|
489
489
|
NSInteger imgHeight = jpgImage.size.height;
|
|
490
490
|
NSInteger imagWidth = jpgImage.size.width;
|
|
491
|
-
|
|
491
|
+
// Round width to multiple of 8 (required for raster image format)
|
|
492
|
+
NSInteger width = ((nWidth + 7) / 8) * 8;
|
|
492
493
|
CGSize size = CGSizeMake(width, imgHeight*width/imagWidth);
|
|
493
494
|
UIImage *scaled = [ImageUtils imageWithImage:jpgImage scaledToFillSize:size];
|
|
494
495
|
if(paddingLeft>0){
|
|
@@ -497,8 +498,16 @@ RCT_EXPORT_METHOD(printPic:(NSString *) base64encodeStr withOptions:(NSDictionar
|
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
unsigned char * graImage = [ImageUtils imageToGreyImage:scaled];
|
|
501
|
+
NSLog(@"Grayscale conversion complete");
|
|
502
|
+
|
|
500
503
|
unsigned char * formatedData = [ImageUtils format_K_threshold:graImage width:size.width height:size.height];
|
|
504
|
+
NSLog(@"Thresholding complete, generating ESC/POS commands");
|
|
505
|
+
|
|
501
506
|
NSData *dataToPrint = [ImageUtils eachLinePixToCmd:formatedData nWidth:size.width nHeight:size.height nMode:0];
|
|
507
|
+
NSLog(@"Image print: %lux%lu pixels, %lu bytes total, %lu bytes per line",
|
|
508
|
+
(unsigned long)size.width, (unsigned long)size.height,
|
|
509
|
+
(unsigned long)[dataToPrint length], (unsigned long)(8 + size.width/8));
|
|
510
|
+
|
|
502
511
|
PrintImageBleWriteDelegate *delegate = [[PrintImageBleWriteDelegate alloc] init];
|
|
503
512
|
delegate.pendingResolve = resolve;
|
|
504
513
|
delegate.pendingReject = reject;
|
|
@@ -543,6 +552,9 @@ RCT_EXPORT_METHOD(printQRCode:(NSString *)content
|
|
|
543
552
|
uint8_t * graImage = [ImageUtils imageToGreyImage:[UIImage imageWithCGImage:image]];
|
|
544
553
|
unsigned char * formatedData = [ImageUtils format_K_threshold:graImage width:size height:size];
|
|
545
554
|
NSData *dataToPrint = [ImageUtils eachLinePixToCmd:formatedData nWidth:size nHeight:size nMode:0];
|
|
555
|
+
NSLog(@"QR code print: %lux%lu pixels, %lu bytes total, %lu bytes per line",
|
|
556
|
+
(unsigned long)size, (unsigned long)size,
|
|
557
|
+
(unsigned long)[dataToPrint length], (unsigned long)(8 + size/8));
|
|
546
558
|
PrintImageBleWriteDelegate *delegate = [[PrintImageBleWriteDelegate alloc] init];
|
|
547
559
|
delegate.pendingResolve=resolve;
|
|
548
560
|
delegate.pendingReject = reject;
|
|
@@ -560,6 +572,7 @@ RCT_EXPORT_METHOD(printEscPosCommands:(NSString *)base64encodeStr
|
|
|
560
572
|
if(RNBluetoothManager.isConnected){
|
|
561
573
|
@try{
|
|
562
574
|
NSData *decoded = [[NSData alloc] initWithBase64EncodedString:base64encodeStr options:0];
|
|
575
|
+
|
|
563
576
|
pendingResolve = resolve;
|
|
564
577
|
pendingReject = reject;
|
|
565
578
|
[RNBluetoothManager writeValue:decoded withDelegate:self];
|
package/ios/RNBluetoothManager.h
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
@property (nonatomic,copy) RCTPromiseResolveBlock scanResolveBlock;
|
|
20
20
|
@property (nonatomic,copy) RCTPromiseRejectBlock scanRejectBlock;
|
|
21
21
|
@property (strong,nonatomic) NSMutableDictionary <NSString *,CBPeripheral *> *foundDevices;
|
|
22
|
+
@property (strong,nonatomic) NSMutableDictionary <NSString *,NSDictionary *> *foundDevicesMetadata;
|
|
22
23
|
@property (strong,nonatomic) NSString *waitingConnect;
|
|
23
24
|
@property (nonatomic,copy) RCTPromiseResolveBlock connectResolveBlock;
|
|
24
25
|
@property (nonatomic,copy) RCTPromiseRejectBlock connectRejectBlock;
|
package/ios/RNBluetoothManager.m
CHANGED
|
@@ -21,23 +21,104 @@ static NSArray<CBUUID *> *supportServices = nil;
|
|
|
21
21
|
static NSDictionary *writeableCharactiscs = nil;
|
|
22
22
|
bool hasListeners;
|
|
23
23
|
static CBPeripheral *connected;
|
|
24
|
+
static CBCharacteristic *cachedWriteCharacteristic = nil; // Cache the writable characteristic
|
|
24
25
|
static RNBluetoothManager *instance;
|
|
25
26
|
static NSObject<WriteDataToBleDelegate> *writeDataDelegate;// delegate of write data resule;
|
|
26
27
|
static NSData *toWrite;
|
|
27
28
|
static NSTimer *timer;
|
|
28
29
|
|
|
30
|
+
-(instancetype)init {
|
|
31
|
+
if (self = [super init]) {
|
|
32
|
+
// Initialize CBCentralManager eagerly so it's ready when methods are called
|
|
33
|
+
// This triggers the centralManagerDidUpdateState callback immediately
|
|
34
|
+
[self centralManager];
|
|
35
|
+
[self initSupportServices];
|
|
36
|
+
}
|
|
37
|
+
return self;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
+(Boolean)isConnected{
|
|
30
41
|
return !(connected==nil);
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
// Helper method to write data with automatic chunking for BLE MTU limits
|
|
45
|
+
+(BOOL)writeDataWithChunking:(NSData *)data
|
|
46
|
+
toCharacteristic:(CBCharacteristic *)characteristic
|
|
47
|
+
onPeripheral:(CBPeripheral *)peripheral {
|
|
48
|
+
CBCharacteristicWriteType writeType = (characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse)
|
|
49
|
+
? CBCharacteristicWriteWithoutResponse
|
|
50
|
+
: CBCharacteristicWriteWithResponse;
|
|
51
|
+
|
|
52
|
+
// BLE has MTU limitations - need to chunk data if it's too large
|
|
53
|
+
// The minimum BLE MTU is 23 bytes, with 3 bytes overhead, leaving 20 bytes usable
|
|
54
|
+
// maximumWriteValueLengthForType often returns incorrect values for WriteWithoutResponse
|
|
55
|
+
// so we force a conservative chunk size
|
|
56
|
+
NSUInteger maxWriteLength = 20; // Safe default that works with all BLE devices
|
|
57
|
+
|
|
58
|
+
NSUInteger dataLength = [data length];
|
|
59
|
+
|
|
60
|
+
if(dataLength <= maxWriteLength) {
|
|
61
|
+
// Data fits in one write - no chunking needed
|
|
62
|
+
[peripheral writeValue:data forCharacteristic:characteristic type:writeType];
|
|
63
|
+
return YES;
|
|
64
|
+
} else {
|
|
65
|
+
// Need to chunk the data
|
|
66
|
+
NSLog(@"BLE chunking: %lu bytes -> %lu byte chunks", dataLength, maxWriteLength);
|
|
67
|
+
NSUInteger offset = 0;
|
|
68
|
+
BOOL allSuccess = YES;
|
|
69
|
+
NSUInteger chunkCount = 0;
|
|
70
|
+
|
|
71
|
+
while(offset < dataLength && allSuccess) {
|
|
72
|
+
NSUInteger chunkSize = MIN(maxWriteLength, dataLength - offset);
|
|
73
|
+
NSData *chunk = [data subdataWithRange:NSMakeRange(offset, chunkSize)];
|
|
74
|
+
|
|
75
|
+
@try {
|
|
76
|
+
[peripheral writeValue:chunk forCharacteristic:characteristic type:writeType];
|
|
77
|
+
chunkCount++;
|
|
78
|
+
|
|
79
|
+
// Small delay between chunks to avoid overwhelming the device
|
|
80
|
+
if(writeType == CBCharacteristicWriteWithoutResponse) {
|
|
81
|
+
[NSThread sleepForTimeInterval:0.02]; // 20ms delay
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
@catch(NSException *e) {
|
|
85
|
+
NSLog(@"BLE chunk write error at offset %lu: %@", offset, e);
|
|
86
|
+
allSuccess = NO;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
offset += chunkSize;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if(allSuccess) {
|
|
93
|
+
NSLog(@"BLE chunking complete: %lu chunks sent", chunkCount);
|
|
94
|
+
}
|
|
95
|
+
return allSuccess;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
33
99
|
+(void)writeValue:(NSData *) data withDelegate:(NSObject<WriteDataToBleDelegate> *) delegate
|
|
34
100
|
{
|
|
35
101
|
@try{
|
|
36
102
|
writeDataDelegate = delegate;
|
|
37
103
|
toWrite = data;
|
|
38
104
|
connected.delegate = instance;
|
|
39
|
-
|
|
40
|
-
//
|
|
105
|
+
|
|
106
|
+
// If we have a cached writable characteristic, use it directly
|
|
107
|
+
if(cachedWriteCharacteristic && connected){
|
|
108
|
+
// Use helper method to write with chunking
|
|
109
|
+
BOOL success = [RNBluetoothManager writeDataWithChunking:toWrite
|
|
110
|
+
toCharacteristic:cachedWriteCharacteristic
|
|
111
|
+
onPeripheral:connected];
|
|
112
|
+
|
|
113
|
+
if(writeDataDelegate) [writeDataDelegate didWriteDataToBle:success];
|
|
114
|
+
toWrite = nil;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// No cached characteristic, need to discover services first
|
|
119
|
+
NSLog(@"No cached characteristic, discovering services...");
|
|
120
|
+
// Discover all services (not just SPP which is for Classic BT, not BLE)
|
|
121
|
+
[connected discoverServices:nil];
|
|
41
122
|
}
|
|
42
123
|
@catch(NSException *e){
|
|
43
124
|
NSLog(@"error in writing data to %@,issue:%@",connected,e);
|
|
@@ -119,7 +200,11 @@ RCT_EXPORT_METHOD(isBluetoothEnabled:(RCTPromiseResolveBlock)resolve
|
|
|
119
200
|
RCT_EXPORT_METHOD(enableBluetooth:(RCTPromiseResolveBlock)resolve
|
|
120
201
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
121
202
|
{
|
|
122
|
-
|
|
203
|
+
// iOS doesn't allow programmatic Bluetooth enabling
|
|
204
|
+
// Bluetooth must be enabled from Settings by the user
|
|
205
|
+
// Return empty array to match Android's return type (array of paired devices)
|
|
206
|
+
// Since iOS Core Bluetooth doesn't have "paired devices" concept, we return empty array
|
|
207
|
+
resolve(@[]);
|
|
123
208
|
}
|
|
124
209
|
//disableBluetooth
|
|
125
210
|
RCT_EXPORT_METHOD(disableBluetooth:(RCTPromiseResolveBlock)resolve
|
|
@@ -141,15 +226,50 @@ RCT_EXPORT_METHOD(scanDevices:(RCTPromiseResolveBlock)resolve
|
|
|
141
226
|
}
|
|
142
227
|
self.scanResolveBlock = resolve;
|
|
143
228
|
self.scanRejectBlock = reject;
|
|
229
|
+
|
|
230
|
+
// Initialize metadata dictionary if needed
|
|
231
|
+
if(!self.foundDevicesMetadata){
|
|
232
|
+
self.foundDevicesMetadata = [[NSMutableDictionary alloc] init];
|
|
233
|
+
}
|
|
234
|
+
|
|
144
235
|
if(connected && connected.identifier){
|
|
145
|
-
|
|
146
|
-
|
|
236
|
+
NSString *name = connected.name ? connected.name : @"";
|
|
237
|
+
NSString *address = connected.identifier.UUIDString;
|
|
238
|
+
|
|
239
|
+
// Create device info with device class for connected device
|
|
240
|
+
NSMutableDictionary *deviceInfo = [[NSMutableDictionary alloc] init];
|
|
241
|
+
[deviceInfo setObject:address forKey:@"address"];
|
|
242
|
+
[deviceInfo setObject:name forKey:@"name"];
|
|
243
|
+
[deviceInfo setObject:@(0) forKey:@"rssi"]; // Unknown RSSI for already connected device
|
|
244
|
+
|
|
245
|
+
// Try to infer if it's a printer from the name
|
|
246
|
+
BOOL isPrinter = NO;
|
|
247
|
+
NSString *upperName = name.uppercaseString;
|
|
248
|
+
if ([upperName containsString:@"PRINT"] ||
|
|
249
|
+
[upperName containsString:@"POS"] ||
|
|
250
|
+
[upperName containsString:@"ESCPOS"] ||
|
|
251
|
+
[upperName containsString:@"THERMAL"] ||
|
|
252
|
+
[upperName containsString:@"RECEIPT"]) {
|
|
253
|
+
isPrinter = YES;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (isPrinter) {
|
|
257
|
+
[deviceInfo setObject:@(1536) forKey:@"majorDeviceClass"]; // Imaging
|
|
258
|
+
[deviceInfo setObject:@(1664) forKey:@"deviceClass"]; // Printer
|
|
259
|
+
} else {
|
|
260
|
+
[deviceInfo setObject:@(7936) forKey:@"majorDeviceClass"]; // Unknown
|
|
261
|
+
[deviceInfo setObject:@(7936) forKey:@"deviceClass"]; // Unknown
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
NSDictionary *peripheralStored = @{address:connected};
|
|
147
265
|
if(!self.foundDevices){
|
|
148
266
|
self.foundDevices = [[NSMutableDictionary alloc] init];
|
|
149
267
|
}
|
|
150
268
|
[self.foundDevices addEntriesFromDictionary:peripheralStored];
|
|
269
|
+
[self.foundDevicesMetadata setObject:deviceInfo forKey:address];
|
|
270
|
+
|
|
151
271
|
if(hasListeners){
|
|
152
|
-
[self sendEventWithName:EVENT_DEVICE_FOUND body:@{@"device":
|
|
272
|
+
[self sendEventWithName:EVENT_DEVICE_FOUND body:@{@"device":deviceInfo}];
|
|
153
273
|
}
|
|
154
274
|
}
|
|
155
275
|
[self.centralManager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@NO}];
|
|
@@ -183,6 +303,13 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
183
303
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
184
304
|
{
|
|
185
305
|
NSLog(@"Trying to connect....%@",address);
|
|
306
|
+
|
|
307
|
+
// Check if Bluetooth is ready
|
|
308
|
+
if(!self.centralManager || self.centralManager.state!=CBManagerStatePoweredOn){
|
|
309
|
+
reject(@"BLUETOOTH_NOT_READY",@"Bluetooth is not enabled or not ready. Please enable Bluetooth and try again.",nil);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
186
313
|
[self callStop];
|
|
187
314
|
if(connected){
|
|
188
315
|
NSString *connectedAddress =connected.identifier.UUIDString;
|
|
@@ -205,6 +332,13 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
205
332
|
// Callbacks:
|
|
206
333
|
// centralManager:didConnectPeripheral:
|
|
207
334
|
// centralManager:didFailToConnectPeripheral:error:
|
|
335
|
+
|
|
336
|
+
// Set connection timeout (30 seconds)
|
|
337
|
+
if(timer && timer.isValid){
|
|
338
|
+
[timer invalidate];
|
|
339
|
+
timer = nil;
|
|
340
|
+
}
|
|
341
|
+
timer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(handleConnectTimeout) userInfo:nil repeats:NO];
|
|
208
342
|
}else{
|
|
209
343
|
//starts the scan.
|
|
210
344
|
_waitingConnect = address;
|
|
@@ -212,35 +346,115 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
212
346
|
[self.centralManager scanForPeripheralsWithServices:nil options:@{CBCentralManagerScanOptionAllowDuplicatesKey:@NO}];
|
|
213
347
|
//Callbacks:
|
|
214
348
|
//centralManager:didDiscoverPeripheral:advertisementData:RSSI:
|
|
349
|
+
|
|
350
|
+
// Set scan + connection timeout (30 seconds)
|
|
351
|
+
if(timer && timer.isValid){
|
|
352
|
+
[timer invalidate];
|
|
353
|
+
timer = nil;
|
|
354
|
+
}
|
|
355
|
+
timer = [NSTimer scheduledTimerWithTimeInterval:30 target:self selector:@selector(handleConnectTimeout) userInfo:nil repeats:NO];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
//disconnect(address)
|
|
359
|
+
RCT_EXPORT_METHOD(disconnect:(NSString *)address
|
|
360
|
+
findEventsWithResolver:(RCTPromiseResolveBlock)resolve
|
|
361
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
362
|
+
{
|
|
363
|
+
NSLog(@"Trying to disconnect device: %@",address);
|
|
364
|
+
|
|
365
|
+
// Check if Bluetooth is ready
|
|
366
|
+
if(!self.centralManager || self.centralManager.state!=CBManagerStatePoweredOn){
|
|
367
|
+
reject(@"BLUETOOTH_NOT_READY",@"Bluetooth is not enabled or not ready.",nil);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if(connected && [connected.identifier.UUIDString isEqualToString:address]){
|
|
372
|
+
[self.centralManager cancelPeripheralConnection:connected];
|
|
373
|
+
// Note: disconnection is async, but we resolve immediately like Android does
|
|
374
|
+
// The didDisconnectPeripheral callback will handle setting connected = nil
|
|
375
|
+
resolve(address);
|
|
376
|
+
} else {
|
|
377
|
+
// Device not connected or different device
|
|
378
|
+
NSLog(@"Device not connected or different device: %@",address);
|
|
379
|
+
resolve(address);
|
|
215
380
|
}
|
|
216
381
|
}
|
|
217
382
|
//unpaire(address)
|
|
383
|
+
RCT_EXPORT_METHOD(unpaire:(NSString *)address
|
|
384
|
+
findEventsWithResolver:(RCTPromiseResolveBlock)resolve
|
|
385
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
386
|
+
{
|
|
387
|
+
NSLog(@"Trying to unpair/disconnect device: %@",address);
|
|
388
|
+
if(connected && [connected.identifier.UUIDString isEqualToString:address]){
|
|
389
|
+
[self.centralManager cancelPeripheralConnection:connected];
|
|
390
|
+
// Note: Core Bluetooth on iOS doesn't support programmatic unpairing
|
|
391
|
+
// The connection will be cancelled, but the device remains paired at OS level
|
|
392
|
+
// Users must unpair manually from iOS Settings if needed
|
|
393
|
+
connected = nil;
|
|
394
|
+
resolve(address);
|
|
395
|
+
} else {
|
|
396
|
+
// Device not connected, just resolve
|
|
397
|
+
NSLog(@"Device not connected, nothing to unpair: %@",address);
|
|
398
|
+
resolve(address);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
218
401
|
|
|
219
402
|
|
|
403
|
+
-(void)handleConnectTimeout{
|
|
404
|
+
NSLog(@"Connection timeout - device not found or unable to connect");
|
|
405
|
+
if(self.centralManager.isScanning){
|
|
406
|
+
[self.centralManager stopScan];
|
|
407
|
+
}
|
|
408
|
+
if(self.connectRejectBlock){
|
|
409
|
+
RCTPromiseRejectBlock rjBlock = self.connectRejectBlock;
|
|
410
|
+
rjBlock(@"CONNECTION_TIMEOUT",@"Could not connect to device - timeout after 30 seconds",nil);
|
|
411
|
+
self.connectRejectBlock = nil;
|
|
412
|
+
self.connectResolveBlock = nil;
|
|
413
|
+
}
|
|
414
|
+
_waitingConnect = nil;
|
|
415
|
+
if(timer && timer.isValid){
|
|
416
|
+
[timer invalidate];
|
|
417
|
+
timer = nil;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
220
421
|
-(void)callStop{
|
|
221
422
|
if(self.centralManager.isScanning){
|
|
222
423
|
[self.centralManager stopScan];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
NSMutableArray *devices = [[NSMutableArray alloc] init];
|
|
427
|
+
for(NSString *key in self.foundDevices){
|
|
428
|
+
NSLog(@"insert found devices:%@ =>%@",key,[self.foundDevices objectForKey:key]);
|
|
429
|
+
NSDictionary *deviceInfo = [self.foundDevicesMetadata objectForKey:key];
|
|
430
|
+
if (deviceInfo) {
|
|
431
|
+
[devices addObject:deviceInfo];
|
|
432
|
+
} else {
|
|
226
433
|
NSString *name = [self.foundDevices objectForKey:key].name;
|
|
227
|
-
if(!name)
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
[devices addObject:@{@"address":key,@"name":name}];
|
|
231
|
-
}
|
|
232
|
-
NSError *error = nil;
|
|
233
|
-
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:devices options:NSJSONWritingPrettyPrinted error:&error];
|
|
234
|
-
NSString * jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
235
|
-
if(hasListeners){
|
|
236
|
-
[self sendEventWithName:EVENT_DEVICE_DISCOVER_DONE body:@{@"found":jsonStr,@"paired":@"[]"}];
|
|
237
|
-
}
|
|
238
|
-
if(self.scanResolveBlock){
|
|
239
|
-
RCTPromiseResolveBlock rsBlock = self.scanResolveBlock;
|
|
240
|
-
rsBlock(@{@"found":jsonStr,@"paired":@"[]"});
|
|
241
|
-
self.scanResolveBlock = nil;
|
|
434
|
+
if(!name) name = @"";
|
|
435
|
+
[devices addObject:@{@"address":key, @"name":name}];
|
|
242
436
|
}
|
|
243
437
|
}
|
|
438
|
+
|
|
439
|
+
NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
|
|
440
|
+
[result setObject:@[] forKey:@"paired"];
|
|
441
|
+
[result setObject:devices forKey:@"found"];
|
|
442
|
+
|
|
443
|
+
NSError *error = nil;
|
|
444
|
+
NSData *resultJsonData = [NSJSONSerialization dataWithJSONObject:result options:0 error:&error];
|
|
445
|
+
NSString *resultJsonStr = [[NSString alloc] initWithData:resultJsonData encoding:NSUTF8StringEncoding];
|
|
446
|
+
|
|
447
|
+
if(hasListeners){
|
|
448
|
+
NSData *foundJsonData = [NSJSONSerialization dataWithJSONObject:devices options:0 error:&error];
|
|
449
|
+
NSString *foundJsonStr = [[NSString alloc] initWithData:foundJsonData encoding:NSUTF8StringEncoding];
|
|
450
|
+
[self sendEventWithName:EVENT_DEVICE_DISCOVER_DONE body:@{@"found":foundJsonStr, @"paired":@"[]"}];
|
|
451
|
+
}
|
|
452
|
+
if(self.scanResolveBlock){
|
|
453
|
+
RCTPromiseResolveBlock rsBlock = self.scanResolveBlock;
|
|
454
|
+
rsBlock(resultJsonStr);
|
|
455
|
+
self.scanResolveBlock = nil;
|
|
456
|
+
}
|
|
457
|
+
|
|
244
458
|
if(timer && timer.isValid){
|
|
245
459
|
[timer invalidate];
|
|
246
460
|
timer = nil;
|
|
@@ -251,9 +465,11 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
251
465
|
- (void) initSupportServices
|
|
252
466
|
{
|
|
253
467
|
if(!supportServices){
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
468
|
+
// Use standard SPP (Serial Port Profile) UUID - same as Android
|
|
469
|
+
CBUUID *spp = [CBUUID UUIDWithString: @"00001101-0000-1000-8000-00805F9B34FB"];
|
|
470
|
+
supportServices = [NSArray arrayWithObject:spp];
|
|
471
|
+
// SPP uses the same UUID for service and characteristic
|
|
472
|
+
writeableCharactiscs = @{spp:@"00001101-0000-1000-8000-00805F9B34FB"};
|
|
257
473
|
}
|
|
258
474
|
}
|
|
259
475
|
|
|
@@ -284,19 +500,108 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
284
500
|
* CBCentralManagerDelegate
|
|
285
501
|
**/
|
|
286
502
|
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
|
|
287
|
-
|
|
503
|
+
NSString *stateString;
|
|
504
|
+
switch (central.state) {
|
|
505
|
+
case CBManagerStateUnknown:
|
|
506
|
+
stateString = @"Unknown";
|
|
507
|
+
break;
|
|
508
|
+
case CBManagerStateResetting:
|
|
509
|
+
stateString = @"Resetting";
|
|
510
|
+
break;
|
|
511
|
+
case CBManagerStateUnsupported:
|
|
512
|
+
stateString = @"Unsupported";
|
|
513
|
+
break;
|
|
514
|
+
case CBManagerStateUnauthorized:
|
|
515
|
+
stateString = @"Unauthorized";
|
|
516
|
+
break;
|
|
517
|
+
case CBManagerStatePoweredOff:
|
|
518
|
+
stateString = @"PoweredOff";
|
|
519
|
+
break;
|
|
520
|
+
case CBManagerStatePoweredOn:
|
|
521
|
+
stateString = @"PoweredOn";
|
|
522
|
+
break;
|
|
523
|
+
default:
|
|
524
|
+
stateString = @"Unknown";
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
NSLog(@"Bluetooth State Changed: %@ (%ld)", stateString, (long)central.state);
|
|
288
528
|
}
|
|
289
529
|
|
|
290
530
|
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI{
|
|
291
531
|
NSLog(@"did discover peripheral: %@",peripheral);
|
|
292
|
-
|
|
532
|
+
|
|
533
|
+
// Extract device information with enhanced metadata
|
|
534
|
+
NSMutableDictionary *deviceInfo = [[NSMutableDictionary alloc] init];
|
|
535
|
+
[deviceInfo setObject:peripheral.identifier.UUIDString forKey:@"address"];
|
|
536
|
+
[deviceInfo setObject:(peripheral.name ? peripheral.name : @"") forKey:@"name"];
|
|
537
|
+
|
|
538
|
+
// Add RSSI for signal strength
|
|
539
|
+
[deviceInfo setObject:RSSI forKey:@"rssi"];
|
|
540
|
+
|
|
541
|
+
// Try to infer device class from advertising data
|
|
542
|
+
// iOS BLE doesn't expose Classic Bluetooth device class, but we can make educated guesses
|
|
543
|
+
NSInteger deviceClass = 0;
|
|
544
|
+
NSInteger majorDeviceClass = 0;
|
|
545
|
+
|
|
546
|
+
// Check if device advertises printer-related services
|
|
547
|
+
NSArray *serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey];
|
|
548
|
+
BOOL isPrinter = NO;
|
|
549
|
+
|
|
550
|
+
if (serviceUUIDs && serviceUUIDs.count > 0) {
|
|
551
|
+
for (CBUUID *uuid in serviceUUIDs) {
|
|
552
|
+
NSString *uuidString = uuid.UUIDString.uppercaseString;
|
|
553
|
+
// Serial Port Profile (SPP) UUID - common for printers
|
|
554
|
+
// Also check for other printer-related service UUIDs
|
|
555
|
+
if ([uuidString isEqualToString:@"00001101-0000-1000-8000-00805F9B34FB"] ||
|
|
556
|
+
[uuidString isEqualToString:@"49535343-FE7D-4AE5-8FA9-9FAFD205E455"] ||
|
|
557
|
+
[uuidString containsString:@"18F0"]) {
|
|
558
|
+
isPrinter = YES;
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Check device name for printer keywords
|
|
565
|
+
NSString *name = peripheral.name ? peripheral.name.uppercaseString : @"";
|
|
566
|
+
if ([name containsString:@"PRINT"] ||
|
|
567
|
+
[name containsString:@"POS"] ||
|
|
568
|
+
[name containsString:@"ESCPOS"] ||
|
|
569
|
+
[name containsString:@"THERMAL"] ||
|
|
570
|
+
[name containsString:@"RECEIPT"]) {
|
|
571
|
+
isPrinter = YES;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (isPrinter) {
|
|
575
|
+
// Android Bluetooth Class values for printers:
|
|
576
|
+
// Major Device Class: 0x0600 (Imaging - printers, scanners, cameras, etc.)
|
|
577
|
+
// Device Class: 0x000680 (Imaging/Printer)
|
|
578
|
+
majorDeviceClass = 1536; // 0x0600 in decimal
|
|
579
|
+
deviceClass = 1664; // 0x0680 in decimal
|
|
580
|
+
} else {
|
|
581
|
+
// Unknown/Unclassified device
|
|
582
|
+
// Major Device Class: 0x1F00 (Uncategorized)
|
|
583
|
+
majorDeviceClass = 7936; // 0x1F00 in decimal
|
|
584
|
+
deviceClass = 7936;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
[deviceInfo setObject:@(deviceClass) forKey:@"deviceClass"];
|
|
588
|
+
[deviceInfo setObject:@(majorDeviceClass) forKey:@"majorDeviceClass"];
|
|
589
|
+
|
|
590
|
+
// Store the peripheral object for connection
|
|
293
591
|
NSDictionary *peripheralStored = @{peripheral.identifier.UUIDString:peripheral};
|
|
294
592
|
if(!self.foundDevices){
|
|
295
593
|
self.foundDevices = [[NSMutableDictionary alloc] init];
|
|
296
594
|
}
|
|
297
595
|
[self.foundDevices addEntriesFromDictionary:peripheralStored];
|
|
596
|
+
|
|
597
|
+
// Store device metadata for later retrieval
|
|
598
|
+
if(!self.foundDevicesMetadata){
|
|
599
|
+
self.foundDevicesMetadata = [[NSMutableDictionary alloc] init];
|
|
600
|
+
}
|
|
601
|
+
[self.foundDevicesMetadata setObject:deviceInfo forKey:peripheral.identifier.UUIDString];
|
|
602
|
+
|
|
298
603
|
if(hasListeners){
|
|
299
|
-
[self sendEventWithName:EVENT_DEVICE_FOUND body:@{@"device":
|
|
604
|
+
[self sendEventWithName:EVENT_DEVICE_FOUND body:@{@"device":deviceInfo}];
|
|
300
605
|
}
|
|
301
606
|
if(_waitingConnect && [_waitingConnect isEqualToString: peripheral.identifier.UUIDString]){
|
|
302
607
|
[self.centralManager connectPeripheral:peripheral options:nil];
|
|
@@ -308,10 +613,24 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
308
613
|
NSLog(@"did connected: %@",peripheral);
|
|
309
614
|
connected = peripheral;
|
|
310
615
|
NSString *pId = peripheral.identifier.UUIDString;
|
|
616
|
+
|
|
617
|
+
// Set peripheral delegate to receive service/characteristic callbacks
|
|
618
|
+
peripheral.delegate = self;
|
|
619
|
+
|
|
311
620
|
if(_waitingConnect && [_waitingConnect isEqualToString: pId] && self.connectResolveBlock){
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
621
|
+
// Cancel connection timeout timer if it's running
|
|
622
|
+
if(timer && timer.isValid){
|
|
623
|
+
[timer invalidate];
|
|
624
|
+
timer = nil;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Pre-discover services to prepare for future write operations
|
|
628
|
+
// This matches Android's behavior where the connection is fully ready after connect
|
|
629
|
+
// Pass nil to discover ALL services (not just SPP which is for Classic BT, not BLE)
|
|
630
|
+
NSLog(@"Pre-discovering all services for faster write operations...");
|
|
631
|
+
[peripheral discoverServices:nil];
|
|
632
|
+
|
|
633
|
+
// Resolve promise immediately (iOS BLE connects fast, service discovery happens async)
|
|
315
634
|
self.connectResolveBlock(nil);
|
|
316
635
|
_waitingConnect = nil;
|
|
317
636
|
self.connectRejectBlock = nil;
|
|
@@ -324,6 +643,9 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
324
643
|
}
|
|
325
644
|
|
|
326
645
|
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error{
|
|
646
|
+
// Clear cached characteristic when disconnecting
|
|
647
|
+
cachedWriteCharacteristic = nil;
|
|
648
|
+
|
|
327
649
|
if(!connected && _waitingConnect && [_waitingConnect isEqualToString:peripheral.identifier.UUIDString]){
|
|
328
650
|
if(self.connectRejectBlock){
|
|
329
651
|
RCTPromiseRejectBlock rjBlock = self.connectRejectBlock;
|
|
@@ -345,6 +667,9 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
345
667
|
}
|
|
346
668
|
|
|
347
669
|
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error{
|
|
670
|
+
// Clear cached characteristic when connection fails
|
|
671
|
+
cachedWriteCharacteristic = nil;
|
|
672
|
+
|
|
348
673
|
if(self.connectRejectBlock){
|
|
349
674
|
RCTPromiseRejectBlock rjBlock = self.connectRejectBlock;
|
|
350
675
|
rjBlock(@"",@"",error);
|
|
@@ -412,38 +737,59 @@ RCT_EXPORT_METHOD(connect:(NSString *)address
|
|
|
412
737
|
*/
|
|
413
738
|
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error{
|
|
414
739
|
if(toWrite && connected
|
|
415
|
-
&& [connected.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]
|
|
416
|
-
&& [service.UUID.UUIDString isEqualToString:supportServices[0].UUIDString]){
|
|
740
|
+
&& [connected.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]){
|
|
417
741
|
if(error){
|
|
418
|
-
NSLog(@"
|
|
742
|
+
NSLog(@"Discover characteristics error:%@",error);
|
|
419
743
|
if(writeDataDelegate)
|
|
420
744
|
{
|
|
421
745
|
[writeDataDelegate didWriteDataToBle:false];
|
|
422
746
|
return;
|
|
423
747
|
}
|
|
424
748
|
}
|
|
749
|
+
|
|
750
|
+
// Try to write to any service that has writable characteristics
|
|
751
|
+
// Don't filter by supportServices - many BLE printers use custom UUIDs
|
|
752
|
+
NSLog(@"Checking service %@ for writable characteristics...", service.UUID.UUIDString);
|
|
753
|
+
|
|
754
|
+
// Auto-detect writable characteristic by checking properties
|
|
425
755
|
for(CBCharacteristic *cc in service.characteristics){
|
|
426
|
-
NSLog(@"
|
|
427
|
-
|
|
756
|
+
NSLog(@"Characteristic found: %@ in service: %@" ,cc,service.UUID.UUIDString);
|
|
757
|
+
|
|
758
|
+
// Check if characteristic supports write operations
|
|
759
|
+
BOOL canWrite = (cc.properties & CBCharacteristicPropertyWrite) ||
|
|
760
|
+
(cc.properties & CBCharacteristicPropertyWriteWithoutResponse);
|
|
761
|
+
|
|
762
|
+
if(canWrite){
|
|
428
763
|
@try{
|
|
429
|
-
[
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
764
|
+
NSLog(@"Writing %lu bytes to characteristic %@ in service %@",[toWrite length], cc.UUID.UUIDString, service.UUID.UUIDString);
|
|
765
|
+
|
|
766
|
+
// Use helper method to write with chunking
|
|
767
|
+
BOOL success = [RNBluetoothManager writeDataWithChunking:toWrite
|
|
768
|
+
toCharacteristic:cc
|
|
769
|
+
onPeripheral:connected];
|
|
770
|
+
|
|
771
|
+
// Cache this characteristic for future writes
|
|
772
|
+
cachedWriteCharacteristic = cc;
|
|
773
|
+
NSLog(@"Cached characteristic %@ for future writes", cc.UUID.UUIDString);
|
|
774
|
+
|
|
775
|
+
if(writeDataDelegate) [writeDataDelegate didWriteDataToBle:success];
|
|
776
|
+
toWrite = nil; // Clear the write buffer
|
|
777
|
+
return; // Success, exit after first successful write
|
|
434
778
|
}
|
|
435
779
|
@catch(NSException *e){
|
|
436
|
-
NSLog(@"
|
|
437
|
-
|
|
780
|
+
NSLog(@"ERROR IN WRITE VALUE: %@",e);
|
|
781
|
+
[writeDataDelegate didWriteDataToBle:false];
|
|
438
782
|
}
|
|
439
783
|
}
|
|
440
784
|
}
|
|
441
785
|
|
|
442
|
-
|
|
786
|
+
// If we get here, no writable characteristic was found in this service
|
|
787
|
+
NSLog(@"No writable characteristic found in service: %@", service.UUID.UUIDString);
|
|
788
|
+
// Don't call didWriteDataToBle:false here - there might be other services to check
|
|
443
789
|
}
|
|
444
790
|
|
|
445
791
|
if(error){
|
|
446
|
-
NSLog(@"
|
|
792
|
+
NSLog(@"Discover characteristics error:%@",error);
|
|
447
793
|
return;
|
|
448
794
|
}
|
|
449
795
|
|