@leonardojc/capacitor-ioboard 1.2.8 → 2.0.0

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.
@@ -0,0 +1,777 @@
1
+ package com.leonardojc.capacitor.ioboard;
2
+
3
+ import com.getcapacitor.JSArray;
4
+ import com.getcapacitor.JSObject;
5
+ import com.getcapacitor.Plugin;
6
+ import com.getcapacitor.PluginCall;
7
+ import com.getcapacitor.PluginMethod;
8
+ import com.getcapacitor.annotation.CapacitorPlugin;
9
+ import android.util.Log;
10
+ import java.util.Arrays;
11
+ import java.util.List;
12
+ import java.util.concurrent.CompletableFuture;
13
+ import java.util.concurrent.TimeUnit;
14
+ import org.json.JSONObject;
15
+
16
+ @CapacitorPlugin(name = "CapacitorIoboard")
17
+ public class CapacitorIoboardPlugin extends Plugin implements SerialConnectionManager.SerialDataListener {
18
+
19
+ private static final String TAG = "CapacitorIoboard";
20
+ private SerialConnectionManager serialManager;
21
+ private IOBoardManager ioboardManager;
22
+ private CompletableFuture<JSObject> currentCommandFuture;
23
+
24
+ @Override
25
+ public void load() {
26
+ super.load();
27
+ Log.d(TAG, "IOBoard Plugin v2.0 loaded - Integrated Serial + Protocol");
28
+
29
+ // Initialize managers
30
+ serialManager = new SerialConnectionManager();
31
+ serialManager.setDataListener(this);
32
+ ioboardManager = new IOBoardManager();
33
+ }
34
+
35
+ // ================== CONNECTION METHODS ==================
36
+
37
+ @PluginMethod
38
+ public void connect(PluginCall call) {
39
+ String portPath = call.getString("portPath", "/dev/ttyS2");
40
+ Integer baudRate = call.getInt("baudRate", 115200);
41
+ Integer dataBits = call.getInt("dataBits", 8);
42
+ Integer stopBits = call.getInt("stopBits", 1);
43
+ String parity = call.getString("parity", "none");
44
+
45
+ Log.d(TAG, "Connecting to " + portPath + " at " + baudRate + " baud");
46
+
47
+ try {
48
+ boolean success = serialManager.openPort(portPath, baudRate, dataBits, stopBits, parity);
49
+
50
+ if (success) {
51
+ JSObject result = new JSObject();
52
+ result.put("success", true);
53
+ result.put("portPath", portPath);
54
+ result.put("baudRate", baudRate);
55
+ result.put("message", "Connected successfully");
56
+ call.resolve(result);
57
+
58
+ // Notify connection state
59
+ notifyListeners("connectionStateChanged", result);
60
+ } else {
61
+ call.reject("Failed to connect to serial port");
62
+ }
63
+ } catch (Exception e) {
64
+ Log.e(TAG, "Connection error: " + e.getMessage(), e);
65
+ call.reject("Connection error: " + e.getMessage());
66
+ }
67
+ }
68
+
69
+ @PluginMethod
70
+ public void disconnect(PluginCall call) {
71
+ Log.d(TAG, "Disconnecting from serial port");
72
+
73
+ try {
74
+ serialManager.closePort();
75
+
76
+ JSObject result = new JSObject();
77
+ result.put("success", true);
78
+ result.put("message", "Disconnected successfully");
79
+ call.resolve(result);
80
+
81
+ // Notify disconnection
82
+ notifyListeners("connectionStateChanged", result);
83
+ } catch (Exception e) {
84
+ Log.e(TAG, "Disconnection error: " + e.getMessage(), e);
85
+ call.reject("Disconnection error: " + e.getMessage());
86
+ }
87
+ }
88
+
89
+ @PluginMethod
90
+ public void isConnected(PluginCall call) {
91
+ JSObject result = new JSObject();
92
+ result.put("connected", serialManager.isConnected());
93
+ result.put("portPath", serialManager.getCurrentPortPath());
94
+ call.resolve(result);
95
+ }
96
+
97
+ @PluginMethod
98
+ public void listPorts(PluginCall call) {
99
+ try {
100
+ List<String> ports = serialManager.listAvailablePorts();
101
+
102
+ JSArray portsArray = new JSArray();
103
+ for (String port : ports) {
104
+ portsArray.put(port);
105
+ }
106
+
107
+ JSObject result = new JSObject();
108
+ result.put("ports", portsArray);
109
+ result.put("count", ports.size());
110
+ call.resolve(result);
111
+ } catch (Exception e) {
112
+ Log.e(TAG, "Error listing ports: " + e.getMessage(), e);
113
+ call.reject("Error listing ports: " + e.getMessage());
114
+ }
115
+ }
116
+
117
+ // ================== IOBoard PROTOCOL METHODS ==================
118
+
119
+ @PluginMethod
120
+ public void getStatus(PluginCall call) {
121
+ Integer address = call.getInt("address", 1);
122
+
123
+ if (!serialManager.isConnected()) {
124
+ call.reject("Not connected to serial port");
125
+ return;
126
+ }
127
+
128
+ try {
129
+ Log.d(TAG, "Getting status for device address: " + address);
130
+
131
+ // Build GET_STATUS command
132
+ byte[] command = ioboardManager.buildCommand("GET_STATUS", address, null);
133
+
134
+ // Send command and wait for response
135
+ sendCommandAndWaitForResponse(command, call, "GET_STATUS");
136
+
137
+ } catch (Exception e) {
138
+ Log.e(TAG, "Error getting status: " + e.getMessage(), e);
139
+ call.reject("Error getting status: " + e.getMessage());
140
+ }
141
+ }
142
+
143
+ @PluginMethod
144
+ public void unlockPallet(PluginCall call) {
145
+ Integer address = call.getInt("address", 1);
146
+ Integer palletNumber = call.getInt("palletNumber", 0);
147
+ Integer red = call.getInt("red", 0);
148
+ Integer green = call.getInt("green", 255);
149
+ Integer blue = call.getInt("blue", 0);
150
+ Integer intensity = call.getInt("intensity", 100);
151
+ Integer blinkTimes = call.getInt("blinkTimes", 0);
152
+ Integer blinkSpeed = call.getInt("blinkSpeed", 1);
153
+
154
+ if (!serialManager.isConnected()) {
155
+ call.reject("Not connected to serial port");
156
+ return;
157
+ }
158
+
159
+ try {
160
+ Log.d(TAG, "Unlocking pallet " + palletNumber + " for device " + address);
161
+
162
+ // Prepare RGB parameters
163
+ JSObject params = new JSObject();
164
+ params.put("palletNumber", palletNumber);
165
+ params.put("red", red);
166
+ params.put("green", green);
167
+ params.put("blue", blue);
168
+ params.put("intensity", intensity);
169
+ params.put("blinkTimes", blinkTimes);
170
+ params.put("blinkSpeed", blinkSpeed);
171
+
172
+ // Build UNLOCK_PALLET command
173
+ byte[] command = ioboardManager.buildCommand("UNLOCK_PALLET", address, params);
174
+
175
+ // Send command and wait for response
176
+ sendCommandAndWaitForResponse(command, call, "UNLOCK_PALLET");
177
+
178
+ } catch (Exception e) {
179
+ Log.e(TAG, "Error unlocking pallet: " + e.getMessage(), e);
180
+ call.reject("Error unlocking pallet: " + e.getMessage());
181
+ }
182
+ }
183
+
184
+ @PluginMethod
185
+ public void controlMultiple(PluginCall call) {
186
+ Integer address = call.getInt("address", 1);
187
+ JSArray leds = call.getArray("leds");
188
+
189
+ if (!serialManager.isConnected()) {
190
+ call.reject("Not connected to serial port");
191
+ return;
192
+ }
193
+
194
+ try {
195
+ Log.d(TAG, "Controlling multiple LEDs for device " + address);
196
+
197
+ // Prepare parameters
198
+ JSObject params = new JSObject();
199
+ params.put("leds", leds);
200
+
201
+ // Build CONTROL_MULTIPLE command
202
+ byte[] command = ioboardManager.buildCommand("CONTROL_MULTIPLE", address, params);
203
+
204
+ // Send command and wait for response
205
+ sendCommandAndWaitForResponse(command, call, "CONTROL_MULTIPLE");
206
+
207
+ } catch (Exception e) {
208
+ Log.e(TAG, "Error controlling multiple LEDs: " + e.getMessage(), e);
209
+ call.reject("Error controlling multiple LEDs: " + e.getMessage());
210
+ }
211
+ }
212
+
213
+ @PluginMethod
214
+ public void startOTAUpdate(PluginCall call) {
215
+ Integer address = call.getInt("address", 1);
216
+ String fileName = call.getString("fileName", "firmware.bin");
217
+ Integer fileSize = call.getInt("fileSize", 0);
218
+ Integer totalPackets = call.getInt("totalPackets", 0);
219
+
220
+ if (!serialManager.isConnected()) {
221
+ call.reject("Not connected to serial port");
222
+ return;
223
+ }
224
+
225
+ try {
226
+ Log.d(TAG, "Starting OTA update for device " + address + " - File: " + fileName);
227
+
228
+ // Prepare OTA parameters
229
+ JSObject params = new JSObject();
230
+ params.put("fileName", fileName);
231
+ params.put("fileSize", fileSize);
232
+ params.put("totalPackets", totalPackets);
233
+
234
+ // Build OTA_NOTIFICATION command
235
+ byte[] command = ioboardManager.buildCommand("OTA_NOTIFICATION", address, params);
236
+
237
+ // Send command and wait for response
238
+ sendCommandAndWaitForResponse(command, call, "OTA_NOTIFICATION");
239
+
240
+ } catch (Exception e) {
241
+ Log.e(TAG, "Error starting OTA update: " + e.getMessage(), e);
242
+ call.reject("Error starting OTA update: " + e.getMessage());
243
+ }
244
+ }
245
+
246
+ @PluginMethod
247
+ public void sendOTAData(PluginCall call) {
248
+ Integer address = call.getInt("address", 1);
249
+ Integer packetNumber = call.getInt("packetNumber", 0);
250
+ JSArray dataArray = call.getArray("data");
251
+
252
+ if (!serialManager.isConnected()) {
253
+ call.reject("Not connected to serial port");
254
+ return;
255
+ }
256
+
257
+ try {
258
+ Log.d(TAG, "Sending OTA data packet " + packetNumber + " to device " + address);
259
+
260
+ // Prepare OTA data parameters
261
+ JSObject params = new JSObject();
262
+ params.put("packetNumber", packetNumber);
263
+ params.put("data", dataArray);
264
+
265
+ // Build OTA_DATA command
266
+ byte[] command = ioboardManager.buildCommand("OTA_DATA", address, params);
267
+
268
+ // Send command and wait for response
269
+ sendCommandAndWaitForResponse(command, call, "OTA_DATA");
270
+
271
+ } catch (Exception e) {
272
+ Log.e(TAG, "Error sending OTA data: " + e.getMessage(), e);
273
+ call.reject("Error sending OTA data: " + e.getMessage());
274
+ }
275
+ }
276
+ String command = call.getString("command");
277
+ Integer address = call.getInt("address");
278
+
279
+ if (command == null || address == null) {
280
+ call.reject("Command and address are required");
281
+ return;
282
+ }
283
+
284
+ try {
285
+ byte[] frame = null;
286
+
287
+ switch (command.toUpperCase()) {
288
+ case "GET_STATUS":
289
+ frame = IOBoardProtocolUtils.createStatusQuery((byte) address.intValue());
290
+ break;
291
+
292
+ case "UNLOCK_PALLET":
293
+ Integer palletNumber = call.getInt("palletNumber");
294
+ Boolean doorLock = call.getBoolean("doorLock", true); // Default unlock
295
+
296
+ // LED RGB parameters with defaults
297
+ Integer red = call.getInt("red", 0); // Default red (0-255)
298
+ Integer green = call.getInt("green", 255); // Default green (0-255)
299
+ Integer blue = call.getInt("blue", 0); // Default blue (0-255)
300
+ Integer intensity = call.getInt("intensity", 100); // Default max intensity (0-100)
301
+ Integer blinkTimes = call.getInt("blinkTimes", 0); // Default no blink (0=no flash, 255=forever)
302
+ Integer blinkSpeed = call.getInt("blinkSpeed", 1); // Default 1 second interval (1-30)
303
+
304
+ if (palletNumber == null) {
305
+ call.reject("palletNumber is required for UNLOCK_PALLET command");
306
+ return;
307
+ }
308
+
309
+ // Create complete single pallet control frame according to protocol
310
+ frame = IOBoardProtocolUtils.createSinglePalletControl(
311
+ (byte) address.intValue(),
312
+ palletNumber.intValue(),
313
+ doorLock.booleanValue(),
314
+ red.intValue(),
315
+ green.intValue(),
316
+ blue.intValue(),
317
+ intensity.intValue(),
318
+ blinkTimes.intValue(),
319
+ blinkSpeed.intValue()
320
+ );
321
+ break;
322
+
323
+ case "CONTROL_MULTIPLE":
324
+ Integer doorLockMask = call.getInt("doorLockMask", 0); // 8-bit mask for door locks
325
+ Integer extendedControl = call.getInt("extendedControl", 0); // 8-bit extended control
326
+ JSArray ledsArray = call.getArray("leds"); // Array of 8 LED configurations
327
+
328
+ // Parse LED configurations for all 8 pallets
329
+ int[][] ledControls = new int[8][6]; // 8 LEDs × 6 parameters each
330
+
331
+ for (int i = 0; i < 8; i++) {
332
+ // Default LED: off (all zeros except blink speed = 1)
333
+ ledControls[i] = new int[]{0, 0, 0, 0, 0, 1};
334
+
335
+ if (ledsArray != null && i < ledsArray.length()) {
336
+ try {
337
+ JSObject ledConfig = ledsArray.getJSObject(i);
338
+ if (ledConfig != null) {
339
+ ledControls[i][0] = ledConfig.getInteger("red", 0); // RGB Red (0-255)
340
+ ledControls[i][1] = ledConfig.getInteger("green", 255); // RGB Green (0-255)
341
+ ledControls[i][2] = ledConfig.getInteger("blue", 0); // RGB Blue (0-255)
342
+ ledControls[i][3] = ledConfig.getInteger("intensity", 100); // Intensity (0-100)
343
+ ledControls[i][4] = ledConfig.getInteger("blinkTimes", 0); // Blink times (0-255)
344
+ ledControls[i][5] = ledConfig.getInteger("blinkSpeed", 1); // Blink speed (1-30)
345
+ }
346
+ } catch (Exception e) {
347
+ Log.w(TAG, "Invalid LED config for pallet " + i + ", using defaults");
348
+ }
349
+ }
350
+ }
351
+
352
+ // Create complete full pallet control frame according to protocol
353
+ frame = IOBoardProtocolUtils.createFullPalletControl(
354
+ (byte) address.intValue(),
355
+ doorLockMask.intValue(),
356
+ extendedControl.intValue(),
357
+ ledControls
358
+ );
359
+ break;
360
+
361
+ case "OTA_NOTIFICATION":
362
+ Integer majorVersion = call.getInt("majorVersion");
363
+ Integer minorVersion = call.getInt("minorVersion");
364
+ Integer patchVersion = call.getInt("patchVersion");
365
+ Integer firmwareSize = call.getInt("firmwareSize");
366
+
367
+ if (majorVersion == null || minorVersion == null || patchVersion == null || firmwareSize == null) {
368
+ call.reject("All version parameters and firmware size are required for OTA_NOTIFICATION");
369
+ return;
370
+ }
371
+
372
+ // Create OTA notification data payload according to protocol
373
+ // Format: Header('?') + Major + Minor + Patch + Size(4 bytes)
374
+ byte[] otaData = new byte[8];
375
+ otaData[0] = (byte) '?'; // Notification header
376
+ otaData[1] = (byte) majorVersion.intValue(); // Major version (0-99)
377
+ otaData[2] = (byte) minorVersion.intValue(); // Minor version (0-99)
378
+ otaData[3] = (byte) patchVersion.intValue(); // Patch version (0-99)
379
+ // Firmware size as 4 bytes (little endian)
380
+ otaData[4] = (byte) (firmwareSize & 0xFF);
381
+ otaData[5] = (byte) ((firmwareSize >> 8) & 0xFF);
382
+ otaData[6] = (byte) ((firmwareSize >> 16) & 0xFF);
383
+ otaData[7] = (byte) ((firmwareSize >> 24) & 0xFF);
384
+
385
+ frame = IOBoardProtocolUtils.createFrame((byte) address.intValue(), IOBoardProtocolUtils.FRAME_TYPE_OTA_REQUEST, otaData);
386
+ break;
387
+
388
+ case "OTA_DATA":
389
+ Integer packetNumber = call.getInt("packetNumber");
390
+ JSArray dataArray = call.getArray("data");
391
+
392
+ if (packetNumber == null || dataArray == null) {
393
+ call.reject("packetNumber and data array are required for OTA_DATA");
394
+ return;
395
+ }
396
+
397
+ // Create OTA data payload according to protocol
398
+ // Format: PacketNumber(4 bytes) + Data(128 bytes)
399
+ byte[] otaDataFrame = new byte[132]; // 4 + 128
400
+
401
+ // Packet number as 4 bytes (little endian)
402
+ otaDataFrame[0] = (byte) (packetNumber & 0xFF);
403
+ otaDataFrame[1] = (byte) ((packetNumber >> 8) & 0xFF);
404
+ otaDataFrame[2] = (byte) ((packetNumber >> 16) & 0xFF);
405
+ otaDataFrame[3] = (byte) ((packetNumber >> 24) & 0xFF);
406
+
407
+ // Copy data (up to 128 bytes, pad with zeros if less)
408
+ for (int i = 0; i < 128; i++) {
409
+ if (i < dataArray.length()) {
410
+ try {
411
+ otaDataFrame[4 + i] = (byte) dataArray.getInt(i);
412
+ } catch (Exception e) {
413
+ otaDataFrame[4 + i] = 0; // Pad with zero on error
414
+ }
415
+ } else {
416
+ otaDataFrame[4 + i] = 0; // Pad remaining with zeros
417
+ }
418
+ }
419
+
420
+ frame = IOBoardProtocolUtils.createFrame((byte) address.intValue(), IOBoardProtocolUtils.FRAME_TYPE_OTA_DATA, otaDataFrame);
421
+ break;
422
+
423
+ default:
424
+ call.reject("Unknown command: " + command);
425
+ return;
426
+ }
427
+
428
+ JSObject result = new JSObject();
429
+ result.put("frame", IOBoardProtocolUtils.frameToHex(frame));
430
+ result.put("command", command);
431
+ result.put("address", address);
432
+
433
+ call.resolve(result);
434
+
435
+ } catch (Exception e) {
436
+ Log.e(TAG, "Error building command", e);
437
+ call.reject("Failed to build command: " + e.getMessage());
438
+ }
439
+ }
440
+
441
+ @PluginMethod
442
+ public void parseResponse(PluginCall call) {
443
+ String hexData = call.getString("data");
444
+
445
+ if (hexData == null || hexData.trim().isEmpty()) {
446
+ call.reject("Data is required");
447
+ return;
448
+ }
449
+
450
+ try {
451
+ Log.d(TAG, "Parsing response: " + hexData);
452
+
453
+ IOBoardProtocolUtils.ParsedResponse parsedResponse = IOBoardProtocolUtils.parseResponse(hexData);
454
+
455
+ JSObject result = new JSObject();
456
+ result.put("valid", parsedResponse.success);
457
+ result.put("rawData", hexData);
458
+ result.put("message", parsedResponse.message);
459
+
460
+ if (parsedResponse.success) {
461
+ result.put("address", parsedResponse.address);
462
+ result.put("frameType", parsedResponse.frameType);
463
+
464
+ if (parsedResponse.data != null && parsedResponse.data.length > 0) {
465
+ result.put("payload", IOBoardProtocolUtils.frameToHex(parsedResponse.data));
466
+
467
+ // Parse response data based on frame type
468
+ switch (parsedResponse.frameType) {
469
+ case IOBoardProtocolUtils.FRAME_TYPE_STATUS_QUERY:
470
+ // Status response: LOCK(1) + Version(3)
471
+ if (parsedResponse.data.length >= 4) {
472
+ result.put("doorLockStatus", parsedResponse.data[0] & 0xFF);
473
+
474
+ JSObject version = new JSObject();
475
+ version.put("major", parsedResponse.data[1] & 0xFF);
476
+ version.put("minor", parsedResponse.data[2] & 0xFF);
477
+ version.put("patch", parsedResponse.data[3] & 0xFF);
478
+ result.put("softwareVersion", version);
479
+ }
480
+ break;
481
+
482
+ case IOBoardProtocolUtils.FRAME_TYPE_OTA_RESPONSE:
483
+ // OTA Response: Header(1) + Code(1) + Data(4)
484
+ if (parsedResponse.data.length >= 2) {
485
+ char responseHeader = (char) (parsedResponse.data[0] & 0xFF);
486
+ int responseCode = parsedResponse.data[1] & 0xFF;
487
+
488
+ result.put("otaResponseHeader", String.valueOf(responseHeader));
489
+ result.put("otaResponseCode", responseCode);
490
+
491
+ // Parse OTA response types
492
+ JSObject otaResponse = new JSObject();
493
+ switch (responseHeader) {
494
+ case '&': // Area request
495
+ otaResponse.put("type", "area_request");
496
+ if (responseCode == 'A') {
497
+ otaResponse.put("area", "A");
498
+ otaResponse.put("description", "Request A zone firmware");
499
+ } else if (responseCode == 'B') {
500
+ otaResponse.put("area", "B");
501
+ otaResponse.put("description", "Request B zone firmware");
502
+ }
503
+ // Parse size confirmation (4 bytes)
504
+ if (parsedResponse.data.length >= 6) {
505
+ int size = (parsedResponse.data[2] & 0xFF) |
506
+ ((parsedResponse.data[3] & 0xFF) << 8) |
507
+ ((parsedResponse.data[4] & 0xFF) << 16) |
508
+ ((parsedResponse.data[5] & 0xFF) << 24);
509
+ otaResponse.put("sizeConfirmation", size);
510
+ }
511
+ break;
512
+
513
+ case '#': // Data request
514
+ otaResponse.put("type", "data_request");
515
+ // Parse packet number (4 bytes)
516
+ if (parsedResponse.data.length >= 6) {
517
+ int packetNum = (parsedResponse.data[2] & 0xFF) |
518
+ ((parsedResponse.data[3] & 0xFF) << 8) |
519
+ ((parsedResponse.data[4] & 0xFF) << 16) |
520
+ ((parsedResponse.data[5] & 0xFF) << 24);
521
+ otaResponse.put("requestedPacket", packetNum);
522
+ otaResponse.put("description", "Request packet " + packetNum);
523
+ }
524
+ break;
525
+
526
+ case 'O': // Completion notification
527
+ otaResponse.put("type", "completion");
528
+ otaResponse.put("description", "OTA upgrade completed successfully");
529
+ break;
530
+
531
+ case 'E': // Failure notification
532
+ otaResponse.put("type", "error");
533
+ String errorDesc = "Unknown error";
534
+ switch (responseCode) {
535
+ case 9: errorDesc = "Illegal version/firmware size"; break;
536
+ case 8: errorDesc = "Non-OTA status"; break;
537
+ case 7: errorDesc = "Write failed"; break;
538
+ case 6: errorDesc = "Verification failed"; break;
539
+ case 5: errorDesc = "Transfer failed"; break;
540
+ }
541
+ otaResponse.put("errorCode", responseCode);
542
+ otaResponse.put("description", errorDesc);
543
+ break;
544
+ }
545
+ result.put("otaResponse", otaResponse);
546
+ }
547
+ break;
548
+
549
+ default:
550
+ // For other frame types, just include raw payload
551
+ result.put("frameTypeName", getFrameTypeName(parsedResponse.frameType));
552
+ break;
553
+ }
554
+ }
555
+ }
556
+
557
+ call.resolve(result);
558
+
559
+ } catch (Exception e) {
560
+ Log.e(TAG, "Error parsing response", e);
561
+ call.reject("Failed to parse response: " + e.getMessage());
562
+ }
563
+ }
564
+
565
+ @PluginMethod
566
+ public void validateFrame(PluginCall call) {
567
+ String hexData = call.getString("data");
568
+
569
+ if (hexData == null || hexData.trim().isEmpty()) {
570
+ call.reject("Data is required");
571
+ return;
572
+ }
573
+
574
+ try {
575
+ // Use parseResponse to validate the frame
576
+ IOBoardProtocolUtils.ParsedResponse parsed = IOBoardProtocolUtils.parseResponse(hexData);
577
+
578
+ JSObject result = new JSObject();
579
+ result.put("valid", parsed.success);
580
+ result.put("data", hexData);
581
+ result.put("message", parsed.message);
582
+
583
+ if (parsed.success) {
584
+ byte[] frameData = IOBoardProtocolUtils.hexStringToBytes(hexData);
585
+ result.put("length", frameData.length);
586
+ }
587
+
588
+ call.resolve(result);
589
+
590
+ } catch (Exception e) {
591
+ Log.e(TAG, "Error validating frame", e);
592
+ call.reject("Failed to validate frame: " + e.getMessage());
593
+ }
594
+ }
595
+
596
+ @PluginMethod
597
+ public void hexToBytes(PluginCall call) {
598
+ String hexData = call.getString("data");
599
+
600
+ if (hexData == null || hexData.trim().isEmpty()) {
601
+ call.reject("Data is required");
602
+ return;
603
+ }
604
+
605
+ try {
606
+ byte[] bytes = IOBoardProtocolUtils.hexStringToBytes(hexData);
607
+
608
+ // Convert byte array to JSArray of integers
609
+ JSArray bytesArray = new JSArray();
610
+ for (byte b : bytes) {
611
+ bytesArray.put((int) b & 0xFF); // Convert to unsigned int
612
+ }
613
+
614
+ JSObject result = new JSObject();
615
+ result.put("bytes", bytesArray);
616
+ result.put("length", bytes.length);
617
+ result.put("hex", hexData);
618
+
619
+ call.resolve(result);
620
+
621
+ } catch (Exception e) {
622
+ Log.e(TAG, "Error converting hex to bytes", e);
623
+ call.reject("Failed to convert hex to bytes: " + e.getMessage());
624
+ }
625
+ }
626
+
627
+ @PluginMethod
628
+ public void bytesToHex(PluginCall call) {
629
+ JSArray bytesArray = call.getArray("bytes");
630
+
631
+ if (bytesArray == null) {
632
+ call.reject("Bytes array is required");
633
+ return;
634
+ }
635
+
636
+ try {
637
+ byte[] bytes = new byte[bytesArray.length()];
638
+ for (int i = 0; i < bytesArray.length(); i++) {
639
+ bytes[i] = (byte) bytesArray.getInt(i);
640
+ }
641
+
642
+ String hexString = IOBoardProtocolUtils.frameToHex(bytes);
643
+
644
+ // Convert byte array back to JSArray for consistency
645
+ JSArray resultBytesArray = new JSArray();
646
+ for (byte b : bytes) {
647
+ resultBytesArray.put((int) b & 0xFF);
648
+ }
649
+
650
+ JSObject result = new JSObject();
651
+ result.put("hex", hexString);
652
+ result.put("length", bytes.length);
653
+ result.put("bytes", resultBytesArray);
654
+
655
+ call.resolve(result);
656
+
657
+ } catch (Exception e) {
658
+ Log.e(TAG, "Error converting bytes to hex", e);
659
+ call.reject("Failed to convert bytes to hex: " + e.getMessage());
660
+ }
661
+ }
662
+
663
+ // ================== HELPER METHODS ==================
664
+
665
+ /**
666
+ * Sends a command and waits for response
667
+ */
668
+ private void sendCommandAndWaitForResponse(byte[] command, PluginCall call, String commandType) {
669
+ try {
670
+ // Set up future for response
671
+ currentCommandFuture = new CompletableFuture<>();
672
+
673
+ // Send command
674
+ boolean sent = serialManager.writeData(command);
675
+ if (!sent) {
676
+ call.reject("Failed to send command");
677
+ return;
678
+ }
679
+
680
+ // Wait for response with timeout
681
+ JSObject response = currentCommandFuture.get(5, TimeUnit.SECONDS);
682
+ call.resolve(response);
683
+
684
+ } catch (Exception e) {
685
+ Log.e(TAG, "Error in sendCommandAndWaitForResponse: " + e.getMessage(), e);
686
+ call.reject("Command timeout or error: " + e.getMessage());
687
+ } finally {
688
+ currentCommandFuture = null;
689
+ }
690
+ }
691
+
692
+ // ================== SERIAL DATA LISTENER IMPLEMENTATION ==================
693
+
694
+ @Override
695
+ public void onDataReceived(byte[] data) {
696
+ try {
697
+ Log.d(TAG, "Received " + data.length + " bytes from serial port");
698
+
699
+ // Parse the response using IOBoardManager
700
+ JSObject response = ioboardManager.parseResponse(data);
701
+
702
+ // Complete the current command future if waiting
703
+ if (currentCommandFuture != null && !currentCommandFuture.isDone()) {
704
+ currentCommandFuture.complete(response);
705
+ }
706
+
707
+ // Notify listeners
708
+ JSObject eventData = new JSObject();
709
+ eventData.put("data", response);
710
+ eventData.put("rawBytes", Arrays.toString(data));
711
+ notifyListeners("dataReceived", eventData);
712
+
713
+ } catch (Exception e) {
714
+ Log.e(TAG, "Error processing received data: " + e.getMessage(), e);
715
+
716
+ // Complete future with error if waiting
717
+ if (currentCommandFuture != null && !currentCommandFuture.isDone()) {
718
+ JSObject errorResponse = new JSObject();
719
+ errorResponse.put("success", false);
720
+ errorResponse.put("error", "Parse error: " + e.getMessage());
721
+ currentCommandFuture.complete(errorResponse);
722
+ }
723
+ }
724
+ }
725
+
726
+ @Override
727
+ public void onConnectionStateChanged(boolean connected) {
728
+ Log.d(TAG, "Connection state changed: " + (connected ? "Connected" : "Disconnected"));
729
+
730
+ JSObject eventData = new JSObject();
731
+ eventData.put("connected", connected);
732
+ eventData.put("portPath", serialManager.getCurrentPortPath());
733
+ notifyListeners("connectionStateChanged", eventData);
734
+ }
735
+
736
+ @Override
737
+ public void onError(String error) {
738
+ Log.e(TAG, "Serial connection error: " + error);
739
+
740
+ JSObject eventData = new JSObject();
741
+ eventData.put("error", error);
742
+ notifyListeners("serialError", eventData);
743
+
744
+ // Complete future with error if waiting
745
+ if (currentCommandFuture != null && !currentCommandFuture.isDone()) {
746
+ JSObject errorResponse = new JSObject();
747
+ errorResponse.put("success", false);
748
+ errorResponse.put("error", error);
749
+ currentCommandFuture.complete(errorResponse);
750
+ }
751
+ }
752
+
753
+ @Override
754
+ public void onDestroy() {
755
+ super.onDestroy();
756
+ Log.d(TAG, "Plugin destroying - cleaning up resources");
757
+
758
+ if (serialManager != null) {
759
+ serialManager.cleanup();
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Helper method to get frame type name
765
+ */
766
+ private String getFrameTypeName(byte frameType) {
767
+ switch (frameType) {
768
+ case IOBoardProtocolUtils.FRAME_TYPE_STATUS_QUERY: return "STATUS_QUERY";
769
+ case IOBoardProtocolUtils.FRAME_TYPE_SINGLE_PALLET: return "SINGLE_PALLET";
770
+ case IOBoardProtocolUtils.FRAME_TYPE_FULL_PALLET: return "FULL_PALLET";
771
+ case IOBoardProtocolUtils.FRAME_TYPE_OTA_REQUEST: return "OTA_REQUEST";
772
+ case IOBoardProtocolUtils.FRAME_TYPE_OTA_DATA: return "OTA_DATA";
773
+ case IOBoardProtocolUtils.FRAME_TYPE_OTA_RESPONSE: return "OTA_RESPONSE";
774
+ default: return "UNKNOWN_0x" + String.format("%02X", frameType & 0xFF);
775
+ }
776
+ }
777
+ }