From 0be3e7dea644aa1b7fedc32f7f819377db5413e1 Mon Sep 17 00:00:00 2001 From: dgis Date: Sat, 13 Mar 2021 18:58:33 +0100 Subject: [PATCH] - Add the serial port support (via USB OTG). --- COPYING.TXT => LICENSE-GPL.TXT | 0 LICENSE-MIT.TXT | 22 + ReadMe.txt | 34 +- app/src/main/AndroidManifest.xml | 3 + app/src/main/assets/ReadMe.txt | 32 +- app/src/main/cpp/emu-jni.c | 121 +++++ app/src/main/cpp/win32-layer.c | 148 ++++- app/src/main/cpp/win32-layer.h | 142 +++-- .../org/emulator/calculator/NativeLib.java | 5 + .../java/org/emulator/calculator/Serial.java | 285 ++++++++++ .../java/org/emulator/calculator/Utils.java | 17 +- .../calculator/usbserial/CustomProber.java | 21 + .../usbserial/DevicesDialogFragment.java | 86 +++ .../calculator/usbserial/DevicesFragment.java | 221 ++++++++ .../usbserial/driver/CdcAcmSerialDriver.java | 323 +++++++++++ .../usbserial/driver/Ch34xSerialDriver.java | 374 +++++++++++++ .../usbserial/driver/CommonUsbSerialPort.java | 270 ++++++++++ .../usbserial/driver/Cp21xxSerialDriver.java | 335 ++++++++++++ .../usbserial/driver/FtdiSerialDriver.java | 428 +++++++++++++++ .../usbserial/driver/ProbeTable.java | 87 +++ .../driver/ProlificSerialDriver.java | 507 ++++++++++++++++++ .../calculator/usbserial/driver/UsbId.java | 67 +++ .../usbserial/driver/UsbSerialDriver.java | 33 ++ .../usbserial/driver/UsbSerialPort.java | 263 +++++++++ .../usbserial/driver/UsbSerialProber.java | 92 ++++ .../util/SerialInputOutputManager.java | 239 +++++++++ .../emulator/forty/eight/MainActivity.java | 97 +++- .../forty/eight/SettingsFragment.java | 42 +- .../main/res/layout/device_list_header.xml | 17 + app/src/main/res/layout/device_list_item.xml | 26 + app/src/main/res/layout/fragment_serial.xml | 35 ++ app/src/main/res/values/strings.xml | 21 +- app/src/main/res/xml/pref_general.xml | 13 + 33 files changed, 4294 insertions(+), 112 deletions(-) rename COPYING.TXT => LICENSE-GPL.TXT (100%) create mode 100644 LICENSE-MIT.TXT create mode 100644 app/src/main/java/org/emulator/calculator/Serial.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/CustomProber.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/DevicesDialogFragment.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/DevicesFragment.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/CdcAcmSerialDriver.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/Ch34xSerialDriver.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/CommonUsbSerialPort.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/Cp21xxSerialDriver.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/FtdiSerialDriver.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/ProbeTable.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/ProlificSerialDriver.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/UsbId.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialDriver.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialPort.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialProber.java create mode 100644 app/src/main/java/org/emulator/calculator/usbserial/util/SerialInputOutputManager.java create mode 100644 app/src/main/res/layout/device_list_header.xml create mode 100644 app/src/main/res/layout/device_list_item.xml create mode 100644 app/src/main/res/layout/fragment_serial.xml diff --git a/COPYING.TXT b/LICENSE-GPL.TXT similarity index 100% rename from COPYING.TXT rename to LICENSE-GPL.TXT diff --git a/LICENSE-MIT.TXT b/LICENSE-MIT.TXT new file mode 100644 index 0000000..98e65da --- /dev/null +++ b/LICENSE-MIT.TXT @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2011-2013 Google Inc. +Copyright (c) 2013 Mike Wakerly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ReadMe.txt b/ReadMe.txt index d791fdb..066e2db 100644 --- a/ReadMe.txt +++ b/ReadMe.txt @@ -30,26 +30,18 @@ NOTES And then, to use with the HP48SX or the HP48GX, you must select this generated file in the "Settings/Port2 File". - By default when you create a new HP49/50 with the embedded readonly file "rom.49g", everything that you store in port 2 is lost just because the file "rom.49g" is READONLY. - But it works exactly like with Windows. If you can write in the ROM file, - it should save the content of port 2 in the ROM file with Android too. - To save the port 2 in the HP49/50 with Emu48 for Android: - * copy "real50g-lc.kml", "real50g-lc.png", "keyb4950.kmi", "rom.49g" in a FOLDER of your Android device, - * in the menu: - - touch "New..." to create a new device - - or touch "Change KML Script..." to change the current KML script and ROM location - * select "[Custom KML script...]" - * select the FOLDER - * pick the calculator (which should be "Eric's Real 50g (Large Cropped)")! - And because, the file "FOLDER/rom.49g" is not readonly anymore, you can save your port 2. - BUT for the moment, it is saved ONLY when you CLOSE (or change) the state file. Not when you end the application. + Since version 2.0, it is now possible from the menu to manage the Flash ROM file (it will fully replaces the ROM). - To speed up printing, set the 'delay' to 0 in the calculator's print options. +- The serial ports, wire or infrared (infrared limited to 2400 baud) are now supported through the USB port in mode OTG. + It uses the library https://github.com/mik3y/usb-serial-for-android, which allows to plug most of the serial USB adapters (only tested with Prolific), + without the need to be root. If it is not automatic, please, activate the OTG mode in your Android device, and then, + you should be able to see it in the Emu48 settings. If the adapter is unplugged then plugged back in, you may need to set the option again. NOT WORKING YET - Disassembler - Debugger -- Serial Ports (Wire or Ir) LINKS @@ -63,6 +55,18 @@ LINKS CHANGES +Version 2.3 (2021-02-xx) + +- Add the serial port support (via USB OTG). + TODO: When stop the app, Serial exception seems to delay the save of the calc state!!!! + TODO: Inform if the connection is not possible. + TODO: What's happen if I hot unplug? + TODO: Check the self test about UART (http://regis.cosnier.free.fr/private/private.php?journal=HP48&index=-4637&nomenu) +- Allows pressing a calculator button with the right button of the mouse but prevents its release to allow the On+A+F key combination (with Android version >= 5.0). +- Update the embedded help file "Emu48.html" to the latest version. +- Open an external web browser when you click an external links in the Help. + + Version 2.2 (2020-12-09) - The KML folder is now well saved when changing the KML script for a custom one via the menu "Change KML Script...". @@ -226,17 +230,17 @@ You should have received a copy of the GNU General Public License along with thi Note: some included files are not covered by the GPL; these include ROM image files (copyrighted by HP), KML files and faceplate images (copyrighted by their authors). The Eric's Real scripts ("real*.kml" and "real*.bmp/png") are embedded in this application with the kind permission of Eric Rechlin. +Portions of this source code (about the usb-serial) were originally created by Google Inc. in 2011-2013 and Mike Wakerly in 2013. TODO - Add a Cancel button to the HP48 memory card creator dialog. - Show KML log on request. -- Serial port support (via USB OTG and maybe Bluetooth). - Manage the HP 48 port 2 with the same kind of interface for the memory card. - The render pixels are very nice. A solution to obtain uniform pixel size could be a preset (a multiplier, auto) so the user could decide and upscale/downscale (Michael P). - Somehow LEFT (Shift on the keyboard) + 7 activates the DIVIDE-key (z-Key)..., but with the NUM-Key it can make it work without problems... I think it might have something to do with the "/" sign on the Shifted-7-key. -- The clock seems unsynchronized sometimes (Michael P). +- The clock seems not synchronized sometimes (Michael P). - Retain a key by right clicking if it is from a mouse. - Sometimes, the calculator seems to lag and finally freeze. - In Chrome OS, sometimes there is no OK button in the KML Script Compilation Result. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 76a5cee..2ec4e10 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + + = 5.0). +- Update the embedded help file "Emu48.html" to the latest version. +- Open an external web browser when you click an external links in the Help. + + Version 2.2 (2020-12-09) - The KML folder is now well saved when changing the KML script for a custom one via the menu "Change KML Script...". @@ -225,3 +229,5 @@ You should have received a copy of the GNU General Public License along with thi Note: some included files are not covered by the GPL; these include ROM image files (copyrighted by HP), KML files and faceplate images (copyrighted by their authors). The Eric's Real scripts ("real*.kml" and "real*.bmp/png") are embedded in this application with the kind permission of Eric Rechlin. + +Portions of this source code (about the usb-serial) were originally created by Google Inc. in 2011-2013 and Mike Wakerly in 2013. diff --git a/app/src/main/cpp/emu-jni.c b/app/src/main/cpp/emu-jni.c index a286d7a..031bf61 100644 --- a/app/src/main/cpp/emu-jni.c +++ b/app/src/main/cpp/emu-jni.c @@ -291,6 +291,119 @@ void setKMLIcon(int imageWidth, int imageHeight, LPBYTE buffer, int bufferSize) } } +int openSerialPort(const TCHAR * serialPort) { + int result = -1; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "openSerialPort", "(Ljava/lang/String;)I"); + jstring utfFileURL = (*jniEnv)->NewStringUTF(jniEnv, serialPort); + result = (*jniEnv)->CallIntMethod(jniEnv, mainActivity, midStr, utfFileURL); + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return result; +} + +int closeSerialPort(int serialPortId) { + int result = -1; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "closeSerialPort", "(I)I"); + result = (*jniEnv)->CallIntMethod(jniEnv, mainActivity, midStr, serialPortId); + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return result; +} + +int setSerialPortParameters(int serialPortId, int baudRate) { + int result = -1; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "setSerialPortParameters", "(II)I"); + result = (*jniEnv)->CallIntMethod(jniEnv, mainActivity, midStr, serialPortId, baudRate); + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return result; +} + +int readSerialPort(int serialPortId, LPBYTE buffer, int nNumberOfBytesToRead) { + int nNumberOfReadBytes = 0; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv && buffer && nNumberOfBytesToRead > 0) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "readSerialPort", "(II)[B"); + jbyteArray readBytes = (jbyteArray)(*jniEnv)->CallObjectMethod(jniEnv, mainActivity, midStr, serialPortId, nNumberOfBytesToRead); + nNumberOfReadBytes = (*jniEnv)->GetArrayLength(jniEnv, readBytes); + jbyte* elements = (*jniEnv)->GetByteArrayElements(jniEnv, readBytes, NULL); + if (elements) { + for(int i = 0; i < nNumberOfReadBytes; i++) + buffer[i] = elements[i]; + (*jniEnv)->ReleaseByteArrayElements(jniEnv, readBytes, elements, JNI_ABORT); + } + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return nNumberOfReadBytes; +} + +int writeSerialPort(int serialPortId, LPBYTE buffer, int bufferSize) { + int result = 0; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "writeSerialPort", "(I[B)I"); + + jbyteArray javaBuffer = NULL; + if(buffer) { + javaBuffer = (*jniEnv)->NewByteArray(jniEnv, bufferSize); + (*jniEnv)->SetByteArrayRegion(jniEnv, javaBuffer, 0, bufferSize, (jbyte *) buffer); + } + result = (*jniEnv)->CallIntMethod(jniEnv, mainActivity, midStr, serialPortId, javaBuffer); + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return result; +} + +int serialPortSetBreak(int serialPortId) { + int result = 0; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "serialPortSetBreak", "(I)I"); + result = (*jniEnv)->CallIntMethod(jniEnv, mainActivity, midStr, serialPortId); + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return result; +} + +int serialPortClearBreak(int serialPortId) { + int result = 0; + JNIEnv *jniEnv = getJNIEnvironment(); + if(jniEnv) { + jclass mainActivityClass = (*jniEnv)->GetObjectClass(jniEnv, mainActivity); + if(mainActivityClass) { + jmethodID midStr = (*jniEnv)->GetMethodID(jniEnv, mainActivityClass, "serialPortClearBreak", "(I)I"); + result = (*jniEnv)->CallIntMethod(jniEnv, mainActivity, midStr, serialPortId); + (*jniEnv)->DeleteLocalRef(jniEnv, mainActivityClass); + } + } + return result; +} + + JNIEXPORT void JNICALL Java_org_emulator_calculator_NativeLib_start(JNIEnv *env, jobject thisz, jobject assetMgr, jobject activity) { chooseCurrentKmlMode = ChooseKmlMode_UNKNOWN; @@ -1169,6 +1282,10 @@ JNIEXPORT void JNICALL Java_org_emulator_calculator_NativeLib_setConfiguration(J //LOGE("NativeLib_setConfiguration port2 end SwitchToState %d", nOldState); SwitchToState(nOldState); } + } else if(_tcscmp(_T("settings_serial_ports_wire"), configKey) == 0) { + _tcsncpy(szSerialWire, _tcscmp(_T("0,0"), configStringValue) == 0 ? NO_SERIAL : configStringValue, sizeof(szSerialWire)); + } else if(_tcscmp(_T("settings_serial_ports_ir"), configKey) == 0) { + _tcsncpy(szSerialIr, _tcscmp(_T("0,0"), configStringValue) == 0 ? NO_SERIAL : configStringValue, sizeof(szSerialIr)); } if(configKey) @@ -1210,4 +1327,8 @@ JNIEXPORT jint JNICALL Java_org_emulator_calculator_NativeLib_getLCDBackgroundCo return palPalEntry[0].peRed << 16 | palPalEntry[0].peGreen << 8 | palPalEntry[0].peBlue; } return -1; +} + +JNIEXPORT void JNICALL Java_org_emulator_calculator_NativeLib_commEvent(JNIEnv *env, jclass clazz, jint commId, jint eventMask) { + commEvent(commId, eventMask); } \ No newline at end of file diff --git a/app/src/main/cpp/win32-layer.c b/app/src/main/cpp/win32-layer.c index f2e7beb..26d65bc 100644 --- a/app/src/main/cpp/win32-layer.c +++ b/app/src/main/cpp/win32-layer.c @@ -42,6 +42,8 @@ const TCHAR * contentScheme = _T("content://"); size_t contentSchemeLength; const TCHAR * documentScheme = _T("document:"); size_t documentSchemeLength; +const TCHAR * comPrefix = _T("\\\\.\\"); +size_t comPrefixLength; TCHAR szFilePathTmp[MAX_PATH]; @@ -66,6 +68,7 @@ void win32Init() { assetsPrefixLength = _tcslen(assetsPrefix); contentSchemeLength = _tcslen(contentScheme); documentSchemeLength = _tcslen(documentScheme); + comPrefixLength = _tcslen(comPrefix); } int abs (int i) { @@ -109,8 +112,42 @@ BOOL SetCurrentDirectory(LPCTSTR path) { extern BOOL settingsPort2en; extern BOOL settingsPort2wr; +#define MAX_CREATED_COMM 30 +static HANDLE comms[MAX_CREATED_COMM]; +static pthread_mutex_t commsLock; + + HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPVOID lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, LPVOID hTemplateFile) { FILE_LOGD("CreateFile(lpFileName: \"%s\", dwDesiredAccess: 0x%08x)", lpFileName, dwShareMode); + + BOOL foundCOMPrefix = _tcsncmp(lpFileName, comPrefix, comPrefixLength) == 0; + if(foundCOMPrefix) { + + int serialPortId = openSerialPort(lpFileName); + if(serialPortId > 0) { + // We try to open a COM/Serial port + HANDLE handle = malloc(sizeof(struct _HANDLE)); + memset(handle, 0, sizeof(struct _HANDLE)); + handle->handleType = HANDLE_TYPE_COM; + handle->commEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + handle->commId = serialPortId; + + pthread_mutex_lock(&commsLock); + handle->commIndex = -1; + for(int i = 0; i < MAX_CREATED_COMM; i++) { + if(comms[i] == NULL) { + handle->commIndex = i; + comms[handle->commIndex] = handle; + break; + } + } + pthread_mutex_unlock(&commsLock); + + return handle; + } else + return (HANDLE) INVALID_HANDLE_VALUE; + } + BOOL forceNormalFile = FALSE; securityExceptionOccured = FALSE; #if EMUXX == 48 @@ -249,6 +286,8 @@ BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD readByteCount = (DWORD) read(hFile->fileDescriptor, lpBuffer, nNumberOfBytesToRead); } else if(hFile->handleType == HANDLE_TYPE_FILE_ASSET) { readByteCount = (DWORD) AAsset_read(hFile->fileAsset, lpBuffer, nNumberOfBytesToRead); + } else if(hFile->handleType == HANDLE_TYPE_COM) { + readByteCount = (DWORD) readSerialPort(hFile->commId, lpBuffer, nNumberOfBytesToRead); } if(lpNumberOfBytesRead) *lpNumberOfBytesRead = readByteCount; @@ -257,12 +296,19 @@ BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD BOOL WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite,LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped) { FILE_LOGD("WriteFile(hFile: %p, lpBuffer: 0x%08x, nNumberOfBytesToWrite: %d)", hFile, lpBuffer, nNumberOfBytesToWrite); - if(hFile->handleType == HANDLE_TYPE_FILE_ASSET) - return FALSE; - ssize_t writenByteCount = write(hFile->fileDescriptor, lpBuffer, nNumberOfBytesToWrite); - if(lpNumberOfBytesWritten) - *lpNumberOfBytesWritten = (DWORD) writenByteCount; - return writenByteCount >= 0; + if(hFile->handleType == HANDLE_TYPE_FILE) { + ssize_t writenByteCount = write(hFile->fileDescriptor, lpBuffer, nNumberOfBytesToWrite); + if(lpNumberOfBytesWritten) + *lpNumberOfBytesWritten = (DWORD) writenByteCount; + return writenByteCount >= 0; + } else if(hFile->handleType == HANDLE_TYPE_COM) { + ssize_t writenByteCount = writeSerialPort(hFile->commId, lpBuffer, nNumberOfBytesToWrite); + commEvent(hFile->commId, EV_TXEMPTY); // Not sure about that (not the same thread)! + if(lpNumberOfBytesWritten) + *lpNumberOfBytesWritten = (DWORD) writenByteCount; + return writenByteCount >= 0; + } + return FALSE; } DWORD SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod) { @@ -472,8 +518,7 @@ BOOL SetEvent(HANDLE hEvent) { return 0; } -BOOL ResetEvent(HANDLE hEvent) -{ +BOOL ResetEvent(HANDLE hEvent) { if(hEvent) { int result = pthread_mutex_lock(&hEvent->eventMutex); _ASSERT(result == 0); @@ -488,8 +533,7 @@ BOOL ResetEvent(HANDLE hEvent) return FALSE; } -int UnlockedWaitForEvent(HANDLE hHandle, uint64_t milliseconds) -{ +int UnlockedWaitForEvent(HANDLE hHandle, uint64_t milliseconds) { int result = 0; if (!hHandle->eventState) { @@ -670,6 +714,8 @@ DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) BOOL WINAPI CloseHandle(HANDLE hObject) { FILE_LOGD("CloseHandle(hObject: %p)", hObject); + if(hObject) + return FALSE; //https://msdn.microsoft.com/en-us/9b84891d-62ca-4ddc-97b7-c4c79482abd9 // Can be a thread/event/file handle! switch(hObject->handleType) { @@ -738,6 +784,29 @@ BOOL WINAPI CloseHandle(HANDLE hObject) { hObject->threadParameter = NULL; free(hObject); return TRUE; + case HANDLE_TYPE_COM: { + FILE_LOGD("CloseHandle() HANDLE_TYPE_COM"); + + closeSerialPort(hObject->commId); + hObject->commId = 0; + + hObject->handleType = HANDLE_TYPE_INVALID; + if(hObject->commState) { + free(hObject->commState); + hObject->commState = NULL; + } + if(hObject->commEvent) { + CloseHandle(hObject->commEvent); + hObject->commEvent = NULL; + } + if(hObject->commIndex != -1) { + pthread_mutex_lock(&commsLock); + comms[hObject->commIndex] = NULL; + pthread_mutex_unlock(&commsLock); + } + free(hObject); + return TRUE; + } default: break; } @@ -2971,8 +3040,36 @@ BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpNumb //TODO return FALSE; } + +// This is not a win32 API +void commEvent(int commId, int eventMask) { + HANDLE commHandleFound = NULL; + pthread_mutex_lock(&commsLock); + for(int i = 0; i < MAX_CREATED_COMM; i++) { + HANDLE commHandle = comms[i]; + if (commHandle && commHandle->commId == commId) { + commHandleFound = commHandle; + break; + } + } + pthread_mutex_unlock(&commsLock); + if(commHandleFound) { + commHandleFound->commEventMask = eventMask; + SetEvent(commHandleFound->commEvent); + } +} + BOOL WaitCommEvent(HANDLE hFile, LPDWORD lpEvtMask, LPOVERLAPPED lpOverlapped) { - //TODO + if(hFile && hFile->handleType == HANDLE_TYPE_COM) { + WaitForSingleObject(hFile->commEvent, INFINITE); + pthread_mutex_lock(&commsLock); + if(lpEvtMask && hFile->commEventMask) { + *lpEvtMask = hFile->commEventMask; //EV_RXCHAR | EV_TXEMPTY | EV_ERR + hFile->commEventMask = 0; + } + pthread_mutex_unlock(&commsLock); + return TRUE; + } return FALSE; } BOOL ClearCommError(HANDLE hFile, LPDWORD lpErrors, LPCOMSTAT lpStat) { @@ -2985,10 +3082,26 @@ BOOL SetCommTimeouts(HANDLE hFile, LPCOMMTIMEOUTS lpCommTimeouts) { } BOOL SetCommMask(HANDLE hFile, DWORD dwEvtMask) { //TODO + if(hFile && hFile->handleType == HANDLE_TYPE_COM) { + if(dwEvtMask == 0) { + // When 0, clear all events and force WaitCommEvent to return. + SetEvent(hFile->commEvent); + return TRUE; + } + } return FALSE; } BOOL SetCommState(HANDLE hFile, LPDCB lpDCB) { - //TODO + if(hFile && hFile->handleType == HANDLE_TYPE_COM && lpDCB) { + int result = setSerialPortParameters(hFile->commId, lpDCB->BaudRate); //TODO 2 stop bits? + if(result > 0) { + if (hFile->commState) + free(hFile->commState); + hFile->commState = malloc(sizeof(struct _DCB)); + memcpy(hFile->commState, lpDCB, sizeof(struct _DCB)); + } + return TRUE; + } return FALSE; } BOOL PurgeComm(HANDLE hFile, DWORD dwFlags) { @@ -2996,14 +3109,17 @@ BOOL PurgeComm(HANDLE hFile, DWORD dwFlags) { return FALSE; } BOOL SetCommBreak(HANDLE hFile) { - //TODO - return FALSE; + if(hFile && hFile->handleType == HANDLE_TYPE_COM) + return serialPortSetBreak(hFile->commId); + return FALSE; } BOOL ClearCommBreak(HANDLE hFile) { - //TODO - return FALSE; + if(hFile && hFile->handleType == HANDLE_TYPE_COM) + return serialPortClearBreak(hFile->commId); + return FALSE; } + int WSAGetLastError() { //TODO // Win9x break with WSAEINTR (a blocking socket call was canceled) diff --git a/app/src/main/cpp/win32-layer.h b/app/src/main/cpp/win32-layer.h index dba0f3d..bdb095a 100644 --- a/app/src/main/cpp/win32-layer.h +++ b/app/src/main/cpp/win32-layer.h @@ -475,6 +475,38 @@ struct _HDC { int windowOriginY; }; +// Comm + +typedef struct _DCB { + DWORD DCBlength; + DWORD BaudRate; + DWORD fBinary: 1; + DWORD fParity: 1; + DWORD fOutxCtsFlow:1; + DWORD fOutxDsrFlow:1; + DWORD fDtrControl:2; + DWORD fDsrSensitivity:1; + DWORD fTXContinueOnXoff: 1; + DWORD fOutX: 1; + DWORD fInX: 1; + DWORD fErrorChar: 1; + DWORD fNull: 1; + DWORD fRtsControl:2; + DWORD fAbortOnError:1; + DWORD fDummy2:17; + WORD wReserved; + WORD XonLim; + WORD XoffLim; + BYTE ByteSize; + BYTE Parity; + BYTE StopBits; + char XonChar; + char XoffChar; + char ErrorChar; + char EofChar; + char EvtChar; + WORD wReserved1; +} DCB, *LPDCB; // Handle @@ -511,41 +543,62 @@ enum HANDLE_TYPE { HANDLE_TYPE_THREAD, HANDLE_TYPE_WINDOW, HANDLE_TYPE_ICON, + HANDLE_TYPE_COM, }; struct _HANDLE { enum HANDLE_TYPE handleType; + union { + struct { + // HANDLE_TYPE_FILE* + int fileDescriptor; + BOOL fileOpenFileFromContentResolver; - // HANDLE_TYPE_FILE* - int fileDescriptor; - BOOL fileOpenFileFromContentResolver; + AAsset* fileAsset; - AAsset* fileAsset; + // HANDLE_TYPE_FILE_MAPPING* + off_t fileMappingOffset; + size_t fileMappingSize; + void* fileMappingAddress; + DWORD fileMappingProtect; + }; - // HANDLE_TYPE_FILE_MAPPING* - off_t fileMappingOffset; - size_t fileMappingSize; - void* fileMappingAddress; - DWORD fileMappingProtect; + struct { + // HANDLE_TYPE_THREAD + pthread_t threadId; + DWORD (*threadStartAddress)(LPVOID); + LPVOID threadParameter; + struct _HANDLE * threadEventMessage; + struct tagMSG threadMessage; + int threadIndex; + }; - // HANDLE_TYPE_THREAD - pthread_t threadId; - DWORD (*threadStartAddress)(LPVOID); - LPVOID threadParameter; - struct _HANDLE * threadEventMessage; - struct tagMSG threadMessage; - int threadIndex; + struct { + // HANDLE_TYPE_EVENT + pthread_cond_t eventCVariable; + pthread_mutex_t eventMutex; + BOOL eventAutoReset; + BOOL eventState; + }; - // HANDLE_TYPE_EVENT - pthread_cond_t eventCVariable; - pthread_mutex_t eventMutex; - BOOL eventAutoReset; - BOOL eventState; + struct { + // HANDLE_TYPE_WINDOW + HDC windowDC; + }; - // HANDLE_TYPE_WINDOW - HDC windowDC; + struct { + // HANDLE_TYPE_ICON + HBITMAP icon; + }; - // HANDLE_TYPE_ICON - HBITMAP icon; + struct { + // HANDLE_TYPE_COM + LPDCB commState; + struct _HANDLE * commEvent; + int commIndex; + int commId; + int commEventMask; + }; + }; }; typedef struct _HANDLE * HANDLE; @@ -1214,6 +1267,13 @@ extern void mainViewResizeCallback(int x, int y); extern int openFileFromContentResolver(const TCHAR * fileURL, int writeAccess); extern int openFileInFolderFromContentResolver(const TCHAR * filename, const TCHAR * folderURL, int writeAccess); extern int closeFileFromContentResolver(int fd); +extern int openSerialPort(const TCHAR * serialPort); +extern int closeSerialPort(int serialPortId); +extern int setSerialPortParameters(int serialPortId, int baudRate); +extern int readSerialPort(int serialPortId, LPBYTE buffer, int nNumberOfBytesToRead); +extern int writeSerialPort(int serialPortId, LPBYTE buffer, int bufferSize); +extern int serialPortSetBreak(int serialPortId); +extern int serialPortClearBreak(int serialPortId); extern int showAlert(const TCHAR * messageText, int flags); extern void sendMenuItemCommand(int menuItem); extern TCHAR szCurrentKml[MAX_PATH]; @@ -1292,38 +1352,8 @@ typedef struct _COMMTIMEOUTS { DWORD WriteTotalTimeoutConstant; } COMMTIMEOUTS,*LPCOMMTIMEOUTS; -typedef struct _DCB { - DWORD DCBlength; - DWORD BaudRate; - DWORD fBinary: 1; - DWORD fParity: 1; - DWORD fOutxCtsFlow:1; - DWORD fOutxDsrFlow:1; - DWORD fDtrControl:2; - DWORD fDsrSensitivity:1; - DWORD fTXContinueOnXoff: 1; - DWORD fOutX: 1; - DWORD fInX: 1; - DWORD fErrorChar: 1; - DWORD fNull: 1; - DWORD fRtsControl:2; - DWORD fAbortOnError:1; - DWORD fDummy2:17; - WORD wReserved; - WORD XonLim; - WORD XoffLim; - BYTE ByteSize; - BYTE Parity; - BYTE StopBits; - char XonChar; - char XoffChar; - char ErrorChar; - char EofChar; - char EvtChar; - WORD wReserved1; -} DCB, *LPDCB; - extern BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpNumberOfBytesTransferred, BOOL bWait); +extern void commEvent(int commId, int eventMask); extern BOOL WaitCommEvent(HANDLE hFile, LPDWORD lpEvtMask, LPOVERLAPPED lpOverlapped); extern BOOL ClearCommError(HANDLE hFile, LPDWORD lpErrors, LPCOMSTAT lpStat); extern BOOL SetCommTimeouts(HANDLE hFile, LPCOMMTIMEOUTS lpCommTimeouts); diff --git a/app/src/main/java/org/emulator/calculator/NativeLib.java b/app/src/main/java/org/emulator/calculator/NativeLib.java index f55a199..a563333 100644 --- a/app/src/main/java/org/emulator/calculator/NativeLib.java +++ b/app/src/main/java/org/emulator/calculator/NativeLib.java @@ -90,4 +90,9 @@ public class NativeLib { public static native int getScreenWidthNative(); public static native int getScreenHeightNative(); public static native int getLCDBackgroundColor(); + + static final int EV_RXCHAR = 0x0001; + static final int EV_TXEMPTY = 0x0004; + static final int EV_ERR = 0x0080; + public static native void commEvent(int commId, int eventMask); } diff --git a/app/src/main/java/org/emulator/calculator/Serial.java b/app/src/main/java/org/emulator/calculator/Serial.java new file mode 100644 index 0000000..ec88a4f --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/Serial.java @@ -0,0 +1,285 @@ +package org.emulator.calculator; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import org.emulator.calculator.usbserial.CustomProber; +import org.emulator.calculator.usbserial.driver.UsbSerialDriver; +import org.emulator.calculator.usbserial.driver.UsbSerialPort; +import org.emulator.calculator.usbserial.driver.UsbSerialProber; +import org.emulator.calculator.usbserial.util.SerialInputOutputManager; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Serial { + + private static final String TAG = "Serial"; + private final boolean debug = true; + + private final Context context; + private final int serialPortId; + private static final String INTENT_ACTION_GRANT_USB = "EMU48.GRANT_USB"; + private static final int WRITE_WAIT_MILLIS = 2000; + + private final Handler mainLooper; + private SerialInputOutputManager usbIoManager; + private UsbSerialPort usbSerialPort; + private enum UsbPermission { Unknown, Requested, Granted, Denied } + private UsbPermission usbPermission = UsbPermission.Unknown; + private boolean connected = false; + private String connectionStatus = ""; + + private final ArrayDeque reveivedByteQueue = new ArrayDeque<>(); + + + public Serial(Context context, int serialPortId) { + this.context = context; + this.serialPortId = serialPortId; + + // Will connect at the next try! + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && Objects.equals(intent.getAction(), INTENT_ACTION_GRANT_USB)) { + usbPermission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) + ? UsbPermission.Granted : UsbPermission.Denied; + // Will connect at the next try! + } + } + }; + mainLooper = new Handler(Looper.getMainLooper()); + } + + public boolean connect(String serialPort) { + if(debug) Log.d(TAG, "connect( " + serialPort + ")"); + + Pattern patternSerialPort = Pattern.compile("\\\\.\\\\(\\d+),(\\d+)"); + Matcher m = patternSerialPort.matcher(serialPort); + if (m.find()) { + String deviceText = m.group(1); + String portText = m.group(2); + int deviceId = 0; + try { + deviceId = Integer.parseInt(deviceText); + } catch (NumberFormatException ex) { + // Catch bad number format + } + int portNum = 0; + try { + portNum = Integer.parseInt(portText); + } catch (NumberFormatException ex) { + // Catch bad number format + } + return connect(deviceId, portNum); + } + return false; + } + + public String getConnectionStatus() { + return connectionStatus; + } + + public boolean connect(int deviceId, int portNum) { + if(debug) Log.d(TAG, "connect(deviceId: " + deviceId + ", portNum" + portNum + ")"); + + UsbDevice device = null; + UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + for(UsbDevice v : usbManager.getDeviceList().values()) + if(v.getDeviceId() == deviceId) + device = v; + if(device == null) { + connectionStatus = "serial_connection_failed_device_not_found"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus); + return false; + } + UsbSerialDriver driver = UsbSerialProber.getDefaultProber().probeDevice(device); + if(driver == null) { + driver = CustomProber.getCustomProber().probeDevice(device); + } + if(driver == null) { + connectionStatus = "serial_connection_failed_no_driver_for_device"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus); + return false; + } + if(driver.getPorts().size() < portNum) { + connectionStatus = "serial_connection_failed_not_enough_ports_at_device"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus); + return false; + } + usbSerialPort = driver.getPorts().get(portNum); + UsbDeviceConnection usbConnection = null; + try { + usbConnection = usbManager.openDevice(driver.getDevice()); + } catch (SecurityException e) { + connectionStatus = "serial_connection_failed_user_has_not_given_permission"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus + ", " + e.getMessage()); + return false; + } catch (Exception e) { + connectionStatus = "serial_connection_failed_for_unknown_reason"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus + ", " + e.getMessage()); + return false; + } + if(usbConnection == null && usbPermission == UsbPermission.Unknown && !usbManager.hasPermission(driver.getDevice())) { + usbPermission = UsbPermission.Requested; + PendingIntent usbPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(INTENT_ACTION_GRANT_USB), 0); + usbManager.requestPermission(driver.getDevice(), usbPermissionIntent); + if(debug) Log.d(TAG, "Request permission"); + return false; + } + if(usbConnection == null) { + if (!usbManager.hasPermission(driver.getDevice())) + connectionStatus = "serial_connection_failed_permission_denied"; + else + connectionStatus = "serial_connection_failed_open_device_failed"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus); + return false; + } + + try { + usbSerialPort.open(usbConnection); + usbIoManager = new SerialInputOutputManager(usbSerialPort, new SerialInputOutputManager.Listener() { + @Override + public void onNewData(byte[] data) { + if(debug) Log.d(TAG, "onNewData: " + Utils.bytesToHex(data)); + boolean wasEmpty = reveivedByteQueue.isEmpty(); + synchronized (reveivedByteQueue) { + for (byte datum : data) + reveivedByteQueue.add(datum); + } + if (wasEmpty) + onReceivedByteQueueNotEmpty(); + } + + @Override + public void onRunError(Exception e) { + onReceivedError(e); + } + }); + Thread usbIo = new Thread(usbIoManager); + usbIo.setDaemon(true); + usbIo.start(); + connected = true; + return true; + } catch (Exception e) { + connectionStatus = "serial_connection_failed_open_failed"; + if(debug) Log.d(TAG, "connectionStatus = " + connectionStatus + ", " + e.getMessage()); + disconnect(); + } + if(debug) Log.d(TAG, "connected!"); + connectionStatus = ""; + return false; + } + + private void onReceivedByteQueueNotEmpty() { + NativeLib.commEvent(serialPortId, NativeLib.EV_RXCHAR); + } + + + private void onReceivedError(Exception e) { + mainLooper.post(() -> { + if(debug) { + Log.d(TAG, "onRunError: " + e.getMessage()); + e.printStackTrace(); + } + disconnect(); + }); + } + + public boolean setParameters(int baudRate) { + return setParameters(baudRate, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); + } + + public boolean setParameters(int baudRate, int dataBits, int stopBits, int parity) { + if(debug) Log.d(TAG, "setParameters(baudRate: " + baudRate + ", dataBits: " + dataBits + ", stopBits: " + stopBits + ", parity: " + parity +")"); + + try { + usbSerialPort.setParameters(baudRate, dataBits, stopBits, parity); + return true; + } catch (Exception e) { + if(debug) Log.d(TAG, "setParameters() failed: " + e.getMessage()); + } + return false; + } + + public byte[] receive(int nNumberOfBytesToRead) { + byte[] result; + synchronized (reveivedByteQueue) { + int nNumberOfReadBytes = Math.min(nNumberOfBytesToRead, reveivedByteQueue.size()); + result = new byte[nNumberOfReadBytes]; + for (int i = 0; i < nNumberOfReadBytes; i++) { + Byte byteRead = reveivedByteQueue.poll(); + if(byteRead != null) + result[i] = byteRead; + } + } + if(result == null) { + result = new byte[0]; + } else if(reveivedByteQueue.size() > 0) { + mainLooper.post(() -> NativeLib.commEvent(serialPortId, NativeLib.EV_RXCHAR)); + } + return result; + } + + public int send(byte[] data) { + if(!connected) + return 0; + + try { + return usbSerialPort.write(data, WRITE_WAIT_MILLIS); + } catch (Exception e) { + onReceivedError(e); + } + return 0; + } + + public int setBreak() { + if(!connected) + return 0; + + try { + usbSerialPort.setBreak(true); + return 1; + } catch (Exception e) { + // Exception mean return 0 + } + return 0; + } + + public int clearBreak() { + if(!connected) + return 0; + + try { + usbSerialPort.setBreak(false); + return 1; + } catch (Exception e) { + // Exception mean return 0 + } + return 0; + } + + public void disconnect() { + if(debug) Log.d(TAG, "disconnect()"); + + connected = false; + if(usbIoManager != null) + usbIoManager.stop(); + usbIoManager = null; + try { + usbSerialPort.close(); + } catch (IOException ignored) {} + usbSerialPort = null; + } +} diff --git a/app/src/main/java/org/emulator/calculator/Utils.java b/app/src/main/java/org/emulator/calculator/Utils.java index 193158e..9e2868d 100644 --- a/app/src/main/java/org/emulator/calculator/Utils.java +++ b/app/src/main/java/org/emulator/calculator/Utils.java @@ -56,7 +56,7 @@ public class Utils { toast.show(); } - static int resId(Context context, String resourceName, String variableName) { + public static int resId(Context context, String resourceName, String variableName) { try { return context.getResources().getIdentifier(variableName, resourceName, context.getApplicationContext().getPackageName()); } catch (Exception e) { @@ -91,9 +91,9 @@ public class Utils { public static void colorizeDrawableWithColor(Context context, Drawable icon, int colorAttribute) { if(icon != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) - icon.setColorFilter(new BlendModeColorFilter(Utils.getThemedColor(context, colorAttribute), BlendMode.SRC_ATOP)); + icon.setColorFilter(new BlendModeColorFilter(getThemedColor(context, colorAttribute), BlendMode.SRC_ATOP)); else - icon.setColorFilter(Utils.getThemedColor(context, colorAttribute), PorterDuff.Mode.SRC_ATOP); + icon.setColorFilter(getThemedColor(context, colorAttribute), PorterDuff.Mode.SRC_ATOP); } } @@ -213,4 +213,15 @@ public class Utils { vibrator.vibrate(durationInMilliSecond); } } + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } } diff --git a/app/src/main/java/org/emulator/calculator/usbserial/CustomProber.java b/app/src/main/java/org/emulator/calculator/usbserial/CustomProber.java new file mode 100644 index 0000000..8916dd2 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/CustomProber.java @@ -0,0 +1,21 @@ +package org.emulator.calculator.usbserial; + +import org.emulator.calculator.usbserial.driver.CdcAcmSerialDriver; +import org.emulator.calculator.usbserial.driver.ProbeTable; +import org.emulator.calculator.usbserial.driver.UsbSerialProber; + +/** + * add devices here, that are not known to DefaultProber + * + * if the App should auto start for these devices, also + * add IDs to app/src/main/res/xml/device_filter.xml + */ +public class CustomProber { + + public static UsbSerialProber getCustomProber() { + ProbeTable customTable = new ProbeTable(); + customTable.addProduct(0x16d0, 0x087e, CdcAcmSerialDriver.class); // e.g. Digispark CDC + return new UsbSerialProber(customTable); + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/DevicesDialogFragment.java b/app/src/main/java/org/emulator/calculator/usbserial/DevicesDialogFragment.java new file mode 100644 index 0000000..54c42c5 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/DevicesDialogFragment.java @@ -0,0 +1,86 @@ +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +package org.emulator.calculator.usbserial; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.appcompat.widget.Toolbar; + +import org.emulator.calculator.Utils; + +public class DevicesDialogFragment extends AppCompatDialogFragment { + + private DevicesFragment devicesFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(AppCompatDialogFragment.STYLE_NO_FRAME, Utils.resId(this, "style", "AppTheme")); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + String title = getString(Utils.resId(this, "string", "serial_fragment_title")); + Dialog dialog = getDialog(); + if(dialog != null) + dialog.setTitle(title); + + View view = inflater.inflate(Utils.resId(this, "layout", "fragment_serial"), container, false); + + // Toolbar + + Toolbar toolbar = view.findViewById(Utils.resId(this, "id", "toolbar")); + toolbar.setTitle(title); + Utils.colorizeDrawableWithColor(requireContext(), toolbar.getNavigationIcon(), android.R.attr.colorForeground); + toolbar.setNavigationOnClickListener(v -> dismiss()); + + devicesFragment = new DevicesFragment(); + devicesFragment.setOnSerialDeviceClickedListener((serialConnectParameters) -> { + if(onSerialDeviceClickedListener != null) + onSerialDeviceClickedListener.onSerialDeviceClicked(serialConnectParameters); + dismiss(); + }); + getChildFragmentManager().beginTransaction().add(Utils.resId(this, "id", "serialport_devices_fragment"), devicesFragment, "devices").commit(); + + + return view; + } + + private DevicesFragment.OnSerialDeviceClickedListener onSerialDeviceClickedListener; + + /** + * Register a callback to be invoked when a serial device has been chosen. + * @param onSerialDeviceClickedListener The callback that will run + */ + public void setOnSerialDeviceClickedListener(DevicesFragment.OnSerialDeviceClickedListener onSerialDeviceClickedListener) { + this.onSerialDeviceClickedListener = onSerialDeviceClickedListener; + } +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/DevicesFragment.java b/app/src/main/java/org/emulator/calculator/usbserial/DevicesFragment.java new file mode 100644 index 0000000..bd8b41d --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/DevicesFragment.java @@ -0,0 +1,221 @@ +package org.emulator.calculator.usbserial; + +import android.content.Context; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.ListFragment; + +import org.emulator.calculator.Utils; +import org.emulator.calculator.usbserial.driver.UsbSerialDriver; +import org.emulator.calculator.usbserial.driver.UsbSerialProber; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DevicesFragment extends ListFragment { + + static class ListItem { + UsbDevice device; + int port; + UsbSerialDriver driver; + + ListItem(UsbDevice device, int port, UsbSerialDriver driver) { + this.device = device; + this.port = port; + this.driver = driver; + } + } + + private final ArrayList listItems = new ArrayList<>(); + private ArrayAdapter listAdapter; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + listAdapter = new ArrayAdapter(getActivity(), 0, listItems) { + @NonNull + @Override + public View getView(int position, View view, @NonNull ViewGroup parent) { + ListItem item = listItems.get(position); + if (view == null) + view = getActivity().getLayoutInflater().inflate(Utils.resId(DevicesFragment.this, "layout", "device_list_item"), parent, false); + TextView text1 = view.findViewById(Utils.resId(DevicesFragment.this, "id", "text1")); + TextView text2 = view.findViewById(Utils.resId(DevicesFragment.this, "id", "text2")); + if(item.driver == null) + text1.setText(getString(Utils.resId(DevicesFragment.this, "string", "serial_ports_device_no_driver"))); + else { + String deviceName = item.driver.getClass().getSimpleName().replace("SerialDriver",""); + if(item.driver.getPorts().size() == 1) + text1.setText(deviceName); + else + text1.setText(String.format(Locale.US, getString(Utils.resId(DevicesFragment.this, "string", "serial_ports_device_item_title")), deviceName, item.port)); + } + text2.setText(String.format(Locale.US, getString(Utils.resId(DevicesFragment.this, "string", "serial_ports_device_item_description")), item.device.getVendorId(), item.device.getProductId())); + return view; + } + }; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setListAdapter(null); + View header = getActivity().getLayoutInflater().inflate(Utils.resId(DevicesFragment.this, "layout", "device_list_header"), null, false); + getListView().addHeaderView(header, null, false); + setEmptyText(getString(Utils.resId(this, "string", "serial_no_device"))); + ((TextView) getListView().getEmptyView()).setTextSize(18); + setListAdapter(listAdapter); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + } + + public void refresh() { + UsbManager usbManager = (UsbManager) getActivity().getSystemService(Context.USB_SERVICE); + UsbSerialProber usbDefaultProber = UsbSerialProber.getDefaultProber(); + UsbSerialProber usbCustomProber = CustomProber.getCustomProber(); + listItems.clear(); + for(UsbDevice device : usbManager.getDeviceList().values()) { + UsbSerialDriver driver = usbDefaultProber.probeDevice(device); + if(driver == null) { + driver = usbCustomProber.probeDevice(device); + } + if(driver != null) { + for(int port = 0; port < driver.getPorts().size(); port++) + listItems.add(new ListItem(device, port, driver)); + } else { + listItems.add(new ListItem(device, 0, null)); + } + } + listAdapter.notifyDataSetChanged(); + } + + public static class SerialConnectParameters { + public int deviceId; + public int port; + public int vendorId; + public int productId; + public String modelName; + + public SerialConnectParameters() { + this.deviceId = 0; + this.port = 0; + this.vendorId = 0; + this.productId = 0; + this.modelName = ""; + } + public SerialConnectParameters(int device, int port, int vendorId, int productId, String modelName) { + this.deviceId = device; + this.port = port; + this.vendorId = vendorId; + this.productId = productId; + this.modelName = modelName; + } + + public String toSettingsString() { + return String.format(Locale.US, "%d,%d,%d,%d,%s", deviceId, port, vendorId, productId, modelName); + } + + public String toWin32String() { + return String.format(Locale.US, "%d,%d", deviceId, port); + } + + public String toDisplayString(Context context) { + if(deviceId == 0 && port == 0) + return context.getResources().getString(Utils.resId(context, "string", "serial_ports_device_no_driver")); + else + return String.format(Locale.US, context.getResources().getString(Utils.resId(context, "string", "serial_ports_device")), modelName, vendorId, productId, deviceId, port); + } + + public static SerialConnectParameters fromSettingsString(String serialPorts) { + SerialConnectParameters serialConnectParameters = new SerialConnectParameters(); + serialConnectParameters.fromSettingsStringExtractor(serialPorts); + return serialConnectParameters; + } + + private void fromSettingsStringExtractor(String serialPorts) { + Pattern patternSerialPort = Pattern.compile("(\\d+),(\\d+),(\\d+),(\\d+),([^,]+)"); + Matcher m = patternSerialPort.matcher(serialPorts); + if (m.find()) { + String deviceText = m.group(1); + String portText = m.group(2); + String vendorIdText = m.group(3); + String productIdText = m.group(4); + modelName = m.group(5); + try { + deviceId = Integer.parseInt(deviceText); + } catch (NumberFormatException ex) { + // Catch bad number format + } + try { + port = Integer.parseInt(portText); + } catch (NumberFormatException ex) { + // Catch bad number format + } + try { + vendorId = Integer.parseInt(vendorIdText); + } catch (NumberFormatException ex) { + // Catch bad number format + } + try { + productId = Integer.parseInt(productIdText); + } catch (NumberFormatException ex) { + // Catch bad number format + } + } + } + } + + /** + * Interface definition for a callback to be invoked when the printer just has print something. + */ + public interface OnSerialDeviceClickedListener { + /** + * Called when the printer just has print something. + */ + void onSerialDeviceClicked(SerialConnectParameters serialConnectParameters); + } + + private OnSerialDeviceClickedListener onSerialDeviceClickedListener; + + /** + * Register a callback to be invoked when a serial device has been chosen. + * @param onSerialDeviceClickedListener The callback that will run + */ + void setOnSerialDeviceClickedListener(OnSerialDeviceClickedListener onSerialDeviceClickedListener) { + this.onSerialDeviceClickedListener = onSerialDeviceClickedListener; + } + + @Override + public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) { + ListItem item = listItems.get(position-1); + if(item.driver == null) { + Toast.makeText(getActivity(), getString(Utils.resId(this, "string", "serial_ports_device_no_driver")), Toast.LENGTH_SHORT).show(); + if(onSerialDeviceClickedListener != null) + onSerialDeviceClickedListener.onSerialDeviceClicked(new SerialConnectParameters(0, 0, + 0, 0, + "")); + } else if(item.device != null) { + if(onSerialDeviceClickedListener != null) + onSerialDeviceClickedListener.onSerialDeviceClicked(new SerialConnectParameters(item.device.getDeviceId(), item.port, + item.device.getVendorId(), item.device.getProductId(), + item.driver.getClass().getSimpleName().replace("SerialDriver",""))); + } + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/CdcAcmSerialDriver.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/CdcAcmSerialDriver.java new file mode 100644 index 0000000..fd9950e --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/CdcAcmSerialDriver.java @@ -0,0 +1,323 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * USB CDC/ACM serial driver implementation. + * + * @author mike wakerly (opensource@hoho.com) + * @see Universal + * Serial Bus Class Definitions for Communication Devices, v1.1 + */ +public class CdcAcmSerialDriver implements UsbSerialDriver { + + private final String TAG = CdcAcmSerialDriver.class.getSimpleName(); + + private final UsbDevice mDevice; + private final List mPorts; + + public CdcAcmSerialDriver(UsbDevice device) { + mDevice = device; + mPorts = new ArrayList<>(); + + int controlInterfaceCount = 0; + int dataInterfaceCount = 0; + for( int i = 0; i < device.getInterfaceCount(); i++) { + if(device.getInterface(i).getInterfaceClass() == UsbConstants.USB_CLASS_COMM) + controlInterfaceCount++; + if(device.getInterface(i).getInterfaceClass() == UsbConstants.USB_CLASS_CDC_DATA) + dataInterfaceCount++; + } + for( int port = 0; port < Math.min(controlInterfaceCount, dataInterfaceCount); port++) { + mPorts.add(new CdcAcmSerialPort(mDevice, port)); + } + if(mPorts.size() == 0) { + mPorts.add(new CdcAcmSerialPort(mDevice, -1)); + } + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + @Override + public List getPorts() { + return mPorts; + } + + public class CdcAcmSerialPort extends CommonUsbSerialPort { + + private UsbInterface mControlInterface; + private UsbInterface mDataInterface; + + private UsbEndpoint mControlEndpoint; + + private int mControlIndex; + + private boolean mRts = false; + private boolean mDtr = false; + + private static final int USB_RECIP_INTERFACE = 0x01; + private static final int USB_RT_ACM = UsbConstants.USB_TYPE_CLASS | USB_RECIP_INTERFACE; + + private static final int SET_LINE_CODING = 0x20; // USB CDC 1.1 section 6.2 + private static final int GET_LINE_CODING = 0x21; + private static final int SET_CONTROL_LINE_STATE = 0x22; + private static final int SEND_BREAK = 0x23; + + public CdcAcmSerialPort(UsbDevice device, int portNumber) { + super(device, portNumber); + } + + @Override + public UsbSerialDriver getDriver() { + return CdcAcmSerialDriver.this; + } + + @Override + protected void openInt(UsbDeviceConnection connection) throws IOException { + if (mPortNumber == -1) { + Log.d(TAG,"device might be castrated ACM device, trying single interface logic"); + openSingleInterface(); + } else { + Log.d(TAG,"trying default interface logic"); + openInterface(); + } + } + + private void openSingleInterface() throws IOException { + // the following code is inspired by the cdc-acm driver in the linux kernel + + mControlIndex = 0; + mControlInterface = mDevice.getInterface(0); + mDataInterface = mDevice.getInterface(0); + if (!mConnection.claimInterface(mControlInterface, true)) { + throw new IOException("Could not claim shared control/data interface"); + } + + for (int i = 0; i < mControlInterface.getEndpointCount(); ++i) { + UsbEndpoint ep = mControlInterface.getEndpoint(i); + if ((ep.getDirection() == UsbConstants.USB_DIR_IN) && (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT)) { + mControlEndpoint = ep; + } else if ((ep.getDirection() == UsbConstants.USB_DIR_IN) && (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK)) { + mReadEndpoint = ep; + } else if ((ep.getDirection() == UsbConstants.USB_DIR_OUT) && (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK)) { + mWriteEndpoint = ep; + } + } + if (mControlEndpoint == null) { + throw new IOException("No control endpoint"); + } + } + + private void openInterface() throws IOException { + Log.d(TAG, "claiming interfaces, count=" + mDevice.getInterfaceCount()); + + int controlInterfaceCount = 0; + int dataInterfaceCount = 0; + mControlInterface = null; + mDataInterface = null; + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface usbInterface = mDevice.getInterface(i); + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_COMM) { + if(controlInterfaceCount == mPortNumber) { + mControlIndex = i; + mControlInterface = usbInterface; + } + controlInterfaceCount++; + } + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CDC_DATA) { + if(dataInterfaceCount == mPortNumber) { + mDataInterface = usbInterface; + } + dataInterfaceCount++; + } + } + + if(mControlInterface == null) { + throw new IOException("No control interface"); + } + Log.d(TAG, "Control iface=" + mControlInterface); + + if (!mConnection.claimInterface(mControlInterface, true)) { + throw new IOException("Could not claim control interface"); + } + + mControlEndpoint = mControlInterface.getEndpoint(0); + if (mControlEndpoint.getDirection() != UsbConstants.USB_DIR_IN || mControlEndpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_INT) { + throw new IOException("Invalid control endpoint"); + } + + if(mDataInterface == null) { + throw new IOException("No data interface"); + } + Log.d(TAG, "data iface=" + mDataInterface); + + if (!mConnection.claimInterface(mDataInterface, true)) { + throw new IOException("Could not claim data interface"); + } + + for (int i = 0; i < mDataInterface.getEndpointCount(); i++) { + UsbEndpoint ep = mDataInterface.getEndpoint(i); + if (ep.getDirection() == UsbConstants.USB_DIR_IN && ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) + mReadEndpoint = ep; + if (ep.getDirection() == UsbConstants.USB_DIR_OUT && ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) + mWriteEndpoint = ep; + } + } + + private int sendAcmControlMessage(int request, int value, byte[] buf) throws IOException { + int len = mConnection.controlTransfer( + USB_RT_ACM, request, value, mControlIndex, buf, buf != null ? buf.length : 0, 5000); + if(len < 0) { + throw new IOException("controlTransfer failed"); + } + return len; + } + + @Override + protected void closeInt() { + try { + mConnection.releaseInterface(mControlInterface); + mConnection.releaseInterface(mDataInterface); + } catch(Exception ignored) {} + } + + @Override + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { + if(baudRate <= 0) { + throw new IllegalArgumentException("Invalid baud rate: " + baudRate); + } + if(dataBits < DATABITS_5 || dataBits > DATABITS_8) { + throw new IllegalArgumentException("Invalid data bits: " + dataBits); + } + byte stopBitsByte; + switch (stopBits) { + case STOPBITS_1: stopBitsByte = 0; break; + case STOPBITS_1_5: stopBitsByte = 1; break; + case STOPBITS_2: stopBitsByte = 2; break; + default: throw new IllegalArgumentException("Invalid stop bits: " + stopBits); + } + + byte parityBitesByte; + switch (parity) { + case PARITY_NONE: parityBitesByte = 0; break; + case PARITY_ODD: parityBitesByte = 1; break; + case PARITY_EVEN: parityBitesByte = 2; break; + case PARITY_MARK: parityBitesByte = 3; break; + case PARITY_SPACE: parityBitesByte = 4; break; + default: throw new IllegalArgumentException("Invalid parity: " + parity); + } + byte[] msg = { + (byte) ( baudRate & 0xff), + (byte) ((baudRate >> 8 ) & 0xff), + (byte) ((baudRate >> 16) & 0xff), + (byte) ((baudRate >> 24) & 0xff), + stopBitsByte, + parityBitesByte, + (byte) dataBits}; + sendAcmControlMessage(SET_LINE_CODING, 0, msg); + } + + @Override + public boolean getDTR() throws IOException { + return mDtr; + } + + @Override + public void setDTR(boolean value) throws IOException { + mDtr = value; + setDtrRts(); + } + + @Override + public boolean getRTS() throws IOException { + return mRts; + } + + @Override + public void setRTS(boolean value) throws IOException { + mRts = value; + setDtrRts(); + } + + private void setDtrRts() throws IOException { + int value = (mRts ? 0x2 : 0) | (mDtr ? 0x1 : 0); + sendAcmControlMessage(SET_CONTROL_LINE_STATE, value, null); + } + + @Override + public EnumSet getControlLines() throws IOException { + EnumSet set = EnumSet.noneOf(ControlLine.class); + if(mRts) set.add(ControlLine.RTS); + if(mDtr) set.add(ControlLine.DTR); + return set; + } + + @Override + public EnumSet getSupportedControlLines() throws IOException { + return EnumSet.of(ControlLine.RTS, ControlLine.DTR); + } + + @Override + public void setBreak(boolean value) throws IOException { + sendAcmControlMessage(SEND_BREAK, value ? 0xffff : 0, null); + } + + } + + public static Map getSupportedDevices() { + final Map supportedDevices = new LinkedHashMap(); + supportedDevices.put(UsbId.VENDOR_ARDUINO, + new int[] { + UsbId.ARDUINO_UNO, + UsbId.ARDUINO_UNO_R3, + UsbId.ARDUINO_MEGA_2560, + UsbId.ARDUINO_MEGA_2560_R3, + UsbId.ARDUINO_SERIAL_ADAPTER, + UsbId.ARDUINO_SERIAL_ADAPTER_R3, + UsbId.ARDUINO_MEGA_ADK, + UsbId.ARDUINO_MEGA_ADK_R3, + UsbId.ARDUINO_LEONARDO, + UsbId.ARDUINO_MICRO, + }); + supportedDevices.put(UsbId.VENDOR_VAN_OOIJEN_TECH, + new int[] { + UsbId.VAN_OOIJEN_TECH_TEENSYDUINO_SERIAL, + }); + supportedDevices.put(UsbId.VENDOR_ATMEL, + new int[] { + UsbId.ATMEL_LUFA_CDC_DEMO_APP, + }); + supportedDevices.put(UsbId.VENDOR_LEAFLABS, + new int[] { + UsbId.LEAFLABS_MAPLE, + }); + supportedDevices.put(UsbId.VENDOR_ARM, + new int[] { + UsbId.ARM_MBED, + }); + return supportedDevices; + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/Ch34xSerialDriver.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/Ch34xSerialDriver.java new file mode 100644 index 0000000..7edd424 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/Ch34xSerialDriver.java @@ -0,0 +1,374 @@ +/* Copyright 2014 Andreas Butti + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Ch34xSerialDriver implements UsbSerialDriver { + + private static final String TAG = Ch34xSerialDriver.class.getSimpleName(); + + private final UsbDevice mDevice; + private final UsbSerialPort mPort; + + private static final int LCR_ENABLE_RX = 0x80; + private static final int LCR_ENABLE_TX = 0x40; + private static final int LCR_MARK_SPACE = 0x20; + private static final int LCR_PAR_EVEN = 0x10; + private static final int LCR_ENABLE_PAR = 0x08; + private static final int LCR_STOP_BITS_2 = 0x04; + private static final int LCR_CS8 = 0x03; + private static final int LCR_CS7 = 0x02; + private static final int LCR_CS6 = 0x01; + private static final int LCR_CS5 = 0x00; + + private static final int GCL_CTS = 0x01; + private static final int GCL_DSR = 0x02; + private static final int GCL_RI = 0x04; + private static final int GCL_CD = 0x08; + private static final int SCL_DTR = 0x20; + private static final int SCL_RTS = 0x40; + + public Ch34xSerialDriver(UsbDevice device) { + mDevice = device; + mPort = new Ch340SerialPort(mDevice, 0); + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + @Override + public List getPorts() { + return Collections.singletonList(mPort); + } + + public class Ch340SerialPort extends CommonUsbSerialPort { + + private static final int USB_TIMEOUT_MILLIS = 5000; + + private final int DEFAULT_BAUD_RATE = 9600; + + private boolean dtr = false; + private boolean rts = false; + + public Ch340SerialPort(UsbDevice device, int portNumber) { + super(device, portNumber); + } + + @Override + public UsbSerialDriver getDriver() { + return Ch34xSerialDriver.this; + } + + @Override + protected void openInt(UsbDeviceConnection connection) throws IOException { + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface usbIface = mDevice.getInterface(i); + if (!mConnection.claimInterface(usbIface, true)) { + throw new IOException("Could not claim data interface"); + } + } + + UsbInterface dataIface = mDevice.getInterface(mDevice.getInterfaceCount() - 1); + for (int i = 0; i < dataIface.getEndpointCount(); i++) { + UsbEndpoint ep = dataIface.getEndpoint(i); + if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { + if (ep.getDirection() == UsbConstants.USB_DIR_IN) { + mReadEndpoint = ep; + } else { + mWriteEndpoint = ep; + } + } + } + + initialize(); + setBaudRate(DEFAULT_BAUD_RATE); + } + + @Override + protected void closeInt() { + try { + for (int i = 0; i < mDevice.getInterfaceCount(); i++) + mConnection.releaseInterface(mDevice.getInterface(i)); + } catch(Exception ignored) {} + } + + private int controlOut(int request, int value, int index) { + final int REQTYPE_HOST_TO_DEVICE = UsbConstants.USB_TYPE_VENDOR | UsbConstants.USB_DIR_OUT; + return mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, request, + value, index, null, 0, USB_TIMEOUT_MILLIS); + } + + + private int controlIn(int request, int value, int index, byte[] buffer) { + final int REQTYPE_DEVICE_TO_HOST = UsbConstants.USB_TYPE_VENDOR | UsbConstants.USB_DIR_IN; + return mConnection.controlTransfer(REQTYPE_DEVICE_TO_HOST, request, + value, index, buffer, buffer.length, USB_TIMEOUT_MILLIS); + } + + + private void checkState(String msg, int request, int value, int[] expected) throws IOException { + byte[] buffer = new byte[expected.length]; + int ret = controlIn(request, value, 0, buffer); + + if (ret < 0) { + throw new IOException("Failed send cmd [" + msg + "]"); + } + + if (ret != expected.length) { + throw new IOException("Expected " + expected.length + " bytes, but get " + ret + " [" + msg + "]"); + } + + for (int i = 0; i < expected.length; i++) { + if (expected[i] == -1) { + continue; + } + + int current = buffer[i] & 0xff; + if (expected[i] != current) { + throw new IOException("Expected 0x" + Integer.toHexString(expected[i]) + " byte, but get 0x" + Integer.toHexString(current) + " [" + msg + "]"); + } + } + } + + private void setControlLines() throws IOException { + if (controlOut(0xa4, ~((dtr ? SCL_DTR : 0) | (rts ? SCL_RTS : 0)), 0) < 0) { + throw new IOException("Failed to set control lines"); + } + } + + private byte getStatus() throws IOException { + byte[] buffer = new byte[2]; + int ret = controlIn(0x95, 0x0706, 0, buffer); + if (ret < 0) + throw new IOException("Error getting control lines"); + return buffer[0]; + } + + private void initialize() throws IOException { + checkState("init #1", 0x5f, 0, new int[]{-1 /* 0x27, 0x30 */, 0x00}); + + if (controlOut(0xa1, 0, 0) < 0) { + throw new IOException("Init failed: #2"); + } + + setBaudRate(DEFAULT_BAUD_RATE); + + checkState("init #4", 0x95, 0x2518, new int[]{-1 /* 0x56, c3*/, 0x00}); + + if (controlOut(0x9a, 0x2518, LCR_ENABLE_RX | LCR_ENABLE_TX | LCR_CS8) < 0) { + throw new IOException("Init failed: #5"); + } + + checkState("init #6", 0x95, 0x0706, new int[]{-1/*0xf?*/, -1/*0xec,0xee*/}); + + if (controlOut(0xa1, 0x501f, 0xd90a) < 0) { + throw new IOException("Init failed: #7"); + } + + setBaudRate(DEFAULT_BAUD_RATE); + + setControlLines(); + + checkState("init #10", 0x95, 0x0706, new int[]{-1/* 0x9f, 0xff*/, -1/*0xec,0xee*/}); + } + + + private void setBaudRate(int baudRate) throws IOException { + final long CH341_BAUDBASE_FACTOR = 1532620800; + final int CH341_BAUDBASE_DIVMAX = 3; + + long factor = CH341_BAUDBASE_FACTOR / baudRate; + int divisor = CH341_BAUDBASE_DIVMAX; + + while ((factor > 0xfff0) && divisor > 0) { + factor >>= 3; + divisor--; + } + + if (factor > 0xfff0) { + throw new UnsupportedOperationException("Unsupported baud rate: " + baudRate); + } + + factor = 0x10000 - factor; + divisor |= 0x0080; // else ch341a waits until buffer full + int ret = controlOut(0x9a, 0x1312, (int) ((factor & 0xff00) | divisor)); + if (ret < 0) { + throw new IOException("Error setting baud rate: #1)"); + } + + ret = controlOut(0x9a, 0x0f2c, (int) (factor & 0xff)); + if (ret < 0) { + throw new IOException("Error setting baud rate: #2"); + } + } + + @Override + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { + if(baudRate <= 0) { + throw new IllegalArgumentException("Invalid baud rate: " + baudRate); + } + setBaudRate(baudRate); + + int lcr = LCR_ENABLE_RX | LCR_ENABLE_TX; + + switch (dataBits) { + case DATABITS_5: + lcr |= LCR_CS5; + break; + case DATABITS_6: + lcr |= LCR_CS6; + break; + case DATABITS_7: + lcr |= LCR_CS7; + break; + case DATABITS_8: + lcr |= LCR_CS8; + break; + default: + throw new IllegalArgumentException("Invalid data bits: " + dataBits); + } + + switch (parity) { + case PARITY_NONE: + break; + case PARITY_ODD: + lcr |= LCR_ENABLE_PAR; + break; + case PARITY_EVEN: + lcr |= LCR_ENABLE_PAR | LCR_PAR_EVEN; + break; + case PARITY_MARK: + lcr |= LCR_ENABLE_PAR | LCR_MARK_SPACE; + break; + case PARITY_SPACE: + lcr |= LCR_ENABLE_PAR | LCR_MARK_SPACE | LCR_PAR_EVEN; + break; + default: + throw new IllegalArgumentException("Invalid parity: " + parity); + } + + switch (stopBits) { + case STOPBITS_1: + break; + case STOPBITS_1_5: + throw new UnsupportedOperationException("Unsupported stop bits: 1.5"); + case STOPBITS_2: + lcr |= LCR_STOP_BITS_2; + break; + default: + throw new IllegalArgumentException("Invalid stop bits: " + stopBits); + } + + int ret = controlOut(0x9a, 0x2518, lcr); + if (ret < 0) { + throw new IOException("Error setting control byte"); + } + } + + @Override + public boolean getCD() throws IOException { + return (getStatus() & GCL_CD) == 0; + } + + @Override + public boolean getCTS() throws IOException { + return (getStatus() & GCL_CTS) == 0; + } + + @Override + public boolean getDSR() throws IOException { + return (getStatus() & GCL_DSR) == 0; + } + + @Override + public boolean getDTR() throws IOException { + return dtr; + } + + @Override + public void setDTR(boolean value) throws IOException { + dtr = value; + setControlLines(); + } + + @Override + public boolean getRI() throws IOException { + return (getStatus() & GCL_RI) == 0; + } + + @Override + public boolean getRTS() throws IOException { + return rts; + } + + @Override + public void setRTS(boolean value) throws IOException { + rts = value; + setControlLines(); + } + + @Override + public EnumSet getControlLines() throws IOException { + int status = getStatus(); + EnumSet set = EnumSet.noneOf(ControlLine.class); + if(rts) set.add(ControlLine.RTS); + if((status & GCL_CTS) == 0) set.add(ControlLine.CTS); + if(dtr) set.add(ControlLine.DTR); + if((status & GCL_DSR) == 0) set.add(ControlLine.DSR); + if((status & GCL_CD) == 0) set.add(ControlLine.CD); + if((status & GCL_RI) == 0) set.add(ControlLine.RI); + return set; + } + + @Override + public EnumSet getSupportedControlLines() throws IOException { + return EnumSet.allOf(ControlLine.class); + } + + @Override + public void setBreak(boolean value) throws IOException { + byte[] req = new byte[2]; + if(controlIn(0x95, 0x1805, 0, req) < 0) { + throw new IOException("Error getting BREAK condition"); + } + if(value) { + req[0] &= ~1; + req[1] &= ~0x40; + } else { + req[0] |= 1; + req[1] |= 0x40; + } + int val = (req[1] & 0xff) << 8 | (req[0] & 0xff); + if(controlOut(0x9a, 0x1805, val) < 0) { + throw new IOException("Error setting BREAK condition"); + } + } + } + + public static Map getSupportedDevices() { + final Map supportedDevices = new LinkedHashMap(); + supportedDevices.put(UsbId.VENDOR_QINHENG, new int[]{ + UsbId.QINHENG_CH340, + UsbId.QINHENG_CH341A, + }); + return supportedDevices; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/CommonUsbSerialPort.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/CommonUsbSerialPort.java new file mode 100644 index 0000000..3129cc9 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/CommonUsbSerialPort.java @@ -0,0 +1,270 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbRequest; +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.EnumSet; + +/** + * A base class shared by several driver implementations. + * + * @author mike wakerly (opensource@hoho.com) + */ +public abstract class CommonUsbSerialPort implements UsbSerialPort { + + private static final String TAG = CommonUsbSerialPort.class.getSimpleName(); + private static final int DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024; + private static final int MAX_READ_SIZE = 16 * 1024; // = old bulkTransfer limit + + protected final UsbDevice mDevice; + protected final int mPortNumber; + + // non-null when open() + protected UsbDeviceConnection mConnection = null; + protected UsbEndpoint mReadEndpoint; + protected UsbEndpoint mWriteEndpoint; + protected UsbRequest mUsbRequest; + + protected final Object mWriteBufferLock = new Object(); + /** Internal write buffer. Guarded by {@link #mWriteBufferLock}. */ + protected byte[] mWriteBuffer; + + public CommonUsbSerialPort(UsbDevice device, int portNumber) { + mDevice = device; + mPortNumber = portNumber; + + mWriteBuffer = new byte[DEFAULT_WRITE_BUFFER_SIZE]; + } + + @Override + public String toString() { + return String.format("<%s device_name=%s device_id=%s port_number=%s>", + getClass().getSimpleName(), mDevice.getDeviceName(), + mDevice.getDeviceId(), mPortNumber); + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + @Override + public int getPortNumber() { + return mPortNumber; + } + + /** + * Returns the device serial number + * @return serial number + */ + @Override + public String getSerial() { + return mConnection.getSerial(); + } + + /** + * Sets the size of the internal buffer used to exchange data with the USB + * stack for write operations. Most users should not need to change this. + * + * @param bufferSize the size in bytes + */ + public final void setWriteBufferSize(int bufferSize) { + synchronized (mWriteBufferLock) { + if (bufferSize == mWriteBuffer.length) { + return; + } + mWriteBuffer = new byte[bufferSize]; + } + } + + @Override + public void open(UsbDeviceConnection connection) throws IOException { + if (mConnection != null) { + throw new IOException("Already open"); + } + mConnection = connection; + try { + openInt(connection); + if (mReadEndpoint == null || mWriteEndpoint == null) { + throw new IOException("Could not get read & write endpoints"); + } + mUsbRequest = new UsbRequest(); + mUsbRequest.initialize(mConnection, mReadEndpoint); + } catch(Exception e) { + close(); + throw e; + } + } + + protected abstract void openInt(UsbDeviceConnection connection) throws IOException; + + @Override + public void close() throws IOException { + if (mConnection == null) { + throw new IOException("Already closed"); + } + try { + mUsbRequest.cancel(); + } catch(Exception ignored) {} + mUsbRequest = null; + try { + closeInt(); + } catch(Exception ignored) {} + try { + mConnection.close(); + } catch(Exception ignored) {} + mConnection = null; + } + + protected abstract void closeInt(); + + /** + * use simple USB request supported by all devices to test if connection is still valid + */ + protected void testConnection() throws IOException { + byte[] buf = new byte[2]; + int len = mConnection.controlTransfer(0x80 /*DEVICE*/, 0 /*GET_STATUS*/, 0, 0, buf, buf.length, 200); + if(len < 0) + throw new IOException("USB get_status request failed"); + } + + @Override + public int read(final byte[] dest, final int timeout) throws IOException { + return read(dest, timeout, true); + } + + protected int read(final byte[] dest, final int timeout, boolean testConnection) throws IOException { + if(mConnection == null) { + throw new IOException("Connection closed"); + } + if(dest.length <= 0) { + throw new IllegalArgumentException("Read buffer to small"); + } + final int nread; + if (timeout != 0) { + // bulkTransfer will cause data loss with short timeout + high baud rates + continuous transfer + // https://stackoverflow.com/questions/9108548/android-usb-host-bulktransfer-is-losing-data + // but mConnection.requestWait(timeout) available since Android 8.0 es even worse, + // as it crashes with short timeout, e.g. + // A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x276a in tid 29846 (pool-2-thread-1), pid 29618 (.usbserial.test) + // /system/lib64/libusbhost.so (usb_request_wait+192) + // /system/lib64/libandroid_runtime.so (android_hardware_UsbDeviceConnection_request_wait(_JNIEnv*, _jobject*, long)+84) + // data loss / crashes were observed with timeout up to 200 msec + long endTime = testConnection ? System.currentTimeMillis() + timeout : 0; + int readMax = Math.min(dest.length, MAX_READ_SIZE); + nread = mConnection.bulkTransfer(mReadEndpoint, dest, readMax, timeout); + // Android error propagation is improvable, nread == -1 can be: timeout, connection lost, buffer undersized, ... + if(nread == -1 && testConnection && System.currentTimeMillis() < endTime) + testConnection(); + + } else { + final ByteBuffer buf = ByteBuffer.wrap(dest); + if (!mUsbRequest.queue(buf, dest.length)) { + throw new IOException("Queueing USB request failed"); + } + final UsbRequest response = mConnection.requestWait(); + if (response == null) { + throw new IOException("Waiting for USB request failed"); + } + nread = buf.position(); + } + if (nread > 0) + return nread; + else + return 0; + } + + @Override + public int write(final byte[] src, final int timeout) throws IOException { + int offset = 0; + + if(mConnection == null) { + throw new IOException("Connection closed"); + } + while (offset < src.length) { + final int writeLength; + final int amtWritten; + + synchronized (mWriteBufferLock) { + final byte[] writeBuffer; + + writeLength = Math.min(src.length - offset, mWriteBuffer.length); + if (offset == 0) { + writeBuffer = src; + } else { + // bulkTransfer does not support offsets, make a copy. + System.arraycopy(src, offset, mWriteBuffer, 0, writeLength); + writeBuffer = mWriteBuffer; + } + + amtWritten = mConnection.bulkTransfer(mWriteEndpoint, writeBuffer, writeLength, timeout); + } + if (amtWritten <= 0) { + throw new IOException("Error writing " + writeLength + + " bytes at offset " + offset + " length=" + src.length); + } + + Log.d(TAG, "Wrote amt=" + amtWritten + " attempted=" + writeLength); + offset += amtWritten; + } + return offset; + } + + @Override + public boolean isOpen() { + return mConnection != null; + } + + @Override + public abstract void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException; + + @Override + public boolean getCD() throws IOException { throw new UnsupportedOperationException(); } + + @Override + public boolean getCTS() throws IOException { throw new UnsupportedOperationException(); } + + @Override + public boolean getDSR() throws IOException { throw new UnsupportedOperationException(); } + + @Override + public boolean getDTR() throws IOException { throw new UnsupportedOperationException(); } + + @Override + public void setDTR(boolean value) throws IOException { throw new UnsupportedOperationException(); } + + @Override + public boolean getRI() throws IOException { throw new UnsupportedOperationException(); } + + @Override + public boolean getRTS() throws IOException { throw new UnsupportedOperationException(); } + + @Override + public void setRTS(boolean value) throws IOException { throw new UnsupportedOperationException(); } + + @Override + public abstract EnumSet getControlLines() throws IOException; + + @Override + public abstract EnumSet getSupportedControlLines() throws IOException; + + @Override + public void purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void setBreak(boolean value) throws IOException { throw new UnsupportedOperationException(); } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/Cp21xxSerialDriver.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/Cp21xxSerialDriver.java new file mode 100644 index 0000000..d6ff1b9 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/Cp21xxSerialDriver.java @@ -0,0 +1,335 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class Cp21xxSerialDriver implements UsbSerialDriver { + + private static final String TAG = Cp21xxSerialDriver.class.getSimpleName(); + + private final UsbDevice mDevice; + private final List mPorts; + + public Cp21xxSerialDriver(UsbDevice device) { + mDevice = device; + mPorts = new ArrayList<>(); + for( int port = 0; port < device.getInterfaceCount(); port++) { + mPorts.add(new Cp21xxSerialPort(mDevice, port)); + } + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + @Override + public List getPorts() { + return mPorts; + } + + public class Cp21xxSerialPort extends CommonUsbSerialPort { + + private static final int USB_WRITE_TIMEOUT_MILLIS = 5000; + + /* + * Configuration Request Types + */ + private static final int REQTYPE_HOST_TO_DEVICE = 0x41; + private static final int REQTYPE_DEVICE_TO_HOST = 0xc1; + + /* + * Configuration Request Codes + */ + private static final int SILABSER_IFC_ENABLE_REQUEST_CODE = 0x00; + private static final int SILABSER_SET_LINE_CTL_REQUEST_CODE = 0x03; + private static final int SILABSER_SET_BREAK_REQUEST_CODE = 0x05; + private static final int SILABSER_SET_MHS_REQUEST_CODE = 0x07; + private static final int SILABSER_SET_BAUDRATE = 0x1E; + private static final int SILABSER_FLUSH_REQUEST_CODE = 0x12; + private static final int SILABSER_GET_MDMSTS_REQUEST_CODE = 0x08; + + private static final int FLUSH_READ_CODE = 0x0a; + private static final int FLUSH_WRITE_CODE = 0x05; + + /* + * SILABSER_IFC_ENABLE_REQUEST_CODE + */ + private static final int UART_ENABLE = 0x0001; + private static final int UART_DISABLE = 0x0000; + + /* + * SILABSER_SET_MHS_REQUEST_CODE + */ + private static final int DTR_ENABLE = 0x101; + private static final int DTR_DISABLE = 0x100; + private static final int RTS_ENABLE = 0x202; + private static final int RTS_DISABLE = 0x200; + + /* + * SILABSER_GET_MDMSTS_REQUEST_CODE + */ + private static final int STATUS_CTS = 0x10; + private static final int STATUS_DSR = 0x20; + private static final int STATUS_RI = 0x40; + private static final int STATUS_CD = 0x80; + + + private boolean dtr = false; + private boolean rts = false; + + // second port of Cp2105 has limited baudRate, dataBits, stopBits, parity + // unsupported baudrate returns error at controlTransfer(), other parameters are silently ignored + private boolean mIsRestrictedPort; + + public Cp21xxSerialPort(UsbDevice device, int portNumber) { + super(device, portNumber); + } + + @Override + public UsbSerialDriver getDriver() { + return Cp21xxSerialDriver.this; + } + + private void setConfigSingle(int request, int value) throws IOException { + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, request, value, + mPortNumber, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Control transfer failed: " + request + " / " + value + " -> " + result); + } + } + + private byte getStatus() throws IOException { + byte[] buffer = new byte[1]; + int result = mConnection.controlTransfer(REQTYPE_DEVICE_TO_HOST, SILABSER_GET_MDMSTS_REQUEST_CODE, 0, + mPortNumber, buffer, buffer.length, USB_WRITE_TIMEOUT_MILLIS); + if (result != 1) { + throw new IOException("Control transfer failed: " + SILABSER_GET_MDMSTS_REQUEST_CODE + " / " + 0 + " -> " + result); + } + return buffer[0]; + } + + @Override + protected void openInt(UsbDeviceConnection connection) throws IOException { + mIsRestrictedPort = mDevice.getInterfaceCount() == 2 && mPortNumber == 1; + if(mPortNumber >= mDevice.getInterfaceCount()) { + throw new IOException("Unknown port number"); + } + UsbInterface dataIface = mDevice.getInterface(mPortNumber); + if (!mConnection.claimInterface(dataIface, true)) { + throw new IOException("Could not claim interface " + mPortNumber); + } + for (int i = 0; i < dataIface.getEndpointCount(); i++) { + UsbEndpoint ep = dataIface.getEndpoint(i); + if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { + if (ep.getDirection() == UsbConstants.USB_DIR_IN) { + mReadEndpoint = ep; + } else { + mWriteEndpoint = ep; + } + } + } + + setConfigSingle(SILABSER_IFC_ENABLE_REQUEST_CODE, UART_ENABLE); + setConfigSingle(SILABSER_SET_MHS_REQUEST_CODE, (dtr ? DTR_ENABLE : DTR_DISABLE) | (rts ? RTS_ENABLE : RTS_DISABLE)); + } + + @Override + protected void closeInt() { + try { + setConfigSingle(SILABSER_IFC_ENABLE_REQUEST_CODE, UART_DISABLE); + } catch (Exception ignored) {} + try { + mConnection.releaseInterface(mDevice.getInterface(mPortNumber)); + } catch(Exception ignored) {} + } + + private void setBaudRate(int baudRate) throws IOException { + byte[] data = new byte[] { + (byte) ( baudRate & 0xff), + (byte) ((baudRate >> 8 ) & 0xff), + (byte) ((baudRate >> 16) & 0xff), + (byte) ((baudRate >> 24) & 0xff) + }; + int ret = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SILABSER_SET_BAUDRATE, + 0, mPortNumber, data, 4, USB_WRITE_TIMEOUT_MILLIS); + if (ret < 0) { + throw new IOException("Error setting baud rate"); + } + } + + @Override + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { + if(baudRate <= 0) { + throw new IllegalArgumentException("Invalid baud rate: " + baudRate); + } + setBaudRate(baudRate); + + int configDataBits = 0; + switch (dataBits) { + case DATABITS_5: + if(mIsRestrictedPort) + throw new UnsupportedOperationException("Unsupported data bits: " + dataBits); + configDataBits |= 0x0500; + break; + case DATABITS_6: + if(mIsRestrictedPort) + throw new UnsupportedOperationException("Unsupported data bits: " + dataBits); + configDataBits |= 0x0600; + break; + case DATABITS_7: + if(mIsRestrictedPort) + throw new UnsupportedOperationException("Unsupported data bits: " + dataBits); + configDataBits |= 0x0700; + break; + case DATABITS_8: + configDataBits |= 0x0800; + break; + default: + throw new IllegalArgumentException("Invalid data bits: " + dataBits); + } + + switch (parity) { + case PARITY_NONE: + break; + case PARITY_ODD: + configDataBits |= 0x0010; + break; + case PARITY_EVEN: + configDataBits |= 0x0020; + break; + case PARITY_MARK: + if(mIsRestrictedPort) + throw new UnsupportedOperationException("Unsupported parity: mark"); + configDataBits |= 0x0030; + break; + case PARITY_SPACE: + if(mIsRestrictedPort) + throw new UnsupportedOperationException("Unsupported parity: space"); + configDataBits |= 0x0040; + break; + default: + throw new IllegalArgumentException("Invalid parity: " + parity); + } + + switch (stopBits) { + case STOPBITS_1: + break; + case STOPBITS_1_5: + throw new UnsupportedOperationException("Unsupported stop bits: 1.5"); + case STOPBITS_2: + if(mIsRestrictedPort) + throw new UnsupportedOperationException("Unsupported stop bits: 2"); + configDataBits |= 2; + break; + default: + throw new IllegalArgumentException("Invalid stop bits: " + stopBits); + } + setConfigSingle(SILABSER_SET_LINE_CTL_REQUEST_CODE, configDataBits); + } + + @Override + public boolean getCD() throws IOException { + return (getStatus() & STATUS_CD) != 0; + } + + @Override + public boolean getCTS() throws IOException { + return (getStatus() & STATUS_CTS) != 0; + } + + @Override + public boolean getDSR() throws IOException { + return (getStatus() & STATUS_DSR) != 0; + } + + @Override + public boolean getDTR() throws IOException { + return dtr; + } + + @Override + public void setDTR(boolean value) throws IOException { + dtr = value; + setConfigSingle(SILABSER_SET_MHS_REQUEST_CODE, dtr ? DTR_ENABLE : DTR_DISABLE); + } + + @Override + public boolean getRI() throws IOException { + return (getStatus() & STATUS_RI) != 0; + } + + @Override + public boolean getRTS() throws IOException { + return rts; + } + + @Override + public void setRTS(boolean value) throws IOException { + rts = value; + setConfigSingle(SILABSER_SET_MHS_REQUEST_CODE, rts ? RTS_ENABLE : RTS_DISABLE); + } + + @Override + public EnumSet getControlLines() throws IOException { + byte status = getStatus(); + EnumSet set = EnumSet.noneOf(ControlLine.class); + if(rts) set.add(ControlLine.RTS); + if((status & STATUS_CTS) != 0) set.add(ControlLine.CTS); + if(dtr) set.add(ControlLine.DTR); + if((status & STATUS_DSR) != 0) set.add(ControlLine.DSR); + if((status & STATUS_CD) != 0) set.add(ControlLine.CD); + if((status & STATUS_RI) != 0) set.add(ControlLine.RI); + return set; + } + + @Override + public EnumSet getSupportedControlLines() throws IOException { + return EnumSet.allOf(ControlLine.class); + } + + @Override + // note: only working on some devices, on other devices ignored w/o error + public void purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + int value = (purgeReadBuffers ? FLUSH_READ_CODE : 0) + | (purgeWriteBuffers ? FLUSH_WRITE_CODE : 0); + + if (value != 0) { + setConfigSingle(SILABSER_FLUSH_REQUEST_CODE, value); + } + } + + @Override + public void setBreak(boolean value) throws IOException { + setConfigSingle(SILABSER_SET_BREAK_REQUEST_CODE, value ? 1 : 0); + } + } + + public static Map getSupportedDevices() { + final Map supportedDevices = new LinkedHashMap(); + supportedDevices.put(UsbId.VENDOR_SILABS, + new int[] { + UsbId.SILABS_CP2102, // same ID for CP2101, CP2103, CP2104, CP2109 + UsbId.SILABS_CP2105, + UsbId.SILABS_CP2108, + }); + return supportedDevices; + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/FtdiSerialDriver.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/FtdiSerialDriver.java new file mode 100644 index 0000000..b36b660 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/FtdiSerialDriver.java @@ -0,0 +1,428 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * Copyright 2020 kai morich + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/* + * driver is implemented from various information scattered over FTDI documentation + * + * baud rate calculation https://www.ftdichip.com/Support/Documents/AppNotes/AN232B-05_BaudRates.pdf + * control bits https://www.ftdichip.com/Firmware/Precompiled/UM_VinculumFirmware_V205.pdf + * device type https://www.ftdichip.com/Support/Documents/AppNotes/AN_233_Java_D2XX_for_Android_API_User_Manual.pdf -> bvdDevice + * + */ + +public class FtdiSerialDriver implements UsbSerialDriver { + + private static final String TAG = FtdiSerialPort.class.getSimpleName(); + + private final UsbDevice mDevice; + private final List mPorts; + + public FtdiSerialDriver(UsbDevice device) { + mDevice = device; + mPorts = new ArrayList<>(); + for( int port = 0; port < device.getInterfaceCount(); port++) { + mPorts.add(new FtdiSerialPort(mDevice, port)); + } + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + @Override + public List getPorts() { + return mPorts; + } + + public class FtdiSerialPort extends CommonUsbSerialPort { + + private static final int USB_WRITE_TIMEOUT_MILLIS = 5000; + private static final int READ_HEADER_LENGTH = 2; // contains MODEM_STATUS + + private static final int REQTYPE_HOST_TO_DEVICE = UsbConstants.USB_TYPE_VENDOR | UsbConstants.USB_DIR_OUT; + private static final int REQTYPE_DEVICE_TO_HOST = UsbConstants.USB_TYPE_VENDOR | UsbConstants.USB_DIR_IN; + + private static final int RESET_REQUEST = 0; + private static final int MODEM_CONTROL_REQUEST = 1; + private static final int SET_BAUD_RATE_REQUEST = 3; + private static final int SET_DATA_REQUEST = 4; + private static final int GET_MODEM_STATUS_REQUEST = 5; + private static final int SET_LATENCY_TIMER_REQUEST = 9; + private static final int GET_LATENCY_TIMER_REQUEST = 10; + + private static final int MODEM_CONTROL_DTR_ENABLE = 0x0101; + private static final int MODEM_CONTROL_DTR_DISABLE = 0x0100; + private static final int MODEM_CONTROL_RTS_ENABLE = 0x0202; + private static final int MODEM_CONTROL_RTS_DISABLE = 0x0200; + private static final int MODEM_STATUS_CTS = 0x10; + private static final int MODEM_STATUS_DSR = 0x20; + private static final int MODEM_STATUS_RI = 0x40; + private static final int MODEM_STATUS_CD = 0x80; + private static final int RESET_ALL = 0; + private static final int RESET_PURGE_RX = 1; + private static final int RESET_PURGE_TX = 2; + + private boolean baudRateWithPort = false; + private boolean dtr = false; + private boolean rts = false; + private int breakConfig = 0; + + public FtdiSerialPort(UsbDevice device, int portNumber) { + super(device, portNumber); + } + + @Override + public UsbSerialDriver getDriver() { + return FtdiSerialDriver.this; + } + + + @Override + protected void openInt(UsbDeviceConnection connection) throws IOException { + if (!connection.claimInterface(mDevice.getInterface(mPortNumber), true)) { + throw new IOException("Could not claim interface " + mPortNumber); + } + if (mDevice.getInterface(mPortNumber).getEndpointCount() < 2) { + throw new IOException("Not enough endpoints"); + } + mReadEndpoint = mDevice.getInterface(mPortNumber).getEndpoint(0); + mWriteEndpoint = mDevice.getInterface(mPortNumber).getEndpoint(1); + + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, RESET_REQUEST, + RESET_ALL, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Reset failed: result=" + result); + } + result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, MODEM_CONTROL_REQUEST, + (dtr ? MODEM_CONTROL_DTR_ENABLE : MODEM_CONTROL_DTR_DISABLE) | + (rts ? MODEM_CONTROL_RTS_ENABLE : MODEM_CONTROL_RTS_DISABLE), + mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Init RTS,DTR failed: result=" + result); + } + + // mDevice.getVersion() would require API 23 + byte[] rawDescriptors = connection.getRawDescriptors(); + if(rawDescriptors == null || rawDescriptors.length < 14) { + throw new IOException("Could not get device descriptors"); + } + int deviceType = rawDescriptors[13]; + baudRateWithPort = deviceType == 7 || deviceType == 8 || deviceType == 9; // ...H devices + } + + @Override + protected void closeInt() { + try { + mConnection.releaseInterface(mDevice.getInterface(mPortNumber)); + } catch(Exception ignored) {} + } + + @Override + public int read(final byte[] dest, final int timeout) throws IOException { + if(dest.length <= READ_HEADER_LENGTH) { + throw new IllegalArgumentException("Read buffer to small"); + // could allocate larger buffer, including space for 2 header bytes, but this would + // result in buffers not being 64 byte aligned any more, causing data loss at continuous + // data transfer at high baud rates when buffers are fully filled. + } + int nread; + if (timeout != 0) { + long endTime = System.currentTimeMillis() + timeout; + do { + nread = super.read(dest, Math.max(1, (int)(endTime - System.currentTimeMillis())), false); + } while (nread == READ_HEADER_LENGTH && System.currentTimeMillis() < endTime); + if(nread <= 0 && System.currentTimeMillis() < endTime) + testConnection(); + } else { + do { + nread = super.read(dest, timeout, false); + } while (nread == READ_HEADER_LENGTH); + } + return readFilter(dest, nread); + } + + private int readFilter(byte[] buffer, int totalBytesRead) throws IOException { + final int maxPacketSize = mReadEndpoint.getMaxPacketSize(); + int destPos = 0; + for(int srcPos = 0; srcPos < totalBytesRead; srcPos += maxPacketSize) { + int length = Math.min(srcPos + maxPacketSize, totalBytesRead) - (srcPos + READ_HEADER_LENGTH); + if (length < 0) + throw new IOException("Expected at least " + READ_HEADER_LENGTH + " bytes"); + System.arraycopy(buffer, srcPos + READ_HEADER_LENGTH, buffer, destPos, length); + destPos += length; + } + //Log.d(TAG, "read filter " + totalBytesRead + " -> " + destPos); + return destPos; + } + + private void setBaudrate(int baudRate) throws IOException { + int divisor, subdivisor, effectiveBaudRate; + if (baudRate > 3500000) { + throw new UnsupportedOperationException("Baud rate to high"); + } else if(baudRate >= 2500000) { + divisor = 0; + subdivisor = 0; + effectiveBaudRate = 3000000; + } else if(baudRate >= 1750000) { + divisor = 1; + subdivisor = 0; + effectiveBaudRate = 2000000; + } else { + divisor = (24000000 << 1) / baudRate; + divisor = (divisor + 1) >> 1; // round + subdivisor = divisor & 0x07; + divisor >>= 3; + if (divisor > 0x3fff) // exceeds bit 13 at 183 baud + throw new UnsupportedOperationException("Baud rate to low"); + effectiveBaudRate = (24000000 << 1) / ((divisor << 3) + subdivisor); + effectiveBaudRate = (effectiveBaudRate +1) >> 1; + } + double baudRateError = Math.abs(1.0 - (effectiveBaudRate / (double)baudRate)); + if(baudRateError >= 0.031) // can happen only > 1.5Mbaud + throw new UnsupportedOperationException(String.format("Baud rate deviation %.1f%% is higher than allowed 3%%", baudRateError*100)); + int value = divisor; + int index = 0; + switch(subdivisor) { + case 0: break; // 16,15,14 = 000 - sub-integer divisor = 0 + case 4: value |= 0x4000; break; // 16,15,14 = 001 - sub-integer divisor = 0.5 + case 2: value |= 0x8000; break; // 16,15,14 = 010 - sub-integer divisor = 0.25 + case 1: value |= 0xc000; break; // 16,15,14 = 011 - sub-integer divisor = 0.125 + case 3: value |= 0x0000; index |= 1; break; // 16,15,14 = 100 - sub-integer divisor = 0.375 + case 5: value |= 0x4000; index |= 1; break; // 16,15,14 = 101 - sub-integer divisor = 0.625 + case 6: value |= 0x8000; index |= 1; break; // 16,15,14 = 110 - sub-integer divisor = 0.75 + case 7: value |= 0xc000; index |= 1; break; // 16,15,14 = 111 - sub-integer divisor = 0.875 + } + if(baudRateWithPort) { + index <<= 8; + index |= mPortNumber+1; + } + Log.d(TAG, String.format("baud rate=%d, effective=%d, error=%.1f%%, value=0x%04x, index=0x%04x, divisor=%d, subdivisor=%d", + baudRate, effectiveBaudRate, baudRateError*100, value, index, divisor, subdivisor)); + + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_BAUD_RATE_REQUEST, + value, index, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Setting baudrate failed: result=" + result); + } + } + + @Override + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { + if(baudRate <= 0) { + throw new IllegalArgumentException("Invalid baud rate: " + baudRate); + } + setBaudrate(baudRate); + + int config = 0; + switch (dataBits) { + case DATABITS_5: + case DATABITS_6: + throw new UnsupportedOperationException("Unsupported data bits: " + dataBits); + case DATABITS_7: + case DATABITS_8: + config |= dataBits; + break; + default: + throw new IllegalArgumentException("Invalid data bits: " + dataBits); + } + + switch (parity) { + case PARITY_NONE: + break; + case PARITY_ODD: + config |= 0x100; + break; + case PARITY_EVEN: + config |= 0x200; + break; + case PARITY_MARK: + config |= 0x300; + break; + case PARITY_SPACE: + config |= 0x400; + break; + default: + throw new IllegalArgumentException("Invalid parity: " + parity); + } + + switch (stopBits) { + case STOPBITS_1: + break; + case STOPBITS_1_5: + throw new UnsupportedOperationException("Unsupported stop bits: 1.5"); + case STOPBITS_2: + config |= 0x1000; + break; + default: + throw new IllegalArgumentException("Invalid stop bits: " + stopBits); + } + + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_DATA_REQUEST, + config, mPortNumber+1,null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Setting parameters failed: result=" + result); + } + breakConfig = config; + } + + private int getStatus() throws IOException { + byte[] data = new byte[2]; + int result = mConnection.controlTransfer(REQTYPE_DEVICE_TO_HOST, GET_MODEM_STATUS_REQUEST, + 0, mPortNumber+1, data, data.length, USB_WRITE_TIMEOUT_MILLIS); + if (result != 2) { + throw new IOException("Get modem status failed: result=" + result); + } + return data[0]; + } + + @Override + public boolean getCD() throws IOException { + return (getStatus() & MODEM_STATUS_CD) != 0; + } + + @Override + public boolean getCTS() throws IOException { + return (getStatus() & MODEM_STATUS_CTS) != 0; + } + + @Override + public boolean getDSR() throws IOException { + return (getStatus() & MODEM_STATUS_DSR) != 0; + } + + @Override + public boolean getDTR() throws IOException { + return dtr; + } + + @Override + public void setDTR(boolean value) throws IOException { + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, MODEM_CONTROL_REQUEST, + value ? MODEM_CONTROL_DTR_ENABLE : MODEM_CONTROL_DTR_DISABLE, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Set DTR failed: result=" + result); + } + dtr = value; + } + + @Override + public boolean getRI() throws IOException { + return (getStatus() & MODEM_STATUS_RI) != 0; + } + + @Override + public boolean getRTS() throws IOException { + return rts; + } + + @Override + public void setRTS(boolean value) throws IOException { + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, MODEM_CONTROL_REQUEST, + value ? MODEM_CONTROL_RTS_ENABLE : MODEM_CONTROL_RTS_DISABLE, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Set DTR failed: result=" + result); + } + rts = value; + } + + @Override + public EnumSet getControlLines() throws IOException { + int status = getStatus(); + EnumSet set = EnumSet.noneOf(ControlLine.class); + if(rts) set.add(ControlLine.RTS); + if((status & MODEM_STATUS_CTS) != 0) set.add(ControlLine.CTS); + if(dtr) set.add(ControlLine.DTR); + if((status & MODEM_STATUS_DSR) != 0) set.add(ControlLine.DSR); + if((status & MODEM_STATUS_CD) != 0) set.add(ControlLine.CD); + if((status & MODEM_STATUS_RI) != 0) set.add(ControlLine.RI); + return set; + } + + @Override + public EnumSet getSupportedControlLines() throws IOException { + return EnumSet.allOf(ControlLine.class); + } + + @Override + public void purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + if (purgeWriteBuffers) { + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, RESET_REQUEST, + RESET_PURGE_RX, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Purge write buffer failed: result=" + result); + } + } + + if (purgeReadBuffers) { + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, RESET_REQUEST, + RESET_PURGE_TX, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Purge read buffer failed: result=" + result); + } + } + } + + @Override + public void setBreak(boolean value) throws IOException { + int config = breakConfig; + if(value) config |= 0x4000; + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_DATA_REQUEST, + config, mPortNumber+1,null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Setting BREAK failed: result=" + result); + } + } + + public void setLatencyTimer(int latencyTime) throws IOException { + int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_LATENCY_TIMER_REQUEST, + latencyTime, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); + if (result != 0) { + throw new IOException("Set latency timer failed: result=" + result); + } + } + + public int getLatencyTimer() throws IOException { + byte[] data = new byte[1]; + int result = mConnection.controlTransfer(REQTYPE_DEVICE_TO_HOST, GET_LATENCY_TIMER_REQUEST, + 0, mPortNumber+1, data, data.length, USB_WRITE_TIMEOUT_MILLIS); + if (result != 1) { + throw new IOException("Get latency timer failed: result=" + result); + } + return data[0]; + } + + } + + public static Map getSupportedDevices() { + final Map supportedDevices = new LinkedHashMap(); + supportedDevices.put(UsbId.VENDOR_FTDI, + new int[] { + UsbId.FTDI_FT232R, + UsbId.FTDI_FT232H, + UsbId.FTDI_FT2232H, + UsbId.FTDI_FT4232H, + UsbId.FTDI_FT231X, // same ID for FT230X, FT231X, FT234XD + }); + return supportedDevices; + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/ProbeTable.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/ProbeTable.java new file mode 100644 index 0000000..f6aeff6 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/ProbeTable.java @@ -0,0 +1,87 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.util.Pair; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Maps (vendor id, product id) pairs to the corresponding serial driver. + * + * @author mike wakerly (opensource@hoho.com) + */ +public class ProbeTable { + + private final Map, Class> mProbeTable = + new LinkedHashMap, Class>(); + + /** + * Adds or updates a (vendor, product) pair in the table. + * + * @param vendorId the USB vendor id + * @param productId the USB product id + * @param driverClass the driver class responsible for this pair + * @return {@code this}, for chaining + */ + public ProbeTable addProduct(int vendorId, int productId, + Class driverClass) { + mProbeTable.put(Pair.create(vendorId, productId), driverClass); + return this; + } + + /** + * Internal method to add all supported products from + * {@code getSupportedProducts} static method. + * + * @param driverClass + * @return + */ + @SuppressWarnings("unchecked") + ProbeTable addDriver(Class driverClass) { + final Method method; + + try { + method = driverClass.getMethod("getSupportedDevices"); + } catch (SecurityException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + + final Map devices; + try { + devices = (Map) method.invoke(null); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + for (Map.Entry entry : devices.entrySet()) { + final int vendorId = entry.getKey(); + for (int productId : entry.getValue()) { + addProduct(vendorId, productId, driverClass); + } + } + + return this; + } + + /** + * Returns the driver for the given (vendor, product) pair, or {@code null} + * if no match. + * + * @param vendorId the USB vendor id + * @param productId the USB product id + * @return the driver class matching this pair, or {@code null} + */ + public Class findDriver(int vendorId, int productId) { + final Pair pair = Pair.create(vendorId, productId); + return mProbeTable.get(pair); + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/ProlificSerialDriver.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/ProlificSerialDriver.java new file mode 100644 index 0000000..aa470be --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/ProlificSerialDriver.java @@ -0,0 +1,507 @@ +/* + * Ported to usb-serial-for-android by Felix Hädicke + * + * Based on the pyprolific driver written by Emmanuel Blot + * See https://github.com/eblot/pyftdi + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.util.Log; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ProlificSerialDriver implements UsbSerialDriver { + + private final String TAG = ProlificSerialDriver.class.getSimpleName(); + + private final static int[] standardBaudRates = { + 75, 150, 300, 600, 1200, 1800, 2400, 3600, 4800, 7200, 9600, 14400, 19200, + 28800, 38400, 57600, 115200, 128000, 134400, 161280, 201600, 230400, 268800, + 403200, 460800, 614400, 806400, 921600, 1228800, 2457600, 3000000, 6000000 + }; + private enum DeviceType { DEVICE_TYPE_01, DEVICE_TYPE_HX}; + + private final UsbDevice mDevice; + private final UsbSerialPort mPort; + + public ProlificSerialDriver(UsbDevice device) { + mDevice = device; + mPort = new ProlificSerialPort(mDevice, 0); + } + + @Override + public List getPorts() { + return Collections.singletonList(mPort); + } + + @Override + public UsbDevice getDevice() { + return mDevice; + } + + class ProlificSerialPort extends CommonUsbSerialPort { + + private static final int USB_READ_TIMEOUT_MILLIS = 1000; + private static final int USB_WRITE_TIMEOUT_MILLIS = 5000; + + private static final int USB_RECIP_INTERFACE = 0x01; + + private static final int PROLIFIC_VENDOR_READ_REQUEST = 0x01; + private static final int PROLIFIC_VENDOR_WRITE_REQUEST = 0x01; + + private static final int PROLIFIC_VENDOR_OUT_REQTYPE = UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR; + private static final int PROLIFIC_VENDOR_IN_REQTYPE = UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR; + private static final int PROLIFIC_CTRL_OUT_REQTYPE = UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_CLASS | USB_RECIP_INTERFACE; + + private static final int WRITE_ENDPOINT = 0x02; + private static final int READ_ENDPOINT = 0x83; + private static final int INTERRUPT_ENDPOINT = 0x81; + + private static final int FLUSH_RX_REQUEST = 0x08; // RX @ Prolific device = write @ usb-serial-for-android library + private static final int FLUSH_TX_REQUEST = 0x09; + + private static final int SET_LINE_REQUEST = 0x20; // same as CDC SET_LINE_CODING + private static final int SET_CONTROL_REQUEST = 0x22; // same as CDC SET_CONTROL_LINE_STATE + private static final int SEND_BREAK_REQUEST = 0x23; // same as CDC SEND_BREAK + private static final int GET_CONTROL_REQUEST = 0x87; + private static final int STATUS_NOTIFICATION = 0xa1; // similar to CDC SERIAL_STATE but different length + + private static final int CONTROL_DTR = 0x01; + private static final int CONTROL_RTS = 0x02; + + private static final int GET_CONTROL_FLAG_CD = 0x02; + private static final int GET_CONTROL_FLAG_DSR = 0x04; + private static final int GET_CONTROL_FLAG_RI = 0x01; + private static final int GET_CONTROL_FLAG_CTS = 0x08; + + private static final int STATUS_FLAG_CD = 0x01; + private static final int STATUS_FLAG_DSR = 0x02; + private static final int STATUS_FLAG_RI = 0x08; + private static final int STATUS_FLAG_CTS = 0x80; + + private static final int STATUS_BUFFER_SIZE = 10; + private static final int STATUS_BYTE_IDX = 8; + + private DeviceType mDeviceType = DeviceType.DEVICE_TYPE_HX; + private UsbEndpoint mInterruptEndpoint; + private int mControlLinesValue = 0; + private int mBaudRate = -1, mDataBits = -1, mStopBits = -1, mParity = -1; + + private int mStatus = 0; + private volatile Thread mReadStatusThread = null; + private final Object mReadStatusThreadLock = new Object(); + private boolean mStopReadStatusThread = false; + private IOException mReadStatusException = null; + + + public ProlificSerialPort(UsbDevice device, int portNumber) { + super(device, portNumber); + } + + @Override + public UsbSerialDriver getDriver() { + return ProlificSerialDriver.this; + } + + private byte[] inControlTransfer(int requestType, int request, int value, int index, int length) throws IOException { + byte[] buffer = new byte[length]; + int result = mConnection.controlTransfer(requestType, request, value, index, buffer, length, USB_READ_TIMEOUT_MILLIS); + if (result != length) { + throw new IOException(String.format("ControlTransfer 0x%x failed: %d",value, result)); + } + return buffer; + } + + private void outControlTransfer(int requestType, int request, int value, int index, byte[] data) throws IOException { + int length = (data == null) ? 0 : data.length; + int result = mConnection.controlTransfer(requestType, request, value, index, data, length, USB_WRITE_TIMEOUT_MILLIS); + if (result != length) { + throw new IOException( String.format("ControlTransfer 0x%x failed: %d", value, result)); + } + } + + private byte[] vendorIn(int value, int index, int length) throws IOException { + return inControlTransfer(PROLIFIC_VENDOR_IN_REQTYPE, PROLIFIC_VENDOR_READ_REQUEST, value, index, length); + } + + private void vendorOut(int value, int index, byte[] data) throws IOException { + outControlTransfer(PROLIFIC_VENDOR_OUT_REQTYPE, PROLIFIC_VENDOR_WRITE_REQUEST, value, index, data); + } + + private void resetDevice() throws IOException { + purgeHwBuffers(true, true); + } + + private void ctrlOut(int request, int value, int index, byte[] data) throws IOException { + outControlTransfer(PROLIFIC_CTRL_OUT_REQTYPE, request, value, index, data); + } + + private void doBlackMagic() throws IOException { + vendorIn(0x8484, 0, 1); + vendorOut(0x0404, 0, null); + vendorIn(0x8484, 0, 1); + vendorIn(0x8383, 0, 1); + vendorIn(0x8484, 0, 1); + vendorOut(0x0404, 1, null); + vendorIn(0x8484, 0, 1); + vendorIn(0x8383, 0, 1); + vendorOut(0, 1, null); + vendorOut(1, 0, null); + vendorOut(2, (mDeviceType == DeviceType.DEVICE_TYPE_HX) ? 0x44 : 0x24, null); + } + + private void setControlLines(int newControlLinesValue) throws IOException { + ctrlOut(SET_CONTROL_REQUEST, newControlLinesValue, 0, null); + mControlLinesValue = newControlLinesValue; + } + + private void readStatusThreadFunction() { + try { + while (!mStopReadStatusThread) { + byte[] buffer = new byte[STATUS_BUFFER_SIZE]; + long endTime = System.currentTimeMillis() + 500; + int readBytesCount = mConnection.bulkTransfer(mInterruptEndpoint, buffer, STATUS_BUFFER_SIZE, 500); + if(readBytesCount == -1 && System.currentTimeMillis() < endTime) + testConnection(); + if (readBytesCount > 0) { + if (readBytesCount != STATUS_BUFFER_SIZE) { + throw new IOException("Invalid status notification, expected " + STATUS_BUFFER_SIZE + " bytes, got " + readBytesCount); + } else if(buffer[0] != (byte)STATUS_NOTIFICATION ) { + throw new IOException("Invalid status notification, expected " + STATUS_NOTIFICATION + " request, got " + buffer[0]); + } else { + mStatus = buffer[STATUS_BYTE_IDX] & 0xff; + } + } + } + } catch (IOException e) { + mReadStatusException = e; + } + //Log.d(TAG, "end control line status thread " + mStopReadStatusThread + " " + (mReadStatusException == null ? "-" : mReadStatusException.getMessage())); + } + + private int getStatus() throws IOException { + if ((mReadStatusThread == null) && (mReadStatusException == null)) { + synchronized (mReadStatusThreadLock) { + if (mReadStatusThread == null) { + byte[] data = vendorIn(GET_CONTROL_REQUEST, 0, 1); + mStatus = 0; + if((data[0] & GET_CONTROL_FLAG_CTS) == 0) mStatus |= STATUS_FLAG_CTS; + if((data[0] & GET_CONTROL_FLAG_DSR) == 0) mStatus |= STATUS_FLAG_DSR; + if((data[0] & GET_CONTROL_FLAG_CD) == 0) mStatus |= STATUS_FLAG_CD; + if((data[0] & GET_CONTROL_FLAG_RI) == 0) mStatus |= STATUS_FLAG_RI; + //Log.d(TAG, "start control line status thread " + mStatus); + mReadStatusThread = new Thread(this::readStatusThreadFunction); + mReadStatusThread.setDaemon(true); + mReadStatusThread.start(); + } + } + } + + /* throw and clear an exception which occured in the status read thread */ + IOException readStatusException = mReadStatusException; + if (mReadStatusException != null) { + mReadStatusException = null; + throw readStatusException; + } + + return mStatus; + } + + private boolean testStatusFlag(int flag) throws IOException { + return ((getStatus() & flag) == flag); + } + + @Override + public void openInt(UsbDeviceConnection connection) throws IOException { + UsbInterface usbInterface = mDevice.getInterface(0); + + if (!connection.claimInterface(usbInterface, true)) { + throw new IOException("Error claiming Prolific interface 0"); + } + + for (int i = 0; i < usbInterface.getEndpointCount(); ++i) { + UsbEndpoint currentEndpoint = usbInterface.getEndpoint(i); + + switch (currentEndpoint.getAddress()) { + case READ_ENDPOINT: + mReadEndpoint = currentEndpoint; + break; + + case WRITE_ENDPOINT: + mWriteEndpoint = currentEndpoint; + break; + + case INTERRUPT_ENDPOINT: + mInterruptEndpoint = currentEndpoint; + break; + } + } + + if (mDevice.getDeviceClass() == 0x02) { + mDeviceType = DeviceType.DEVICE_TYPE_01; + } else { + byte[] rawDescriptors = connection.getRawDescriptors(); + if(rawDescriptors == null || rawDescriptors.length <8) { + Log.w(TAG, "Could not get device descriptors, Assuming that it is a HX device"); + mDeviceType = DeviceType.DEVICE_TYPE_HX; + } else { + byte maxPacketSize0 = rawDescriptors[7]; + if (maxPacketSize0 == 64) { + mDeviceType = DeviceType.DEVICE_TYPE_HX; + } else if ((mDevice.getDeviceClass() == 0x00) + || (mDevice.getDeviceClass() == 0xff)) { + mDeviceType = DeviceType.DEVICE_TYPE_01; + } else { + Log.w(TAG, "Could not detect PL2303 subtype, Assuming that it is a HX device"); + mDeviceType = DeviceType.DEVICE_TYPE_HX; + } + } + } + setControlLines(mControlLinesValue); + resetDevice(); + doBlackMagic(); + } + + @Override + public void closeInt() { + try { + synchronized (mReadStatusThreadLock) { + if (mReadStatusThread != null) { + try { + mStopReadStatusThread = true; + mReadStatusThread.join(); + } catch (Exception e) { + Log.w(TAG, "An error occured while waiting for status read thread", e); + } + mStopReadStatusThread = false; + mReadStatusThread = null; + mReadStatusException = null; + } + } + resetDevice(); + } catch(Exception ignored) {} + try { + mConnection.releaseInterface(mDevice.getInterface(0)); + } catch(Exception ignored) {} + } + + private int filterBaudRate(int baudRate) { +// if(BuildConfig.DEBUG && (baudRate & (3<<29)) == (1<<29)) { +// return baudRate & ~(1<<29); // for testing purposes accept without further checks +// } + if (baudRate <= 0) { + throw new IllegalArgumentException("Invalid baud rate: " + baudRate); + } + for(int br : standardBaudRates) { + if (br == baudRate) { + return baudRate; + } + } + /* + * Formula taken from Linux + FreeBSD. + * baudrate = baseline / (mantissa * 4^exponent) + * where + * mantissa = buf[8:0] + * exponent = buf[11:9] + * + * Note: The formula does not work for all PL2303 variants. + * Ok for PL2303HX. Not ok for PL2303TA. Other variants unknown. + */ + int baseline, mantissa, exponent; + baseline = 12000000 * 32; + mantissa = baseline / baudRate; + if (mantissa == 0) { // > unrealistic 384 MBaud + throw new UnsupportedOperationException("Baud rate to high"); + } + exponent = 0; + while (mantissa >= 512) { + if (exponent < 7) { + mantissa >>= 2; /* divide by 4 */ + exponent++; + } else { // < 45.8 baud + throw new UnsupportedOperationException("Baud rate to low"); + } + } + int effectiveBaudRate = (baseline / mantissa) >> (exponent << 1); + double baudRateError = Math.abs(1.0 - (effectiveBaudRate / (double)baudRate)); + if(baudRateError >= 0.031) // > unrealistic 11.6 Mbaud + throw new UnsupportedOperationException(String.format("Baud rate deviation %.1f%% is higher than allowed 3%%", baudRateError*100)); + int buf = mantissa + (exponent<<9) + (1<<31); + + Log.d(TAG, String.format("baud rate=%d, effective=%d, error=%.1f%%, value=0x%08x, mantissa=%d, exponent=%d", + baudRate, effectiveBaudRate, baudRateError*100, buf, mantissa, exponent)); + return buf; + } + + @Override + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { + baudRate = filterBaudRate(baudRate); + if ((mBaudRate == baudRate) && (mDataBits == dataBits) + && (mStopBits == stopBits) && (mParity == parity)) { + // Make sure no action is performed if there is nothing to change + return; + } + + byte[] lineRequestData = new byte[7]; + lineRequestData[0] = (byte) (baudRate & 0xff); + lineRequestData[1] = (byte) ((baudRate >> 8) & 0xff); + lineRequestData[2] = (byte) ((baudRate >> 16) & 0xff); + lineRequestData[3] = (byte) ((baudRate >> 24) & 0xff); + + switch (stopBits) { + case STOPBITS_1: + lineRequestData[4] = 0; + break; + case STOPBITS_1_5: + lineRequestData[4] = 1; + break; + case STOPBITS_2: + lineRequestData[4] = 2; + break; + default: + throw new IllegalArgumentException("Invalid stop bits: " + stopBits); + } + + switch (parity) { + case PARITY_NONE: + lineRequestData[5] = 0; + break; + case PARITY_ODD: + lineRequestData[5] = 1; + break; + case PARITY_EVEN: + lineRequestData[5] = 2; + break; + case PARITY_MARK: + lineRequestData[5] = 3; + break; + case PARITY_SPACE: + lineRequestData[5] = 4; + break; + default: + throw new IllegalArgumentException("Invalid parity: " + parity); + } + + if(dataBits < DATABITS_5 || dataBits > DATABITS_8) { + throw new IllegalArgumentException("Invalid data bits: " + dataBits); + } + lineRequestData[6] = (byte) dataBits; + + ctrlOut(SET_LINE_REQUEST, 0, 0, lineRequestData); + + resetDevice(); + + mBaudRate = baudRate; + mDataBits = dataBits; + mStopBits = stopBits; + mParity = parity; + } + + @Override + public boolean getCD() throws IOException { + return testStatusFlag(STATUS_FLAG_CD); + } + + @Override + public boolean getCTS() throws IOException { + return testStatusFlag(STATUS_FLAG_CTS); + } + + @Override + public boolean getDSR() throws IOException { + return testStatusFlag(STATUS_FLAG_DSR); + } + + @Override + public boolean getDTR() throws IOException { + return (mControlLinesValue & CONTROL_DTR) != 0; + } + + @Override + public void setDTR(boolean value) throws IOException { + int newControlLinesValue; + if (value) { + newControlLinesValue = mControlLinesValue | CONTROL_DTR; + } else { + newControlLinesValue = mControlLinesValue & ~CONTROL_DTR; + } + setControlLines(newControlLinesValue); + } + + @Override + public boolean getRI() throws IOException { + return testStatusFlag(STATUS_FLAG_RI); + } + + @Override + public boolean getRTS() throws IOException { + return (mControlLinesValue & CONTROL_RTS) != 0; + } + + @Override + public void setRTS(boolean value) throws IOException { + int newControlLinesValue; + if (value) { + newControlLinesValue = mControlLinesValue | CONTROL_RTS; + } else { + newControlLinesValue = mControlLinesValue & ~CONTROL_RTS; + } + setControlLines(newControlLinesValue); + } + + + @Override + public EnumSet getControlLines() throws IOException { + int status = getStatus(); + EnumSet set = EnumSet.noneOf(ControlLine.class); + if((mControlLinesValue & CONTROL_RTS) != 0) set.add(ControlLine.RTS); + if((status & STATUS_FLAG_CTS) != 0) set.add(ControlLine.CTS); + if((mControlLinesValue & CONTROL_DTR) != 0) set.add(ControlLine.DTR); + if((status & STATUS_FLAG_DSR) != 0) set.add(ControlLine.DSR); + if((status & STATUS_FLAG_CD) != 0) set.add(ControlLine.CD); + if((status & STATUS_FLAG_RI) != 0) set.add(ControlLine.RI); + return set; + } + + @Override + public EnumSet getSupportedControlLines() throws IOException { + return EnumSet.allOf(ControlLine.class); + } + + @Override + public void purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + if (purgeWriteBuffers) { + vendorOut(FLUSH_RX_REQUEST, 0, null); + } + + if (purgeReadBuffers) { + vendorOut(FLUSH_TX_REQUEST, 0, null); + } + } + + @Override + public void setBreak(boolean value) throws IOException { + ctrlOut(SEND_BREAK_REQUEST, value ? 0xffff : 0, 0, null); + } + } + + public static Map getSupportedDevices() { + final Map supportedDevices = new LinkedHashMap(); + supportedDevices.put(UsbId.VENDOR_PROLIFIC, + new int[] { UsbId.PROLIFIC_PL2303, }); + return supportedDevices; + } +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbId.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbId.java new file mode 100644 index 0000000..8f37e49 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbId.java @@ -0,0 +1,67 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +/** + * Registry of USB vendor/product ID constants. + * + * Culled from various sources; see + * usb.ids for one listing. + * + * @author mike wakerly (opensource@hoho.com) + */ +public final class UsbId { + + public static final int VENDOR_FTDI = 0x0403; + public static final int FTDI_FT232R = 0x6001; + public static final int FTDI_FT2232H = 0x6010; + public static final int FTDI_FT4232H = 0x6011; + public static final int FTDI_FT232H = 0x6014; + public static final int FTDI_FT231X = 0x6015; // same ID for FT230X, FT231X, FT234XD + + public static final int VENDOR_ATMEL = 0x03EB; + public static final int ATMEL_LUFA_CDC_DEMO_APP = 0x2044; + + public static final int VENDOR_ARDUINO = 0x2341; + public static final int ARDUINO_UNO = 0x0001; + public static final int ARDUINO_MEGA_2560 = 0x0010; + public static final int ARDUINO_SERIAL_ADAPTER = 0x003b; + public static final int ARDUINO_MEGA_ADK = 0x003f; + public static final int ARDUINO_MEGA_2560_R3 = 0x0042; + public static final int ARDUINO_UNO_R3 = 0x0043; + public static final int ARDUINO_MEGA_ADK_R3 = 0x0044; + public static final int ARDUINO_SERIAL_ADAPTER_R3 = 0x0044; + public static final int ARDUINO_LEONARDO = 0x8036; + public static final int ARDUINO_MICRO = 0x8037; + + public static final int VENDOR_VAN_OOIJEN_TECH = 0x16c0; + public static final int VAN_OOIJEN_TECH_TEENSYDUINO_SERIAL = 0x0483; + + public static final int VENDOR_LEAFLABS = 0x1eaf; + public static final int LEAFLABS_MAPLE = 0x0004; + + public static final int VENDOR_SILABS = 0x10c4; + public static final int SILABS_CP2102 = 0xea60; // same ID for CP2101, CP2103, CP2104, CP2109 + public static final int SILABS_CP2105 = 0xea70; + public static final int SILABS_CP2108 = 0xea71; + + public static final int VENDOR_PROLIFIC = 0x067b; + public static final int PROLIFIC_PL2303 = 0x2303; + + public static final int VENDOR_QINHENG = 0x1a86; + public static final int QINHENG_CH340 = 0x7523; + public static final int QINHENG_CH341A = 0x5523; + + // at www.linux-usb.org/usb.ids listed for NXP/LPC1768, but all processors supported by ARM mbed DAPLink firmware report these ids + public static final int VENDOR_ARM = 0x0d28; + public static final int ARM_MBED = 0x0204; + + private UsbId() { + throw new IllegalAccessError("Non-instantiable class"); + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialDriver.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialDriver.java new file mode 100644 index 0000000..26e5fa2 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialDriver.java @@ -0,0 +1,33 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbDevice; + +import java.util.List; + +/** + * + * @author mike wakerly (opensource@hoho.com) + */ +public interface UsbSerialDriver { + + /** + * Returns the raw {@link UsbDevice} backing this port. + * + * @return the device + */ + public UsbDevice getDevice(); + + /** + * Returns all available ports for this device. This list must have at least + * one entry. + * + * @return the ports + */ + public List getPorts(); +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialPort.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialPort.java new file mode 100644 index 0000000..ab257f6 --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialPort.java @@ -0,0 +1,263 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; + +import java.io.Closeable; +import java.io.IOException; +import java.util.EnumSet; + +/** + * Interface for a single serial port. + * + * @author mike wakerly (opensource@hoho.com) + */ +public interface UsbSerialPort extends Closeable { + + /** 5 data bits. */ + public static final int DATABITS_5 = 5; + + /** 6 data bits. */ + public static final int DATABITS_6 = 6; + + /** 7 data bits. */ + public static final int DATABITS_7 = 7; + + /** 8 data bits. */ + public static final int DATABITS_8 = 8; + + /** No flow control. */ + public static final int FLOWCONTROL_NONE = 0; + + /** RTS/CTS input flow control. */ + public static final int FLOWCONTROL_RTSCTS_IN = 1; + + /** RTS/CTS output flow control. */ + public static final int FLOWCONTROL_RTSCTS_OUT = 2; + + /** XON/XOFF input flow control. */ + public static final int FLOWCONTROL_XONXOFF_IN = 4; + + /** XON/XOFF output flow control. */ + public static final int FLOWCONTROL_XONXOFF_OUT = 8; + + /** No parity. */ + public static final int PARITY_NONE = 0; + + /** Odd parity. */ + public static final int PARITY_ODD = 1; + + /** Even parity. */ + public static final int PARITY_EVEN = 2; + + /** Mark parity. */ + public static final int PARITY_MARK = 3; + + /** Space parity. */ + public static final int PARITY_SPACE = 4; + + /** 1 stop bit. */ + public static final int STOPBITS_1 = 1; + + /** 1.5 stop bits. */ + public static final int STOPBITS_1_5 = 3; + + /** 2 stop bits. */ + public static final int STOPBITS_2 = 2; + + /** values for get[Supported]ControlLines() */ + public enum ControlLine { RTS, CTS, DTR, DSR, CD, RI }; + + /** + * Returns the driver used by this port. + */ + public UsbSerialDriver getDriver(); + + /** + * Returns the currently-bound USB device. + */ + public UsbDevice getDevice(); + + /** + * Port number within driver. + */ + public int getPortNumber(); + + /** + * The serial number of the underlying UsbDeviceConnection, or {@code null}. + * + * @return value from {@link UsbDeviceConnection#getSerial()} + * @throws SecurityException starting with target SDK 29 (Android 10) if permission for USB device is not granted + */ + public String getSerial(); + + /** + * Opens and initializes the port. Upon success, caller must ensure that + * {@link #close()} is eventually called. + * + * @param connection an open device connection, acquired with + * {@link UsbManager#openDevice(UsbDevice)} + * @throws IOException on error opening or initializing the port. + */ + public void open(UsbDeviceConnection connection) throws IOException; + + /** + * Closes the port and {@link UsbDeviceConnection} + * + * @throws IOException on error closing the port. + */ + public void close() throws IOException; + + /** + * Reads as many bytes as possible into the destination buffer. + * + * @param dest the destination byte buffer + * @param timeout the timeout for reading in milliseconds, 0 is infinite + * @return the actual number of bytes read + * @throws IOException if an error occurred during reading + */ + public int read(final byte[] dest, final int timeout) throws IOException; + + /** + * Writes as many bytes as possible from the source buffer. + * + * @param src the source byte buffer + * @param timeout the timeout for writing in milliseconds, 0 is infinite + * @return the actual number of bytes written + * @throws IOException if an error occurred during writing + */ + public int write(final byte[] src, final int timeout) throws IOException; + + /** + * Sets various serial port parameters. + * + * @param baudRate baud rate as an integer, for example {@code 115200}. + * @param dataBits one of {@link #DATABITS_5}, {@link #DATABITS_6}, + * {@link #DATABITS_7}, or {@link #DATABITS_8}. + * @param stopBits one of {@link #STOPBITS_1}, {@link #STOPBITS_1_5}, or {@link #STOPBITS_2}. + * @param parity one of {@link #PARITY_NONE}, {@link #PARITY_ODD}, + * {@link #PARITY_EVEN}, {@link #PARITY_MARK}, or {@link #PARITY_SPACE}. + * @throws IOException on error setting the port parameters + * @throws UnsupportedOperationException if values are not supported by a specific device + */ + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException; + + /** + * Gets the CD (Carrier Detect) bit from the underlying UART. + * + * @return the current state + * @throws IOException if an error occurred during reading + * @throws UnsupportedOperationException if not supported + */ + public boolean getCD() throws IOException; + + /** + * Gets the CTS (Clear To Send) bit from the underlying UART. + * + * @return the current state + * @throws IOException if an error occurred during reading + * @throws UnsupportedOperationException if not supported + */ + public boolean getCTS() throws IOException; + + /** + * Gets the DSR (Data Set Ready) bit from the underlying UART. + * + * @return the current state + * @throws IOException if an error occurred during reading + * @throws UnsupportedOperationException if not supported + */ + public boolean getDSR() throws IOException; + + /** + * Gets the DTR (Data Terminal Ready) bit from the underlying UART. + * + * @return the current state + * @throws IOException if an error occurred during reading + * @throws UnsupportedOperationException if not supported + */ + public boolean getDTR() throws IOException; + + /** + * Sets the DTR (Data Terminal Ready) bit on the underlying UART, if supported. + * + * @param value the value to set + * @throws IOException if an error occurred during writing + * @throws UnsupportedOperationException if not supported + */ + public void setDTR(boolean value) throws IOException; + + /** + * Gets the RI (Ring Indicator) bit from the underlying UART. + * + * @return the current state + * @throws IOException if an error occurred during reading + * @throws UnsupportedOperationException if not supported + */ + public boolean getRI() throws IOException; + + /** + * Gets the RTS (Request To Send) bit from the underlying UART. + * + * @return the current state + * @throws IOException if an error occurred during reading + * @throws UnsupportedOperationException if not supported + */ + public boolean getRTS() throws IOException; + + /** + * Sets the RTS (Request To Send) bit on the underlying UART, if supported. + * + * @param value the value to set + * @throws IOException if an error occurred during writing + * @throws UnsupportedOperationException if not supported + */ + public void setRTS(boolean value) throws IOException; + + /** + * Gets all control line values from the underlying UART, if supported. + * Requires less USB calls than calling getRTS() + ... + getRI() individually. + * + * @return EnumSet.contains(...) is {@code true} if set, else {@code false} + * @throws IOException if an error occurred during reading + */ + public EnumSet getControlLines() throws IOException; + + /** + * Gets all control line supported flags. + * + * @return EnumSet.contains(...) is {@code true} if supported, else {@code false} + * @throws IOException if an error occurred during reading + */ + public EnumSet getSupportedControlLines() throws IOException; + + /** + * Purge non-transmitted output data and / or non-read input data. + * + * @param purgeWriteBuffers {@code true} to discard non-transmitted output data + * @param purgeReadBuffers {@code true} to discard non-read input data + * @throws IOException if an error occurred during flush + * @throws UnsupportedOperationException if not supported + */ + public void purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException; + + /** + * send BREAK condition. + * + * @param value set/reset + */ + public void setBreak(boolean value) throws IOException; + + /** + * Returns the current state of the connection. + */ + public boolean isOpen(); + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialProber.java b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialProber.java new file mode 100644 index 0000000..b28a56d --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/driver/UsbSerialProber.java @@ -0,0 +1,92 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.driver; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author mike wakerly (opensource@hoho.com) + */ +public class UsbSerialProber { + + private final ProbeTable mProbeTable; + + public UsbSerialProber(ProbeTable probeTable) { + mProbeTable = probeTable; + } + + public static UsbSerialProber getDefaultProber() { + return new UsbSerialProber(getDefaultProbeTable()); + } + + public static ProbeTable getDefaultProbeTable() { + final ProbeTable probeTable = new ProbeTable(); + probeTable.addDriver(CdcAcmSerialDriver.class); + probeTable.addDriver(Cp21xxSerialDriver.class); + probeTable.addDriver(FtdiSerialDriver.class); + probeTable.addDriver(ProlificSerialDriver.class); + probeTable.addDriver(Ch34xSerialDriver.class); + return probeTable; + } + + /** + * Finds and builds all possible {@link UsbSerialDriver UsbSerialDrivers} + * from the currently-attached {@link UsbDevice} hierarchy. This method does + * not require permission from the Android USB system, since it does not + * open any of the devices. + * + * @param usbManager + * @return a list, possibly empty, of all compatible drivers + */ + public List findAllDrivers(final UsbManager usbManager) { + final List result = new ArrayList(); + + for (final UsbDevice usbDevice : usbManager.getDeviceList().values()) { + final UsbSerialDriver driver = probeDevice(usbDevice); + if (driver != null) { + result.add(driver); + } + } + return result; + } + + /** + * Probes a single device for a compatible driver. + * + * @param usbDevice the usb device to probe + * @return a new {@link UsbSerialDriver} compatible with this device, or + * {@code null} if none available. + */ + public UsbSerialDriver probeDevice(final UsbDevice usbDevice) { + final int vendorId = usbDevice.getVendorId(); + final int productId = usbDevice.getProductId(); + + final Class driverClass = + mProbeTable.findDriver(vendorId, productId); + if (driverClass != null) { + final UsbSerialDriver driver; + try { + final Constructor ctor = + driverClass.getConstructor(UsbDevice.class); + driver = ctor.newInstance(usbDevice); + } catch (NoSuchMethodException | IllegalArgumentException | InstantiationException | + IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + return driver; + } + return null; + } + +} diff --git a/app/src/main/java/org/emulator/calculator/usbserial/util/SerialInputOutputManager.java b/app/src/main/java/org/emulator/calculator/usbserial/util/SerialInputOutputManager.java new file mode 100644 index 0000000..64df56c --- /dev/null +++ b/app/src/main/java/org/emulator/calculator/usbserial/util/SerialInputOutputManager.java @@ -0,0 +1,239 @@ +/* Copyright 2011-2013 Google Inc. + * Copyright 2013 mike wakerly + * + * Project home page: https://github.com/mik3y/usb-serial-for-android + */ + +package org.emulator.calculator.usbserial.util; + +import android.os.Process; +import android.util.Log; + +import org.emulator.calculator.usbserial.driver.UsbSerialPort; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Utility class which services a {@link UsbSerialPort} in its {@link #run()} method. + * + * @author mike wakerly (opensource@hoho.com) + */ +public class SerialInputOutputManager implements Runnable { + + private static final String TAG = SerialInputOutputManager.class.getSimpleName(); + private static final boolean DEBUG = false; + private static final int BUFSIZ = 4096; + + /** + * default read timeout is infinite, to avoid data loss with bulkTransfer API + */ + private int mReadTimeout = 0; + private int mWriteTimeout = 0; + + private final Object mReadBufferLock = new Object(); + private final Object mWriteBufferLock = new Object(); + + private ByteBuffer mReadBuffer = ByteBuffer.allocate(BUFSIZ); + private ByteBuffer mWriteBuffer = ByteBuffer.allocate(BUFSIZ); + + public enum State { + STOPPED, + RUNNING, + STOPPING + } + + private int mThreadPriority = Process.THREAD_PRIORITY_URGENT_AUDIO; + private State mState = State.STOPPED; // Synchronized by 'this' + private Listener mListener; // Synchronized by 'this' + private final UsbSerialPort mSerialPort; + + public interface Listener { + /** + * Called when new incoming data is available. + */ + public void onNewData(byte[] data); + + /** + * Called when {@link SerialInputOutputManager#run()} aborts due to an error. + */ + public void onRunError(Exception e); + } + + public SerialInputOutputManager(UsbSerialPort serialPort) { + mSerialPort = serialPort; + } + + public SerialInputOutputManager(UsbSerialPort serialPort, Listener listener) { + mSerialPort = serialPort; + mListener = listener; + } + + public synchronized void setListener(Listener listener) { + mListener = listener; + } + + public synchronized Listener getListener() { + return mListener; + } + + /** + * setThreadPriority. By default use higher priority than UI thread to prevent data loss + * + * @param threadPriority see {@link Process#setThreadPriority(int)} + * */ + public void setThreadPriority(int threadPriority) { + if (mState != State.STOPPED) + throw new IllegalStateException("threadPriority only configurable before SerialInputOutputManager is started"); + mThreadPriority = threadPriority; + } + + /** + * read/write timeout + */ + public void setReadTimeout(int timeout) { + // when set if already running, read already blocks and the new value will not become effective now + if(mReadTimeout == 0 && timeout != 0 && mState != State.STOPPED) + throw new IllegalStateException("readTimeout only configurable before SerialInputOutputManager is started"); + mReadTimeout = timeout; + } + + public int getReadTimeout() { + return mReadTimeout; + } + + public void setWriteTimeout(int timeout) { + mWriteTimeout = timeout; + } + + public int getWriteTimeout() { + return mWriteTimeout; + } + + /** + * read/write buffer size + */ + public void setReadBufferSize(int bufferSize) { + if (getReadBufferSize() == bufferSize) + return; + synchronized (mReadBufferLock) { + mReadBuffer = ByteBuffer.allocate(bufferSize); + } + } + + public int getReadBufferSize() { + return mReadBuffer.capacity(); + } + + public void setWriteBufferSize(int bufferSize) { + if(getWriteBufferSize() == bufferSize) + return; + synchronized (mWriteBufferLock) { + ByteBuffer newWriteBuffer = ByteBuffer.allocate(bufferSize); + if(mWriteBuffer.position() > 0) + newWriteBuffer.put(mWriteBuffer.array(), 0, mWriteBuffer.position()); + mWriteBuffer = newWriteBuffer; + } + } + + public int getWriteBufferSize() { + return mWriteBuffer.capacity(); + } + + /* + * when writeAsync is used, it is recommended to use readTimeout != 0, + * else the write will be delayed until read data is available + */ + public void writeAsync(byte[] data) { + synchronized (mWriteBufferLock) { + mWriteBuffer.put(data); + } + } + + public synchronized void stop() { + if (getState() == State.RUNNING) { + Log.i(TAG, "Stop requested"); + mState = State.STOPPING; + } + } + + public synchronized State getState() { + return mState; + } + + /** + * Continuously services the read and write buffers until {@link #stop()} is + * called, or until a driver exception is raised. + */ + @Override + public void run() { + if(mThreadPriority != Process.THREAD_PRIORITY_DEFAULT) + setThreadPriority(mThreadPriority); + + synchronized (this) { + if (getState() != State.STOPPED) { + throw new IllegalStateException("Already running"); + } + mState = State.RUNNING; + } + + Log.i(TAG, "Running ..."); + try { + while (true) { + if (getState() != State.RUNNING) { + Log.i(TAG, "Stopping mState=" + getState()); + break; + } + step(); + } + } catch (Exception e) { + Log.w(TAG, "Run ending due to exception: " + e.getMessage(), e); + final Listener listener = getListener(); + if (listener != null) { + listener.onRunError(e); + } + } finally { + synchronized (this) { + mState = State.STOPPED; + Log.i(TAG, "Stopped"); + } + } + } + + private void step() throws IOException { + // Handle incoming data. + byte[] buffer = null; + synchronized (mReadBufferLock) { + buffer = mReadBuffer.array(); + } + int len = mSerialPort.read(buffer, mReadTimeout); + if (len > 0) { + if (DEBUG) Log.d(TAG, "Read data len=" + len); + final Listener listener = getListener(); + if (listener != null) { + final byte[] data = new byte[len]; + System.arraycopy(buffer, 0, data, 0, len); + listener.onNewData(data); + } + } + + // Handle outgoing data. + buffer = null; + synchronized (mWriteBufferLock) { + len = mWriteBuffer.position(); + if (len > 0) { + buffer = new byte[len]; + mWriteBuffer.rewind(); + mWriteBuffer.get(buffer, 0, len); + mWriteBuffer.clear(); + } + } + if (buffer != null) { + if (DEBUG) { + Log.d(TAG, "Writing data len=" + len); + } + mSerialPort.write(buffer, mWriteTimeout); + } + } + +} diff --git a/app/src/main/java/org/emulator/forty/eight/MainActivity.java b/app/src/main/java/org/emulator/forty/eight/MainActivity.java index 2574e4d..cf11b96 100644 --- a/app/src/main/java/org/emulator/forty/eight/MainActivity.java +++ b/app/src/main/java/org/emulator/forty/eight/MainActivity.java @@ -65,8 +65,10 @@ import org.emulator.calculator.MainScreenView; import org.emulator.calculator.NativeLib; import org.emulator.calculator.PrinterSimulator; import org.emulator.calculator.PrinterSimulatorFragment; +import org.emulator.calculator.Serial; import org.emulator.calculator.Settings; import org.emulator.calculator.Utils; +import org.emulator.calculator.usbserial.DevicesFragment; import java.io.BufferedReader; import java.io.File; @@ -152,7 +154,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); drawer = findViewById(R.id.drawer_layout); @@ -2031,7 +2033,87 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On changeHeaderIcon(); } - private Bitmap getApplicationIconBitmap() { + private int serialIndex = 1; + private HashMap serialsById = new HashMap<>(); + + @SuppressWarnings("UnusedDeclaration") + int openSerialPort(String serialPort) { + Integer serialPortId = serialIndex; + Serial serial = new Serial(this, serialPortId); + if(serial.connect(serialPort)) { + serialsById.put(serialPortId, serial); + serialIndex++; + return serialPortId; + } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(MainActivity.this, Utils.resId(MainActivity.this, "string", serial.getConnectionStatus()), Toast.LENGTH_LONG).show(); + } + }); + return 0; + } + } + + @SuppressWarnings("UnusedDeclaration") + int closeSerialPort(int serialPortId) { + Integer serialPortIdInt = serialPortId; + Serial serial = serialsById.get(serialPortIdInt); + if(serial != null) { + serialsById.remove(serialPortIdInt); + serial.disconnect(); + return 1; + } + return 0; + } + + @SuppressWarnings("UnusedDeclaration") + int setSerialPortParameters(int serialPortId, int baudRate) { + Integer serialPortIdInt = serialPortId; + Serial serial = serialsById.get(serialPortIdInt); + if(serial != null && serial.setParameters(baudRate)) + return 1; + return 0; + } + + @SuppressWarnings("UnusedDeclaration") + byte[] readSerialPort(int serialPortId, int nNumberOfBytesToRead) { + Integer serialPortIdInt = serialPortId; + Serial serial = serialsById.get(serialPortIdInt); + if(serial != null) + return serial.receive(nNumberOfBytesToRead); + return new byte[0]; + } + + @SuppressWarnings("UnusedDeclaration") + int writeSerialPort(int serialPortId, byte[] buffer) { + Integer serialPortIdInt = serialPortId; + Serial serial = serialsById.get(serialPortIdInt); + if(serial != null) + return serial.send(buffer); + return 0; + } + + @SuppressWarnings("UnusedDeclaration") + int serialPortSetBreak(int serialPortId) { + Integer serialPortIdInt = serialPortId; + Serial serial = serialsById.get(serialPortIdInt); + if(serial != null) + return serial.setBreak(); + return 0; + } + + @SuppressWarnings("UnusedDeclaration") + int serialPortClearBreak(int serialPortId) { + Integer serialPortIdInt = serialPortId; + Serial serial = serialsById.get(serialPortIdInt); + if(serial != null) + return serial.clearBreak(); + return 0; + } + + + private Bitmap getApplicationIconBitmap() { Drawable drawable = getApplicationInfo().loadIcon(getPackageManager()); if (drawable instanceof BitmapDrawable) { BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; @@ -2062,7 +2144,8 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On "settings_background_kml_color", "settings_background_fallback_color", "settings_printer_model", "settings_macro", "settings_kml", "settings_port1", "settings_port2", - "settings_flash_port2" }; + "settings_flash_port2", + "settings_serial_ports_wire", "settings_serial_ports_ir" }; for (String settingKey : settingKeys) updateFromPreferences(settingKey, false); } else { @@ -2184,6 +2267,14 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On if(settingsFlashPort2Url != null) loadFlashROM(Uri.parse(settingsFlashPort2Url), null); break; + case "settings_serial_ports_wire": + NativeLib.setConfiguration("settings_serial_ports_wire", isDynamicValue, 0, 0, + DevicesFragment.SerialConnectParameters.fromSettingsString(settings.getString("settings_serial_ports_wire", "")).toWin32String()); + break; + case "settings_serial_ports_ir": + NativeLib.setConfiguration("settings_serial_ports_ir", isDynamicValue, 0, 0, + DevicesFragment.SerialConnectParameters.fromSettingsString(settings.getString("settings_serial_ports_ir", "")).toWin32String()); + break; } } } diff --git a/app/src/main/java/org/emulator/forty/eight/SettingsFragment.java b/app/src/main/java/org/emulator/forty/eight/SettingsFragment.java index 85e3018..558ac59 100644 --- a/app/src/main/java/org/emulator/forty/eight/SettingsFragment.java +++ b/app/src/main/java/org/emulator/forty/eight/SettingsFragment.java @@ -19,12 +19,7 @@ import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.graphics.BlendMode; -import android.graphics.BlendModeColorFilter; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Vibrator; import android.text.InputType; @@ -51,6 +46,8 @@ import org.emulator.calculator.EmuApplication; import org.emulator.calculator.NativeLib; import org.emulator.calculator.Settings; import org.emulator.calculator.Utils; +import org.emulator.calculator.usbserial.DevicesDialogFragment; +import org.emulator.calculator.usbserial.DevicesFragment; import java.util.HashSet; import java.util.Locale; @@ -372,6 +369,41 @@ public class SettingsFragment extends AppCompatDialogFragment { } else preferenceFlashPort2.setEnabled(false); } + + + // Serial ports + + Preference preferenceSerialPortsWire = findPreference("settings_serial_ports_wire"); + if(preferenceSerialPortsWire != null) { + String serialPortsWire = settings.getString(preferenceSerialPortsWire.getKey(), ""); + serialPortsWire = DevicesFragment.SerialConnectParameters.fromSettingsString(serialPortsWire).toDisplayString(requireContext()); + preferenceSerialPortsWire.setSummary(serialPortsWire); + preferenceSerialPortsWire.setOnPreferenceClickListener(preference -> { + DevicesDialogFragment devicesDialogFragment = new DevicesDialogFragment(); + devicesDialogFragment.setOnSerialDeviceClickedListener((serialConnectParameters) -> { + preferenceSerialPortsWire.setSummary(serialConnectParameters.toDisplayString(requireContext())); + settings.putString(preferenceSerialPortsWire.getKey(), serialConnectParameters.toSettingsString()); + }); + devicesDialogFragment.show(getChildFragmentManager(), "DevicesDialogFragment"); + return true; + }); + } + + Preference preferenceSerialPortsIr = findPreference("settings_serial_ports_ir"); + if(preferenceSerialPortsIr != null) { + String serialPortsIr = settings.getString(preferenceSerialPortsIr.getKey(), ""); + serialPortsIr = DevicesFragment.SerialConnectParameters.fromSettingsString(serialPortsIr).toDisplayString(requireContext()); + preferenceSerialPortsIr.setSummary(serialPortsIr); + preferenceSerialPortsIr.setOnPreferenceClickListener(preference -> { + DevicesDialogFragment devicesDialogFragment = new DevicesDialogFragment(); + devicesDialogFragment.setOnSerialDeviceClickedListener((serialConnectParameters) -> { + preferenceSerialPortsIr.setSummary(serialConnectParameters.toDisplayString(requireContext())); + settings.putString(preferenceSerialPortsIr.getKey(), serialConnectParameters.toSettingsString()); + }); + devicesDialogFragment.show(getChildFragmentManager(), "DevicesDialogFragment"); + return true; + }); + } } void updatePort2LoadFilename(String port2Filename) { diff --git a/app/src/main/res/layout/device_list_header.xml b/app/src/main/res/layout/device_list_header.xml new file mode 100644 index 0000000..aaacf25 --- /dev/null +++ b/app/src/main/res/layout/device_list_header.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/layout/device_list_item.xml b/app/src/main/res/layout/device_list_item.xml new file mode 100644 index 0000000..05d846b --- /dev/null +++ b/app/src/main/res/layout/device_list_item.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_serial.xml b/app/src/main/res/layout/fragment_serial.xml new file mode 100644 index 0000000..5bb7520 --- /dev/null +++ b/app/src/main/res/layout/fragment_serial.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index feddeb6..bf616af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,8 +127,23 @@ The overlapping LCD mode has been changed to "Manual". The overlapping LCD mode has been changed to "Auto". + USB Devices + USB Devices + no USB devices found + %s (%04X:%04X) [%d,%d] + No driver + %s, Port %d + Vendor %04X, Product %04X + Failed to connect the USB serial device: device not found (check the settings). + Failed to connect the USB serial device: no driver for device (check the settings). + Failed to connect the USB serial device: not enough ports at device (check the settings). + Failed to connect the USB serial device: user has not given permission. + Failed to connect the USB serial device: for unknown reason. + Failed to connect the USB serial device: permission denied. + Failed to connect the USB serial device: open device failed. + Failed to connect the USB serial device: open failed. - General + General Authentic Calculator Speed Enable Virtual LCD Delay Automatically Save Files @@ -200,4 +215,8 @@ Use Real Replay Speed Manual Replay Speed + Serial Ports + Wire + Infrared (2400 baud) + diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml index 9c5c20b..b20b6ec 100644 --- a/app/src/main/res/xml/pref_general.xml +++ b/app/src/main/res/xml/pref_general.xml @@ -176,4 +176,17 @@ /> + + + + +