From e1dab044d51afa1b9e6095a6e3874dc270b68c4a Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 30 Mar 2025 18:50:52 +1030 Subject: [PATCH 01/35] ANDROID: update USB api to allow setting baud rate and timeout - updated mui version --- samples/distro-examples/devio/android_usb.bas | 5 +-- .../sourceforge/smallbasic/MainActivity.java | 9 +++--- .../sourceforge/smallbasic/UsbConnection.java | 30 +++++++++++++----- .../net/sourceforge/smallbasic/WebServer.java | 3 +- .../smallbasic/WebServerTest.java} | 28 ++++++++++------ src/platform/android/build.gradle | 2 +- src/platform/android/jni/module.cpp | 19 ++++++++--- src/platform/android/webui/package.json | 20 ++++++------ .../android/webui/public/sb-desktop-32x32.png | Bin 2856 -> 5727 bytes src/platform/android/webui/server/pom.xml | 16 ---------- 10 files changed, 75 insertions(+), 57 deletions(-) rename src/platform/android/{webui/server/src/main/java/net/sourceforge/smallbasic/Server.java => app/src/test/java/net/sourceforge/smallbasic/WebServerTest.java} (83%) delete mode 100644 src/platform/android/webui/server/pom.xml diff --git a/samples/distro-examples/devio/android_usb.bas b/samples/distro-examples/devio/android_usb.bas index 47398c6b..320b7d8a 100644 --- a/samples/distro-examples/devio/android_usb.bas +++ b/samples/distro-examples/devio/android_usb.bas @@ -3,8 +3,9 @@ import android usb = android.openUsbSerial(0x16C0) while 1 - usb.send("hello"); + input k + n = usb.send(k); + print "sent "; n print usb.receive() - delay 1000 wend diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java index 2f0a909a..6a82a07d 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java @@ -655,14 +655,15 @@ public void speak(final byte[] textBytes) { public boolean usbClose() { if (_usbConnection != null) { _usbConnection.close(); + _usbConnection = null; } return true; } - public String usbConnect(int vendorId) { + public String usbConnect(int vendorId, int baud, int timeout) { String result; try { - _usbConnection = new UsbConnection(getApplicationContext(), vendorId); + _usbConnection = new UsbConnection(getApplicationContext(), vendorId, baud, timeout); result = "[tag-connected]"; } catch (IOException e) { result = e.getLocalizedMessage(); @@ -725,12 +726,12 @@ protected void onStop() { } } - private void checkPermission(final String permission, final int result) { + private void checkPermission(final String permission, final int requestCode) { runOnUiThread(new Runnable() { @Override public void run() { String[] permissions = {permission}; - ActivityCompat.requestPermissions(MainActivity.this, permissions, result); + ActivityCompat.requestPermissions(MainActivity.this, permissions, requestCode); } }); } diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java index 492e2bdf..9eb0d0b0 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java @@ -38,13 +38,15 @@ public class UsbConnection extends BroadcastReceiver { private UsbEndpoint _endpointIn; private UsbEndpoint _endpointOut; private final int _bufferSize; + private final int _timeoutMillis; /** * Constructs a new UsbConnection */ - public UsbConnection(Context context, int vendorId) throws IOException { + public UsbConnection(Context context, int vendorId, int baud, int timeout) throws IOException { _usbManager = getUsbManager(context); _usbDevice = getDevice(_usbManager, vendorId); + _timeoutMillis = timeout > 0 ? timeout : TIMEOUT_MILLIS; if (_usbDevice == null) { throw getIoException(context, R.string.USB_DEVICE_NOT_FOUND); } @@ -112,14 +114,26 @@ public UsbConnection(Context context, int vendorId) throws IOException { throw getIoException(context, R.string.USB_DATA_NOT_CLAIMED); } + byte baudLsb; + byte baudMsb; + + if (baud > 0) { + baudLsb = (byte) (baud); + baudMsb = (byte) (baud >> 8 & 0xff); + } else { + // defaults to 19200 (little endian) + baudLsb = 0; + baudMsb = 0x4b; + } + // Set line coding (19200 baud, 8N1) byte[] lineCoding = new byte[] { - (byte) 0x00, // 19200 baud rate (little endian) - (byte) 0x4B, - 0, 0, - 0, // 1 stop bit - 0, // No parity - 8 // 8 data bits + baudLsb, + baudMsb, + 0, 0, + 0, // 1 stop bit + 0, // No parity + 8 // 8 data bits }; if (_connection.controlTransfer( @@ -190,7 +204,7 @@ public void onReceive(Context context, Intent intent) { public String receive() { String result; byte[] dataIn = new byte[_bufferSize]; - int bytesRead = _connection.bulkTransfer(_endpointIn, dataIn, dataIn.length, TIMEOUT_MILLIS); + int bytesRead = _connection.bulkTransfer(_endpointIn, dataIn, dataIn.length, _timeoutMillis); if (bytesRead > 0) { result = new String(dataIn, 0, bytesRead, StandardCharsets.UTF_8); } else { diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java index 9fc5c75c..a3cee7f8 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java @@ -53,7 +53,7 @@ public WebServer() { /** * Runs the WebServer in a new thread */ - public void run(final int portNum, final String token) { + public Thread run(final int portNum, final String token) { Thread socketThread = new Thread(new Runnable() { public void run() { try { @@ -64,6 +64,7 @@ public void run() { } }); socketThread.start(); + return socketThread; } protected abstract void deleteFile(String remoteHost, String fileName) throws IOException; diff --git a/src/platform/android/webui/server/src/main/java/net/sourceforge/smallbasic/Server.java b/src/platform/android/app/src/test/java/net/sourceforge/smallbasic/WebServerTest.java similarity index 83% rename from src/platform/android/webui/server/src/main/java/net/sourceforge/smallbasic/Server.java rename to src/platform/android/app/src/test/java/net/sourceforge/smallbasic/WebServerTest.java index dd5c2c37..86ae6a36 100644 --- a/src/platform/android/webui/server/src/main/java/net/sourceforge/smallbasic/Server.java +++ b/src/platform/android/app/src/test/java/net/sourceforge/smallbasic/WebServerTest.java @@ -1,6 +1,7 @@ package net.sourceforge.smallbasic; -import sun.misc.IOUtils; +import org.junit.Ignore; +import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.File; @@ -18,13 +19,15 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Collection; -import java.util.Objects; -public class Server { - private static final String BASIC_HOME = "../basic/"; +public class WebServerTest { + private static final String APP_HOME = "../"; + private static final String BASIC_HOME = APP_HOME + "webui/basic/"; + private static final String ASSET_HOME = APP_HOME + "app/build/"; - public static void main(String[] args ) throws IOException { - // ln -s ../../../../../../../../app/src/main/java/net/sourceforge/smallbasic/WebServer.java . + @Test + @Ignore("For separate MUI app testing, comment for app build") + public void serverTest() throws InterruptedException { WebServer webServer = new WebServer() { @Override protected byte[] decodeBase64(String data) { @@ -39,7 +42,7 @@ protected void deleteFile(String hostName, String fileName) throws IOException { @Override protected void execStream(String remoteHost, InputStream inputStream) { try { - byte[] data = IOUtils.readAllBytes(inputStream); + byte[] data = inputStream.readAllBytes(); log(new String(data)); } catch (IOException e) { @@ -49,7 +52,7 @@ protected void execStream(String remoteHost, InputStream inputStream) { @Override protected Response getFile(String hostName, String path, boolean asset) throws IOException { - String prefix = asset ? "../build/" : BASIC_HOME; + String prefix = asset ? ASSET_HOME : BASIC_HOME; File file = new File(prefix + path); return new Response(Files.newInputStream(file.toPath()), file.length()); } @@ -58,7 +61,12 @@ protected Response getFile(String hostName, String path, boolean asset) throws I protected Collection getFileData(String hostName) throws IOException { final File folder = new File(BASIC_HOME); Collection result = new ArrayList<>(); - for (final File fileEntry : Objects.requireNonNull(folder.listFiles())) { + File[] fileList = folder.listFiles(); + if (fileList == null) { + log("Error: no files in: " + folder.getAbsolutePath()); + fileList = new File[0]; + } + for (final File fileEntry : fileList) { BasicFileAttributes attr = Files.readAttributes(fileEntry.toPath(), BasicFileAttributes.class); if (!attr.isDirectory()) { FileTime lastModifiedTime = attr.lastModifiedTime(); @@ -125,6 +133,6 @@ private void copy(InputStream in, OutputStream out) throws IOException { } } }; - webServer.run(8080, "ABC123"); + webServer.run(8080, "ABC123").join(); } } diff --git a/src/platform/android/build.gradle b/src/platform/android/build.gradle index 649fd189..b283b0cc 100644 --- a/src/platform/android/build.gradle +++ b/src/platform/android/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.9.0' + classpath 'com.android.tools.build:gradle:8.9.1' } } diff --git a/src/platform/android/jni/module.cpp b/src/platform/android/jni/module.cpp index 2cf5c4f9..986d0498 100644 --- a/src/platform/android/jni/module.cpp +++ b/src/platform/android/jni/module.cpp @@ -46,11 +46,18 @@ static int cmd_usb_receive(var_s *self, int argc, slib_par_t *args, var_s *retva static int cmd_usb_send(var_s *self, int argc, slib_par_t *args, var_s *retval) { int result; - if (argc != 1 || !is_object(self) || !v_is_type(args[0].var_p, V_STR)) { + if (argc != 1 || !is_object(self)) { v_setstr(retval, ERR_PARAM); result = 0; } else { - v_setint(retval, runtime->getIntegerFromString("usbSend", v_getstr(args[0].var_p))); + if (v_is_type(args[0].var_p, V_STR)) { + auto str = v_getstr(args[0].var_p); + v_setint(retval, runtime->getIntegerFromString("usbSend", str)); + } else { + auto str = v_str(args[0].var_p); + v_setint(retval, runtime->getIntegerFromString("usbSend", str)); + free(str); + } result = 1; } return result; @@ -59,7 +66,7 @@ static int cmd_usb_send(var_s *self, int argc, slib_par_t *args, var_s *retval) static int cmd_usb_connect(int argc, slib_par_t *args, var_t *retval) { int result = 0; - if (argc != 1 || !v_is_type(args[0].var_p, V_INT)) { + if (argc < 1 || argc > 3 || !v_is_type(args[0].var_p, V_INT)) { v_setstr(retval, "Expected: vendorId"); } else { runtime->getOutput()->redraw(); @@ -68,10 +75,12 @@ static int cmd_usb_connect(int argc, slib_par_t *args, var_t *retval) { JNIEnv *env; app->activity->vm->AttachCurrentThread(&env, nullptr); int vendorId = v_getint(args[0].var_p); + int baud = argc >= 2 ? (int)v_getint(args[1].var_p) : 0; + int timeout = argc == 3 ? (int)v_getint(args[2].var_p) : 0; jclass clazz = env->GetObjectClass(app->activity->clazz); - const char *signature = "(I)Ljava/lang/String;"; + const char *signature = "(III)Ljava/lang/String;"; jmethodID methodId = env->GetMethodID(clazz, "usbConnect", signature); - auto jstr = (jstring)env->CallObjectMethod(app->activity->clazz, methodId, vendorId); + auto jstr = (jstring)env->CallObjectMethod(app->activity->clazz, methodId, vendorId, baud, timeout); const char *str = env->GetStringUTFChars(jstr, JNI_FALSE); if (strncmp(str, "[tag-connected]", 15) == 0) { diff --git a/src/platform/android/webui/package.json b/src/platform/android/webui/package.json index 5987686c..2d2a158a 100644 --- a/src/platform/android/webui/package.json +++ b/src/platform/android/webui/package.json @@ -6,28 +6,28 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@mui/icons-material": "^6.4.1", - "@mui/material": "^6.4.1", - "@mui/x-data-grid": "^7.24.1", + "@mui/icons-material": "^7.0.1", + "@mui/material": "^7.0.1", + "@mui/x-data-grid": "^7.28.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", "react-router-hash-link": "^2.4.3" }, "devDependencies": { - "@eslint/js": "^9.19.0", + "@eslint/js": "^9.23.0", "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.19.0", + "eslint": "^9.23.0", "eslint-config-react": "^1.1.7", "eslint-define-config": "^2.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.18", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-unused-imports": "^4.1.4", - "globals": "^15.14.0", - "npm-check-updates": "^17.1.14", - "vite": "^6.0.11", + "globals": "^16.0.0", + "npm-check-updates": "^17.1.16", + "vite": "^6.2.3", "vite-plugin-eslint": "^1.8.1" }, "scripts": { diff --git a/src/platform/android/webui/public/sb-desktop-32x32.png b/src/platform/android/webui/public/sb-desktop-32x32.png index 8f908c30e2e2fa3b5e21ea2b192b0884502a79a6..b6cfb248d67b08821ed9c0af1934a5f574b63e7a 100644 GIT binary patch literal 5727 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 bd`15Q8P!bJbc$(}00000NkvXXu0mjf#-oGl diff --git a/src/platform/android/webui/server/pom.xml b/src/platform/android/webui/server/pom.xml deleted file mode 100644 index 407dba55..00000000 --- a/src/platform/android/webui/server/pom.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - 4.0.0 - net.sourceforge.smallbasic - smallbasic - 1.0-SNAPSHOT - smallbasic - - UTF-8 - 1.8 - 1.8 - - From 82c463cd47d8aa2498918201e6ce35d7520b3c18 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sat, 5 Apr 2025 20:52:32 +1030 Subject: [PATCH 02/35] ANDROID: experimental bluetooth support --- src/languages/messages.en.h | 1 + .../android/app/src/main/AndroidManifest.xml | 2 + .../smallbasic/BluetoothConnection.java | 253 ++++++++++++++++++ .../smallbasic/BluetoothRxThread.java | 77 ++++++ .../smallbasic/BluetoothTxThread.java | 71 +++++ .../sourceforge/smallbasic/MainActivity.java | 85 +++++- .../sourceforge/smallbasic/UsbConnection.java | 70 +++-- .../app/src/main/res/values/strings.xml | 2 + src/platform/android/jni/module.cpp | 193 ++++++++++++- src/platform/teensy/samples/bt.bas | 47 ++++ src/platform/teensy/samples/hc-06.bas | 31 +++ src/platform/teensy/src/teensy.cpp | 142 +++++++--- 12 files changed, 904 insertions(+), 70 deletions(-) create mode 100644 src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothConnection.java create mode 100644 src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothRxThread.java create mode 100644 src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothTxThread.java create mode 100644 src/platform/teensy/samples/bt.bas create mode 100644 src/platform/teensy/samples/hc-06.bas diff --git a/src/languages/messages.en.h b/src/languages/messages.en.h index 1bca8feb..9f1859b1 100644 --- a/src/languages/messages.en.h +++ b/src/languages/messages.en.h @@ -233,3 +233,4 @@ #define ERR_DIRWALK_CANT_OPEN "DIRWALK: can't open %s" #define ERR_LINE_LENGTH "Line length limit exceeded at text: '%s'" #define ERR_ABNORMAL_EXIT "Abnormal exit" +#define ERR_CONNECTION "Not connected" diff --git a/src/platform/android/app/src/main/AndroidManifest.xml b/src/platform/android/app/src/main/AndroidManifest.xml index 82119154..43d8f280 100644 --- a/src/platform/android/app/src/main/AndroidManifest.xml +++ b/src/platform/android/app/src/main/AndroidManifest.xml @@ -71,6 +71,8 @@ + + diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothConnection.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothConnection.java new file mode 100644 index 00000000..5a946d6e --- /dev/null +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothConnection.java @@ -0,0 +1,253 @@ +package net.sourceforge.smallbasic; + +import android.Manifest; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresPermission; +import androidx.core.app.ActivityCompat; + +import java.io.IOException; +import java.util.UUID; + +/** + * Bluetooth (non BLE) communications + */ +public class BluetoothConnection extends BroadcastReceiver { + private static final String TAG = "smallbasic"; + private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + private static final int CONNECT_PERMISSION = 1000; + + private final BluetoothAdapter _bluetoothAdapter; + private final Context _context; + private final String _deviceName; + private BluetoothTxThread _txThread; + private BluetoothRxThread _rxThread; + private BluetoothSocket _socket; + private boolean _error; + + public BluetoothConnection(Activity activity, String deviceName) throws IOException { + this._context = activity; + this._deviceName = deviceName; + this._bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + this._error = false; + if (_bluetoothAdapter == null) { + throw createIoException(activity, R.string.BLUETOOTH_ERROR); + } + + if (checkPermission(activity, Manifest.permission.BLUETOOTH_CONNECT)) { + requestPermission(activity, Manifest.permission.BLUETOOTH_CONNECT); + throw createIoException(activity, R.string.PERMISSION_ERROR); + } + + if (checkPermission(activity, Manifest.permission.BLUETOOTH_SCAN)) { + requestPermission(activity, Manifest.permission.BLUETOOTH_SCAN); + throw createIoException(activity, R.string.PERMISSION_ERROR); + } + + if (!_bluetoothAdapter.isEnabled()) { + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + activity.startActivity(enableBtIntent); + } + + // Start discovery of nearby Bluetooth devices + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + activity.registerReceiver(this, filter); + _bluetoothAdapter.startDiscovery(); + } + + /** + * Closes the connection + */ + public void close() { + _bluetoothAdapter.cancelDiscovery(); + unregisterReceiver(); + closeSocket(); + stopTxThread(); + stopRxThread(); + Log.d(TAG, "BT connection closed"); + } + + /** + * Returns information about the connected device + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + public String getDescription() { + String result; + if (_socket != null) { + result = String.format("Remote %s [%s]", + _socket.getRemoteDevice().getName(), + _socket.getRemoteDevice().getAddress()); + } else { + result = String.format("Local: %s [%s] Waiting for %s", + _bluetoothAdapter.getName(), + _bluetoothAdapter.getAddress(), + _deviceName); + } + return result; + } + + /** + * Returns whether the bluetooth connection is open + */ + public boolean isConnected() { + return _socket != null && _socket.isConnected(); + } + + /** + * Whether a connection error has occurred + */ + public boolean isError() { + return _error; + } + + /** + * Handles receiving the request permission response event + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive entered"); + String action = intent.getAction(); + if (BluetoothDevice.ACTION_FOUND.equals(action)) { + BluetoothDevice discoveredDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (discoveredDevice != null) { + String deviceName = discoveredDevice.getName(); + String deviceAddress = discoveredDevice.getAddress(); + Log.d(TAG, "Found device: " + deviceName + " at " + deviceAddress); + if (_deviceName.equals(deviceName)) { + _bluetoothAdapter.cancelDiscovery(); + unregisterReceiver(); + connectToDevice(discoveredDevice); + } + } + } + } + + /** + * Receives the next packet of data from the connection + */ + public String receive(String strDefault) { + String result; + if (_rxThread != null) { + result = _rxThread.read(); + } else { + result = strDefault; + } + return result; + } + + /** + * Sends the given data to the connection + */ + public boolean send(String data) { + boolean result; + if (_txThread != null) { + result = _txThread.send(data); + } else { + result = false; + } + return result; + } + + /** + * Returns whether the given permission is granted + */ + private boolean checkPermission(Activity activity, String permission) { + return ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED; + } + + /** + * Close the socket at termination of the thread + */ + private void closeSocket() { + try { + if (_socket != null) { + _socket.close(); + _socket = null; + Log.d(TAG, "BT socket closed."); + } + } + catch (Exception e) { + Log.e(TAG, "Error closing socket", e); + } + } + + /** + * Connects to the target device and commences communication + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void connectToDevice(BluetoothDevice device) { + try { + _socket = device.createRfcommSocketToServiceRecord(SPP_UUID); + _socket.connect(); + Log.d(TAG, "Connected to device: " + device.getName()); + _txThread = new BluetoothTxThread(_socket); + _rxThread = new BluetoothRxThread(_socket); + } catch (Exception e) { + _error = true; + Log.e(TAG, "Connection failed", e); + } + } + + /** + * Builds an Exception with the given resource string + */ + @NonNull + private static IOException createIoException(Context context, int resourceId) { + return new IOException(context.getResources().getString(resourceId)); + } + + /** + * Invokes the display of a permission prompt + */ + private void requestPermission(Activity activity, String permission) { + new Handler(Looper.getMainLooper()).post(() -> { + String[] permissions = {permission}; + ActivityCompat.requestPermissions(activity, permissions, CONNECT_PERMISSION); + }); + Log.d(TAG, "requesting permission: " + permission); + } + + /** + * Stops the receive thread + */ + private void stopRxThread() { + if (_rxThread != null) { + _rxThread.stopThread(); + _rxThread = null; + } + } + + /** + * Stops the transmit thread + */ + private void stopTxThread() { + if (_txThread != null) { + _txThread.stopThread(); + _txThread = null; + } + } + + /** + * Disconnects our BroadcastReceiver listener + */ + private void unregisterReceiver() { + try { + _context.unregisterReceiver(this); + } catch (IllegalArgumentException e) { + // ignored + } + } +} diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothRxThread.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothRxThread.java new file mode 100644 index 00000000..553bfae9 --- /dev/null +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothRxThread.java @@ -0,0 +1,77 @@ +package net.sourceforge.smallbasic; + +import android.bluetooth.BluetoothSocket; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Thread for receiving data on a bluetooth connection + */ +public class BluetoothRxThread extends Thread { + private static final String TAG = "smallbasic"; + private static final int QUEUE_SIZE = 100; + private static final int READ_BUFFER_SIZE = 1024; + private final AtomicBoolean _running; + private final InputStream _inputStream; + private final BlockingQueue _queue; + + public BluetoothRxThread(BluetoothSocket socket) throws IOException { + this._inputStream = socket.getInputStream(); + this._queue = new ArrayBlockingQueue<>(QUEUE_SIZE); + this._running = new AtomicBoolean(true); + start(); + } + + public boolean isRunning() { + return _running.get(); + } + + /** + * Returns any data from the queue without blocking + */ + public String read() { + String result; + byte[] data = _queue.poll(); + if (data != null && data.length > 0) { + result = new String(data, StandardCharsets.UTF_8); + } else { + result = ""; + } + return result; + } + + @Override + public void run() { + byte[] buffer = new byte[READ_BUFFER_SIZE]; + int ticks = 0; + try { + while (_running.get() && !Thread.currentThread().isInterrupted()) { + ticks++; + // read() is blocking + int size = _inputStream.read(buffer); + if (size > 0) { + byte[] data = Arrays.copyOf(buffer, size); + // blocks if the queue is full + _queue.put(data); + } + } + } catch (Exception e) { + Log.d(TAG, "Run failed:", e); + } finally { + _running.set(false); + } + Log.d(TAG, "Bluetooth RX thread terminated with: " + ticks); + } + + public void stopThread() { + _running.set(false); + interrupt(); + } +} diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothTxThread.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothTxThread.java new file mode 100644 index 00000000..d4f8ee97 --- /dev/null +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/BluetoothTxThread.java @@ -0,0 +1,71 @@ +package net.sourceforge.smallbasic; + +import android.bluetooth.BluetoothSocket; +import android.util.Log; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Thread for sending data on a bluetooth connection + */ +public class BluetoothTxThread extends Thread { + private static final String TAG = "smallbasic"; + private static final int QUEUE_SIZE = 100; + private final AtomicBoolean _running; + private final BlockingQueue _queue; + private final OutputStream _outputStream; + + public BluetoothTxThread(BluetoothSocket socket) throws IOException { + this._outputStream = socket.getOutputStream(); + this._queue = new ArrayBlockingQueue<>(QUEUE_SIZE); + this._running = new AtomicBoolean(true); + start(); + } + + public boolean isRunning() { + return _running.get(); + } + + @Override + public void run() { + int ticks = 0; + try { + while (_running.get() && !Thread.currentThread().isInterrupted()) { + ticks++; + byte[] data = _queue.take(); + _outputStream.write(data); + _outputStream.flush(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + Log.d(TAG, "Run failed:", e); + } finally { + _running.set(false); + } + Log.d(TAG, "Bluetooth TX thread terminated with: " + ticks); + } + + /** + * Add data to the send queue without blocking + */ + public boolean send(String data) { + boolean result; + if (data == null || data.isEmpty()) { + result = false; + } else { + result = _queue.offer(data.getBytes(StandardCharsets.UTF_8)); + } + return result; + } + + public void stopThread() { + _running.set(false); + interrupt(); + } +} diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java index 6a82a07d..54284150 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java @@ -37,6 +37,7 @@ import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import androidx.annotation.RequiresPermission; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; @@ -90,6 +91,8 @@ public class MainActivity extends NativeActivity { private static final String SCHEME_BAS = "qrcode.bas"; private static final String SCHEME = "smallbasic://x/"; private static final String CP1252 = "Cp1252"; + private static final String TAG_CONNECTED = "[--tag-connected--]"; + private static final String TAG_ERROR = "[--tag-error--]"; private static final int BASE_FONT_SIZE = 18; private static final long LOCATION_INTERVAL = 1000; private static final float LOCATION_DISTANCE = 1; @@ -109,6 +112,7 @@ public class MainActivity extends NativeActivity { private TextToSpeechAdapter _tts; private Storage _storage; private UsbConnection _usbConnection; + private BluetoothConnection _bluetoothConnection; static { System.loadLibrary("smallbasic"); @@ -191,6 +195,67 @@ public void onClick(DialogInterface dialog, int which) { return result.value; } + public boolean bluetoothClose() { + if (_bluetoothConnection != null) { + _bluetoothConnection.close(); + _bluetoothConnection = null; + } + return true; + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + public String bluetoothConnect(String deviceName) { + String result; + try { + _bluetoothConnection = new BluetoothConnection(this, deviceName); + result = TAG_CONNECTED; + } catch (IOException e) { + result = e.getLocalizedMessage(); + } + return result; + } + + public int bluetoothConnected() { + int result; + if (_bluetoothConnection == null || _bluetoothConnection.isError()) { + result = -1; + } else { + result = _bluetoothConnection.isConnected() ? 1 : 0; + } + return result; + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + public String bluetoothDescription() { + String result; + if (_bluetoothConnection != null && !_bluetoothConnection.isError()) { + result = _bluetoothConnection.getDescription(); + } else { + result = TAG_ERROR; + } + return result; + } + + public String bluetoothReceive() { + String result; + if (_bluetoothConnection != null && !_bluetoothConnection.isError()) { + result = _bluetoothConnection.receive(TAG_ERROR); + } else { + result = TAG_ERROR; + } + return result; + } + + public int bluetoothSend(final byte[] data) { + int result; + if (_bluetoothConnection != null && !_bluetoothConnection.isError()) { + result = _bluetoothConnection.send(getString(data)) ? 1 : 0; + } else { + result = -1; + } + return result; + } + public void browseFile(final byte[] pathBytes) { try { String url = new String(pathBytes, CP1252); @@ -212,10 +277,8 @@ public boolean closeLibHandlers() { if (_tts != null) { _tts.stop(); } - if (_usbConnection != null) { - _usbConnection.close(); - _usbConnection = null; - } + usbClose(); + bluetoothClose(); return removeLocationUpdates(); } @@ -664,19 +727,29 @@ public String usbConnect(int vendorId, int baud, int timeout) { String result; try { _usbConnection = new UsbConnection(getApplicationContext(), vendorId, baud, timeout); - result = "[tag-connected]"; + result = TAG_CONNECTED; } catch (IOException e) { result = e.getLocalizedMessage(); } return result; } + public String usbDescription() { + String result; + if (_usbConnection != null) { + result = _usbConnection.getDescription(); + } else { + result = TAG_ERROR; + } + return result; + } + public String usbReceive() { String result; if (_usbConnection != null) { result = _usbConnection.receive(); } else { - result = ""; + result = TAG_ERROR; } return result; } diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java index 9eb0d0b0..b6032d62 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java @@ -1,5 +1,6 @@ package net.sourceforge.smallbasic; +import android.annotation.SuppressLint; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; @@ -37,28 +38,32 @@ public class UsbConnection extends BroadcastReceiver { private UsbInterface _controlInterface; private UsbEndpoint _endpointIn; private UsbEndpoint _endpointOut; + private Context _context; private final int _bufferSize; private final int _timeoutMillis; + private final int _baud; /** * Constructs a new UsbConnection */ public UsbConnection(Context context, int vendorId, int baud, int timeout) throws IOException { + _context = context; _usbManager = getUsbManager(context); _usbDevice = getDevice(_usbManager, vendorId); - _timeoutMillis = timeout > 0 ? timeout : TIMEOUT_MILLIS; + _timeoutMillis = timeout < 0 ? TIMEOUT_MILLIS : timeout; + if (_usbDevice == null) { - throw getIoException(context, R.string.USB_DEVICE_NOT_FOUND); + throw createIoException(context, R.string.USB_DEVICE_NOT_FOUND); } if (!_usbManager.hasPermission(_usbDevice)) { requestPermission(context); - throw getIoException(context, R.string.PERMISSION_ERROR); + throw createIoException(context, R.string.PERMISSION_ERROR); } _connection = _usbManager.openDevice(_usbDevice); if (_connection == null) { - throw getIoException(context, R.string.USB_CONNECTION_ERROR); + throw createIoException(context, R.string.USB_CONNECTION_ERROR); } // Find the CDC interfaces and endpoints @@ -91,13 +96,13 @@ public UsbConnection(Context context, int vendorId, int baud, int timeout) throw } if (controlInterface == null) { - throw getIoException(context, R.string.USB_CONTROL_NOT_FOUND); + throw createIoException(context, R.string.USB_CONTROL_NOT_FOUND); } if (endpointIn == null) { - throw getIoException(context, R.string.ENDPOINT_IN_ERROR); + throw createIoException(context, R.string.ENDPOINT_IN_ERROR); } if (endpointOut == null) { - throw getIoException(context, R.string.ENDPOINT_OUT_ERROR); + throw createIoException(context, R.string.ENDPOINT_OUT_ERROR); } _endpointIn = endpointIn; @@ -108,25 +113,27 @@ public UsbConnection(Context context, int vendorId, int baud, int timeout) throw // Claim interfaces if (!_connection.claimInterface(controlInterface, true)) { - throw getIoException(context, R.string.USB_CONTROL_NOT_CLAIMED); + throw createIoException(context, R.string.USB_CONTROL_NOT_CLAIMED); } if (!_connection.claimInterface(dataInterface, true)) { - throw getIoException(context, R.string.USB_DATA_NOT_CLAIMED); + throw createIoException(context, R.string.USB_DATA_NOT_CLAIMED); } byte baudLsb; byte baudMsb; - if (baud > 0) { + if (baud >= 300) { baudLsb = (byte) (baud); baudMsb = (byte) (baud >> 8 & 0xff); + _baud = baud; } else { // defaults to 19200 (little endian) baudLsb = 0; baudMsb = 0x4b; + _baud = 19200; } - // Set line coding (19200 baud, 8N1) + // Set line coding (baud, 8N1) byte[] lineCoding = new byte[] { baudLsb, baudMsb, @@ -144,7 +151,7 @@ public UsbConnection(Context context, int vendorId, int baud, int timeout) throw lineCoding, lineCoding.length, TIMEOUT_MILLIS) < 0) { - throw getIoException(context, R.string.USB_ENCODING_ERROR); + throw createIoException(context, R.string.USB_ENCODING_ERROR); } if (_connection.controlTransfer( @@ -155,7 +162,7 @@ public UsbConnection(Context context, int vendorId, int baud, int timeout) throw null, 0, TIMEOUT_MILLIS) < 0) { - throw getIoException(context, R.string.USB_LINE_STATE_ERROR); + throw createIoException(context, R.string.USB_LINE_STATE_ERROR); } } @@ -163,6 +170,14 @@ public UsbConnection(Context context, int vendorId, int baud, int timeout) throw * Closes the USB connection */ public void close() { + if (_context != null) { + try { + _context.unregisterReceiver(this); + } catch (IllegalArgumentException e) { + // ignored + } + _context = null; + } if (_connection != null) { if (_controlInterface != null) { _connection.releaseInterface(_controlInterface); @@ -180,6 +195,25 @@ public void close() { _endpointOut = null; } + /** + * Returns information about the connected USB device + */ + @SuppressLint("DefaultLocale") + public String getDescription() { + String result; + if (_usbDevice != null) { + result = String.format("%s %s %s %s [%d bps]", + _usbDevice.getDeviceName(), + _usbDevice.getProductName(), + _usbDevice.getManufacturerName(), + _usbDevice.getSerialNumber(), + _baud); + } else { + result = ""; + } + return result; + } + /** * Handles receiving the request permission response event */ @@ -241,6 +275,11 @@ private int calcBufferSize(UsbEndpoint endpointIn) { return result; } + @NonNull + private static IOException createIoException(Context context, int resourceId) { + return new IOException(context.getResources().getString(resourceId)); + } + /** * Returns the UsbDevice matching the given vendorId */ @@ -255,11 +294,6 @@ private UsbDevice getDevice(UsbManager usbManager, int vendorId) { return result; } - @NonNull - private static IOException getIoException(Context context, int resourceId) { - return new IOException(context.getResources().getString(resourceId)); - } - /** * Returns the UsbManager */ diff --git a/src/platform/android/app/src/main/res/values/strings.xml b/src/platform/android/app/src/main/res/values/strings.xml index 8da69aeb..652de1cf 100644 --- a/src/platform/android/app/src/main/res/values/strings.xml +++ b/src/platform/android/app/src/main/res/values/strings.xml @@ -18,6 +18,8 @@ Input endpoint not found Output endpoint not found Not permitted + Bluetooth is not supported on this device. +