Repository: yutils/YSerialPort Branch: master Commit: 4394d2b66f63 Files: 86 Total size: 885.6 KB Directory structure: gitextract_r9qdw748/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── doc/ │ │ └── yujingtest.jks │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── yujing/ │ │ └── chuankou/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── yujing/ │ │ │ └── chuankou/ │ │ │ ├── App.kt │ │ │ ├── activity/ │ │ │ │ ├── MainActivity.java │ │ │ │ ├── SendActivity.java │ │ │ │ ├── myTest/ │ │ │ │ │ ├── MyMainActivity.java │ │ │ │ │ ├── SendFileActivity.java │ │ │ │ │ └── SyncActivity.java │ │ │ │ └── wifi/ │ │ │ │ ├── SerialPortToWiFiActivity.kt │ │ │ │ └── socket/ │ │ │ │ ├── ClientSocket.kt │ │ │ │ └── Server.kt │ │ │ ├── base/ │ │ │ │ ├── BaseActivity.java │ │ │ │ └── KBaseActivity.kt │ │ │ ├── config/ │ │ │ │ └── Config.kt │ │ │ └── utils/ │ │ │ └── Setting.java │ │ ├── res/ │ │ │ ├── drawable/ │ │ │ │ ├── bt_click.xml │ │ │ │ ├── item_click.xml │ │ │ │ └── set_selector.xml │ │ │ ├── layout/ │ │ │ │ ├── activity_main.xml │ │ │ │ ├── activity_my_main.xml │ │ │ │ ├── activity_send.xml │ │ │ │ ├── activity_send_file.xml │ │ │ │ ├── activity_serialport_to_wifi.xml │ │ │ │ └── view_set.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ └── xml/ │ │ │ ├── file_paths.xml │ │ │ ├── network_config.xml │ │ │ └── provider_paths.xml │ │ └── res_screen/ │ │ ├── values/ │ │ │ └── dimens.xml │ │ ├── values-sw1080dp/ │ │ │ └── dimens.xml │ │ ├── values-sw320dp/ │ │ │ └── dimens.xml │ │ ├── values-sw332dp/ │ │ │ └── dimens.xml │ │ ├── values-sw345dp/ │ │ │ └── dimens.xml │ │ ├── values-sw360dp/ │ │ │ └── dimens.xml │ │ ├── values-sw375dp/ │ │ │ └── dimens.xml │ │ ├── values-sw392dp/ │ │ │ └── dimens.xml │ │ ├── values-sw411dp/ │ │ │ └── dimens.xml │ │ ├── values-sw432dp/ │ │ │ └── dimens.xml │ │ ├── values-sw454dp/ │ │ │ └── dimens.xml │ │ ├── values-sw480dp/ │ │ │ └── dimens.xml │ │ ├── values-sw508dp/ │ │ │ └── dimens.xml │ │ ├── values-sw540dp/ │ │ │ └── dimens.xml │ │ ├── values-sw576dp/ │ │ │ └── dimens.xml │ │ ├── values-sw617dp/ │ │ │ └── dimens.xml │ │ ├── values-sw664dp/ │ │ │ └── dimens.xml │ │ ├── values-sw720dp/ │ │ │ └── dimens.xml │ │ ├── values-sw785dp/ │ │ │ └── dimens.xml │ │ ├── values-sw864dp/ │ │ │ └── dimens.xml │ │ └── values-sw960dp/ │ │ └── dimens.xml │ └── test/ │ └── java/ │ └── com/ │ └── yujing/ │ └── chuankou/ │ ├── ExampleUnitTest.java │ └── Values_sw.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── yserialport/ ├── .gitignore ├── CMakeLists.txt ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src/ └── main/ ├── AndroidManifest.xml ├── cpp/ │ ├── YSerialPort.c │ ├── YSerialPort.h │ └── gen_YSerialPort_h.sh ├── java/ │ └── com/ │ └── yujing/ │ ├── serialport/ │ │ ├── SerialPort.java │ │ └── SerialPortFinder.java │ └── yserialport/ │ ├── DataListener.java │ ├── InputStreamReadListener.java │ ├── ThreadMode.kt │ ├── YBytes.java │ ├── YListener.java │ ├── YReadInputStream.java │ └── YSerialPort.java └── res/ └── values/ └── strings.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea .DS_Store /build /captures .externalNativeBuild /gradles/ ================================================ FILE: README.md ================================================ # YSerialPort 源Android-SerialPort-API,重新封装代码,实现读取串口数据,实现重新组包一次性读取完整数据。可连续读取任意长度数据。 重写串口so库名称和调用函数名称,不与其他串口工具形成so冲突或者类名冲突。 已经多次长时间测试:串口打印机,PLC通信,串口电子秤,串口条码读卡器,串口二维码读卡器,串口LED屏,串口NFC读卡器,读M1区一次性读取64个扇区,读CPU区一次读取16KB数据。 理论上兼容 安卓4.4~安卓14.0 2.2.7之后版本最低支撑安卓4.4(api19) [![platform](https://img.shields.io/badge/platform-Android-lightgrey.svg)](https://developer.android.google.cn/studio/index.html) ![Gradle](https://img.shields.io/badge/Gradle-8.14.1-brightgreen.svg) [![last commit](https://img.shields.io/github/last-commit/yutils/YSerialPort.svg)](https://github.com/yutils/YSerialPort/commits/master) ![repo size](https://img.shields.io/github/repo-size/yutils/YSerialPort.svg) ![android studio](https://img.shields.io/badge/android%20studio-2024.3.2-green.svg) [![maven](https://img.shields.io/badge/maven-address-green.svg)](https://search.maven.org/artifact/com.kotlinx/yserialport) **[releases里面有APK文件。点击前往](https://github.com/yutils/YSerialPort/releases)** **[releases里面有AAR包。点击前往](https://github.com/yutils/YSerialPort/releases)** **如果拉取整个项目,请用最新AS(AS2024.3.2以上)打开** # `不建议直接拉取项目编译,请仔细看完 ` # ## Gradle采用java17 ### 设置方法: Project Structure ----> SDK Location ----> JDK location was moved to Gradle Settings. ----> Gradle JDK ----> JDK17 ## 已经从jitpack.io仓库移动至maven中央仓库 ## 引用 ### [添加依赖,当前最新版:————> 2.2.8    ![最新版](https://img.shields.io/badge/%E6%9C%80%E6%96%B0%E7%89%88-2.2.7-green.svg)](https://search.maven.org/artifact/com.kotlinx/yserialport) ``` dependencies { //更新地址 https://github.com/yutils/YSerialPort 建议过几天访问看下有没有新版本 implementation 'com.kotlinx:yserialport:2.2.8' } ``` 注:如果引用失败,看下面方案 ``` allprojects { repositories { google() //mavenCentral() //如果还是不容易拉取,可以试试直接用maven.org maven { url 'https://repo1.maven.org/maven2' } } ``` # 使用方法 可以参考SendActivity.java ## 基础使用方法 ```java YSerialPort ySerialPort=new YSerialPort(this,"/dev/ttyS4","9600"); //设置数据监听 ySerialPort.addDataListener(new DataListener(){ @Override public void value(String hexString,byte[]bytes){ //结果回调:haxString , bytes } }); ySerialPort.start(); ``` ## 扩展使用方法 **java** **同步** ```java //拿流用法,自己通过流收发数据 SerialPort serialPort = SerialPort.newBuilder(new File("/dev/ttyS4"), 9600) .parity(0) // 校验位;0:无校验位(NONE,默认);1:奇校验位(ODD);2:偶校验位(EVEN) .dataBits(8) // 数据位,默认8;可选值为5~8 .stopBits(1) // 停止位,默认1;1:1位停止位;2:2位停止位 .build(); serialPort.getInputStream();//获取输入流 serialPort.getOutputStream();//获取输出流 serialPort.tryClose();//关闭 //同步收发 (不用每次都创建serialPort对象) SerialPort serialPort=SerialPort.newBuilder(new File("/dev/ttyS4"),9600).build(); byte[] bytes=YSerialPort.sendSyncOnce(serialPort,bys,1000); byte[] bytes=YSerialPort.sendSyncTime(serialPort,bys,20,1000); byte[] bytes=YSerialPort.sendSyncLength(serialPort,bys,20,1000); serialPort.tryClose();//关闭 //发送并等待返回,死等 byte[] bytes=YSerialPort.sendSyncOnce("/dev/ttyS4","9600",bytes); //发送并等待返回,直到超时,如果超时则向上抛异常 byte[] bytes=YSerialPort.sendSyncOnce("/dev/ttyS4","9600",bytes,500); //一直不停组包,(maxGroupTime每次组包时间)当在maxGroupTime时间内没有数据,就返回并关闭连接 byte[] bytes=YSerialPort.sendSyncTime("/dev/ttyS4","9600",bytes,500); //一直不停组包,(maxGroupTime每次组包时间)当在maxGroupTime时间内没有数据,就返回并关闭连接(如果一直有数据,最多接收时间为maxTime) byte[] bytes=YSerialPort.sendSyncTime("/dev/ttyS4","9600",bytes,500,3000); //一直不停组包,当数据长度达到minLength或超时,返回并关闭连接 byte[] bytes=YSerialPort.sendSyncLength("/dev/ttyS4","9600",bytes,500,3000); ``` **异步** ```java //String[] device = YSerialPort.getDevices();//获取串口列表 //String[] baudRate = YSerialPort.getBaudRates();//获取波特率列表 //YSerialPort.saveDevice(getApplication(), "/dev/ttyS4");//设置默认串口,可以不设置 //String device=YSerialPort.readDevice(getApplication());//获取上面设置的串口 //YSerialPort.saveBaudRate(getApplication(), "9600");//设置默认波特率,可以不设置 //String baudRate=YSerialPort.readBaudRate(getApplication());//获取上面设置的波特率 //异步收发 YSerialPort ySerialPort=new YSerialPort(this,"/dev/ttyS4","9600"); //设置数据监听 ySerialPort.addDataListener(new DataListener(){ @Override public void value(String hexString,byte[]bytes){ //结果回调:haxString , bytes } }); //设置回调线程为主线程,默认主线程 ySerialPort.setThreadMode(ThreadMode.MAIN); //设置自动组包,每次组包时长为40毫秒,如果40毫秒读取不到数据则返回结果 ySerialPort.setToAuto(); //ySerialPort.setToAuto(40); //或者,设置手动组包,读取长度100,超时时间为50毫秒。如果读取到数据大于等于100立即返回,否则直到读取到超时为止 //ySerialPort.setToManual(100,50); //启动 ySerialPort.start(); //发送文字 ySerialPort.send("你好".getBytes(Charset.forName("GB18030"))); //退出页面时候注销 @Override protected void onDestroy(){ super.onDestroy(); ySerialPort.onDestroy(); } //如果要自己解析inputStream,请在start()之前实现此方法 ySerialPort.setInputStreamReadListener(inputStream->{ int count=0; while(count==0) count=inputStream.available(); byte[]bytes=new byte[count]; //readCount,已经成功读取的字节的个数,这儿需读取count个数据,不够则循环读取,如果采用inputStream.read(bytes);可能读不完 int readCount=0; while(readCount //结果回调:haxString , bytes } //设置自动组包,每次组包时长为40毫秒,如果40毫秒读取不到数据则返回结果 ySerialPort.setToAuto() //ySerialPort.setToAuto(40) //或者,设置手动组包,读取长度100,超时时间为50毫秒。如果读取到数据大于等于100立即返回,否则直到读取到超时为止 //ySerialPort.setToManual(100,50) //启动 ySerialPort.start() //发送文字 ySerialPort.send("你好".toByteArray(Charset.forName("GB18030"))) //退出页面时候注销 override fun onDestroy() { super.onDestroy() ySerialPort.onDestroy() } ``` ## 代码混淆 ``` -keepclasseswithmembernames class * { native ; } #-keep class com.yujing.** { *; } -keep class com.yujing.serialport.* { *; } -keep class com.yujing.yserialport.* { *; } ``` ## 根据协议头组包完整示例 ```kotlin //原理就是准备一个字符串,每次都数据都拼接到后面,然后判断协议头是否正确?(对了就取对应长度数据然后剪掉用过的数据长度):(如果不对就剪掉协议头数据,重新开始) class Test { var ySerialPort: YSerialPort? = null //log var showLog = false //最后一次组包剩余数据 private var oldSurplus = "" fun test() { ySerialPort = YSerialPort(context, "/dev/ttyS2", "9600") ySerialPort?.addDataListener { hexString, bytes -> dataFilter(hexString) } ySerialPort?.start() } //拆包组包,举例:数据包:02 2B 30 30 30 30 30 30 30 31 42 03 //其中:协议头 02 2B正2D负 6位重量 1位小数点位数 2位校验(2B到30) 03结束位 @Synchronized fun dataFilter(hexStr: String) { //先组装上一次剩余数据 oldSurplus += hexStr.replace(" ", "") if (oldSurplus.isEmpty()) return //定义一个包长度 var dataLength = 24 //判断长度 if (oldSurplus.length < dataLength) { if (showLog) YLog.d("数据长度不够,等一手!$oldSurplus") return } //验证协议头,非02开头就抛弃 if (oldSurplus.substring(0, 2) != "02") { if (showLog) YLog.d("数据异常,抛弃数据头!$oldSurplus") //剪掉数据头,如果剩余数据大于一个完整包,递归一次 oldSurplus = oldSurplus.substring(2) if (oldSurplus.length >= dataLength) dataFilter("") return } //如果有数据长度位,获取长度位+包头+包尾=包长,dataLength重新赋值新的包长 //dataLength=数据长度+包头+包尾 //验证协议尾,同上 //验证校验码 //...... //获取数据处理 【这儿就是一个完整的数据包!】 【这儿就是一个完整的数据包!】 【这儿就是一个完整的数据包!】 handle(YConvert.hexStringToByte(oldSurplus.substring(0, dataLength))) //剪去用过的数据 oldSurplus = oldSurplus.substring(dataLength) //剩余数据可能还能有完整包。所以递归一次 if (oldSurplus.length >= dataLength) dataFilter("") } //处理数据。dataByteArray的长度应该是8 private fun handle(dataByteArray: ByteArray) { //处理分析,转换成对象obj //..... //通知前端 //YBusUtil.post("某某设备发送的数据", obj) } //退出页面时候注销 fun onDestroy() { ySerialPort?.onDestroy() } } ``` 串口文件位置:/proc/tty/drivers 感谢:[Android-SerialPort-API](https://github.com/licheedev/Android-SerialPort-API) 不懂的问我QQ:3373217 (别问为啥手机没串口,别问模拟器怎么调试串口(可以映射),别问USB转的串口为什么不能识别) 如果是USB转串口,请参考这个工程: [CH340/CH341的USB转串口](https://github.com/yutils/CH34xUART) ================================================ FILE: app/.gitignore ================================================ /build /release ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'kotlin-parcelize' } android { compileSdk 34 namespace 'com.yujing.chuankou' defaultConfig { applicationId "com.yujing.chuankou" minSdk 19 targetSdk 34 versionCode 28 versionName "2.2.8" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true } //签名 signingConfigs { debug { storeFile file("doc/yujingtest.jks") storePassword "123456" keyAlias "test" keyPassword "123456" } release { storeFile file("doc/yujingtest.jks") storePassword "123456" keyAlias "test" keyPassword "123456" } } buildTypes { debug { //app名称 manifestPlaceholders = ["app_name": "@string/app_name_debug"] //包名后缀 //applicationIdSuffix ".debug" //版本名后缀 //versionNameSuffix "-debug" //混淆 minifyEnabled false //Zipalign优化 zipAlignEnabled false // 移除无用的resource文件 shrinkResources false //签名配置 signingConfig signingConfigs.debug } release { //app名称 manifestPlaceholders = ["app_name": "@string/app_name"] //混淆 minifyEnabled true //Zipalign优化 zipAlignEnabled false //移除无用的resource文件 shrinkResources false //签名配置 signingConfig signingConfigs.release //前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明,后一个文件是自己的定义混淆文件 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } //打包 android.applicationVariants.configureEach { variant -> //def appName = variant.productFlavors[0].name //获取渠道名称 def appName = "YSerialPort" //rootProject.name def createDate = new Date().format("YYYYMMdd", TimeZone.getTimeZone("GMT+08:00")) def createTime = new Date().format("HHmmss", TimeZone.getTimeZone("GMT+08:00")) variant.outputs.configureEach { it.outputFileName = "${appName}_v${versionName}_${versionCode}_${createDate}_${createTime}_${buildType.name}.apk" } } //资源合并 sourceSets { main { res.srcDirs = ['src/main/res', 'src/main/res_screen'] } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } buildFeatures { dataBinding = true } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.6.1'//再高 minSdk 就不能是 19 了 implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' //再高 minSdk 就不能是 19 了 implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' implementation 'com.kotlinx:yutils:2.2.5' //bugly,异常采集 implementation 'com.tencent.bugly:crashreport:4.1.9.3' //gson https://central.sonatype.com/artifact/com.google.code.gson/gson //implementation 'com.google.code.gson:gson:2.11.0' //implementation 'com.kotlinx:yserialport:2.2.7' implementation project(':yserialport') } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider -dontwarn org.conscrypt.Conscrypt$Version -dontwarn org.conscrypt.Conscrypt -dontwarn org.conscrypt.ConscryptHostnameVerifier -dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE -keepclasseswithmembernames class * { native ; } -keep class com.yujing.** { *; } #-keep class com.yujing.serialport.* { *; } #-keep class com.yujing.yserialport.* { *; } ================================================ FILE: app/src/androidTest/java/com/yujing/chuankou/ExampleInstrumentedTest.java ================================================ package com.yujing.chuankou; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; /** * Instrumented test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() { } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/yujing/chuankou/App.kt ================================================ package com.yujing.chuankou import android.app.Application import android.content.pm.PackageManager import android.os.Build import com.tencent.bugly.crashreport.CrashReport import com.yujing.utils.* import java.io.File import java.util.Date class App : Application() { //单列 companion object { private var instance: App? = null fun get(): App { return instance!! } } override fun onCreate() { super.onCreate() instance = this YUtils.init(this) //保存日志开 YLog.saveOpen(YPath.getFilePath(this, "log")) YLog.setLogSaveListener { type, tag, msg -> return@setLogSaveListener type != YLog.DEBUG } //保存最近30天日志 YLog.delDaysAgo(30) //本地记录,kotlin val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) else packageManager.getPackageInfo(packageName, 0) val code = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode val name= info.versionName val strategy = CrashReport.UserStrategy(this) strategy.setCrashHandleCallback(object : CrashReport.CrashHandleCallback() { override fun onCrashHandleStart(crashType: Int, errorType: String?, errorMessage: String?, errorStack: String?): MutableMap { //0:Java crash; 1:Java caught exception; 2:Native crash; 3:Unity error; 4:ANR; 5:Cocos JS error; 6:Cocos Lua error val str = "包名:${packageName}\n版本:verCode=${code}\tverName=${name}\n异常类型:$crashType\n错误类型:$errorType\n错误原因:$errorMessage\n$errorStack" val path = "${YPath.get()}/crash/${YDate.date2String(Date(), "yyyy_MM_dd_HH_mm_ss")}.txt" //存本地磁盘,路径Android/data/包名/files/crash/ YFileUtil.stringToFile(File(path), str) //提交bugly额外信息 val map = super.onCrashHandleStart(crashType, errorType, errorMessage, errorStack) ?: mutableMapOf("异常保存路径" to path) //重启APP,startActivity(packageManager.getLaunchIntentForPackage(packageName)?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) return map } }) strategy.deviceID = YUtils.getAndroidId() //设置id strategy.appPackageName = packageName //App的包名 //CrashReport.setIsDevelopmentDevice(this, BuildConfig.DEBUG) //是否是debug CrashReport.initCrashReport(this, "a365f21e2f", YUtils.isDebug(this), strategy) //初始化Bugly } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/MainActivity.java ================================================ package com.yujing.chuankou.activity; import android.app.AlertDialog; import com.yujing.chuankou.R; import com.yujing.chuankou.activity.myTest.MyMainActivity; import com.yujing.chuankou.activity.wifi.SerialPortToWiFiActivity; import com.yujing.chuankou.base.BaseActivity; import com.yujing.chuankou.databinding.ActivityMainBinding; import com.yujing.utils.YPermissions; /** * 首页 */ public class MainActivity extends BaseActivity { public MainActivity() { super(R.layout.activity_main); } @Override protected void init() { YPermissions.Companion.requestAll(this); binding.ButtonQuit.setOnClickListener(v -> finish()); binding.btnAuthor.setOnClickListener(v -> startActivity(MyMainActivity.class)); binding.btnToWifi.setOnClickListener(v -> startActivity(SerialPortToWiFiActivity.class)); binding.ButtonSendWords.setOnClickListener(v -> startActivity(SendActivity.class)); binding.ButtonAbout.setOnClickListener(v -> { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("关于——余静的串口测试工具"); builder.setMessage(R.string.about_msg); builder.show(); }); } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/SendActivity.java ================================================ package com.yujing.chuankou.activity; import com.yujing.chuankou.R; import com.yujing.chuankou.base.KBaseActivity; import com.yujing.chuankou.config.Config; import com.yujing.chuankou.databinding.ActivitySendBinding; import com.yujing.chuankou.utils.Setting; import com.yujing.utils.YConvert; import com.yujing.utils.YLog; import com.yujing.utils.YShared; import com.yujing.utils.YToast; import com.yujing.yserialport.DataListener; import com.yujing.yserialport.ThreadMode; import com.yujing.yserialport.YSerialPort; import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * @author yujing * 可以参考此类用法 */ public class SendActivity extends KBaseActivity { YSerialPort ySerialPort; final String SEND_STRING = "SEND_STRING"; final String SEND_HEX = "SEND_HEX"; SimpleDateFormat simpleDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]", Locale.getDefault()); public SendActivity() { super(R.layout.activity_send); } @Override protected void init() { //YReadInputStream.setShowLog(true); //非阻塞读取线程,轮询不休息,将增加cpu消耗 //YReadInputStream.setSleep(false); //上次使用的数据 binding.editText.setText(YShared.get(this, SEND_STRING)); binding.etHex.setText(YShared.get(this, SEND_HEX)); binding.editText.setSelection(binding.editText.getText().toString().length()); binding.button.setOnClickListener(v -> sendString()); binding.btHex.setOnClickListener(v -> sendHexString()); //退出 binding.rlBack.setOnClickListener(v -> finish()); //清空 binding.llClearSerialPortResult.setOnClickListener(v -> binding.tvResult.setText("")); binding.llClearSerialPortSend.setOnClickListener(v -> binding.tvSend.setText("")); //初始化串口 ySerialPort = new YSerialPort(this, Config.getDevice(), Config.getBaudRate()); // 自定义组包 // ySerialPort.setInputStreamReadListener(inputStream -> { // int count = 0; // while (count == 0) // count = inputStream.available(); // byte[] bytes = new byte[count]; // //readCount,已经成功读取的字节的个数,这儿需读取count个数据,不够则循环读取,如果采用inputStream.read(bytes);可能读不完 // int readCount = 0; // while (readCount < count) // readCount += inputStream.read(bytes, readCount, count - readCount); // return bytes; // }); //添加监听 ySerialPort.addDataListener(dataListener); ySerialPort.setThreadMode(ThreadMode.MAIN);//设置回调线程为主线程 if (Config.getDevice() != null && Config.getBaudRate() != null) ySerialPort.start(); //设置 Setting.setting(this, binding.includeSet, () -> { if (Config.getDevice() != null && Config.getBaudRate() != null) ySerialPort.reStart(Config.getDevice(), Config.getBaudRate()); binding.tvResult.setText(""); binding.tvSend.setText(""); }); } private void sendHexString() { String str = binding.etHex.getText().toString().replace("\n", "").replace(" ", ""); if (str.isEmpty()) { YToast.show("未输入内容!"); return; } //去空格后 binding.etHex.setText(str); //保存数据,下次打开页面直接填写历史记录 YShared.write(getApplicationContext(), SEND_HEX, str); //发送 YLog.i("发送串口:" + ySerialPort.getDevice() + "\t\t波特率:" + ySerialPort.getBaudRate() + "\t\t内容:" + str); ySerialPort.send(YConvert.hexStringToByte(str)); //显示 if (binding.tvSend.getText().toString().length() > 10000) binding.tvSend.setText(binding.tvSend.getText().toString().substring(0, 2000)); binding.tvSend.setText("HEX " + simpleDateFormat.format(new Date()) + ":" + str + "\n" + binding.tvSend.getText().toString()); } private void sendString() { String str = binding.editText.getText().toString(); if (str.isEmpty()) { YToast.show("未输入内容!"); return; } //保存数据,下次打开页面直接填写历史记录 YShared.write(getApplicationContext(), SEND_STRING, str); //发送 YLog.i("发送串口:" + ySerialPort.getDevice() + "\t\t波特率:" + ySerialPort.getBaudRate() + "\t\t内容:" + str); ySerialPort.send(str.getBytes(Charset.forName("GB18030")), value -> { if (!value) YToast.show("串口异常"); }); //显示 if (binding.tvSend.getText().toString().length() > 10000) binding.tvSend.setText(binding.tvSend.getText().toString().substring(0, 2000)); binding.tvSend.setText("STR " + simpleDateFormat.format(new Date()) + ":" + str + "\n" + binding.tvSend.getText().toString()); } DataListener dataListener = (hexString, bytes) -> { //显示 if (binding.tvResult.getText().toString().length() > 10000) binding.tvResult.setText(binding.tvResult.getText().toString().substring(0, 2000)); binding.tvResult.setText("HEX " + simpleDateFormat.format(new Date()) + ":" + YConvert.bytesToHexString(bytes) + "\n" + binding.tvResult.getText().toString()); }; @Override public void onDestroy() { super.onDestroy(); ySerialPort.onDestroy(); } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/myTest/MyMainActivity.java ================================================ package com.yujing.chuankou.activity.myTest; import com.yujing.chuankou.R; import com.yujing.chuankou.base.BaseActivity; import com.yujing.chuankou.databinding.ActivityMyMainBinding; public class MyMainActivity extends BaseActivity { public MyMainActivity() { super(R.layout.activity_my_main); } @Override protected void init() { binding.ButtonQuit.setOnClickListener(v -> finish()); binding.ButtonSendFile.setOnClickListener(v -> startActivity(SendFileActivity.class)); binding.btSync.setOnClickListener(v -> startActivity(SyncActivity.class)); } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/myTest/SendFileActivity.java ================================================ package com.yujing.chuankou.activity.myTest; import android.content.Intent; import android.net.Uri; import com.yujing.chuankou.R; import com.yujing.chuankou.base.BaseActivity; import com.yujing.chuankou.config.Config; import com.yujing.chuankou.databinding.ActivitySendFileBinding; import com.yujing.chuankou.utils.Setting; import com.yujing.utils.YConvert; import com.yujing.utils.YPermissions; import com.yujing.utils.YShow; import com.yujing.utils.YToast; import com.yujing.utils.YUri; import com.yujing.yserialport.YSerialPort; import java.io.File; public class SendFileActivity extends BaseActivity { File sendFile = null;//要发送的文件 YSerialPort ySerialPort; public SendFileActivity() { super(R.layout.activity_send_file); } @Override protected void init() { YPermissions.Companion.requestAll(this); //选择文件 binding.buttonBrowse.setOnClickListener(v -> onClick()); //发送文件 binding.btSend.setOnClickListener(v -> sendFile()); ySerialPort = new YSerialPort(this, Config.getDevice(), Config.getBaudRate()); ySerialPort.addDataListener((hexString, bytes2) -> runOnUiThread(() -> binding.tvResult.setText(binding.tvResult.getText().equals("") ? hexString : binding.tvResult.getText() + "\n" + hexString)) ); ySerialPort.start(); //设置 Setting.setting(this, binding.includeSet, () -> { if (Config.getDevice() != null && Config.getBaudRate() != null) ySerialPort.reStart(Config.getDevice(), Config.getBaudRate()); binding.tvResult.setText(""); }); binding.ButtonQuit.setOnClickListener(v -> finish()); } //直接发送文件到串口 private void sendFile() { if (sendFile == null) { YToast.show("请先选择文件"); return; } byte[] bytes = YConvert.fileToByte(sendFile); YShow.show(this, "发送中...", "进度:" + 0 + "/" + bytes.length); //初始化 ySerialPort.send(bytes, aBoolean -> YToast.show("发送:" + (aBoolean ? "成功" : "失败")), integer -> { YShow.setMessageOther("进度:" + integer + "/" + bytes.length); if (integer == bytes.length) { YShow.finish(); YToast.show("发送完成"); ySerialPort.onDestroy(); } }); YToast.show("正在发送请稍后..."); } public void onClick() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*");//设置类型,我这里是任意类型,任意后缀的可以这样写。 intent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(intent, 1); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1) { if (resultCode == RESULT_OK) { Uri uri = data.getData(); if (uri != null) { String path = YUri.getPath(this, uri); if (path != null) { File file = new File(path); if (file.exists()) { sendFile = file; String upLoadFilePath = file.toString(); String upLoadFileName = file.getName(); binding.FilePath.setText(upLoadFilePath); } } } } } } @Override public void onDestroy() { super.onDestroy(); ySerialPort.onDestroy(); } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/myTest/SyncActivity.java ================================================ package com.yujing.chuankou.activity.myTest; import com.yujing.chuankou.R; import com.yujing.chuankou.base.KBaseActivity; import com.yujing.chuankou.config.Config; import com.yujing.chuankou.databinding.ActivitySendBinding; import com.yujing.chuankou.utils.Setting; import com.yujing.utils.YConvert; import com.yujing.utils.YLog; import com.yujing.utils.YShared; import com.yujing.utils.YToast; import com.yujing.yserialport.YSerialPort; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * 同步发送 * * @author yujing 2021年7月27日10:57:10 */ public class SyncActivity extends KBaseActivity { final String SEND_STRING = "SEND_STRING"; final String SEND_HEX = "SEND_HEX"; String device = null; String baudRate = null; SimpleDateFormat simpleDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]", Locale.getDefault()); public SyncActivity() { super(R.layout.activity_send); } @Override protected void init() { //YReadInputStream.setShowLog(true); //非阻塞读取线程,轮询不休息,将增加cpu消耗 //YReadInputStream.setSleep(false); binding.tvTitle.setText("同步发送数据"); //上次使用的数据 binding.editText.setText(YShared.get(this, SEND_STRING)); binding.etHex.setText(YShared.get(this, SEND_HEX)); binding.editText.setSelection(binding.editText.getText().toString().length()); binding.button.setOnClickListener(v -> sendString()); binding.btHex.setOnClickListener(v -> sendHexString()); //退出 binding.rlBack.setOnClickListener(v -> finish()); //清空 binding.llClearSerialPortResult.setOnClickListener(v -> binding.tvResult.setText("")); binding.llClearSerialPortSend.setOnClickListener(v -> binding.tvSend.setText("")); //串口波特率 device = Config.getDevice(); baudRate = Config.getBaudRate(); //设置 Setting.setting(this, binding.includeSet, () -> { if (Config.getDevice() != null && Config.getBaudRate() != null) { device = Config.getDevice(); baudRate = Config.getBaudRate(); } binding.tvResult.setText(""); binding.tvSend.setText(""); }); } private void sendHexString() { if (device == null || baudRate == null) { YToast.show("请先配置串口!"); return; } String str = binding.etHex.getText().toString().replaceAll("\n", "").replace(" ", ""); if (str.isEmpty()) { YToast.show("未输入内容!"); return; } //去空格后 binding.etHex.setText(str); //保存数据,下次打开页面直接填写历史记录 YShared.write(getApplicationContext(), SEND_HEX, str); //发送 YLog.i("发送串口:" + device + "\t\t波特率:" + baudRate + "\t\t内容:" + str); new Thread(() -> { try { //最多等待500毫秒 byte[] re = YSerialPort.sendSyncTime(device, baudRate, YConvert.hexStringToByte(str), 500); //至少读取100毫秒,读满10字节返回 // byte[] re = YSerialPort.sendSyncContinuity(device, baudRate, YConvert.hexStringToByte(str), 100,10); //回显 showData(re); } catch (Exception e) { //回显 showDataFail("发送失败,原因:" + e.getMessage()); } }).start(); //显示 if (binding.tvSend.getText().toString().length() > 10000) binding.tvSend.setText(binding.tvSend.getText().toString().substring(0, 2000)); binding.tvSend.setText("HEX " + simpleDateFormat.format(new Date()) + ":" + str + "\n" + binding.tvSend.getText().toString()); } private void sendString() { String str = binding.editText.getText().toString(); if (str.isEmpty()) { YToast.show("未输入内容!"); return; } //保存数据,下次打开页面直接填写历史记录 YShared.write(getApplicationContext(), SEND_STRING, str); //发送 YLog.i("发送串口:" + device + "\t\t波特率:" + baudRate + "\t\t内容:" + str); new Thread(() -> { try { //最多等待500毫秒 byte[] re = YSerialPort.sendSyncTime(device, baudRate, YConvert.hexStringToByte(str), 500); //至少读取100毫秒,读满10字节返回 //byte[] re = YSerialPort.sendSyncContinuity(device, baudRate, YConvert.hexStringToByte(str), 100,10); //回显 showData(re); } catch (Exception e) { //回显 showDataFail("失败:" + e.getMessage()); } }).start(); //显示 if (binding.tvSend.getText().toString().length() > 10000) binding.tvSend.setText(binding.tvSend.getText().toString().substring(0, 2000)); binding.tvSend.setText( "STR " + simpleDateFormat.format(new Date()) + ":" + str + "\n" + binding.tvSend.getText().toString()); } private void showData(byte[] bytes) { runOnUiThread(() -> { String hexString = YConvert.bytesToHexString(bytes); //显示 if (binding.tvResult.getText().toString().length() > 10000) binding.tvResult.setText(binding.tvResult.getText().toString().substring(0, 2000)); binding.tvResult.setText( "HEX " + simpleDateFormat.format(new Date()) + ":" + hexString + "\n" + binding.tvResult.getText().toString()); }); } private void showDataFail(String fail) { runOnUiThread(() -> { //显示 if (binding.tvResult.getText().toString().length() > 10000) binding.tvResult.setText(binding.tvResult.getText().toString().substring(0, 2000)); binding.tvResult.setText( "ERROR " + simpleDateFormat.format(new Date()) + ":" + fail + "\n" + binding.tvResult.getText().toString()); }); } @Override public void onDestroy() { super.onDestroy(); } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/wifi/SerialPortToWiFiActivity.kt ================================================ package com.yujing.chuankou.activity.wifi import android.app.AlertDialog import android.content.DialogInterface import android.graphics.Color import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.TextView import com.yujing.chuankou.R import com.yujing.chuankou.activity.wifi.socket.Server import com.yujing.chuankou.base.KBaseActivity import com.yujing.chuankou.config.Config import com.yujing.chuankou.databinding.ActivitySerialportToWifiBinding import com.yujing.chuankou.utils.Setting import com.yujing.contract.YListener import com.yujing.contract.YListener2 import com.yujing.utils.YApp import com.yujing.utils.YCheck import com.yujing.utils.YConvert import com.yujing.utils.YLog import com.yujing.utils.YSave import com.yujing.utils.YScreenUtil import com.yujing.utils.YShared import com.yujing.utils.YToast import com.yujing.utils.YUtils import com.yujing.yserialport.YSerialPort import java.net.Socket import java.nio.charset.Charset import java.text.SimpleDateFormat import java.util.Date import java.util.Locale /** * 串口转TCP(WiFi) * @author 2021年8月6日15:34:13 */ class SerialPortToWiFiActivity : KBaseActivity(R.layout.activity_serialport_to_wifi) { var ySerialPort: YSerialPort? = null val SEND_STRING = "SEND_STRING" val SEND_HEX = "SEND_HEX" val SEND_WIFI_HEX = "SEND_WIFI_HEX" //服务 val server = Server() //wifiPort var wifiPort: String get() = YSave.get(YApp.get(), "wifiPort", String::class.java, "8888") set(value) = YSave.put(YApp.get(), "wifiPort", value) var simpleDateFormat = SimpleDateFormat("[HH:mm:ss.SSS]", Locale.getDefault()) override fun init() { //上次使用的数据 binding.run { editText.setText(YShared.get(YApp.get(), SEND_STRING)) etHex.setText(YShared.get(YApp.get(), SEND_HEX)) etHexWifi.setText(YShared.get(YApp.get(), SEND_WIFI_HEX)) editText.setSelection(editText.text!!.length) tvPort.text = "端口:$wifiPort" //按钮,退出 rlBack.setOnClickListener { finish() } //按钮,发送hex btHex.setOnClickListener { sendHexSerialPort() } //按钮,发送文本 button.setOnClickListener { sendStringSerialPort() } //按钮,发送hex,wifi btHexWifi.setOnClickListener { sendHexWiFi() } //设置server端口 llWifiSet.setOnClickListener { wifiSetting() } //打开server llWifiOpen.setOnClickListener { wifiOpen() } //清屏 llClearSerialPortResult.setOnClickListener { tvResult.text = "" } llClearSerialPortSend.setOnClickListener { tvSend.text = "" } llClearWifiResult.setOnClickListener { tvResultWifi.text = "" } llClearWifiSend.setOnClickListener { tvSendWifi.text = "" } //显示本机ip地址 tvIp.isSelected = true tvIp.text = "本机IP:" + YUtils.getIPv4().toTypedArray().contentToString() } //初始化串口 ySerialPort = YSerialPort(this, Config.device, Config.baudRate).apply { //自定义组包 setInputStreamReadListener { inputStream -> var count = 0 while (count == 0) count = inputStream.available() //获取真正长度 val bytes = ByteArray(count) var readCount = 0 // 已经成功读取的字节的个数 while (readCount < count) readCount += inputStream.read(bytes, readCount, count - readCount) return@setInputStreamReadListener bytes } //收到串口数据 addDataListener { hexString: String, byteArray: ByteArray -> //转发给WiFi Thread { sendHexWiFi(byteArray) }.start() showByteArray(binding.tvResult, byteArray) } start() } //设置 Setting.setting(this, binding.includeSet) { if (Config.device != null && Config.baudRate != null) ySerialPort?.reStart(Config.device, Config.baudRate) binding.tvResult.text = "" binding.tvSend.text = "" } //设置TCP服务 server.run { //服务,连接监听,刷新显示列表 connectListener = YListener { var s = "" for (item in socketList) s += item.socket.inetAddress.hostAddress + "\n" binding.tvConnectList.text = s } //服务,收到数据 readListener = YListener2 { socket, byteArray -> //转发给串口 Thread { sendHexSerialPort(byteArray) }.start() showByteArray(binding.tvResultWifi, byteArray) } } //默认打开服务 wifiOpen() } //打开wifi private fun wifiOpen() { if (YCheck.isPort(wifiPort)) { if (server.server == null || server.server!!.isClosed) { server.startListener = YListener { binding.tvServerStatus.text = "服务状态:已打开" binding.tvServerStatusTips.text = "单击关闭服务" } server.start(wifiPort.toInt()) } else { server.close() binding.tvServerStatus.text = "服务状态:已关闭" binding.tvServerStatusTips.text = "单击打开服务" } } else { YToast.showSpeak("端口不正确") } } //设置wifi串口 private fun wifiSetting() { if (server.server != null && !server.server!!.isClosed) return YToast.show("请先关闭服务") val editText = EditText(this).apply { val dp10 = YScreenUtil.dp2px(10F) setPadding(dp10, dp10, dp10, dp10) inputType = EditorInfo.TYPE_CLASS_NUMBER maxLines = 1 maxLines = 5 setBackgroundColor(Color.parseColor("#EEEEEE")) setText(wifiPort) } AlertDialog.Builder(this).apply { setIcon(android.R.drawable.ic_menu_info_details) setCancelable(true) setTitle("请输入端口号") setMessage("\n提示:端口 应该在1-65535之间") setView(editText) setPositiveButton("确定", null) setNegativeButton("取消") { dialogInterface, which -> dialogInterface.dismiss() } }.create().apply { show() getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { dismiss() val str = editText.text.toString().trim() if (YCheck.isPort(str)) { if (str != wifiPort) { wifiPort = str binding.tvPort.text = "端口:$wifiPort" } } else { YToast.showSpeak("端口不正确") } } } } //发送16进制给串口 private fun sendHexSerialPort() { val str: String = binding.etHex.text.toString().replace("\n", "").replace(" ", "") if (str.isEmpty()) return YToast.showSpeak("未输入内容") //去空格后 binding.etHex.setText(str) //保存数据,下次打开页面直接填写历史记录 YShared.write(applicationContext, SEND_HEX, str) YLog.d(ySerialPort?.device + " " + ySerialPort?.baudRate + " " + str) sendHexSerialPort(YConvert.hexStringToByte(str)) } //发送给串口 private fun sendHexSerialPort(byteArray: ByteArray) { ySerialPort?.send(byteArray) showByteArray(binding.tvSend, byteArray) } //显示 不超过2000字符 private fun showByteArray(tv: TextView, byteArray: ByteArray) { runOnUiThread { tv.run { if (text.toString().length > 10000) text = text.toString().substring(0, 2000) text = "HEX ${simpleDateFormat.format(Date())}:${YConvert.bytesToHexString(byteArray)}\n${text}" } } } //发送文本进制给串口 private fun sendStringSerialPort() { val str = binding.editText.text.toString() if (str.isEmpty()) return YToast.showSpeak("未输入内容") //保存数据,下次打开页面直接填写历史记录 YShared.write(applicationContext, SEND_STRING, str) ySerialPort?.send( str.toByteArray(Charset.forName("GB18030")) ) { value: Boolean? -> if (!value!!) YToast.showSpeak("串口异常") } //显示 binding.tvSend.run { if (text.toString().length > 10000) text = text.toString().substring(0, 2000) text = "STR ${simpleDateFormat.format(Date())}:$str\n${text}" } } //发送16进制给wifi private fun sendHexWiFi() { if (server.socketList.isEmpty()) return YToast.showSpeak("没有已连接的设备") val str: String = binding.etHexWifi.text.toString().replace("\n", "").replace(" ", "") if (str.isEmpty()) return YToast.showSpeak("未输入内容") //去空格后 binding.etHexWifi.setText(str) //保存数据,下次打开页面直接填写历史记录 YShared.write(applicationContext, SEND_WIFI_HEX, str) sendHexWiFi(YConvert.hexStringToByte(str)) } //发送给wifi private fun sendHexWiFi(byteArray: ByteArray) { //有wifi连接才转发 if (server.socketList.size > 0) { //转发给wifi server.send(byteArray) showByteArray(binding.tvSendWifi, byteArray) } } override fun onDestroy() { super.onDestroy() ySerialPort?.onDestroy() server.close() } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/wifi/socket/ClientSocket.kt ================================================ package com.yujing.chuankou.activity.wifi.socket import com.yujing.contract.YListener import com.yujing.contract.YListener1 import com.yujing.contract.YListener2 import com.yujing.utils.YConvert import com.yujing.utils.YLog import com.yujing.utils.YReadInputStream import java.net.Socket class ClientSocket(val socket: Socket) { //线程 var readThread: Thread? = null //读取数据监听 var readListener: YListener2? = null //关闭监听 var closeListener: YListener? = null //读取 @Synchronized fun init() { readThread?.interrupt() readThread = Thread { while (!Thread.interrupted()) { try { val inputStream = socket.getInputStream() val bytes = YReadInputStream.readOnce(inputStream) YLog.d("接收到的原始数据:" + YConvert.bytesToHexString(bytes)) readListener?.value(socket,bytes) } catch (e: Exception) { Thread.currentThread().interrupt() break } } YLog.d("退出soket读取线程:"+socket.inetAddress.hostAddress) closeListener?.value() } readThread?.start() } //发送 fun send(bytes: ByteArray) { Thread { try { val os = socket.getOutputStream() os.write(bytes) os.flush() } catch (e: Exception) { //连接已经断开 YLog.d("设备已断开") socket.close() close() closeListener?.value() } }.start() } fun close() { readThread?.interrupt() socket.close() } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/activity/wifi/socket/Server.kt ================================================ package com.yujing.chuankou.activity.wifi.socket import com.yujing.contract.YListener import com.yujing.contract.YListener2 import com.yujing.utils.YLog import com.yujing.utils.YThread import com.yujing.utils.YToast import java.net.ServerSocket import java.net.Socket import java.net.SocketException /** * 服务,广播服务 * @author yujing 2021年8月6日15:30:40 */ /* 用法 //服务 val server = Server() //服务,连接监听,刷新显示列表 server.connectListener = YListener { var s = "" for (item in server.socketList) { s += item.socket.inetAddress.hostAddress s += "\n" } binding.tvConnectList.text = s } //服务,收到数据 server.readListener = YListener2 { socket, byteArray -> } if (YCheck.isPort(wifiPort)) { if (server.server == null || server.server!!.isClosed) { server.startListener = YListener { //服务状态:已打开 } //打开服务 server.start(wifiPort.toInt()) } else { //关闭服务 server.close() } } else { showSpeak("端口不正确") } override fun onDestroy() { super.onDestroy() server.close() } */ class Server { val socketList: MutableList = ArrayList() //读取数据监听 var readListener: YListener2? = null //连接或者关闭监听 var connectListener: YListener? = null //启动成功监听 var startListener: YListener? = null //server var server: ServerSocket? = null fun start(port: Int) { Thread { YLog.d("启动服务") try { server = ServerSocket(port) YLog.d("启动服务成功") YThread.runOnUiThread { startListener?.value() } while (!server!!.isClosed) { val socket = server!!.accept() connect(socket) } } catch (e: SocketException) { if (e.message == "Socket closed") { YLog.d("服务已关闭") } else { YLog.e("服务已关闭", e) YThread.runOnUiThread { YToast.showSpeak("服务已关闭") } } } catch (e: Exception) { YLog.e("启动服务失败", e) YThread.runOnUiThread { YToast.showSpeak("启动服务失败") } } }.start() } //新增一个连接 private fun connect(socket: Socket) { Thread { val socketClient = ClientSocket(socket) socketList.add(socketClient) socketClient.init() //读取监听 socketClient.readListener = YListener2 { socket, data -> YThread.runOnUiThread { readListener?.value(socket, data) } } //关闭监听 socketClient.closeListener = YListener { check() } check() }.start() } //检查当前列表 @Synchronized private fun check() { //删除——循环 var i = 0 while (i < socketList.size) { val next = socketList[i] if (next.socket.isClosed) { next.close() socketList.removeAt(i) } i++ } YThread.runOnUiThread { connectListener?.value() } } //发送 fun send(bytes: ByteArray) { var i = 0 while (i < socketList.size) { val item = socketList[i] item.send(bytes) i++ } } //关闭 fun close() { for (item in socketList) { item.close() } socketList.clear() server?.close() server = null } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/base/BaseActivity.java ================================================ package com.yujing.chuankou.base; import androidx.databinding.ViewDataBinding; import com.yujing.base.YBaseActivity; /** * 基础activity * * @author yujing 2021年1月13日17:21:58 */ /* 用法举例 public class MainActivity extends BaseActivity { public MainActivity() { super(R.layout.activity_main); } @Override protected void init() { } } */ public abstract class BaseActivity extends YBaseActivity { protected B binding; public BaseActivity(int layout) { super(layout); } @Override public void initBefore() { binding = getBinding(); } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/base/KBaseActivity.kt ================================================ @file:Suppress("MemberVisibilityCanBePrivate") package com.yujing.chuankou.base import android.annotation.SuppressLint import androidx.databinding.ViewDataBinding import com.yujing.base.YBaseActivity import com.yujing.utils.YUtils /** * 基础activity * * @param ViewDataBinding * @author yujing 2020年9月7日21:40:20 */ /* 用法: //kotlin class AboutActivity : KBaseActivity(R.layout.activity_about) { override fun init() { binding.include.ivBack.setOnClickListener { finish() } binding.include.tvTitle.text = "关于我们" } } //java public class OldActivity extends KBaseActivity { public OldActivity() { super(R.layout.activity_1101); } @Override protected void init() { } } */ abstract class KBaseActivity(layout: Int) : YBaseActivity(layout) { override fun initAfter() { YUtils.setFullScreen(this, true) YUtils.setImmersive(this, true) } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { YUtils.setFullScreen(this, true) YUtils.setImmersive(this, true) } } @SuppressLint("MissingSuperCall") override fun onDestroy() { super.onDestroy() } } ================================================ FILE: app/src/main/java/com/yujing/chuankou/config/Config.kt ================================================ package com.yujing.chuankou.config import com.yujing.utils.YSaveFiles object Config { //串口,此值可以在 sdcard/Android/data/包名/files/device.txt 中修改 @JvmStatic var device: String? get() = YSaveFiles.get("device", null) set(value) = YSaveFiles.set("device", value) //波特率 @JvmStatic var baudRate: String? get() = YSaveFiles.get("baudRate", null) set(value) = YSaveFiles.set("baudRate", value) } ================================================ FILE: app/src/main/java/com/yujing/chuankou/utils/Setting.java ================================================ package com.yujing.chuankou.utils; import android.app.Activity; import android.app.AlertDialog; import com.yujing.chuankou.config.Config; import com.yujing.chuankou.databinding.ViewSetBinding; import com.yujing.contract.YListener; import com.yujing.yserialport.YSerialPort; public class Setting { /** * 设置 * * @param activity activity * @param includeSet include * @param yListener 是否设置完成 */ public static void setting(Activity activity, ViewSetBinding includeSet, YListener yListener) { if (Config.getDevice() != null) { includeSet.tvCk.setText(Config.getDevice()); } if (Config.getBaudRate() != null) { includeSet.tvBtl.setText(Config.getBaudRate()); } includeSet.llCk.setOnClickListener(v -> { int index = 0;//单选框默认值:从0开始 new AlertDialog.Builder(activity) .setTitle("选择串口")//设置对话框标题 .setIcon(android.R.drawable.ic_menu_info_details)//设置对话框图标 .setSingleChoiceItems(YSerialPort.getSerialPortFinder().getAllDevices(), index, (dialog, which) -> { String device = YSerialPort.getSerialPortFinder().getAllDevicesPath()[which]; includeSet.tvCk.setText(device); Config.setDevice(device); dialog.dismiss(); //回调 if (yListener != null) yListener.value(); }) .show(); }); includeSet.llBtl.setOnClickListener(v -> { int index = 0;//单选框默认值:从0开始 new AlertDialog.Builder(activity) .setTitle("选择波特率")//设置对话框标题 .setIcon(android.R.drawable.ic_menu_info_details)//设置对话框图标 .setSingleChoiceItems(YSerialPort.getBaudRates(), index, (dialog, which) -> { String baudRate = YSerialPort.getBaudRates()[which]; includeSet.tvBtl.setText(baudRate); Config.setBaudRate(baudRate); //回调 if (yListener != null) yListener.value(); dialog.dismiss(); }).show(); }); } } ================================================ FILE: app/src/main/res/drawable/bt_click.xml ================================================ ================================================ FILE: app/src/main/res/drawable/item_click.xml ================================================ ================================================ FILE: app/src/main/res/drawable/set_selector.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_my_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_send.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_send_file.xml ================================================