Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,50 @@ public void notifyDataObservers(String key, Object newValue) {
observer.onReceiveValue(this, key, newValue);
}
}
/**
* Send a list of byte values to the connected Bluetooth device, followed by the specified delimiter byte.
*
* @param list the list of numeric values to write
* @param delimiter the delimiter byte as a String (e.g., "0" or "0x0A")
*/
@SimpleFunction(description = "Send a list of byte values to the connected Bluetooth device, followed by the specified delimiter byte.")
public void SendBytesWithDelimiter(YailList list, String delimiter) {

// Send main bytes
SendBytes(list);

// Convert the delimiter from String → int
int value;

try {
if (delimiter.startsWith("0x") || delimiter.startsWith("0X")) {
value = Integer.parseInt(delimiter.substring(2), 16);
} else {
value = Integer.parseInt(delimiter);
}

if (value < 0 || value > 255) {
form.dispatchErrorOccurredEvent(
this,
"SendBytesWithDelimiter",
ErrorMessages.ERROR_BLUETOOTH_INVALID_DATA,
delimiter
);
return;
}

// Send exactly one byte
Send1ByteNumber(value);

} catch (NumberFormatException e) {
form.dispatchErrorOccurredEvent(
this,
"SendBytesWithDelimiter",
ErrorMessages.ERROR_BLUETOOTH_INVALID_DATA,
delimiter
);
}
}
@Override
public String getDataValue(String key) {
String value = "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
import java.util.ArrayList;
import java.util.List;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
* An abstract base class for the BluetoothClient and BluetoothServer
* component.
Expand All @@ -60,6 +67,63 @@ public abstract class BluetoothConnectionBase extends AndroidNonvisibleComponent
private InputStream inputStream;
private OutputStream outputStream;

// -----------------------------
// Timeout fields (NEW - minimal additions)
// -----------------------------
// connection timeout is used by callers that will implement a connect wrapper (client/server).
// For this base class we expose the properties for consistency; client/server will use them.
private int connectionTimeoutMillis = 15000; // default 15s
private int readTimeoutMillis = 10000; // default 10s
private int writeTimeoutMillis = 5000; // default 5s

@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "Connection timeout in milliseconds. Default 15000.")
public int ConnectionTimeout() {
return connectionTimeoutMillis;
}

@SimpleProperty
public void ConnectionTimeout(int millis) {
if (millis < 1000) {
connectionTimeoutMillis = 1000;
} else {
connectionTimeoutMillis = millis;
}
}

@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "Read timeout in milliseconds. Default 10000.")
public int ReadTimeout() {
return readTimeoutMillis;
}

@SimpleProperty
public void ReadTimeout(int millis) {
if (millis < 100) {
readTimeoutMillis = 100;
} else {
readTimeoutMillis = millis;
}
}

@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "Write timeout in milliseconds. Default 5000.")
public int WriteTimeout() {
return writeTimeoutMillis;
}

@SimpleProperty
public void WriteTimeout(int millis) {
if (millis < 100) {
writeTimeoutMillis = 100;
} else {
writeTimeoutMillis = millis;
}
}
// -----------------------------
// End timeout fields
// -----------------------------

/**
* Creates a new BluetoothConnectionBase.
*/
Expand Down Expand Up @@ -512,17 +576,9 @@ protected void write(String functionName, byte b) {
return;
}

try {
outputStream.write(b);
outputStream.flush();
} catch (IOException e) {
Log.e(logTag, "IO Exception during Writing" + e.getMessage());
if (disconnectOnError) {
Disconnect();
}
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_WRITE, e.getMessage());
}
// Wrap single byte write in timed write
byte[] arr = new byte[] { b };
write(functionName, arr);
}

/**
Expand All @@ -538,16 +594,49 @@ protected void write(String functionName, byte[] bytes) {
return;
}

// Implement write with timeout wrapper
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<?> future = exec.submit(new Runnable() {
@Override
public void run() {
try {
outputStream.write(bytes);
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});

try {
outputStream.write(bytes);
outputStream.flush();
} catch (IOException e) {
Log.e(logTag, "IO Exception during Writing" + e.getMessage());
future.get(writeTimeoutMillis, TimeUnit.MILLISECONDS);
} catch (TimeoutException te) {
future.cancel(true);
Log.e(logTag, "Write timed out.");
if (disconnectOnError) {
Disconnect();
}
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_WRITE, e.getMessage());
bluetoothError(functionName, ErrorMessages.ERROR_BLUETOOTH_TIMEOUT);
} catch (Exception e) {
// unwrap IOException wrapped in RuntimeException
Throwable cause = e.getCause();
if (cause instanceof IOException) {
Log.e(logTag, "IO Exception during Writing" + cause.getMessage());
if (disconnectOnError) {
Disconnect();
}
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_WRITE, cause.getMessage());
} else {
Log.e(logTag, "Exception during Writing" + e.getMessage());
if (disconnectOnError) {
Disconnect();
}
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_WRITE, e.getMessage());
}
} finally {
exec.shutdownNow();
}
}

Expand Down Expand Up @@ -785,7 +874,7 @@ protected final byte[] read(String functionName, int numberOfBytes) {
int totalBytesRead = 0;
while (totalBytesRead < numberOfBytes) {
try {
int numBytesRead = inputStream.read(bytes, totalBytesRead, bytes.length - totalBytesRead);
int numBytesRead = timedRead(bytes, totalBytesRead, bytes.length - totalBytesRead);
if (numBytesRead == -1) {
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_END_OF_STREAM);
Expand All @@ -800,14 +889,21 @@ protected final byte[] read(String functionName, int numberOfBytes) {
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_READ, e.getMessage());
break;
} catch (TimeoutException te) {
Log.e(logTag, "Read timed out.");
if (disconnectOnError) {
Disconnect();
}
bluetoothError(functionName, ErrorMessages.ERROR_BLUETOOTH_TIMEOUT);
break;
}
}
buffer.write(bytes, 0, totalBytesRead);
} else {
// Read one byte at a time until a delimiter byte is read.
while (true) {
try {
int value = inputStream.read();
int value = timedReadSingle();
if (value == -1) {
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_END_OF_STREAM);
Expand All @@ -825,13 +921,95 @@ protected final byte[] read(String functionName, int numberOfBytes) {
bluetoothError(functionName,
ErrorMessages.ERROR_BLUETOOTH_UNABLE_TO_READ, e.getMessage());
break;
} catch (TimeoutException te) {
Log.e(logTag, "Read timed out.");
if (disconnectOnError) {
Disconnect();
}
bluetoothError(functionName, ErrorMessages.ERROR_BLUETOOTH_TIMEOUT);
break;
}
}
}

return buffer.toByteArray();
}

/**
* Helper that reads into buffer with read timeout applied.
* Returns number of bytes read or -1 on end of stream.
*/
private int timedRead(final byte[] b, final int off, final int len)
throws IOException, TimeoutException {
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Integer> future = exec.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
try {
return inputStream.read(b, off, len);
} catch (IOException ioe) {
throw ioe;
} catch (Throwable t) {
throw new IOException("Read failed: " + t.getMessage());
}
}
});

try {
Integer result = future.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
return result == null ? -1 : result.intValue();
} catch (TimeoutException te) {
future.cancel(true);
throw te;
} catch (Exception e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new IOException(e.getMessage());
}
} finally {
exec.shutdownNow();
}
}

/**
* Helper that reads a single byte with read timeout.
* Returns byte value (0-255) or -1 on end of stream.
*/
private int timedReadSingle() throws IOException, TimeoutException {
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Integer> future = exec.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
try {
return inputStream.read();
} catch (IOException ioe) {
throw ioe;
} catch (Throwable t) {
throw new IOException("Read failed: " + t.getMessage());
}
}
});

try {
Integer result = future.get(readTimeoutMillis, TimeUnit.MILLISECONDS);
return result == null ? -1 : result.intValue();
} catch (TimeoutException te) {
future.cancel(true);
throw te;
} catch (Exception e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new IOException(e.getMessage());
}
} finally {
exec.shutdownNow();
}
}

// OnDestroyListener implementation

@Override
Expand Down
Loading