├── .gitignore
├── .idea
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── gradle.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── leonxtp
│ │ └── ymodemble
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── leonxtp
│ │ │ └── ymodemble
│ │ │ └── MainActivity.java
│ └── res
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── leonxtp
│ └── ymodemble
│ └── ExampleUnitTest.java
├── build.gradle
├── findbugs-android-exclude.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── leonxtp
│ │ └── library
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── leonxtp
│ │ │ └── library
│ │ │ ├── CRC16.java
│ │ │ ├── FileStreamThread.java
│ │ │ ├── InputStreamSource.java
│ │ │ ├── L.java
│ │ │ ├── SourceScheme.java
│ │ │ ├── TimeOutHelper.java
│ │ │ ├── YModem.java
│ │ │ ├── YModemListener.java
│ │ │ └── YModemUtil.java
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── leonxtp
│ └── library
│ └── ExampleUnitTest.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .DS_Store
5 | /build
6 | /captures
7 | .externalNativeBuild
8 |
9 | # built application files
10 | *.apk
11 |
12 | # files for the dex VM
13 | *.dex
14 |
15 | # Java class files
16 | *.class
17 |
18 | # generated files
19 | bin/
20 | gen/
21 |
22 | # Local configuration file (sdk path, etc)
23 | local.properties
24 |
25 | # Eclipse project files
26 | .classpath
27 | .project
28 |
29 | # Proguard folder generated by Eclipse
30 | proguard/
31 |
32 | # Intellij project files
33 | *.ipr
34 | *.iws
35 | .idea/
36 | .gradle/
37 |
38 | # Local configuration file (sdk path, etc)
39 |
40 | # Windows thumbnail db
41 | Thumbs.db
42 |
43 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
44 |
45 | build/
46 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
17 |
22 |
27 |
85 | * STX 08 F7 data[1000] CPMEOF[24] CRC CRC ----------------------->*
86 | * <---------------------------------------------------------------* ACK
87 | * EOT ----------------------------------------------------------->*
88 | * <---------------------------------------------------------------* ACK
89 | * SOH 00 FF ZERO[128] ------------------------------------------->*
90 | * <---------------------------------------------------------------* ACK
91 | * <---------------------------------------------------------------* MD5_OK
92 | ```
93 |
94 | ## Reference
95 | [Wikipedia YMODEM](https://en.wikipedia.org/wiki/YMODEM)
96 | [xmodem、ymodem、zmodem](http://web.cecs.pdx.edu/~rootd/catdoc/guide/TheGuide_226.html)
97 | [aesirot ymodem on github](https://github.com/aesirot/ymodem)
98 |
99 | ## License
100 |
101 | MIT
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 26
5 | buildToolsVersion "26.0.1"
6 | defaultConfig {
7 | applicationId "com.leonxtp.ymodemble"
8 | minSdkVersion 18
9 | targetSdkVersion 26
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(dir: 'libs', include: ['*.jar'])
24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
25 | exclude group: 'com.android.support', module: 'support-annotations'
26 | })
27 | compile 'com.android.support:appcompat-v7:26.+'
28 | compile 'com.android.support.constraint:constraint-layout:1.0.2'
29 | testCompile 'junit:junit:4.12'
30 |
31 | compile project(':library')
32 | }
33 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/leonxtp/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/leonxtp/ymodemble/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.leonxtp.ymodemble;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.leonxtp.ymodemble", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 | * Created by leonxtp on 2017/9/16. 13 | * Modified by leonxtp on 2017/9/16 14 | */ 15 | 16 | public class FileStreamThread extends Thread { 17 | 18 | private Context mContext; 19 | private InputStream inputStream = null; 20 | private DataRaderListener listener; 21 | private String filePath; 22 | private AtomicBoolean isDataAcknowledged = new AtomicBoolean(false); 23 | private boolean isKeepRunning = false; 24 | private int fileByteSize = 0; 25 | 26 | public FileStreamThread(Context mContext, String filePath, DataRaderListener listener) { 27 | this.mContext = mContext; 28 | this.filePath = filePath; 29 | this.listener = listener; 30 | } 31 | 32 | public int getFileByteSize() throws IOException { 33 | if (fileByteSize == 0 || inputStream == null) { 34 | initStream(); 35 | } 36 | return fileByteSize; 37 | } 38 | 39 | @Override 40 | public void run() { 41 | try { 42 | prepareData(); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | private void prepareData() throws IOException { 49 | initStream(); 50 | byte[] block = new byte[1024]; 51 | int dataLength; 52 | byte blockSequence = 1;//The data package of a file is actually started from 1 53 | isDataAcknowledged.set(true); 54 | isKeepRunning = true; 55 | while (isKeepRunning) { 56 | 57 | if (!isDataAcknowledged.get()) { 58 | try { 59 | //We need to sleep for a while as the sending 1024 bytes data from ble would take several seconds 60 | //In my circumstances, this can be up to 3 seconds. 61 | Thread.sleep(100); 62 | } catch (InterruptedException e) { 63 | e.printStackTrace(); 64 | } 65 | continue; 66 | } 67 | 68 | if ((dataLength = inputStream.read(block)) == -1) { 69 | L.f("The file data has all been read..."); 70 | if (listener != null) { 71 | onStop(); 72 | listener.onFinish(); 73 | } 74 | break; 75 | } 76 | 77 | byte[] packige = YModemUtil.getDataPackage(block, dataLength, blockSequence); 78 | 79 | if (listener != null) { 80 | listener.onDataReady(packige); 81 | } 82 | 83 | blockSequence++; 84 | isDataAcknowledged.set(false); 85 | } 86 | 87 | } 88 | 89 | /** 90 | * When received response from the terminal ,we should keep the thread keep going 91 | */ 92 | public void keepReading() { 93 | isDataAcknowledged.set(true); 94 | } 95 | 96 | public void release() { 97 | onStop(); 98 | listener = null; 99 | } 100 | 101 | private void onStop() { 102 | isKeepRunning = false; 103 | isDataAcknowledged.set(false); 104 | fileByteSize = 0; 105 | onReadFinished(); 106 | } 107 | 108 | private void initStream() { 109 | if (inputStream == null) { 110 | try { 111 | inputStream = YModemUtil.getInputStream(mContext, filePath); 112 | fileByteSize = inputStream.available(); 113 | } catch (IOException e) { 114 | e.printStackTrace(); 115 | } 116 | } 117 | } 118 | 119 | private void onReadFinished() { 120 | if (inputStream != null) { 121 | try { 122 | inputStream.close(); 123 | inputStream = null; 124 | } catch (IOException e) { 125 | e.printStackTrace(); 126 | } 127 | } 128 | } 129 | 130 | public interface DataRaderListener { 131 | void onDataReady(byte[] data); 132 | 133 | void onFinish(); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/InputStreamSource.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.BufferedInputStream; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * Get InputStream from different source, files from sd card/assets supported. 12 | *
13 | * Created by leonxtp on 2017/9/17. 14 | * Modified by leonxtp on 2017/9/17 15 | */ 16 | 17 | public class InputStreamSource { 18 | 19 | private static final int BUFFER_SIZE = 32 * 1024; 20 | private static final String ERROR_UNSUPPORTED_SCHEME = "Unsupported file source"; 21 | 22 | InputStream getStream(Context context, String imageUri) throws IOException { 23 | switch (SourceScheme.ofUri(imageUri)) { 24 | 25 | case FILE: 26 | return getStreamFromFile(imageUri); 27 | 28 | case ASSETS: 29 | return getStreamFromAssets(context, imageUri); 30 | 31 | case UNKNOWN: 32 | default: 33 | return getStreamFromOtherSource(imageUri); 34 | } 35 | } 36 | 37 | private InputStream getStreamFromFile(String fileUri) throws IOException { 38 | String filePath = SourceScheme.FILE.crop(fileUri); 39 | return new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE); 40 | } 41 | 42 | private InputStream getStreamFromAssets(Context context, String fileUri) throws IOException { 43 | String filePath = SourceScheme.ASSETS.crop(fileUri); 44 | return context.getAssets().open(filePath); 45 | } 46 | 47 | private InputStream getStreamFromOtherSource(String fileUri) throws IOException { 48 | throw new UnsupportedOperationException(String.format(ERROR_UNSUPPORTED_SCHEME, fileUri)); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/L.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | import android.util.Log; 4 | 5 | public class L { 6 | 7 | private static final String TAG = "YMODEM"; 8 | 9 | private L() { 10 | /* cannot be instantiated */ 11 | throw new UnsupportedOperationException("cannot be instantiated"); 12 | } 13 | 14 | private static final boolean DEBUGGING = true; 15 | 16 | public static void f(String msg) { 17 | if (DEBUGGING) { 18 | L.e(TAG, msg); 19 | } 20 | } 21 | 22 | // 下面四个是默认tag的函数 23 | public static void i(String msg) { 24 | if (DEBUGGING) 25 | L.i(TAG, msg); 26 | } 27 | 28 | public static void d(String msg) { 29 | if (DEBUGGING) 30 | L.d(TAG, msg); 31 | } 32 | 33 | public static void e(String msg) { 34 | if (DEBUGGING) 35 | L.e(TAG, msg); 36 | } 37 | 38 | public static void v(String msg) { 39 | if (DEBUGGING) 40 | L.v(TAG, msg); 41 | } 42 | 43 | public static void w(String msg) { 44 | if (DEBUGGING) 45 | L.w(TAG, msg); 46 | } 47 | 48 | // 下面是传入自定义tag的函数 49 | public static void i(String tag, String msg) { 50 | if (DEBUGGING) 51 | Log.i(tag, msg); 52 | } 53 | 54 | public static void d(String tag, String msg) { 55 | if (DEBUGGING) 56 | Log.d(tag, msg); 57 | } 58 | 59 | public static void e(String tag, String msg) { 60 | if (DEBUGGING) 61 | Log.e(tag, msg); 62 | } 63 | 64 | public static void v(String tag, String msg) { 65 | if (DEBUGGING) 66 | Log.v(tag, msg); 67 | } 68 | 69 | public static void w(String tag, String msg) { 70 | if (DEBUGGING) 71 | Log.w(tag, msg); 72 | } 73 | 74 | public static void e(String tag, String msg, Throwable throwable) { 75 | if (DEBUGGING) 76 | Log.e(tag, msg, throwable); 77 | } 78 | 79 | public static void wtf(String tag, String msg) { 80 | if (DEBUGGING) 81 | Log.wtf(tag, msg); 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/SourceScheme.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * Thanks to Universal-Image-Loader: 7 | * https://github.com/nostra13/Android-Universal-Image-Loader/blob/master/library/src/main/java/com/nostra13/universalimageloader/core/download/ImageDownloader.java 8 | */ 9 | public enum SourceScheme { 10 | 11 | FILE("file"), ASSETS("assets"), UNKNOWN(""); 12 | 13 | private String scheme; 14 | private String uriPrefix; 15 | 16 | SourceScheme(String scheme) { 17 | this.scheme = scheme; 18 | uriPrefix = scheme + "://"; 19 | } 20 | 21 | /** 22 | * Defines scheme of incoming URI 23 | * 24 | * @param uri URI for scheme detection 25 | * @return SourceScheme of incoming URI 26 | */ 27 | public static SourceScheme ofUri(String uri) { 28 | if (uri != null) { 29 | for (SourceScheme s : values()) { 30 | if (s.belongsTo(uri)) { 31 | return s; 32 | } 33 | } 34 | } 35 | return UNKNOWN; 36 | } 37 | 38 | private boolean belongsTo(String uri) { 39 | return uri.toLowerCase(Locale.US).startsWith(uriPrefix); 40 | } 41 | 42 | /** 43 | * Removed scheme part ("scheme://") from incoming URI 44 | */ 45 | public String crop(String uri) { 46 | if (!belongsTo(uri)) { 47 | throw new IllegalArgumentException(String.format("URI [%1$s] doesn't have expected scheme [%2$s]", uri, scheme)); 48 | } 49 | return uri.substring(uriPrefix.length()); 50 | } 51 | } -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/TimeOutHelper.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | import android.os.Handler; 4 | 5 | /** 6 | * A timer util for counting the time past after we sent a package to the terminal 7 | *
8 | * Created by leonxtp on 2017/9/17. 9 | * Modified by leonxtp on 2017/9/17 10 | */ 11 | 12 | public class TimeOutHelper { 13 | 14 | private ITimeOut listener; 15 | 16 | private Handler timeoutHanldler = new Handler(); 17 | 18 | private Runnable timer = new Runnable() { 19 | 20 | @Override 21 | public void run() { 22 | stopTimer(); 23 | if (listener != null) { 24 | listener.onTimeOut(); 25 | } 26 | } 27 | }; 28 | 29 | public void startTimer(ITimeOut timeoutListener, long delay) { 30 | listener = timeoutListener; 31 | timeoutHanldler.postDelayed(timer, delay); 32 | } 33 | 34 | public void stopTimer() { 35 | timeoutHanldler.removeCallbacksAndMessages(null); 36 | } 37 | 38 | public void unRegisterListener() { 39 | listener = null; 40 | } 41 | 42 | public interface ITimeOut { 43 | void onTimeOut(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/YModem.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * THE YMODEM: 9 | * *SENDER: ANDROID APP *------------------------------------------* RECEIVER: BLE DEVICE* 10 | * HELLO BOOTLOADER ---------------------------------------------->* 11 | * <---------------------------------------------------------------* C 12 | * SOH 00 FF filename0fileSizeInByte0MD5[90] ZERO[38] CRC CRC----->* 13 | * <---------------------------------------------------------------* ACK C 14 | * STX 01 FE data[1024] CRC CRC ---------------------------------->* 15 | * <---------------------------------------------------------------* ACK 16 | * STX 02 FF data[1024] CRC CRC ---------------------------------->* 17 | * <---------------------------------------------------------------* ACK 18 | * ... 19 | * ... 20 | *
21 | * STX 08 F7 data[1000] CPMEOF[24] CRC CRC ----------------------->* 22 | * <---------------------------------------------------------------* ACK 23 | * EOT ----------------------------------------------------------->* 24 | * <---------------------------------------------------------------* ACK 25 | * SOH 00 FF ZERO[128] ------------------------------------------->* 26 | * <---------------------------------------------------------------* ACK 27 | * <---------------------------------------------------------------* MD5_OK 28 | *
29 | * Created by leonxtp on 2017/9/16. 30 | * Modified by leonxtp on 2017/9/16 31 | */ 32 | 33 | public class YModem implements FileStreamThread.DataRaderListener { 34 | 35 | private static final int STEP_HELLO = 0x00; 36 | private static final int STEP_FILE_NAME = 0x01; 37 | private static final int STEP_FILE_BODY = 0x02; 38 | private static final int STEP_EOT = 0x03; 39 | private static final int STEP_END = 0x04; 40 | private static int CURR_STEP = STEP_HELLO; 41 | 42 | private static final byte ACK = 0x06; /* ACKnowlege */ 43 | private static final byte NAK = 0x15; /* Negative AcKnowlege */ 44 | private static final byte CAN = 0x18; /* CANcel character */ 45 | private static final byte ST_C = 'C'; 46 | private static final String MD5_OK = "MD5_OK"; 47 | private static final String MD5_ERR = "MD5_ERR"; 48 | 49 | private Context mContext; 50 | private String filePath; 51 | private String fileNameString = "LPK001_Android"; 52 | private String fileMd5String = "63e7bb6eed1de3cece411a7e3e8e763b"; 53 | private YModemListener listener; 54 | 55 | private TimeOutHelper timerHelper = new TimeOutHelper(); 56 | private FileStreamThread streamThread; 57 | 58 | //bytes has been sent of this transmission 59 | private int bytesSent = 0; 60 | //package data of current sending, used for int case of fail 61 | private byte[] currSending = null; 62 | private int packageErrorTimes = 0; 63 | private static final int MAX_PACKAGE_SEND_ERROR_TIMES = 6; 64 | //the timeout interval for a single package 65 | private static final int PACKAGE_TIME_OUT = 6000; 66 | 67 | /** 68 | * Construct of the YModemBLE,you may don't need the fileMD5 checking,remove it 69 | * 70 | * @param filePath absolute path of the file 71 | * @param fileNameString file name for sending to the terminal 72 | * @param fileMd5String md5 for terminal checking after transmission finished 73 | * @param listener 74 | */ 75 | public YModem(Context context, String filePath, 76 | String fileNameString, String fileMd5String, 77 | YModemListener listener) { 78 | this.filePath = filePath; 79 | this.fileNameString = fileNameString; 80 | this.fileMd5String = fileMd5String; 81 | this.mContext = context; 82 | this.listener = listener; 83 | } 84 | 85 | /** 86 | * Start the transmission 87 | */ 88 | public void start() { 89 | sayHello(); 90 | } 91 | 92 | /** 93 | * Stop the transmission when you don't need it or shut it down in accident 94 | */ 95 | public void stop() { 96 | bytesSent = 0; 97 | currSending = null; 98 | packageErrorTimes = 0; 99 | if (streamThread != null) { 100 | streamThread.release(); 101 | } 102 | timerHelper.stopTimer(); 103 | timerHelper.unRegisterListener(); 104 | } 105 | 106 | /** 107 | * Method for the outer caller when received data from the terminal 108 | */ 109 | public void onReceiveData(byte[] respData) { 110 | //Stop the package timer 111 | timerHelper.stopTimer(); 112 | if (respData != null && respData.length > 0) { 113 | L.f("YModem received " + respData.length + " bytes."); 114 | switch (CURR_STEP) { 115 | case STEP_HELLO: 116 | handleHello(respData); 117 | break; 118 | case STEP_FILE_NAME: 119 | handleFileName(respData); 120 | break; 121 | case STEP_FILE_BODY: 122 | handleFileBody(respData); 123 | break; 124 | case STEP_EOT: 125 | handleEOT(respData); 126 | break; 127 | case STEP_END: 128 | handleEnd(respData); 129 | break; 130 | default: 131 | break; 132 | } 133 | } else { 134 | L.f("The terminal do responsed something, but received nothing??"); 135 | } 136 | } 137 | 138 | /** 139 | * ============================================================================== 140 | * Methods for sending data begin 141 | * ============================================================================== 142 | */ 143 | private void sayHello() { 144 | streamThread = new FileStreamThread(mContext, filePath, this); 145 | CURR_STEP = STEP_HELLO; 146 | L.f("sayHello!!!"); 147 | byte[] hello = YModemUtil.getYModelHello(); 148 | sendPackageData(hello); 149 | } 150 | 151 | private void sendFileName() { 152 | CURR_STEP = STEP_FILE_NAME; 153 | L.f("sendFileName"); 154 | try { 155 | int fileByteSize = streamThread.getFileByteSize(); 156 | byte[] fileNamePackage = YModemUtil.getFileNamePackage(fileNameString, fileByteSize 157 | , fileMd5String); 158 | sendPackageData(fileNamePackage); 159 | } catch (IOException e) { 160 | e.printStackTrace(); 161 | } 162 | } 163 | 164 | private void startSendFileData() { 165 | CURR_STEP = STEP_FILE_BODY; 166 | L.f("startSendFileData"); 167 | streamThread.start(); 168 | } 169 | 170 | //Callback from the data reading thread when a data package is ready 171 | @Override 172 | public void onDataReady(byte[] data) { 173 | sendPackageData(data); 174 | } 175 | 176 | private void sendEOT() { 177 | CURR_STEP = STEP_EOT; 178 | L.f("sendEOT"); 179 | if (listener != null) { 180 | listener.onDataReady(YModemUtil.getEOT()); 181 | } 182 | } 183 | 184 | private void sendEND() { 185 | CURR_STEP = STEP_END; 186 | L.f("sendEND"); 187 | if (listener != null) { 188 | try { 189 | listener.onDataReady(YModemUtil.getEnd()); 190 | } catch (IOException e) { 191 | e.printStackTrace(); 192 | } 193 | } 194 | } 195 | 196 | private void sendPackageData(byte[] packageData) { 197 | if (listener != null && packageData != null) { 198 | currSending = packageData; 199 | //Start the timer, it will be cancelled when reponse received, 200 | // or trigger the timeout and resend the current package data 201 | timerHelper.startTimer(timeoutListener, PACKAGE_TIME_OUT); 202 | listener.onDataReady(packageData); 203 | } 204 | } 205 | 206 | /** 207 | * ============================================================================== 208 | * Method for handling the response of a package 209 | * ============================================================================== 210 | */ 211 | private void handleHello(byte[] value) { 212 | int character = value[0]; 213 | if (character == ST_C) {//Receive "C" for "HELLO" 214 | L.f("Received 'C'"); 215 | packageErrorTimes = 0; 216 | sendFileName(); 217 | } else { 218 | handleOthers(character); 219 | } 220 | } 221 | 222 | //The file name package was responsed 223 | private void handleFileName(byte[] value) { 224 | if (value.length == 2 && value[0] == ACK && value[1] == ST_C) {//Receive 'ACK C' for file name 225 | L.f("Received 'ACK C'"); 226 | packageErrorTimes = 0; 227 | startSendFileData(); 228 | } else if (value[0] == ST_C) {//Receive 'C' for file name, this package should be resent 229 | L.f("Received 'C'"); 230 | handlePackageFail("Received 'C' without 'ACK' after sent file name"); 231 | } else { 232 | handleOthers(value[0]); 233 | } 234 | } 235 | 236 | private void handleFileBody(byte[] value) { 237 | if (value.length == 1 && value[0] == ACK) {//Receive ACK for file data 238 | L.f("Received 'ACK'"); 239 | packageErrorTimes = 0; 240 | bytesSent += currSending.length; 241 | try { 242 | if (listener != null) { 243 | listener.onProgress(bytesSent, streamThread.getFileByteSize()); 244 | } 245 | } catch (IOException e) { 246 | e.printStackTrace(); 247 | } 248 | streamThread.keepReading(); 249 | 250 | } else if (value.length == 1 && value[0] == ST_C) { 251 | L.f("Received 'C'"); 252 | //Receive C for file data, the ymodem cannot handle this circumstance, transmission failed... 253 | handlePackageFail("Received 'C' after sent file data"); 254 | } else { 255 | handleOthers(value[0]); 256 | } 257 | } 258 | 259 | private void handleEOT(byte[] value) { 260 | if (value[0] == ACK) { 261 | L.f("Received 'ACK'"); 262 | packageErrorTimes = 0; 263 | sendEND(); 264 | } else if (value[0] == ST_C) {//As we haven't received ACK, we should resend EOT 265 | handlePackageFail("Received 'C' after sent EOT"); 266 | } else { 267 | handleOthers(value[0]); 268 | } 269 | } 270 | 271 | private void handleEnd(byte[] character) { 272 | if (character[0] == ACK) {//The last ACK represents that the transmission has been finished, but we should validate the file 273 | L.f("Received 'ACK'"); 274 | packageErrorTimes = 0; 275 | } else if ((new String(character)).equals(MD5_OK)) {//The file data has been checked,Well Done! 276 | L.f("Received 'MD5_OK'"); 277 | stop(); 278 | if (listener != null) { 279 | listener.onSuccess(); 280 | } 281 | } else if ((new String(character)).equals(MD5_ERR)) {//Oops...Transmission Failed... 282 | L.f("Received 'MD5_ERR'"); 283 | stop(); 284 | if (listener != null) { 285 | listener.onFailed("MD5 check failed!!!"); 286 | } 287 | } else { 288 | handleOthers(character[0]); 289 | } 290 | } 291 | 292 | private void handleOthers(int character) { 293 | if (character == NAK) {//We need to resend this package as the terminal failed when checking the crc 294 | L.f("Received 'NAK'"); 295 | handlePackageFail("Received NAK"); 296 | } else if (character == CAN) {//Some big problem occurred, transmission failed... 297 | L.f("Received 'CAN'"); 298 | if (listener != null) { 299 | listener.onFailed("Received CAN"); 300 | } 301 | stop(); 302 | } 303 | } 304 | 305 | //Handle a failed package data ,resend it up to MAX_PACKAGE_SEND_ERROR_TIMES times. 306 | //If still failed, then the transmission failed. 307 | private void handlePackageFail(String reason) { 308 | packageErrorTimes++; 309 | L.f("Fail:" + reason + " for " + packageErrorTimes + " times"); 310 | if (packageErrorTimes < MAX_PACKAGE_SEND_ERROR_TIMES) { 311 | sendPackageData(currSending); 312 | } else { 313 | //Still, we stop the transmission, release the resources 314 | stop(); 315 | if (listener != null) { 316 | listener.onFailed(reason); 317 | } 318 | } 319 | } 320 | 321 | /* The InputStream data reading thread was done */ 322 | @Override 323 | public void onFinish() { 324 | sendEOT(); 325 | } 326 | 327 | //The timeout listener 328 | private TimeOutHelper.ITimeOut timeoutListener = new TimeOutHelper.ITimeOut() { 329 | @Override 330 | public void onTimeOut() { 331 | L.f("------ time out ------"); 332 | if (currSending != null) { 333 | handlePackageFail("package timeout..."); 334 | } 335 | } 336 | }; 337 | 338 | public static class Builder { 339 | private Context context; 340 | private String filePath; 341 | private String fileNameString; 342 | private String fileMd5String; 343 | private YModemListener listener; 344 | 345 | public Builder with(Context context) { 346 | this.context = context; 347 | return this; 348 | } 349 | 350 | public Builder filePath(String filePath) { 351 | this.filePath = filePath; 352 | return this; 353 | } 354 | 355 | public Builder fileName(String fileName) { 356 | this.fileNameString = fileName; 357 | return this; 358 | } 359 | 360 | public Builder checkMd5(String fileMd5String) { 361 | this.fileMd5String = fileMd5String; 362 | return this; 363 | } 364 | 365 | public Builder callback(YModemListener listener) { 366 | this.listener = listener; 367 | return this; 368 | } 369 | 370 | public YModem build() { 371 | return new YModem(context, filePath, fileNameString, fileMd5String, listener); 372 | } 373 | 374 | } 375 | 376 | } 377 | -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/YModemListener.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | /** 4 | * Listener of the transmission process 5 | */ 6 | public interface YModemListener { 7 | 8 | /* the data package has been encapsulated */ 9 | void onDataReady(byte[] data); 10 | 11 | /*just the file data progress*/ 12 | void onProgress(int currentSent, int total); 13 | 14 | /* the file has been correctly sent to the terminal */ 15 | void onSuccess(); 16 | 17 | /* the task has failed with several remedial measures like retrying some times*/ 18 | void onFailed(String reason); 19 | 20 | } -------------------------------------------------------------------------------- /library/src/main/java/com/leonxtp/library/YModemUtil.java: -------------------------------------------------------------------------------- 1 | package com.leonxtp.library; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.DataOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.util.Arrays; 10 | 11 | /** 12 | * Util for encapsulating data package of ymodem protocol 13 | *
14 | * Created by leonxtp on 2017/9/16.
15 | * Modified by leonxtp on 2017/9/16
16 | */
17 |
18 | public class YModemUtil {
19 |
20 | /*This is my concrete ymodem start signal, customise it to your needs*/
21 | private static final String HELLO = "HELLO BOOTLOADER";
22 |
23 | private static final byte SOH = 0x01; /* Start Of Header with data size :128*/
24 | private static final byte STX = 0x02; /* Start Of Header with data size : 1024*/
25 | private static final byte EOT = 0x04; /* End Of Transmission */
26 | private static final byte CPMEOF = 0x1A;/* Fill the last package if not long enough */
27 |
28 | private static CRC16 crc16 = new CRC16();
29 |
30 | /**
31 | * Get the first package data for hello with a terminal
32 | */
33 | public static byte[] getYModelHello() {
34 | return HELLO.getBytes();
35 | }
36 |
37 | /**
38 | * Get the file name package data
39 | *
40 | * @param fileNameString file name in String
41 | * @param fileByteSize file byte size of int value
42 | * @param fileMd5String the md5 of the file in String
43 | */
44 | public static byte[] getFileNamePackage(String fileNameString,
45 | int fileByteSize,
46 | String fileMd5String) throws IOException {
47 |
48 | byte seperator = 0x0;
49 | String fileSize = fileByteSize + "";
50 | byte[] byteFileSize = fileSize.getBytes();
51 |
52 | byte[] fileNameBytes1 = concat(fileNameString.getBytes(),
53 | new byte[]{seperator},
54 | byteFileSize);
55 |
56 | byte[] fileNameBytes2 = Arrays.copyOf(concat(fileNameBytes1,
57 | new byte[]{seperator},
58 | fileMd5String.getBytes()), 128);
59 |
60 | byte seq = 0x00;
61 | return getDataPackage(fileNameBytes2, 128, seq);
62 | }
63 |
64 | /**
65 | * Get a encapsulated package data block
66 | *
67 | * @param block byte data array
68 | * @param dataLength the actual content length in the block without 0 filled in it.
69 | * @param sequence the package serial number
70 | * @return a encapsulated package data block
71 | */
72 | public static byte[] getDataPackage(byte[] block, int dataLength, byte sequence) throws IOException {
73 |
74 | byte[] header = getDataHeader(sequence, block.length == 1024 ? STX : SOH);
75 |
76 | //The last package, fill CPMEOF if the dataLength is not sufficient
77 | if (dataLength < block.length) {
78 | int startFil = dataLength;
79 | while (startFil < block.length) {
80 | block[startFil] = CPMEOF;
81 | startFil++;
82 | }
83 | }
84 |
85 | //We should use short size when writing into the data package as it only needs 2 bytes
86 | short crc = (short) crc16.calcCRC(block);
87 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
88 | DataOutputStream dos = new DataOutputStream(baos);
89 | dos.writeShort(crc);
90 | dos.close();
91 |
92 | byte[] crcBytes = baos.toByteArray();
93 |
94 | return concat(header, block, crcBytes);
95 | }
96 |
97 | /**
98 | * Get the EOT package
99 | */
100 | public static byte[] getEOT() {
101 | return new byte[]{EOT};
102 | }
103 |
104 | /**
105 | * Get the Last package
106 | */
107 | public static byte[] getEnd() throws IOException {
108 | byte seq = 0x00;
109 | return getDataPackage(new byte[128], 128, seq);
110 | }
111 |
112 | /**
113 | * Get InputStream from Assets, you can customize it from the other sources
114 | *
115 | * @param fileAbsolutePath absolute path of the file in asstes
116 | */
117 | public static InputStream getInputStream(Context context, String fileAbsolutePath) throws IOException {
118 | return new InputStreamSource().getStream(context, fileAbsolutePath);
119 | }
120 |
121 | private static byte[] getDataHeader(byte sequence, byte start) {
122 | //The serial number of the package increases Cyclically up to 256
123 | byte modSequence = (byte) (sequence % 0x256);
124 | byte complementSeq = (byte) ~modSequence;
125 |
126 | return concat(new byte[]{start},
127 | new byte[]{modSequence},
128 | new byte[]{complementSeq});
129 | }
130 |
131 | private static byte[] concat(byte[] a, byte[] b, byte[] c) {
132 | int aLen = a.length;
133 | int bLen = b.length;
134 | int cLen = c.length;
135 | byte[] concated = new byte[aLen + bLen + cLen];
136 | System.arraycopy(a, 0, concated, 0, aLen);
137 | System.arraycopy(b, 0, concated, aLen, bLen);
138 | System.arraycopy(c, 0, concated, aLen + bLen, cLen);
139 | return concated;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |