Skip to content

Commit 152b3ce

Browse files
committed
ANDROID: can now communicate with the teensy board via USB
1 parent d3cf528 commit 152b3ce

File tree

10 files changed

+192
-40
lines changed

10 files changed

+192
-40
lines changed

samples/distro-examples/devio/android_usb.bas

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import android
22

3-
usb = android.usbConnect(0x16C0)
3+
usb = android.openUsbSerial(0x16C0)
44

55
while 1
66
usb.send("hello");

src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ public String usbConnect(int vendorId) {
662662
String result;
663663
try {
664664
_usbConnection = new UsbConnection(getApplicationContext(), vendorId);
665-
result = "[connected]";
665+
result = "[tag-connected]";
666666
} catch (IOException e) {
667667
result = e.getLocalizedMessage();
668668
}
@@ -679,10 +679,14 @@ public String usbReceive() {
679679
return result;
680680
}
681681

682-
public void usbSend(final byte[] data) {
682+
public int usbSend(final byte[] data) {
683+
int result;
683684
if (_usbConnection != null) {
684-
_usbConnection.send(data);
685+
result = _usbConnection.send(getString(data));
686+
} else {
687+
result = -1;
685688
}
689+
return result;
686690
}
687691

688692
@Override

src/platform/android/app/src/main/java/net/sourceforge/smallbasic/UsbConnection.java

+145-21
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.content.Context;
66
import android.content.Intent;
77
import android.content.IntentFilter;
8+
import android.hardware.usb.UsbConstants;
89
import android.hardware.usb.UsbDevice;
910
import android.hardware.usb.UsbDeviceConnection;
1011
import android.hardware.usb.UsbEndpoint;
@@ -16,25 +17,27 @@
1617
import android.util.Log;
1718
import android.widget.Toast;
1819

20+
import androidx.annotation.NonNull;
21+
1922
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
2024

2125
/**
2226
* Usb host mode communications
2327
*/
2428
public class UsbConnection extends BroadcastReceiver {
2529
private static final String TAG = "smallbasic";
2630
private static final String ACTION_USB_PERMISSION = "smallbasic.android.USB_PERMISSION";
27-
private static final String PERMISSION_ERROR = "Not permitted";
28-
private static final String USB_DEVICE_NOT_FOUND = "Usb device not found";
29-
private static final int TIMEOUT_MILLIS = 1000;
30-
private static final int BUFFER_SIZE = 64;
31+
private static final int TIMEOUT_MILLIS = 5000;
3132

32-
private final UsbDeviceConnection _connection;
33-
private final UsbInterface _usbInterface;
3433
private final UsbDevice _usbDevice;
3534
private final UsbManager _usbManager;
36-
private final UsbEndpoint _endpointIn;
37-
private final UsbEndpoint _endpointOut;
35+
private UsbDeviceConnection _connection;
36+
private UsbInterface _dataInterface;
37+
private UsbInterface _controlInterface;
38+
private UsbEndpoint _endpointIn;
39+
private UsbEndpoint _endpointOut;
40+
private final int _bufferSize;
3841

3942
/**
4043
* Constructs a new UsbConnection
@@ -43,29 +46,124 @@ public UsbConnection(Context context, int vendorId) throws IOException {
4346
_usbManager = getUsbManager(context);
4447
_usbDevice = getDevice(_usbManager, vendorId);
4548
if (_usbDevice == null) {
46-
throw new IOException(USB_DEVICE_NOT_FOUND);
49+
throw getIoException(context, R.string.USB_DEVICE_NOT_FOUND);
4750
}
4851

4952
if (!_usbManager.hasPermission(_usbDevice)) {
5053
requestPermission(context);
51-
throw new IOException(PERMISSION_ERROR);
54+
throw getIoException(context, R.string.PERMISSION_ERROR);
5255
}
5356

54-
_usbInterface = _usbDevice.getInterface(0);
55-
_endpointIn = _usbInterface.getEndpoint(0);
56-
_endpointOut = _usbInterface.getEndpoint(1);
5757
_connection = _usbManager.openDevice(_usbDevice);
58-
_connection.claimInterface(_usbInterface, true);
58+
if (_connection == null) {
59+
throw getIoException(context, R.string.USB_CONNECTION_ERROR);
60+
}
61+
62+
// Find the CDC interfaces and endpoints
63+
UsbInterface controlInterface = null;
64+
UsbInterface dataInterface = null;
65+
UsbEndpoint endpointIn = null;
66+
UsbEndpoint endpointOut = null;
67+
68+
// Look for CDC control interface
69+
for (int i = 0; i < _usbDevice.getInterfaceCount(); i++) {
70+
UsbInterface anInterface = _usbDevice.getInterface(i);
71+
Log.i(TAG, "Interface " + i + " - Class: " + anInterface.getInterfaceClass());
72+
if (anInterface.getInterfaceClass() == UsbConstants.USB_CLASS_COMM) {
73+
controlInterface = anInterface;
74+
} else if (anInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CDC_DATA) {
75+
dataInterface = anInterface;
76+
for (int j = 0; j < anInterface.getEndpointCount(); j++) {
77+
UsbEndpoint ep = anInterface.getEndpoint(j);
78+
Log.i(TAG, " Endpoint " + j + " - Type: " + ep.getType() +
79+
", Direction: " + ep.getDirection());
80+
if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
81+
if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
82+
endpointIn = ep;
83+
} else {
84+
endpointOut = ep;
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
if (controlInterface == null) {
92+
throw getIoException(context, R.string.USB_CONTROL_NOT_FOUND);
93+
}
94+
if (endpointIn == null) {
95+
throw getIoException(context, R.string.ENDPOINT_IN_ERROR);
96+
}
97+
if (endpointOut == null) {
98+
throw getIoException(context, R.string.ENDPOINT_OUT_ERROR);
99+
}
100+
101+
_endpointIn = endpointIn;
102+
_endpointOut = endpointOut;
103+
_controlInterface = controlInterface;
104+
_dataInterface = dataInterface;
105+
_bufferSize = calcBufferSize(_endpointIn);
106+
107+
// Claim interfaces
108+
if (!_connection.claimInterface(controlInterface, true)) {
109+
throw getIoException(context, R.string.USB_CONTROL_NOT_CLAIMED);
110+
}
111+
if (!_connection.claimInterface(dataInterface, true)) {
112+
throw getIoException(context, R.string.USB_DATA_NOT_CLAIMED);
113+
}
114+
115+
// Set line coding (19200 baud, 8N1)
116+
byte[] lineCoding = new byte[] {
117+
(byte) 0x00, // 19200 baud rate (little endian)
118+
(byte) 0x4B,
119+
0, 0,
120+
0, // 1 stop bit
121+
0, // No parity
122+
8 // 8 data bits
123+
};
124+
125+
if (_connection.controlTransfer(
126+
0x21, // bmRequestType (0x21)
127+
0x20, // bRequest (SET_LINE_CODING)
128+
0, // wValue
129+
_controlInterface.getId(), // wIndex
130+
lineCoding,
131+
lineCoding.length,
132+
TIMEOUT_MILLIS) < 0) {
133+
throw getIoException(context, R.string.USB_ENCODING_ERROR);
134+
}
135+
136+
if (_connection.controlTransfer(
137+
0x21, // bmRequestType (0x21)
138+
0x22, // SET_CONTROL_LINE_STATE
139+
0x03, // wValue Enable DTR (bit 0) and RTS (bit 1)
140+
_controlInterface.getId(),
141+
null,
142+
0,
143+
TIMEOUT_MILLIS) < 0) {
144+
throw getIoException(context, R.string.USB_LINE_STATE_ERROR);
145+
}
59146
}
60147

61148
/**
62149
* Closes the USB connection
63150
*/
64151
public void close() {
65-
if (_connection != null && _usbInterface != null) {
66-
_connection.releaseInterface(_usbInterface);
152+
if (_connection != null) {
153+
if (_controlInterface != null) {
154+
_connection.releaseInterface(_controlInterface);
155+
}
156+
if (_dataInterface != null) {
157+
_connection.releaseInterface(_dataInterface);
158+
}
67159
_connection.close();
160+
_connection = null;
68161
}
162+
163+
_dataInterface = null;
164+
_controlInterface = null;
165+
_endpointIn = null;
166+
_endpointOut = null;
69167
}
70168

71169
/**
@@ -77,7 +175,7 @@ public void onReceive(Context context, Intent intent) {
77175
if (ACTION_USB_PERMISSION.equals(intent.getAction())) {
78176
boolean permitted = _usbManager.hasPermission(_usbDevice);
79177
String name = _usbDevice.getProductName();
80-
final String message = "USB connection [" + name + "] access " + (permitted ? "permitted" : "denied");
178+
final String message = name + " access " + (permitted ? "permitted" : "denied");
81179
final BroadcastReceiver receiver = this;
82180
new Handler(Looper.getMainLooper()).post(() -> {
83181
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
@@ -91,10 +189,10 @@ public void onReceive(Context context, Intent intent) {
91189
*/
92190
public String receive() {
93191
String result;
94-
byte[] dataIn = new byte[BUFFER_SIZE];
192+
byte[] dataIn = new byte[_bufferSize];
95193
int bytesRead = _connection.bulkTransfer(_endpointIn, dataIn, dataIn.length, TIMEOUT_MILLIS);
96194
if (bytesRead > 0) {
97-
result = new String(dataIn, 0, bytesRead);
195+
result = new String(dataIn, 0, bytesRead, StandardCharsets.UTF_8);
98196
} else {
99197
result = "";
100198
}
@@ -104,8 +202,29 @@ public String receive() {
104202
/**
105203
* Sends the given data to the usb connection
106204
*/
107-
public void send(byte[] dataOut) {
108-
_connection.bulkTransfer(_endpointOut, dataOut, dataOut.length, TIMEOUT_MILLIS);
205+
public int send(String data) {
206+
byte[] dataOut = data.getBytes(StandardCharsets.UTF_8);
207+
return _connection.bulkTransfer(_endpointOut, dataOut, dataOut.length, TIMEOUT_MILLIS);
208+
}
209+
210+
/**
211+
* Calculates the optimum receive buffer size
212+
*/
213+
private int calcBufferSize(UsbEndpoint endpointIn) {
214+
// Get the maximum packet size from the endpoint
215+
int maxPacketSize = endpointIn.getMaxPacketSize();
216+
217+
// Calculate buffer size as a multiple of the maximum packet size
218+
// 4 is a good multiplier for most applications
219+
int result = maxPacketSize * 4;
220+
221+
// Ensure minimum reasonable size
222+
result = Math.max(result, 1024);
223+
224+
// Optional: Cap at a maximum size if memory is a concern
225+
result = Math.min(result, 16384); // 16KB max
226+
227+
return result;
109228
}
110229

111230
/**
@@ -122,6 +241,11 @@ private UsbDevice getDevice(UsbManager usbManager, int vendorId) {
122241
return result;
123242
}
124243

244+
@NonNull
245+
private static IOException getIoException(Context context, int resourceId) {
246+
return new IOException(context.getResources().getString(resourceId));
247+
}
248+
125249
/**
126250
* Returns the UsbManager
127251
*/

src/platform/android/app/src/main/res/values/strings.xml

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
<string name="CANCEL">Cancel</string>
99
<string name="YES">Yes</string>
1010
<string name="NO">No</string>
11+
<string name="USB_DEVICE_NOT_FOUND">Usb device not found</string>
12+
<string name="USB_CONTROL_NOT_FOUND">Usb control interface not found</string>
13+
<string name="USB_CONNECTION_ERROR">Failed to obtain usb connection</string>
14+
<string name="USB_CONTROL_NOT_CLAIMED">Failed to claim USB control interface</string>
15+
<string name="USB_DATA_NOT_CLAIMED">Failed to claim USB data interface</string>
16+
<string name="USB_ENCODING_ERROR">Error setting line encoding</string>
17+
<string name="USB_LINE_STATE_ERROR">Error setting line state</string>
18+
<string name="ENDPOINT_IN_ERROR">Input endpoint not found</string>
19+
<string name="ENDPOINT_OUT_ERROR">Output endpoint not found</string>
20+
<string name="PERMISSION_ERROR">Not permitted</string>
1121
<style name="SBTheme" parent="android:Theme.Holo.Light">
1222
<item name="android:windowActionBar">false</item>
1323
<item name="android:windowNoTitle">true</item>

src/platform/android/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
mavenCentral()
66
}
77
dependencies {
8-
classpath 'com.android.tools.build:gradle:8.8.1'
8+
classpath 'com.android.tools.build:gradle:8.9.0'
99
}
1010
}
1111

src/platform/android/jni/module.cpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ static int cmd_usb_send(var_s *self, int argc, slib_par_t *args, var_s *retval)
5050
v_setstr(retval, ERR_PARAM);
5151
result = 0;
5252
} else {
53-
runtime->setString("usbSend", v_getstr(args[0].var_p));
53+
v_setint(retval, runtime->getIntegerFromString("usbSend", v_getstr(args[0].var_p)));
5454
result = 1;
5555
}
5656
return result;
@@ -74,7 +74,7 @@ static int cmd_usb_connect(int argc, slib_par_t *args, var_t *retval) {
7474
auto jstr = (jstring)env->CallObjectMethod(app->activity->clazz, methodId, vendorId);
7575
const char *str = env->GetStringUTFChars(jstr, JNI_FALSE);
7676

77-
if (strncmp(str, "[connected]", 11) == 0) {
77+
if (strncmp(str, "[tag-connected]", 15) == 0) {
7878
map_init(retval);
7979
retval->v.m.id = USB_OBJECT_ID;
8080
retval->v.m.cls_id = USB_CLASS_ID;
@@ -268,7 +268,7 @@ struct LibFuncs {
268268
{"LOCATION", cmd_location},
269269
{"SENSOR", cmd_sensor},
270270
{"REQUEST", cmd_request},
271-
{"USBCONNECT", cmd_usb_connect}
271+
{"OPENUSBSERIAL", cmd_usb_connect}
272272
};
273273

274274
extern "C" int sblib_proc_count(void) {

src/platform/android/jni/runtime.cpp

+13
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,19 @@ int Runtime::getInteger(const char *methodName) {
411411
return result;
412412
}
413413

414+
int Runtime::getIntegerFromString(const char *methodName, const char *value) {
415+
JNIEnv *env;
416+
_app->activity->vm->AttachCurrentThread(&env, nullptr);
417+
jclass clazz = env->GetObjectClass(_app->activity->clazz);
418+
jbyteArray valueByteArray = newByteArray(env, value);
419+
jmethodID methodId = env->GetMethodID(clazz, methodName, "([B)I");
420+
jint result = env->CallIntMethod(_app->activity->clazz, methodId, valueByteArray);
421+
env->DeleteLocalRef(valueByteArray);
422+
env->DeleteLocalRef(clazz);
423+
_app->activity->vm->DetachCurrentThread();
424+
return result;
425+
}
426+
414427
int Runtime::getUnicodeChar(int keyCode, int metaState) {
415428
JNIEnv *env;
416429
_app->activity->vm->AttachCurrentThread(&env, nullptr);

src/platform/android/jni/runtime.h

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ struct Runtime : public System {
4545
String getString(const char *methodName);
4646
String getStringBytes(const char *methodName);
4747
int getInteger(const char *methodName);
48+
int getIntegerFromString(const char *methodName, const char *value);
4849
int getUnicodeChar(int keyCode, int metaState);
4950
void redraw() { _graphics->redraw(); }
5051
void handleKeyEvent(MAEvent &event);

src/platform/android/webui/package.json

+10-10
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,28 @@
66
"dependencies": {
77
"@emotion/react": "^11.14.0",
88
"@emotion/styled": "^11.14.0",
9-
"@mui/icons-material": "^6.2.0",
10-
"@mui/material": "^6.2.0",
11-
"@mui/x-data-grid": "^7.23.2",
9+
"@mui/icons-material": "^6.4.1",
10+
"@mui/material": "^6.4.1",
11+
"@mui/x-data-grid": "^7.24.1",
1212
"react": "^18.3.1",
1313
"react-dom": "^18.3.1",
1414
"react-router-dom": "^6.27.0",
1515
"react-router-hash-link": "^2.4.3"
1616
},
1717
"devDependencies": {
18-
"@eslint/js": "^9.16.0",
18+
"@eslint/js": "^9.19.0",
1919
"@vitejs/plugin-react": "^4.3.4",
20-
"eslint": "^9.16.0",
20+
"eslint": "^9.19.0",
2121
"eslint-config-react": "^1.1.7",
2222
"eslint-define-config": "^2.1.0",
2323
"eslint-plugin-import": "^2.31.0",
24-
"eslint-plugin-react": "^7.37.2",
24+
"eslint-plugin-react": "^7.37.4",
2525
"eslint-plugin-react-hooks": "^5.1.0",
26-
"eslint-plugin-react-refresh": "^0.4.16",
26+
"eslint-plugin-react-refresh": "^0.4.18",
2727
"eslint-plugin-unused-imports": "^4.1.4",
28-
"globals": "^15.13.0",
29-
"npm-check-updates": "^17.1.11",
30-
"vite": "^6.0.3",
28+
"globals": "^15.14.0",
29+
"npm-check-updates": "^17.1.14",
30+
"vite": "^6.0.11",
3131
"vite-plugin-eslint": "^1.8.1"
3232
},
3333
"scripts": {

0 commit comments

Comments
 (0)