@leonardojc/capacitor-ioboard 1.2.7 → 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.
@@ -1,381 +1,926 @@
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 org.json.JSONObject;
12
-
13
- @CapacitorPlugin(name = "CapacitorIoboard")
14
- public class CapacitorIoboardPlugin extends Plugin implements IOBoardManager.SerialPortInterface {
15
-
16
- private static final String TAG = "CapacitorIoboard";
17
- private IOBoardManager ioboardManager;
18
-
19
- @Override
20
- public void load() {
21
- super.load();
22
- ioboardManager = new IOBoardManager(this); // Pass this plugin as SerialPortInterface
23
- }
24
-
25
- // SerialPortInterface implementation - simplified approach
26
- @Override
27
- public void openPort(String portPath, int baudRate, IOBoardManager.SerialPortCallback callback) {
28
- Log.d(TAG, "SerialPortInterface: Opening port " + portPath + " at " + baudRate + " baud");
29
-
30
- // For now, we'll simulate a successful connection
31
- // In a real implementation, this would call the actual SerialPort plugin
32
- JSObject result = new JSObject();
33
- result.put("success", true);
34
- result.put("message", "Port opened successfully");
35
- callback.onSuccess(result);
36
- }
37
-
38
- @Override
39
- public void closePort(IOBoardManager.SerialPortCallback callback) {
40
- Log.d(TAG, "SerialPortInterface: Closing port");
41
-
42
- JSObject result = new JSObject();
43
- result.put("success", true);
44
- result.put("message", "Port closed successfully");
45
- callback.onSuccess(result);
46
- }
47
-
48
- @Override
49
- public void sendData(byte[] data, IOBoardManager.SerialPortCallback callback) {
50
- Log.d(TAG, "SerialPortInterface: Sending data: " + IOBoardProtocolUtils.frameToHex(data));
51
-
52
- JSObject result = new JSObject();
53
- result.put("success", true);
54
- result.put("message", "Data sent successfully");
55
- callback.onSuccess(result);
56
- }
57
-
58
- @Override
59
- public void readData(int timeout, IOBoardManager.SerialPortCallback callback) {
60
- Log.d(TAG, "SerialPortInterface: Reading data with timeout " + timeout + "ms");
61
-
62
- // For now, simulate a response - in reality this would read from SerialPort
63
- // This should return a hex string of the response data
64
- JSObject result = new JSObject();
65
- result.put("success", true);
66
- result.put("data", "0D0701018A750A"); // Example response
67
- callback.onSuccess(result);
68
- }
69
-
70
- // New simplified API methods
71
- @PluginMethod
72
- public void connect(PluginCall call) {
73
- String portPath = call.getString("portPath", "/dev/ttyS2");
74
- Integer baudRate = call.getInt("baudRate", 115200);
75
- Integer dataBits = call.getInt("dataBits", 8);
76
- Integer stopBits = call.getInt("stopBits", 1);
77
- String parity = call.getString("parity", "none");
78
- String flowControl = call.getString("flowControl", "none");
79
- Integer timeout = call.getInt("timeout", 5000);
80
-
81
- try {
82
- IOBoardManager.SerialConfig config = new IOBoardManager.SerialConfig(
83
- portPath, baudRate, dataBits, stopBits, parity, flowControl, timeout
84
- );
85
-
86
- IOBoardManager.IOBoardResponse response = ioboardManager.connect(config);
87
-
88
- JSObject result = new JSObject();
89
- result.put("success", response.success);
90
- result.put("message", response.message);
91
-
92
- call.resolve(result);
93
- } catch (Exception e) {
94
- Log.e(TAG, "Error connecting", e);
95
- call.reject("Failed to connect: " + e.getMessage());
96
- }
97
- }
98
-
99
- @PluginMethod
100
- public void disconnect(PluginCall call) {
101
- try {
102
- IOBoardManager.IOBoardResponse response = ioboardManager.disconnect();
103
-
104
- JSObject result = new JSObject();
105
- result.put("success", response.success);
106
- result.put("message", response.message);
107
-
108
- call.resolve(result);
109
- } catch (Exception e) {
110
- Log.e(TAG, "Error disconnecting", e);
111
- call.reject("Failed to disconnect: " + e.getMessage());
112
- }
113
- }
114
-
115
- @PluginMethod
116
- public void isConnected(PluginCall call) {
117
- try {
118
- boolean connected = ioboardManager.isConnected();
119
- JSObject result = new JSObject();
120
- result.put("connected", connected);
121
- call.resolve(result);
122
- } catch (Exception e) {
123
- Log.e(TAG, "Error checking connection status", e);
124
- call.reject("Failed to check connection status: " + e.getMessage());
125
- }
126
- }
127
-
128
- @PluginMethod
129
- public void getStatus(PluginCall call) {
130
- Integer address = call.getInt("address");
131
- Integer timeout = call.getInt("timeout", 5000);
132
-
133
- if (address == null || address < 1 || address > 63) {
134
- call.reject("Invalid address. Must be between 1 and 63");
135
- return;
136
- }
137
-
138
- try {
139
- IOBoardManager.StatusResponse response = ioboardManager.getStatus(address, timeout);
140
-
141
- JSObject result = new JSObject();
142
- result.put("success", response.success);
143
- result.put("message", response.message);
144
-
145
- if (response.success && response.data != null) {
146
- JSObject data = new JSObject();
147
- data.put("doorLockStatus", response.data.doorLockStatus);
148
-
149
- JSObject version = new JSObject();
150
- version.put("major", response.data.softwareVersion.major);
151
- version.put("minor", response.data.softwareVersion.minor);
152
- version.put("patch", response.data.softwareVersion.patch);
153
- data.put("softwareVersion", version);
154
-
155
- result.put("data", data);
156
- }
157
-
158
- call.resolve(result);
159
- } catch (Exception e) {
160
- Log.e(TAG, "Error getting status", e);
161
- call.reject("Failed to get status: " + e.getMessage());
162
- }
163
- }
164
-
165
- @PluginMethod
166
- public void unlockPallet(PluginCall call) {
167
- Integer address = call.getInt("address");
168
- Integer palletNumber = call.getInt("palletNumber");
169
- String ledColor = call.getString("ledColor", "green");
170
- Integer timeout = call.getInt("timeout", 5000);
171
-
172
- if (address == null || address < 1 || address > 63) {
173
- call.reject("Invalid address. Must be between 1 and 63");
174
- return;
175
- }
176
-
177
- if (palletNumber == null || palletNumber < 0 || palletNumber > 7) {
178
- call.reject("Invalid pallet number. Must be between 0 and 7");
179
- return;
180
- }
181
-
182
- try {
183
- IOBoardManager.IOBoardResponse response = ioboardManager.unlockPallet(
184
- address, palletNumber, ledColor, timeout
185
- );
186
-
187
- JSObject result = new JSObject();
188
- result.put("success", response.success);
189
- result.put("message", response.message);
190
-
191
- if (response.success) {
192
- JSObject data = new JSObject();
193
- data.put("address", address);
194
- data.put("palletNumber", palletNumber);
195
- data.put("ledColor", ledColor);
196
- result.put("data", data);
197
- }
198
-
199
- call.resolve(result);
200
- } catch (Exception e) {
201
- Log.e(TAG, "Error unlocking pallet", e);
202
- call.reject("Failed to unlock pallet: " + e.getMessage());
203
- }
204
- }
205
-
206
- @PluginMethod
207
- public void controlMultiplePallets(PluginCall call) {
208
- Integer address = call.getInt("address");
209
- Integer doorLockMask = call.getInt("doorLockMask", 0);
210
- String ledColor = call.getString("ledColor", "green");
211
- Integer timeout = call.getInt("timeout", 5000);
212
-
213
- if (address == null || address < 1 || address > 63) {
214
- call.reject("Invalid address. Must be between 1 and 63");
215
- return;
216
- }
217
-
218
- try {
219
- IOBoardManager.IOBoardResponse response = ioboardManager.controlMultiplePallets(
220
- address, doorLockMask, ledColor, timeout
221
- );
222
-
223
- JSObject result = new JSObject();
224
- result.put("success", response.success);
225
- result.put("message", response.message);
226
-
227
- if (response.success) {
228
- JSObject data = new JSObject();
229
- data.put("address", address);
230
- data.put("doorLockMask", doorLockMask);
231
-
232
- // Calculate affected pallets from mask
233
- JSArray affectedPallets = new JSArray();
234
- for (int i = 0; i < 8; i++) {
235
- if ((doorLockMask & (1 << i)) != 0) {
236
- affectedPallets.put(i);
237
- }
238
- }
239
- data.put("affectedPallets", affectedPallets);
240
- result.put("data", data);
241
- }
242
-
243
- call.resolve(result);
244
- } catch (Exception e) {
245
- Log.e(TAG, "Error controlling multiple pallets", e);
246
- call.reject("Failed to control multiple pallets: " + e.getMessage());
247
- }
248
- }
249
-
250
- @PluginMethod
251
- public void scanDevices(PluginCall call) {
252
- Integer startAddress = call.getInt("startAddress", 1);
253
- Integer endAddress = call.getInt("endAddress", 63);
254
- Integer timeout = call.getInt("timeout", 1000);
255
-
256
- if (startAddress < 1 || startAddress > 63) {
257
- call.reject("Invalid start address. Must be between 1 and 63");
258
- return;
259
- }
260
-
261
- if (endAddress < 1 || endAddress > 63 || endAddress < startAddress) {
262
- call.reject("Invalid end address. Must be between 1 and 63 and >= start address");
263
- return;
264
- }
265
-
266
- try {
267
- IOBoardManager.ScanResponse response = ioboardManager.scanDevices(
268
- startAddress, endAddress, timeout
269
- );
270
-
271
- JSObject result = new JSObject();
272
- result.put("success", response.success);
273
- result.put("message", response.message);
274
-
275
- if (response.success && response.devices != null) {
276
- JSArray devicesArray = new JSArray();
277
- for (IOBoardManager.DeviceInfo device : response.devices) {
278
- JSObject deviceObj = new JSObject();
279
- deviceObj.put("address", device.address);
280
- deviceObj.put("responding", device.responding);
281
-
282
- if (device.status != null) {
283
- JSObject statusObj = new JSObject();
284
- statusObj.put("doorLockStatus", device.status.doorLockStatus);
285
- deviceObj.put("status", statusObj);
286
- }
287
-
288
- devicesArray.put(deviceObj);
289
- }
290
- result.put("devices", devicesArray);
291
- }
292
-
293
- call.resolve(result);
294
- } catch (Exception e) {
295
- Log.e(TAG, "Error scanning devices", e);
296
- call.reject("Failed to scan devices: " + e.getMessage());
297
- }
298
- }
299
-
300
- @PluginMethod
301
- public void sendOTANotification(PluginCall call) {
302
- Integer address = call.getInt("address");
303
- Integer majorVersion = call.getInt("majorVersion");
304
- Integer minorVersion = call.getInt("minorVersion");
305
- Integer patchVersion = call.getInt("patchVersion");
306
- Integer firmwareSize = call.getInt("firmwareSize");
307
-
308
- if (address == null || address < 1 || address > 63) {
309
- call.reject("Invalid address. Must be between 1 and 63");
310
- return;
311
- }
312
-
313
- if (majorVersion == null || minorVersion == null || patchVersion == null || firmwareSize == null) {
314
- call.reject("All OTA parameters are required");
315
- return;
316
- }
317
-
318
- try {
319
- IOBoardManager.IOBoardResponse response = ioboardManager.sendOTANotification(
320
- address, majorVersion, minorVersion, patchVersion, firmwareSize
321
- );
322
-
323
- JSObject result = new JSObject();
324
- result.put("success", response.success);
325
- result.put("message", response.message);
326
-
327
- if (response.success) {
328
- JSObject data = new JSObject();
329
- data.put("address", address);
330
- data.put("version", majorVersion + "." + minorVersion + "." + patchVersion);
331
- data.put("firmwareSize", firmwareSize);
332
- result.put("data", data);
333
- }
334
-
335
- call.resolve(result);
336
- } catch (Exception e) {
337
- Log.e(TAG, "Error sending OTA notification", e);
338
- call.reject("Failed to send OTA notification: " + e.getMessage());
339
- }
340
- }
341
-
342
- @PluginMethod
343
- public void sendOTAData(PluginCall call) {
344
- Integer address = call.getInt("address");
345
- Integer packetNumber = call.getInt("packetNumber");
346
- String dataBase64 = call.getString("data");
347
-
348
- if (address == null || address < 1 || address > 63) {
349
- call.reject("Invalid address. Must be between 1 and 63");
350
- return;
351
- }
352
-
353
- if (packetNumber == null || dataBase64 == null) {
354
- call.reject("Packet number and data are required");
355
- return;
356
- }
357
-
358
- try {
359
- IOBoardManager.IOBoardResponse response = ioboardManager.sendOTAData(
360
- address, packetNumber, dataBase64
361
- );
362
-
363
- JSObject result = new JSObject();
364
- result.put("success", response.success);
365
- result.put("message", response.message);
366
-
367
- if (response.success) {
368
- JSObject data = new JSObject();
369
- data.put("address", address);
370
- data.put("packetNumber", packetNumber);
371
- data.put("dataLength", dataBase64.length());
372
- result.put("data", data);
373
- }
374
-
375
- call.resolve(result);
376
- } catch (Exception e) {
377
- Log.e(TAG, "Error sending OTA data", e);
378
- call.reject("Failed to send OTA data: " + e.getMessage());
379
- }
380
- }
381
- }
1
+ package com.leonardojc.capacitor.ioboard;
2
+
3
+ import android.util.Log;
4
+ import com.getcapacitor.JSArray;
5
+ import com.getcapacitor.JSObject;
6
+ import com.getcapacitor.Plugin;
7
+ import com.getcapacitor.PluginCall;
8
+ import com.getcapacitor.PluginMethod;
9
+ import com.getcapacitor.annotation.CapacitorPlugin;
10
+ import java.util.List;
11
+ import java.util.ArrayList;
12
+
13
+ @CapacitorPlugin(name = "CapacitorIoboard")
14
+ public class CapacitorIoboardPlugin extends Plugin implements SerialConnectionManager.SerialDataListener {
15
+
16
+ private static final String TAG = "CapacitorIoboard";
17
+ private SerialConnectionManager serialManager;
18
+ private IOBoardManager ioBoardManager;
19
+ private boolean isConnected = false;
20
+ private String currentDeviceInfo = "";
21
+ private String lastError = "";
22
+
23
+ // Helper method to convert bytes to hex string for debugging
24
+ private String bytesToHex(byte[] bytes) {
25
+ StringBuilder hexString = new StringBuilder();
26
+ for (byte b : bytes) {
27
+ String hex = Integer.toHexString(0xFF & b);
28
+ if (hex.length() == 1) {
29
+ hexString.append('0');
30
+ }
31
+ hexString.append(hex.toUpperCase()).append(" ");
32
+ }
33
+ return hexString.toString().trim();
34
+ }
35
+
36
+ // Helper method to convert string to detailed debug info
37
+ private String getDetailedCommandInfo(String command) {
38
+ StringBuilder info = new StringBuilder();
39
+ info.append("String: '").append(command).append("'");
40
+ info.append(", Length: ").append(command.length());
41
+ info.append(", Chars: [");
42
+ for (int i = 0; i < command.length(); i++) {
43
+ char c = command.charAt(i);
44
+ if (i > 0) info.append(", ");
45
+ if (c == '\n') {
46
+ info.append("\\n(10)");
47
+ } else if (c == '\r') {
48
+ info.append("\\r(13)");
49
+ } else if (c >= 32 && c <= 126) {
50
+ info.append("'").append(c).append("'(").append((int)c).append(")");
51
+ } else {
52
+ info.append("(").append((int)c).append(")");
53
+ }
54
+ }
55
+ info.append("]");
56
+ return info.toString();
57
+ }
58
+
59
+ @Override
60
+ public void load() {
61
+ Log.d(TAG, "Plugin loaded");
62
+ serialManager = new SerialConnectionManager();
63
+ serialManager.setDataListener(this);
64
+ ioBoardManager = new IOBoardManager();
65
+ }
66
+
67
+ @PluginMethod
68
+ public void connect(PluginCall call) {
69
+ String portName = call.getString("portName");
70
+ Integer baudRate = call.getInt("baudRate", 115200);
71
+
72
+ if (portName == null) {
73
+ call.reject("Port name is required");
74
+ return;
75
+ }
76
+
77
+ try {
78
+ boolean connected = serialManager.openPort(portName, baudRate, 8, 1, "none");
79
+ if (connected) {
80
+ isConnected = true;
81
+ currentDeviceInfo = "Connected to " + portName + " at " + baudRate + " baud";
82
+
83
+ JSObject result = new JSObject();
84
+ result.put("success", true);
85
+ result.put("deviceInfo", currentDeviceInfo);
86
+ call.resolve(result);
87
+
88
+ notifyListeners("connectionStatusChanged", result);
89
+ } else {
90
+ lastError = "Failed to connect to " + portName;
91
+ call.reject(lastError);
92
+ }
93
+ } catch (Exception e) {
94
+ lastError = "Connection error: " + e.getMessage();
95
+ Log.e(TAG, lastError, e);
96
+ call.reject(lastError);
97
+ }
98
+ }
99
+
100
+ @PluginMethod
101
+ public void disconnect(PluginCall call) {
102
+ try {
103
+ serialManager.closePort();
104
+ isConnected = false;
105
+ currentDeviceInfo = "";
106
+
107
+ JSObject result = new JSObject();
108
+ result.put("success", true);
109
+ call.resolve(result);
110
+
111
+ notifyListeners("connectionStatusChanged", result);
112
+ } catch (Exception e) {
113
+ lastError = "Disconnect error: " + e.getMessage();
114
+ Log.e(TAG, lastError, e);
115
+ call.reject(lastError);
116
+ }
117
+ }
118
+
119
+ @PluginMethod
120
+ public void getStatus(PluginCall call) {
121
+ Log.d(TAG, "📊 getStatus called");
122
+
123
+ if (serialManager == null) {
124
+ call.reject("Serial connection not initialized");
125
+ return;
126
+ }
127
+
128
+ if (!serialManager.isConnected()) {
129
+ call.reject("Not connected to serial port");
130
+ return;
131
+ }
132
+
133
+ try {
134
+ int address = call.getInt("address", 1);
135
+ byte[] statusFrame = createStatusQueryFrame(address);
136
+ boolean success = serialManager.writeData(statusFrame);
137
+
138
+ if (success) {
139
+ JSObject result = new JSObject();
140
+ result.put("success", true);
141
+ result.put("message", "Status query sent successfully");
142
+ result.put("address", address);
143
+ call.resolve(result);
144
+ } else {
145
+ call.reject("Failed to send status query");
146
+ }
147
+
148
+ } catch (Exception e) {
149
+ Log.e(TAG, "Error in getStatus: " + e.getMessage(), e);
150
+ call.reject("Error getting status: " + e.getMessage());
151
+ }
152
+ }
153
+
154
+ @PluginMethod
155
+ public void getConnectionInfo(PluginCall call) {
156
+ Log.d(TAG, "🔌 getConnectionInfo called");
157
+
158
+ try {
159
+ List<String> ports = serialManager.listAvailablePorts();
160
+ boolean isConnected = serialManager.isConnected();
161
+ String currentPort = serialManager.getCurrentPortPath();
162
+
163
+ JSObject result = new JSObject();
164
+ result.put("connected", isConnected);
165
+ result.put("currentPort", currentPort != null ? currentPort : "");
166
+ result.put("availablePorts", new JSArray(ports));
167
+
168
+ call.resolve(result);
169
+ } catch (Exception e) {
170
+ Log.e(TAG, "Error getting connection info: " + e.getMessage(), e);
171
+ call.reject("Error getting connection info: " + e.getMessage());
172
+ }
173
+ }
174
+
175
+ @PluginMethod
176
+ public void controlMultiplePallets(PluginCall call) {
177
+ Log.d(TAG, "🎯 controlMultiplePallets called");
178
+
179
+ if (serialManager == null) {
180
+ call.reject("Serial connection not initialized");
181
+ return;
182
+ }
183
+
184
+ if (!serialManager.isConnected()) {
185
+ call.reject("Not connected to serial port");
186
+ return;
187
+ }
188
+
189
+ try {
190
+ // TEMPORAL: Generar exactamente el frame de la documentación
191
+ // Address: 01; Unlock: 01; Extended output: 80;
192
+ // All 8 LEDs: flashing red, brightness 255, flashing 16 times, interval 1 second
193
+ Log.d(TAG, "🧪 TESTING: Generating exact documentation frame");
194
+
195
+ int address = 1;
196
+ String color = "RED";
197
+ int intensity = 255; // FF en hex
198
+ int blinkTimes = 16; // 10 en hex
199
+ int blinkSpeed = 1; // 01 en hex
200
+
201
+ // Usar todos los pallets (1-8) como en el ejemplo
202
+ List<Integer> allPallets = new ArrayList<>();
203
+ for (int i = 1; i <= 8; i++) {
204
+ allPallets.add(i);
205
+ }
206
+
207
+ byte[] frame = createMultiplePalletsFrame(address, color, intensity, blinkTimes, blinkSpeed, allPallets);
208
+ boolean success = serialManager.writeData(frame);
209
+
210
+ if (success) {
211
+ JSObject result = new JSObject();
212
+ result.put("success", true);
213
+ result.put("message", String.format("TEST: Documentation frame sent - %d pallets with %s LEDs (intensity=%d, blink=%d)",
214
+ allPallets.size(), color, intensity, blinkTimes));
215
+ result.put("address", address);
216
+ result.put("palletsCount", allPallets.size());
217
+ call.resolve(result);
218
+ } else {
219
+ call.reject("Failed to send multiple pallets command");
220
+ }
221
+
222
+ } catch (Exception e) {
223
+ Log.e(TAG, "Error in controlMultiplePallets: " + e.getMessage(), e);
224
+ call.reject("Error controlling multiple pallets: " + e.getMessage());
225
+ }
226
+ }
227
+
228
+ @PluginMethod
229
+ public void listPorts(PluginCall call) {
230
+ try {
231
+ List<String> ports = serialManager.listAvailablePorts();
232
+ Log.d(TAG, "Found " + ports.size() + " ports: " + ports.toString());
233
+
234
+ JSArray portsArray = new JSArray();
235
+ for (String port : ports) {
236
+ portsArray.put(port);
237
+ }
238
+
239
+ JSObject result = new JSObject();
240
+ result.put("success", true);
241
+ result.put("ports", portsArray);
242
+ call.resolve(result);
243
+ } catch (Exception e) {
244
+ Log.e(TAG, "Error listing ports", e);
245
+ JSObject errorResult = new JSObject();
246
+ errorResult.put("success", false);
247
+ errorResult.put("error", "Error listing ports: " + e.getMessage());
248
+ call.resolve(errorResult);
249
+ }
250
+ }
251
+
252
+ @PluginMethod
253
+ public void sendRawCommand(PluginCall call) {
254
+ if (!isConnected) {
255
+ JSObject errorResult = new JSObject();
256
+ errorResult.put("success", false);
257
+ errorResult.put("error", "Not connected to device");
258
+ call.resolve(errorResult);
259
+ return;
260
+ }
261
+
262
+ String command = call.getString("command");
263
+ if (command == null) {
264
+ JSObject errorResult = new JSObject();
265
+ errorResult.put("success", false);
266
+ errorResult.put("error", "Command is required");
267
+ call.resolve(errorResult);
268
+ return;
269
+ }
270
+
271
+ try {
272
+ // Detailed logging before sending
273
+ Log.d(TAG, "=== SENDING COMMAND ===");
274
+ Log.d(TAG, "Command details: " + getDetailedCommandInfo(command));
275
+
276
+ byte[] commandBytes = command.getBytes();
277
+ Log.d(TAG, "Bytes to send (length=" + commandBytes.length + "): " + bytesToHex(commandBytes));
278
+ Log.d(TAG, "Raw bytes: " + java.util.Arrays.toString(commandBytes));
279
+
280
+ boolean sent = serialManager.writeData(commandBytes);
281
+
282
+ if (sent) {
283
+ Log.i(TAG, "✅ Command successfully written to serial port");
284
+ Log.i(TAG, "HEX sent: " + bytesToHex(commandBytes));
285
+ } else {
286
+ Log.e(TAG, "❌ Failed to write command to serial port");
287
+ }
288
+
289
+ JSObject result = new JSObject();
290
+ result.put("success", sent);
291
+ result.put("command", command);
292
+ result.put("hexSent", bytesToHex(commandBytes));
293
+ result.put("bytesLength", commandBytes.length);
294
+ if (!sent) {
295
+ result.put("error", "Failed to send command");
296
+ }
297
+ call.resolve(result);
298
+ } catch (Exception e) {
299
+ String error = "Error sending raw command: " + e.getMessage();
300
+ Log.e(TAG, error, e);
301
+ JSObject errorResult = new JSObject();
302
+ errorResult.put("success", false);
303
+ errorResult.put("error", error);
304
+ call.resolve(errorResult);
305
+ }
306
+ }
307
+
308
+ // SerialDataListener implementation
309
+ @Override
310
+ public void onDataReceived(byte[] data) {
311
+ String dataString = new String(data);
312
+ Log.d(TAG, "Data received: " + dataString);
313
+
314
+ JSObject result = new JSObject();
315
+ result.put("rawData", dataString);
316
+ result.put("timestamp", System.currentTimeMillis());
317
+
318
+ notifyListeners("dataReceived", result);
319
+ }
320
+
321
+ @Override
322
+ public void onConnectionStateChanged(boolean connected) {
323
+ Log.d(TAG, "Connection state changed: " + connected);
324
+ isConnected = connected;
325
+
326
+ if (!connected) {
327
+ currentDeviceInfo = "";
328
+ lastError = "Connection lost";
329
+ }
330
+
331
+ JSObject result = new JSObject();
332
+ result.put("isConnected", connected);
333
+ result.put("timestamp", System.currentTimeMillis());
334
+ notifyListeners("connectionStatusChanged", result);
335
+ }
336
+
337
+ @Override
338
+ public void onError(String error) {
339
+ Log.e(TAG, "Serial error: " + error);
340
+ lastError = error;
341
+
342
+ JSObject result = new JSObject();
343
+ result.put("error", error);
344
+ result.put("timestamp", System.currentTimeMillis());
345
+ notifyListeners("serialError", result);
346
+ }
347
+
348
+ @PluginMethod
349
+ public void unlockPallet(PluginCall call) {
350
+ Log.d(TAG, "🔓 unlockPallet called");
351
+
352
+ int address = call.getInt("address", 1);
353
+ int pallet = call.getInt("pallet", 1);
354
+ String color = call.getString("color", "FF0000");
355
+
356
+ Log.d(TAG, String.format("Parameters - address: %d, pallet: %d, color: %s", address, pallet, color));
357
+
358
+ if (serialManager == null) {
359
+ call.reject("Serial connection not initialized");
360
+ return;
361
+ }
362
+
363
+ if (!serialManager.isConnected()) {
364
+ call.reject("Not connected to serial port");
365
+ return;
366
+ }
367
+
368
+ try {
369
+ byte[] frame = createUnlockPalletFrame(address, pallet, color);
370
+ boolean success = serialManager.writeData(frame);
371
+
372
+ if (success) {
373
+ JSObject result = new JSObject();
374
+ result.put("success", true);
375
+ result.put("message", "Unlock command sent successfully");
376
+ call.resolve(result);
377
+ } else {
378
+ call.reject("Failed to send unlock command");
379
+ }
380
+
381
+ } catch (Exception e) {
382
+ Log.e(TAG, "Error in unlockPallet: " + e.getMessage(), e);
383
+ call.reject("Error sending unlock command: " + e.getMessage());
384
+ }
385
+ }
386
+
387
+ // Parser de respuestas del IOBoard
388
+ public static class IOBoardResponse {
389
+ public boolean isValid = false;
390
+ public int address = 0;
391
+ public int type = 0;
392
+ public byte[] data = new byte[0];
393
+ public String firmware = "";
394
+ public int doorStatus = 0;
395
+ public String rawHex = "";
396
+
397
+ public String toString() {
398
+ return String.format("IOBoardResponse{valid=%s, addr=%d, type=%d, firmware='%s', doorStatus=0x%02X, rawHex='%s'}",
399
+ isValid, address, type, firmware, doorStatus, rawHex);
400
+ }
401
+ }
402
+
403
+ public static IOBoardResponse parseIOBoardResponse(byte[] response) {
404
+ IOBoardResponse result = new IOBoardResponse();
405
+
406
+ // Convertir a hex para logging
407
+ StringBuilder hexBuilder = new StringBuilder();
408
+ for (byte b : response) {
409
+ hexBuilder.append(String.format("%02X ", b & 0xFF));
410
+ }
411
+ result.rawHex = hexBuilder.toString().trim();
412
+
413
+ Log.d(TAG, String.format("📥 Parsing IOBoard response: %s (%d bytes)", result.rawHex, response.length));
414
+
415
+ // Verificar longitud mínima (SOI + LEN + ADDR + TYPE + CRC = 6 bytes mínimo)
416
+ if (response.length < 6) {
417
+ Log.w(TAG, "❌ Response too short: " + response.length + " bytes");
418
+ return result;
419
+ }
420
+
421
+ // Verificar SOI (Start of Information = 0x0A para respuestas)
422
+ if ((response[0] & 0xFF) != 0x0A) {
423
+ Log.w(TAG, String.format("❌ Invalid SOI: expected 0x0A, got 0x%02X", response[0] & 0xFF));
424
+ return result;
425
+ }
426
+
427
+ // Obtener longitud del payload (incluye LEN + ADDRESS + TYPE + DATA + CRC + EOI)
428
+ int payloadLength = response[1] & 0xFF;
429
+ Log.d(TAG, String.format("📏 Payload length: %d bytes (includes LEN byte itself)", payloadLength));
430
+
431
+ // Calcular el tamaño del frame válido según documentación: SOI + payload
432
+ // El payloadLength ya incluye todo desde LEN hasta EOI
433
+ int expectedFrameLength = 1 + payloadLength; // SOI(1) + payload_completo(que incluye EOI)
434
+
435
+ // Detectar y eliminar byte FC extra si está presente
436
+ byte[] cleanResponse = response;
437
+ if (response.length > expectedFrameLength) {
438
+ // Verificar si el último byte es FC (0xFC)
439
+ int lastByte = response[response.length - 1] & 0xFF;
440
+ if (lastByte == 0xFC) {
441
+ Log.d(TAG, String.format("🔧 Detected FC byte at end, removing: got %d bytes, expected %d",
442
+ response.length, expectedFrameLength));
443
+ cleanResponse = new byte[response.length - 1];
444
+ System.arraycopy(response, 0, cleanResponse, 0, cleanResponse.length);
445
+ } else {
446
+ Log.d(TAG, String.format("⚠️ Frame longer than expected but last byte is not FC: 0x%02X", lastByte));
447
+ }
448
+ }
449
+
450
+ // Calcular el tamaño del frame válido: SOI + payload_completo
451
+ // Según documentación MTC3P08L: [SOI][LEN][ADDR][TYPE][DATA][CRC][EOI]
452
+ // El payloadLength ya incluye desde LEN hasta EOI
453
+ int validFrameLength = 1 + payloadLength; // SOI(1) + payload_completo(LEN_value que incluye EOI)
454
+
455
+ // Usar el frame limpio (sin FC)
456
+ byte[] validFrame;
457
+ if (cleanResponse.length >= validFrameLength) {
458
+ validFrame = new byte[validFrameLength];
459
+ System.arraycopy(cleanResponse, 0, validFrame, 0, validFrameLength);
460
+ } else {
461
+ validFrame = response;
462
+ }
463
+
464
+ // Verificar que tengamos suficientes bytes para el frame válido
465
+ if (validFrame.length < validFrameLength) {
466
+ Log.w(TAG, String.format("❌ Incomplete frame: expected %d bytes, got %d", validFrameLength, validFrame.length));
467
+ return result;
468
+ }
469
+
470
+ // Verificar EOI al final del frame (según documentación debe ser 0x0A)
471
+ int eoiPos = validFrameLength - 1;
472
+ if ((validFrame[eoiPos] & 0xFF) != 0x0A) {
473
+ Log.w(TAG, String.format("❌ Invalid EOI: expected 0x0A at position %d, got 0x%02X",
474
+ eoiPos, validFrame[eoiPos] & 0xFF));
475
+ }
476
+
477
+ // Extraer campos del payload (saltando SOI y LEN)
478
+ result.address = validFrame[2] & 0xFF;
479
+ result.type = validFrame[3] & 0xFF;
480
+
481
+ // Calcular longitud de datos basado en el tipo de frame
482
+ // TYPE 00 (Status): usa checksum de 8 bits (1 byte)
483
+ // TYPE 01: usa checksum de 8 bits (1 byte)
484
+ // TYPE 02: usa CRC16-Modbus (2 bytes)
485
+ int checksumLength = (result.type == 0x02) ? 2 : 1; // TYPE 02 usa CRC16, otros usan checksum de 8 bits
486
+ int dataLength = payloadLength - (3 + checksumLength); // payload - LEN(1) - ADDR(1) - TYPE(1) - CHECKSUM/CRC
487
+ if (dataLength > 0) {
488
+ result.data = new byte[dataLength];
489
+ System.arraycopy(validFrame, 4, result.data, 0, dataLength);
490
+ }
491
+
492
+ Log.d(TAG, String.format("🔍 Parsed - Address: %d, Type: %d, Data length: %d",
493
+ result.address, result.type, dataLength));
494
+
495
+ // Verificar CRC (antes del EOI final)
496
+ // Frame estructura: [SOI][LEN][ADDR][TYPE][DATA][CRC_LOW][CRC_HIGH][EOI]
497
+ // CRC está justo antes del EOI
498
+ int crcPos = validFrameLength - 3; // EOI(-1) + CRC_HIGH(-1) + CRC_LOW = -3
499
+ if (crcPos + 1 < validFrame.length) {
500
+ // Probar ambos órdenes de bytes para el CRC
501
+ int receivedCRC_BE = ((validFrame[crcPos + 1] & 0xFF) << 8) | (validFrame[crcPos] & 0xFF); // Big-endian
502
+ int receivedCRC_LE = ((validFrame[crcPos] & 0xFF) << 8) | (validFrame[crcPos + 1] & 0xFF); // Little-endian
503
+
504
+ // Probar diferentes variantes del CRC para encontrar la correcta
505
+
506
+ // Variante 1: Desde LEN hasta antes del CRC (actual)
507
+ byte[] frameForCRC1 = new byte[payloadLength - 2]; // payload_length - CRC(2)
508
+ System.arraycopy(validFrame, 1, frameForCRC1, 0, frameForCRC1.length);
509
+ int calculatedCRC1 = calculateCRC16Modbus(frameForCRC1);
510
+
511
+ // Variante 2: Sin incluir LEN (solo desde ADDRESS)
512
+ byte[] frameForCRC2 = new byte[payloadLength - 3]; // payload_length - LEN(1) - CRC(2)
513
+ System.arraycopy(validFrame, 2, frameForCRC2, 0, frameForCRC2.length);
514
+ int calculatedCRC2 = calculateCRC16Modbus(frameForCRC2);
515
+
516
+ // Variante 3: Todo el frame menos CRC y EOI (incluyendo SOI)
517
+ byte[] frameForCRC3 = new byte[validFrame.length - 3]; // todo menos CRC(2) y EOI(1)
518
+ System.arraycopy(validFrame, 0, frameForCRC3, 0, frameForCRC3.length);
519
+ int calculatedCRC3 = calculateCRC16Modbus(frameForCRC3);
520
+
521
+ // Debug: mostrar todas las variantes
522
+ StringBuilder crc1Hex = new StringBuilder();
523
+ for (byte b : frameForCRC1) { crc1Hex.append(String.format("%02X ", b & 0xFF)); }
524
+
525
+ StringBuilder crc2Hex = new StringBuilder();
526
+ for (byte b : frameForCRC2) { crc2Hex.append(String.format("%02X ", b & 0xFF)); }
527
+
528
+ StringBuilder crc3Hex = new StringBuilder();
529
+ for (byte b : frameForCRC3) { crc3Hex.append(String.format("%02X ", b & 0xFF)); }
530
+
531
+ Log.d(TAG, String.format("🔍 CRC V1 (LEN+): %s = 0x%04X", crc1Hex.toString().trim(), calculatedCRC1));
532
+ Log.d(TAG, String.format("🔍 CRC V2 (ADDR+): %s = 0x%04X", crc2Hex.toString().trim(), calculatedCRC2));
533
+ Log.d(TAG, String.format("🔍 CRC V3 (SOI+): %s = 0x%04X", crc3Hex.toString().trim(), calculatedCRC3));
534
+ Log.d(TAG, String.format("🔍 CRC Debug - CRC bytes: 0x%02X 0x%02X at positions %d,%d",
535
+ validFrame[crcPos] & 0xFF, validFrame[crcPos + 1] & 0xFF, crcPos, crcPos + 1));
536
+
537
+ // También probar interpretando el CRC como byte único
538
+ int receivedCRC_Single = validFrame[crcPos] & 0xFF; // Solo el primer byte del "CRC"
539
+
540
+ // Verificar todas las variantes contra todos los formatos posibles
541
+ boolean crcValid = false;
542
+ String validVariant = "";
543
+
544
+ // Probar V1 (LEN+)
545
+ if (receivedCRC_BE == calculatedCRC1) {
546
+ Log.d(TAG, "✅ CRC verification passed (V1-LEN+, Big-Endian)!");
547
+ crcValid = true; validVariant = "V1-BE";
548
+ } else if (receivedCRC_LE == calculatedCRC1) {
549
+ Log.d(TAG, "✅ CRC verification passed (V1-LEN+, Little-Endian)!");
550
+ crcValid = true; validVariant = "V1-LE";
551
+ }
552
+
553
+ // Probar V2 (ADDR+)
554
+ if (!crcValid && receivedCRC_BE == calculatedCRC2) {
555
+ Log.d(TAG, "✅ CRC verification passed (V2-ADDR+, Big-Endian)!");
556
+ crcValid = true; validVariant = "V2-BE";
557
+ } else if (!crcValid && receivedCRC_LE == calculatedCRC2) {
558
+ Log.d(TAG, "✅ CRC verification passed (V2-ADDR+, Little-Endian)!");
559
+ crcValid = true; validVariant = "V2-LE";
560
+ }
561
+
562
+ // Probar V3 (SOI+)
563
+ if (!crcValid && receivedCRC_BE == calculatedCRC3) {
564
+ Log.d(TAG, "✅ CRC verification passed (V3-SOI+, Big-Endian)!");
565
+ crcValid = true; validVariant = "V3-BE";
566
+ } else if (!crcValid && receivedCRC_LE == calculatedCRC3) {
567
+ Log.d(TAG, "✅ CRC verification passed (V3-SOI+, Little-Endian)!");
568
+ crcValid = true; validVariant = "V3-LE";
569
+ }
570
+
571
+ // Probar como checksum de 8 bits
572
+ if (!crcValid) {
573
+ int checksum1 = calculateSimpleChecksum(frameForCRC1);
574
+ int checksum2 = calculateSimpleChecksum(frameForCRC2);
575
+ int checksum3 = calculateSimpleChecksum(frameForCRC3);
576
+
577
+ Log.d(TAG, String.format("🔍 Simple checksums - V1: 0x%02X, V2: 0x%02X, V3: 0x%02X vs received: 0x%02X",
578
+ checksum1 & 0xFF, checksum2 & 0xFF, checksum3 & 0xFF, receivedCRC_Single));
579
+
580
+ if (receivedCRC_Single == (checksum1 & 0xFF)) {
581
+ Log.d(TAG, "✅ Checksum verification passed (V1-8bit)!");
582
+ crcValid = true; validVariant = "V1-8bit";
583
+ } else if (receivedCRC_Single == (checksum2 & 0xFF)) {
584
+ Log.d(TAG, "✅ Checksum verification passed (V2-8bit)!");
585
+ crcValid = true; validVariant = "V2-8bit";
586
+ } else if (receivedCRC_Single == (checksum3 & 0xFF)) {
587
+ Log.d(TAG, "✅ Checksum verification passed (V3-8bit)!");
588
+ crcValid = true; validVariant = "V3-8bit";
589
+ }
590
+ }
591
+
592
+ if (!crcValid) {
593
+ Log.w(TAG, "❌ CRC/Checksum mismatch in all variants and formats!");
594
+ Log.i(TAG, "ℹ️ IOBoard may use proprietary checksum algorithm - data parsing will continue");
595
+ } else {
596
+ Log.i(TAG, String.format("🎯 IOBoard CRC/Checksum format identified: %s", validVariant));
597
+ }
598
+ } else {
599
+ Log.w(TAG, "⚠️ No CRC found in response");
600
+ }
601
+
602
+ // Parse específico según el tipo de respuesta
603
+ if (result.type == 0x00) { // Status Response
604
+ parseStatusResponse(result);
605
+ }
606
+
607
+ result.isValid = true;
608
+ Log.d(TAG, String.format("✅ Valid IOBoard response parsed: %s", result.toString()));
609
+
610
+ return result;
611
+ }
612
+
613
+ private static void parseStatusResponse(IOBoardResponse response) {
614
+ if (response.data.length >= 6) {
615
+ // Byte 0: Door status
616
+ response.doorStatus = response.data[0] & 0xFF;
617
+
618
+ // Bytes 1-3: Firmware version (ejemplo: 01 00 63 = v1.0.99)
619
+ int major = response.data[1] & 0xFF;
620
+ int minor = response.data[2] & 0xFF;
621
+ int patch = response.data[3] & 0xFF;
622
+ response.firmware = String.format("v%d.%d.%d", major, minor, patch);
623
+
624
+ Log.d(TAG, String.format("📊 Status - Door: 0x%02X, Firmware: %s",
625
+ response.doorStatus, response.firmware));
626
+
627
+ // Bytes 4-5: Información adicional del sistema
628
+ if (response.data.length >= 6) {
629
+ int systemInfo1 = response.data[4] & 0xFF;
630
+ int systemInfo2 = response.data[5] & 0xFF;
631
+ Log.d(TAG, String.format("🔧 System Info: 0x%02X 0x%02X", systemInfo1, systemInfo2));
632
+ }
633
+ }
634
+ }
635
+
636
+ // Create MTC3P08L protocol frame for unlock pallet command
637
+ private byte[] createUnlockPalletFrame(int address, int palletNumber, String ledColor) {
638
+ // MTC3P08L Protocol Frame Structure (según documentación):
639
+ // [SOI][LEN][ADDR][TYPE][DATA][CRC][EOI]
640
+ // TYPE 01 = Single pallet parameter setting
641
+ // DATA = [NUM][LOCK][LED] = [1][1][6] = 8 bytes
642
+
643
+ byte soi = 0x0D; // Start of frame (según documentación)
644
+ byte eoi = 0x0A; // End of frame (según documentación)
645
+ byte dataLen = 8; // NUM(1) + LOCK(1) + LED(6) = 8 bytes
646
+ byte len = (byte) (dataLen + 7); // N+7 según documentación = 8+7 = 15 (0x0F)
647
+ byte addr = (byte) address;
648
+ byte type = 0x01; // Single pallet parameter setting
649
+
650
+ // DATA section:
651
+ byte palletNum = (byte) palletNumber; // NUM: 00-07 para pallets 1-8
652
+ byte doorLock = 0x01; // LOCK: 1=unlock, 0=no action
653
+
654
+ // LED section (6 bytes): [RGB_R][RGB_G][RGB_B][Intensity][BlinkTimes][BlinkSpeed]
655
+ byte red = 0, green = 0, blue = 0;
656
+ switch (ledColor.toLowerCase()) {
657
+ case "red":
658
+ red = (byte) 255;
659
+ break;
660
+ case "green":
661
+ green = (byte) 255;
662
+ break;
663
+ case "blue":
664
+ blue = (byte) 255;
665
+ break;
666
+ case "white":
667
+ red = green = blue = (byte) 255;
668
+ break;
669
+ case "yellow":
670
+ red = green = (byte) 255;
671
+ break;
672
+ case "cyan":
673
+ green = blue = (byte) 255;
674
+ break;
675
+ case "magenta":
676
+ red = blue = (byte) 255;
677
+ break;
678
+ default:
679
+ green = (byte) 255; // Default to green
680
+ break;
681
+ }
682
+
683
+ byte intensity = (byte) 255; // Max intensity
684
+ byte blinkTimes = 16; // 16 veces como en el ejemplo
685
+ byte blinkSpeed = 1; // 1 segundo
686
+
687
+ // Calculate CRC16-Modbus (x16+x15+x2+1) según documentación
688
+ byte[] dataForCrc = new byte[] {
689
+ soi, len, addr, type, palletNum, doorLock, red, green, blue, intensity, blinkTimes, blinkSpeed
690
+ };
691
+ int crc16 = calculateCRC16Modbus(dataForCrc);
692
+
693
+ // CRC16 en formato little-endian (low byte first, high byte second)
694
+ byte crcLow = (byte) (crc16 & 0xFF);
695
+ byte crcHigh = (byte) ((crc16 >> 8) & 0xFF);
696
+
697
+ return new byte[] {
698
+ soi, len, addr, type, // Header
699
+ palletNum, doorLock, // NUM, LOCK
700
+ red, green, blue, intensity, blinkTimes, blinkSpeed, // LED (6 bytes)
701
+ crcLow, crcHigh, // CRC (2 bytes)
702
+ eoi // End of frame
703
+ };
704
+ }
705
+
706
+ // Create Full Pallet Control frame (TYPE 02) - controls all 8 pallets
707
+ private byte[] createMultiplePalletsFrame(int address, String color, int intensity,
708
+ int blinkTimes, int blinkSpeed, List<Integer> pallets) {
709
+ // MTC3P08L Full Pallet Control Frame Structure (TYPE 02):
710
+ // [SOI][LEN][ADDR][TYPE][LOCK][EXT][LED1(6bytes)][LED2(6bytes)]...[LED8(6bytes)][CRC][EOI]
711
+ // Total: 1+1+1+1+1+1+(8*6)+2+1 = 57 bytes (0x39)
712
+
713
+ Log.d(TAG, "🎨 Creating Full Pallet Control frame: address=" + address +
714
+ ", color=" + color + ", intensity=" + intensity +
715
+ ", blinkTimes=" + blinkTimes + ", blinkSpeed=" + blinkSpeed +
716
+ ", pallets=" + pallets.toString());
717
+
718
+ byte soi = 0x0D; // Start of frame
719
+ byte eoi = 0x0A; // End of frame
720
+ byte len = 0x39; // Fixed length: 57 bytes for full pallet control
721
+ byte addr = (byte) address;
722
+ byte type = 0x02; // Full Pallet Control command
723
+
724
+ // Door lock control - following the documentation example:
725
+ // Example shows LOCK=0x01 for "Unlock: 01" (seems to be action code, not bit mask)
726
+ // For now, use 0x01 to match the documentation example exactly
727
+ byte lockControl = 0x01; // Unlock action (as per documentation example)
728
+
729
+ byte extControl = (byte) 0x80; // Extended output control (fixed as per example)
730
+
731
+ // Convert color to RGB
732
+ byte red = 0x00, green = 0x00, blue = 0x00;
733
+ switch (color.toUpperCase()) {
734
+ case "RED":
735
+ red = (byte) 0xFF;
736
+ break;
737
+ case "GREEN":
738
+ green = (byte) 0xFF;
739
+ break;
740
+ case "BLUE":
741
+ blue = (byte) 0xFF;
742
+ break;
743
+ case "YELLOW":
744
+ red = (byte) 0xFF;
745
+ green = (byte) 0xFF;
746
+ break;
747
+ case "PURPLE":
748
+ red = (byte) 0xFF;
749
+ blue = (byte) 0xFF;
750
+ break;
751
+ case "CYAN":
752
+ green = (byte) 0xFF;
753
+ blue = (byte) 0xFF;
754
+ break;
755
+ case "WHITE":
756
+ red = (byte) 0xFF;
757
+ green = (byte) 0xFF;
758
+ blue = (byte) 0xFF;
759
+ break;
760
+ default:
761
+ Log.w(TAG, "⚠️ Unknown color: " + color + ", using RED as default");
762
+ red = (byte) 0xFF;
763
+ break;
764
+ }
765
+
766
+ // Build data array for CRC calculation
767
+ // CRC includes SOI + all data up to (but not including) the CRC field itself
768
+ List<Byte> dataForCrc = new ArrayList<>();
769
+ dataForCrc.add(soi); // Include SOI in CRC calculation
770
+ dataForCrc.add(len);
771
+ dataForCrc.add(addr);
772
+ dataForCrc.add(type);
773
+ dataForCrc.add(lockControl);
774
+ dataForCrc.add(extControl);
775
+
776
+ // Add 8 LED groups (6 bytes each) - one for each pallet
777
+ for (int i = 1; i <= 8; i++) {
778
+ if (pallets.contains(i)) {
779
+ // This pallet should have the specified color and effects
780
+ dataForCrc.add(red);
781
+ dataForCrc.add(green);
782
+ dataForCrc.add(blue);
783
+ dataForCrc.add((byte) intensity);
784
+ dataForCrc.add((byte) blinkTimes);
785
+ dataForCrc.add((byte) blinkSpeed);
786
+ } else {
787
+ // This pallet should be off (all zeros)
788
+ dataForCrc.add((byte) 0x00); // R
789
+ dataForCrc.add((byte) 0x00); // G
790
+ dataForCrc.add((byte) 0x00); // B
791
+ dataForCrc.add((byte) 0x00); // Intensity (off)
792
+ dataForCrc.add((byte) 0x00); // BlinkTimes
793
+ dataForCrc.add((byte) 0x01); // BlinkSpeed (minimum value)
794
+ }
795
+ }
796
+
797
+ // Convert to byte array for CRC calculation
798
+ byte[] crcData = new byte[dataForCrc.size()];
799
+ for (int i = 0; i < dataForCrc.size(); i++) {
800
+ crcData[i] = dataForCrc.get(i);
801
+ }
802
+
803
+ // Calculate CRC16-Modbus (TYPE 02 uses CRC16, not simple checksum like TYPE 01)
804
+ int crc16 = calculateCRC16Modbus(crcData);
805
+
806
+ // CRC16 en formato little-endian (low byte first) - documentation shows 0C F1
807
+ byte crcLow = (byte) (crc16 & 0xFF);
808
+ byte crcHigh = (byte) ((crc16 >> 8) & 0xFF);
809
+
810
+ // Build final frame - DON'T add SOI again since it's already in dataForCrc
811
+ List<Byte> frame = new ArrayList<>();
812
+ frame.addAll(dataForCrc); // This already includes SOI + all data
813
+ frame.add(crcLow); // CRC low byte first (little-endian)
814
+ frame.add(crcHigh); // CRC high byte second
815
+ frame.add(eoi);
816
+
817
+ // Convert to byte array
818
+ byte[] frameArray = new byte[frame.size()];
819
+ for (int i = 0; i < frame.size(); i++) {
820
+ frameArray[i] = frame.get(i);
821
+ }
822
+
823
+ // Log the frame for debugging
824
+ StringBuilder hexFrame = new StringBuilder();
825
+ for (byte b : frameArray) {
826
+ hexFrame.append(String.format("%02X ", b & 0xFF));
827
+ }
828
+ Log.d(TAG, "🎯 Full Pallet Control frame created: " + hexFrame.toString().trim());
829
+ Log.d(TAG, "📊 Frame details - LockControl: 0x" + String.format("%02X", lockControl & 0xFF) +
830
+ ", ExtControl: 0x" + String.format("%02X", extControl & 0xFF) +
831
+ ", Color: " + color + " (" + String.format("%02X%02X%02X", red & 0xFF, green & 0xFF, blue & 0xFF) + ")" +
832
+ ", CRC16: 0x" + String.format("%02X%02X", crcLow & 0xFF, crcHigh & 0xFF) + " (little-endian)");
833
+
834
+ return frameArray;
835
+ }
836
+
837
+ // Implementación CRC16-Modbus (x16+x15+x2+1) según documentación MTC3P08L
838
+ private static int calculateCRC16Modbus(byte[] data) {
839
+ int crc = 0xFFFF; // Initial value
840
+
841
+ for (byte b : data) {
842
+ crc ^= (b & 0xFF); // XOR with byte
843
+
844
+ for (int i = 0; i < 8; i++) {
845
+ if ((crc & 0x0001) != 0) {
846
+ crc >>= 1;
847
+ crc ^= 0xA001; // Polynomial for Modbus (reversed)
848
+ } else {
849
+ crc >>= 1;
850
+ }
851
+ }
852
+ }
853
+
854
+ return crc;
855
+ }
856
+
857
+ // Create Status Query frame (Type 00) - matches documentation exactly
858
+ private byte[] createStatusQueryFrame(int address) {
859
+ // MTC3P08L Status Query Frame Structure (from documentation):
860
+ // 0D 07 01 00 B2 D9 0A
861
+ // [SOI][LEN][ADDR][TYPE][CRC][EOI]
862
+ // TYPE 00 = Status query (no data needed) - uses CRC16-Modbus like TYPE 02
863
+
864
+ byte soi = 0x0D; // Start of frame
865
+ byte eoi = 0x0A; // End of frame
866
+ byte len = 0x07; // Length: 7 (as per documentation)
867
+ byte addr = (byte) address;
868
+ byte type = 0x00; // Status query
869
+
870
+ // Calculate CRC16-Modbus for TYPE 00 (same as TYPE 02)
871
+ // CRC includes: SOI + LEN + ADDR + TYPE (no data for status query)
872
+ byte[] dataForCrc = new byte[] { soi, len, addr, type };
873
+ int crc16 = calculateCRC16Modbus(dataForCrc);
874
+
875
+ // CRC16 en formato little-endian (low byte first, high byte second)
876
+ byte crcLow = (byte) (crc16 & 0xFF);
877
+ byte crcHigh = (byte) ((crc16 >> 8) & 0xFF);
878
+
879
+ return new byte[] {
880
+ soi, len, addr, type, // Header (no data for status query)
881
+ crcLow, crcHigh, // CRC16-Modbus (2 bytes)
882
+ eoi // End of frame
883
+ };
884
+ }
885
+
886
+ // Test function to verify CRC calculation against documentation example
887
+ private void testCRCAgainstDocumentation() {
888
+ // Documentation example: 0D 0F 01 01 00 01 FF 00 00 FF 10 01 1D 6F 0A
889
+ // Expected CRC: 0x6F1D (little-endian: 1D 6F)
890
+ byte[] testData = new byte[] {
891
+ 0x0D, 0x0F, 0x01, 0x01, 0x00, 0x01, (byte)0xFF, 0x00, 0x00, (byte)0xFF, 0x10, 0x01
892
+ };
893
+
894
+ int calculatedCRC = calculateCRC16Modbus(testData);
895
+ int expectedCRC = 0x6F1D;
896
+
897
+ Log.d(TAG, "🧪 CRC TEST:");
898
+ Log.d(TAG, "Expected CRC: 0x" + Integer.toHexString(expectedCRC).toUpperCase() + " (bytes: 1D 6F)");
899
+ Log.d(TAG, "Calculated CRC: 0x" + Integer.toHexString(calculatedCRC).toUpperCase() +
900
+ " (bytes: " + String.format("%02X %02X", calculatedCRC & 0xFF, (calculatedCRC >> 8) & 0xFF) + ")");
901
+
902
+ if (calculatedCRC == expectedCRC) {
903
+ Log.i(TAG, "✅ CRC16-Modbus calculation is CORRECT!");
904
+ } else {
905
+ Log.w(TAG, "❌ CRC16-Modbus calculation mismatch - needs adjustment");
906
+ }
907
+ }
908
+
909
+ // Método para calcular checksum simple (suma de bytes)
910
+ private static int calculateSimpleChecksum(byte[] data) {
911
+ int sum = 0;
912
+ for (byte b : data) {
913
+ sum += (b & 0xFF);
914
+ }
915
+ return sum & 0xFF; // Retorna solo el byte menos significativo
916
+ }
917
+
918
+ @Override
919
+ protected void handleOnDestroy() {
920
+ Log.d(TAG, "Plugin destroyed");
921
+ if (serialManager != null) {
922
+ serialManager.closePort();
923
+ }
924
+ super.handleOnDestroy();
925
+ }
926
+ }