Android 串口通信

引言

在iot项目中,Android 端总会有和硬件通信。

通信这里:串口通信,蓝牙通信或者局域网通信。

这里讲一下串口通信。

什么是串口?

“串口”(Serial Port)通常是指一种用于与外部设备进行串行通信的接口。如下是其中一种DB9的形式:

更加简单的,还有这样的形式:

只要有三条线,TX、RX和GND,或者A、B和GND,就可以去实现通讯。

...................................................... 奇怪??????我们手机上看不到这些玩意。

标准的Android智能手机和平板电脑通常并不直接暴露硬件级别的串口接口(如RS-232)给用户或开发者。

这是因为这些设备为了便携性和成本考虑,往往采用了不同的通信方式(如USB、蓝牙、Wi-Fi等)来与外部设备交互。

然而,在一些特定的Android设备(如自动售卖机的大尺寸屏幕、嵌入式Android设备等)或者通过特定的硬件扩展(如USB转串口适配器)上,开发者可能能够访问到串口接口。

为了在Android应用中使用串口通信,开发者可能会使用到一些第三方库或框架,这些库或框架通过JNI(Java Native Interface)技术调用底层Linux系统的串口通信接口。这些库通常提供了更高级别的API,使得开发者能够在不直接处理底层细节的情况下实现串口通信。

应用场景


不知道大家有没有接触过自动售卖机,在自动售卖机里面装有Android屏。在海外,大部分国家都没有普及扫码支付,都是使用一些纸币、硬币以及刷卡进行支付。而这些支付大部分都是使用串口的形式去对接,比如投入多少钱,通过串口发送给数据Android屏,Android屏收到后,触发指令出商品,今天呢,我们就来聊聊串口开发。
 

正文

串口常用于连接计算机与外部设备,如打印机、调制解调器、传感器等。串口的主要特点是通信速度比较慢,但传输距离可以很长。常见的串口标准有RS-232、RS-485、TTL等。

通讯参数

一般这些数据,都是下位机提供给上位机的【上位机指的就是我们的Android屏幕,下位机指的就是上面我们提到的外部设备】,我们按照参数打开串口就可以收发数据了。

通讯接口是什么?

就是 RS232和RS485 。

RS232和RS485在收发数据上的区别主要体现在传输方式、传输距离、通信模式以及电平标准等方面。

RS232支持全双工和半双工两种传输方式,全双工可以实现数据的双向同时传输,而半双工则只能实现数据的单向传输,简单来说,就是只能一边来发送数据,另外一边不能主动发数据,只能响应数据,类似客户端和服务器的通讯一样。

RS485属于半双工总线,即在同一时刻,总线上只能有一个设备在发送数据,而其他设备则处于接收状态。

 波特率是什么?

34800,9600

波特率表示的是单位时间内传输的码元符号的个数,波特率越高,单位时间内传输的数据量就越大。但过大也会存在丢包的情况,视情况设定。

停止位、数据位、校验位的解释:

1.停止位:用于表示单个数据包的结束。常见的停止位有1位、1.5位和2位。停止位的主要作用是提供一个时间间隔,以确保数据包的完整性和正确性。例如,如果设置为1位停止位,则每个数据包后面都会跟随一个逻辑高电平(或逻辑低电平)的时间间隔,用于标识数据包的结束。

2.数据位:用于传输实际的数据信息。数据位的长度可以根据需要进行设置,常见的有5位、6位、7位和8位等。数据位越长,每个数据包所能携带的信息量就越大。在串口通信中,数据位通常是固定的,例如常用的ASCII码就是基于7位或8位数据位进行传输的。

3.校验位:用于检测数据传输过程中的错误。校验位可以通过多种方式生成,如奇校验、偶校验或无校验等。如果设置了校验位,则接收方会根据校验位的值来判断接收到的数据是否存在错误。如果存在错误,则可以根据具体的协议进行错误处理或重传。

下位机的数据发送案例

 厂家自定义协议

我们简单来看看他的协议,以及我们应该如何发送数据和接收数据。
(1)需要厂家提供通讯参数

(2)通讯文档,比如,查询下位机状态,还有很多协议内容,这里就讲一个:

有了这些信息,先不着急写代码,先使用串口工具测试一下收发数据是否正常。打开串口通讯工具,设置通讯参数,然后发送数据就可以了。

例如:  发送  AA 01 02 DD     接收  AA 02 02 01 DD

粘包、如何知道返回的数据对应谁的,数据通知…等等

在真实项目中,并不会如上面怎么简单,发送数据和接收数据就可以了,需要考虑:

  1. 数据丢包情况,需要重发,只到收到数据为止。
  2. 数据粘包的情况,需要和下位机约定好规则。
  3. 数据发送过来是二进制,我们需要转换,具体也是和下位机约定好规则

如何知道返回的数据对应谁的?

简单来说,就是你发送一个数据的时候,记录到一个变量里面。等读到数据后,你把数据和变量里面记录的内容发送上来,然后再继续发送下一个数据。以此类推。这样你就会知道数据是谁的了。

注意,这样的话,数据的发送,你就需要存储到一个集合里面,不断的往里面取,而不是异步随便调用send方法发送数据了。
 

 如何处理粘包的情况?

粘包是指在串口通信过程中,由于多种原因导致的多个独立的数据包在传输过程中被接收端视为一个连续的数据流,从而使得数据包之间的边界变得不明确,进而使得数据的解析变得困难。

比如:本来下位机返回的是AA 03 03 07 00 DD变成了AA 03 03 07 00 DD AA 03 03 07 00 DD,或者AA 03 03 07 00 DDAA 03 03两条数据连在一起情况。

怎么会出现这样的问题呢?

1、发送方发送数据的速度较快:当发送方连续发送多个数据包,且发送速度较快时,如果接收方的处理速度跟不上,就可能导致多个数据包在接收端被合并成一个大的数据流,即发生粘包现象。【降低上位机数据发送的频率】
2、接收方处理数据的速度较慢:接收方的处理速度是影响是否发生粘包的重要因素。如果接收方的处理速度较慢,无法及时将接收到的数据按照数据包进行分割和处理,就会发生粘包。【优化下位机代码】
3、传输数据量太大:有时候传输数据量太大,导致数据截断,或者缓存区不够。

处理办法:

**添加固定长度头部和尾部:**发送方在每个数据包前添加固定长度的头部,头部中包含数据包的长度信息,接收方根据头部中的长度信息来解析数据。如下:

左边蓝色是上位机发送给下位机的

右边橙色是下位机返回给上位机的。

消息头,数据内容长度,结束,这样我们就可以很好的处理数据了,如果数据发回来的不完整,或者连在一起,我们可以视情况,对数据进行解析分段,或者丢弃。

使用

Android SerialPort库通过JNI调用底层Linux的串口设备驱动,使得开发者可以通过简单的API来进行串口通信操作。

android-serialport-api下有两个主要的类

谷歌给的库:

https://code.google.com/archive/p/android-serialport-api/ 仅支持串口名称及波特率 。

那么我找到了两个做了扩展的两个库,根据情况用:

1、下面示例就讲这个

GitHub - licheedev/Android-SerialPort-API: Fork自Google开源的Android串口通信Demo,修改成Android Studio项目

2、这个库用着比较简单 

GitHub - xmaihh/Android-Serialport: 移植谷歌官方串口库,仅支持串口名称及波特率,该项目添加支持校验位、数据位、停止位、流控配置项

使用见: 快速使用Android串口_io.github.xmaihh:serialport-CSDN博客

示例:

 添加依赖

在 module 中的 build.gradle 中的 dependencies 中添加以下依赖:

dependencies {//串口implementation 'com.github.licheedev:Android-SerialPort-API:2.0.0'
}

低版本的 gradle 在Project 中的 build.gradle 中的 allprojects 中添加以下 maven仓库 (不添加任然无法加载SerialPort);

allprojects {repositories {maven { url "https://jitpack.io" }//maven仓库}
}

高版本的 gradle 已经废弃了 allprojects 在 settings.gradle 中 repositories 添加以下maven仓库(不添加任然无法加载SerialPort);

dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {google()mavenCentral()jcenter() // Warning: this repository is going to shut down soonmaven { url "https://jitpack.io" }//maven仓库}
}

编写串口处理类

1、串口处理类:SerialHandle ;简单概括这个类,就是通过串口对象去获取两个流(输入流、输出流),通过者两个流来监听数据或者写入指令,硬件收到后执行。同时注意配置参数(只要支持串口通讯的硬件,一般说明书上都会有写)

package com.chj233.serialmode.serialUtil;import android.serialport.SerialPort;
import android.util.Log;import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;/*** 串口实处理类*/
public class SerialHandle implements Runnable {private static final String TAG = "串口处理类";private String path = "";//串口地址private SerialPort mSerialPort;//串口对象private InputStream mInputStream;//串口的输入流对象private BufferedInputStream mBuffInputStream;//用于监听硬件返回的信息private OutputStream mOutputStream;//串口的输出流对象 用于发送指令private SerialInter serialInter;//串口回调接口private ScheduledFuture readTask;//串口读取任务/*** 添加串口回调** @param serialInter*/public void addSerialInter(SerialInter serialInter) {this.serialInter = serialInter;}/*** 打开串口** @param devicePath 串口地址(根据平板的说明说填写)* @param baudrate   波特率(根据对接的硬件填写 - 硬件说明书上"通讯"中会有标注)* @param isRead     是否持续监听串口返回的数据* @return 是否打开成功*/public boolean open(String devicePath, int baudrate, boolean isRead) {return open(devicePath, baudrate, 7, 1, 2, isRead);}/*** 打开串口** @param devicePath 串口地址(根据平板的说明说填写)* @param baudrate   波特率(根据对接的硬件填写 - 硬件说明书上"通讯"中会有标注)* @param dataBits   数据位(根据对接的硬件填写 - 硬件说明书上"通讯"中会有标注)* @param stopBits   停止位(根据对接的硬件填写 - 硬件说明书上"通讯"中会有标注)* @param parity     校验位(根据对接的硬件填写 - 硬件说明书上"通讯"中会有标注)* @param isRead     是否持续监听串口返回的数据* @return 是否打开成功*/public boolean open(String devicePath, int baudrate, int dataBits, int stopBits, int parity, boolean isRead) {boolean isSucc = false;try {if (mSerialPort != null) close();File device = new File(devicePath);mSerialPort = SerialPort // 串口对象.newBuilder(device, baudrate) // 串口地址地址,波特率.dataBits(dataBits) // 数据位,默认8;可选值为5~8.stopBits(stopBits) // 停止位,默认1;1:1位停止位;2:2位停止位.parity(parity) // 校验位;0:无校验位(NONE,默认);1:奇校验位(ODD);2:偶校验位(EVEN).build(); // 打开串口并返回mInputStream = mSerialPort.getInputStream();mBuffInputStream = new BufferedInputStream(mInputStream);mOutputStream = mSerialPort.getOutputStream();isSucc = true;path = devicePath;if (isRead) readData();//开启识别} catch (Throwable tr) {close();isSucc = false;} finally {return isSucc;}}// 读取数据private void readData() {if (readTask != null) {readTask.cancel(true);try {Thread.sleep(160);} catch (InterruptedException e) {e.printStackTrace();}//此处睡眠:当取消任务时 线程池已经执行任务,无法取消,所以等待线程池的任务执行完毕readTask = null;}readTask = SerialManage.getInstance().getScheduledExecutor()//获取线程池.scheduleAtFixedRate(this, 0, 150, TimeUnit.MILLISECONDS);//执行一个循环任务}@Override//每隔 150 毫秒会触发一次runpublic void run() {if (Thread.currentThread().isInterrupted()) return;try {int available = mBuffInputStream.available();if (available == 0) return;byte[] received = new byte[1024];int size = mBuffInputStream.read(received);//读取以下串口是否有新的数据if (size > 0 && serialInter != null) serialInter.readData(path, received, size);} catch (IOException e) {Log.e(TAG, "串口读取数据异常:" + e.toString());}}/*** 关闭串口*/public void close(){try{if (mInputStream != null) mInputStream.close();}catch (Exception e){Log.e(TAG,"串口输入流对象关闭异常:" +e.toString());}try{if (mOutputStream != null) mOutputStream.close();}catch (Exception e){Log.e(TAG,"串口输出流对象关闭异常:" +e.toString());}try{if (mSerialPort != null) mSerialPort.close();mSerialPort = null;}catch (Exception e){Log.e(TAG,"串口对象关闭异常:" +e.toString());}}/*** 向串口发送指令*/public void send(final String msg) {byte[] bytes = hexStr2bytes(msg);//字符转成byte数组try {mOutputStream.write(bytes);//通过输出流写入数据} catch (Exception e) {e.printStackTrace();}}/*** 把十六进制表示的字节数组字符串,转换成十六进制字节数组** @param* @return byte[]*/private byte[] hexStr2bytes(String hex) {int len = (hex.length() / 2);byte[] result = new byte[len];char[] achar = hex.toUpperCase().toCharArray();for (int i = 0; i < len; i++) {int pos = i * 2;result[i] = (byte) (hexChar2byte(achar[pos]) << 4 | hexChar2byte(achar[pos + 1]));}return result;}/*** 把16进制字符[0123456789abcde](含大小写)转成字节* @param c* @return*/private static int hexChar2byte(char c) {switch (c) {case '0':return 0;case '1':return 1;case '2':return 2;case '3':return 3;case '4':return 4;case '5':return 5;case '6':return 6;case '7':return 7;case '8':return 8;case '9':return 9;case 'a':case 'A':return 10;case 'b':case 'B':return 11;case 'c':case 'C':return 12;case 'd':case 'D':return 13;case 'e':case 'E':return 14;case 'f':case 'F':return 15;default:return -1;}}}

2、串口回调SerialInter;简单概括一下这个类,就是将SerialHandle类中产生的结果,返回给上一层的业务代码,解偶合。

package com.chj233.serialmode.serialUtil;/*** 串口回调*/
public interface SerialInter {/*** 连接结果回调* @param path 串口地址(当有多个串口需要统一处理时,可以用地址来区分)* @param isSucc 连接是否成功*/void connectMsg(String path,boolean isSucc);/*** 读取到的数据回调* @param path 串口地址(当有多个串口需要统一处理时,可以用地址来区分)* @param bytes 读取到的数据* @param size 数据长度*/void readData(String path,byte[] bytes,int size);}

 3、串口统一管理SerialManage;简单概括一下这个类,用于管理串口的连接以及发送等功能,尤其是发送指令,极短时间内发送多个指令(例如:1毫秒内发送10个指令),多个指令之间会相互干扰。可能执行了第一个指令,可能一个都没执行。这个类不是必须的,如果有更好的方法可以自己定义。

package com.chj233.serialmode.serialUtil;import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;/*** 串口管理类*/
public class SerialManage {private static SerialManage instance;private ScheduledExecutorService scheduledExecutor;//线程池 同一管理保证只有一个private SerialHandle serialHandle;//串口连接 发送 读取处理对象private Queue<String> queueMsg = new ConcurrentLinkedQueue<String>();//线程安全到队列private ScheduledFuture sendStrTask;//循环发送任务private boolean isConnect = false;//串口是否连接private SerialManage() {scheduledExecutor = Executors.newScheduledThreadPool(8);//初始化8个线程}public static SerialManage getInstance() {if (instance == null) {synchronized (SerialManage.class) {if (instance == null) {instance = new SerialManage();}}}return instance;}/*** 获取线程池** @return*/public ScheduledExecutorService getScheduledExecutor() {return scheduledExecutor;}/*** 串口初始化** @param serialInter*/public void init(SerialInter serialInter) {if (serialHandle == null) {serialHandle = new SerialHandle();startSendTask();}serialHandle.addSerialInter(serialInter);}/*** 打开串口*/public void open() {isConnect = serialHandle.open("/dev/ttyS1", 9600, true);//设置地址,波特率,开启读取串口数据}/*** 发送指令** @param msg*/public void send(String msg) {/*此处没有直接使用 serialHandle.send(msg); 方法去发送指令因为 某些硬件在极短时间内只能响应一个指令,232通讯一次发送多个指令会有物理干扰,让硬件接收到指令不准确;所以 此处将指令添加到队列中,排队执行,确保每个指令一定执行.若不相信可以试试用serialHandle.send(msg)方法循环发送10个不同的指令,看看10个指令的执行结果。*/queueMsg.offer(msg);//向队列添加指令}/*** 关闭串口*/public void colse() {serialHandle.close();//关闭串口}//启动发送发送任务private void startSendTask() {cancelSendTask();//先检查是否已经启动了任务 ? 若有则取消//每隔100毫秒检查一次 队列中是否有新的指令需要执行sendStrTask = scheduledExecutor.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {if (!isConnect) return;//串口未连接 退出if (serialHandle == null) return;//串口未初始化 退出String msg = queueMsg.poll();//取出指令if (msg == null || "".equals(msg)) return;//无效指令 退出serialHandle.send(msg);//发送指令}}, 0, 100, TimeUnit.MILLISECONDS);}//取消发送任务private void cancelSendTask() {if (sendStrTask == null) return;sendStrTask.cancel(true);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}sendStrTask = null;}}

demo调用

package com.chj233.serialmode;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.util.Log;
import android.view.View;import com.chj233.serialmode.serialUtil.SerialInter;
import com.chj233.serialmode.serialUtil.SerialManage;public class MainActivity extends AppCompatActivity implements SerialInter {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);SerialManage.getInstance().init(this);//串口初始化SerialManage.getInstance().open();//打开串口findViewById(R.id.send_but).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage.getInstance().send("Z");//发送指令 Z }});}@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失败";Log.e("串口连接回调", "串口 "+ path + " -连接" + msg);}@Override//若在串口开启的方法中 传入false 此处不会返回数据public void readData(String path, byte[] bytes, int size) {
//        Log.e("串口数据回调","串口 "+ path + " -获取数据" + bytes);}
}

多串口的使用
使用思想:一个单例对象控制一个串口,多串口时,写多个“SerialManage”就可以了。这里仅仅做举例不去考虑代码是否优雅,可以自行优化这段代码。(此案例中的SerialManage1、SerialManage2、SerialManage3、SerialManage4需要自己去复制,参照上面的SerialManage)

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//初始化串口1SerialManage1.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失败";Log.e("串口连接回调", "串口 "+ path + " -连接" + msg);}@Override//若在串口开启的方法中 传入false 此处不会返回数据public void readData(String path, byte[] bytes, int size) {
//        Log.e("串口数据回调","串口 "+ path + " -获取数据" + bytes);}});//开启串口1SerialManage1.getInstance().open();//初始化串口2SerialManage2.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失败";Log.e("串口连接回调", "串口 "+ path + " -连接" + msg);}@Override//若在串口开启的方法中 传入false 此处不会返回数据public void readData(String path, byte[] bytes, int size) {
//        Log.e("串口数据回调","串口 "+ path + " -获取数据" + bytes);}});//打开串口2SerialManage2.getInstance().open();//初始化串口3SerialManage3.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失败";Log.e("串口连接回调", "串口 "+ path + " -连接" + msg);}@Override//若在串口开启的方法中 传入false 此处不会返回数据public void readData(String path, byte[] bytes, int size) {
//        Log.e("串口数据回调","串口 "+ path + " -获取数据" + bytes);}});//打开串口3SerialManage3.getInstance().open();//初始化串口4SerialManage4.getInstance().init(new SerialInter(){@Overridepublic void connectMsg(String path, boolean isSucc) {String msg = isSucc ? "成功" : "失败";Log.e("串口连接回调", "串口 "+ path + " -连接" + msg);}@Override//若在串口开启的方法中 传入false 此处不会返回数据public void readData(String path, byte[] bytes, int size) {
//        Log.e("串口数据回调","串口 "+ path + " -获取数据" + bytes);}});//打开串口4SerialManage4.getInstance().open();findViewById(R.id.send_but1).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage1.getInstance().send("Z");//给串口1发送指令 Z}});findViewById(R.id.send_but2).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage2.getInstance().send("Z");//给串口2发送指令 Z}});findViewById(R.id.send_but3).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage3.getInstance().send("Z");//给串口3发送指令 Z}});findViewById(R.id.send_but4).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {SerialManage4.getInstance().send("Z");//给串口4发送指令 Z}});}}

总结

串口通讯对于Android开发者来说,仅需关注如何连接、操作(发送指令)、读取数据;无论是232、485还是422,对于开发者来说连接、操作、读取代码都是一样的。

参考文章 : 

Android串口开发:Serialport(如何进行串口开发,数据发送,TX和RX,A和B,粘包)_android 串口-CSDN博客

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/23139.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围

一 查看springboot自带的tomcat版本&#xff1a; 可直接在项目中找到Maven Dependencies中找到tomcat版本 二、查看SpringBoot内置tomcat版本的支持范围 我这边是跟据maven仓库查看的 首先跟据链接打开maven仓库&#xff1a;https://mvnrepository.com/ 然后搜索&#xff1a…

磐维数据库双中心容灾流复制集群搭建

1. 架构 磐维数据库PanWeiDB V2.0.0基于gs_sdr工具&#xff0c;在不借助额外存储介质的情况下实现跨Region的异地容灾。提供流式容灾搭建&#xff0c;容灾升主&#xff0c;计划内主备切换&#xff0c;容灾解除、容灾状态监控等功能。 2. 部署双中心磐维集群 2.1. 主集群 角色…

Spring事务原理 二

在上一篇博文《Spring事务原理 一》中&#xff0c;我们熟悉了Spring声明式事务的AOP原理&#xff0c;以及事务执行的大体流程。 本文中&#xff0c;介绍了Spring事务的核心组件、传播行为的源码实现。下一篇中&#xff0c;我们将结合案例&#xff0c;来讲解实战中有关事务的易…

【机器学习】【KMeans聚类分析实战】用户分群聚类详解——SSE、CH 指数、SC全解析,实战电信客户分群案例

1.引言 在实际数据分析中&#xff0c;聚类算法常用于客户分群、图像分割等场景。如何确定聚类数 k 是聚类分析中的关键问题之一。本文将以“用户分群”为例&#xff0c;展示如何通过 KMeans 聚类&#xff0c;利用 SSE&#xff08;误差平方和&#xff0c;也称 Inertia&#xff…

20-R 绘图 - 饼图

R 绘图 - 饼图 R 语言提供来大量的库来实现绘图功能。 饼图&#xff0c;或称饼状图&#xff0c;是一个划分为几个扇形的圆形统计图表&#xff0c;用于描述量、频率或百分比之间的相对关系。 R 语言使用 pie() 函数来实现饼图&#xff0c;语法格式如下&#xff1a; pie(x, l…

搭建 Hadoop 3.3.6 伪分布式

搭建 Hadoop 3.3.6 伪分布式 IP 192.168.157.132 初始化操作 更改yum源 # 1_1.安装Wget yum install wget# 1_2.备份CentOS-Base.repo文件 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo_bak# 2.下载阿里yum源配置 wget -O /etc/yum.repos.d/Cen…

python电影数据分析及可视化系统建设

博主介绍&#xff1a;✌程序猿徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

手机壁纸设计中,金属质感字体可以为壁纸增添独特的视觉效果和高端感

在手机壁纸设计中&#xff0c;金属质感字体可以为壁纸增添独特的视觉效果和高端感。以下是一些关于金属质感字体在手机壁纸设计中的应用建议和案例分析&#xff1a; 1. 金属质感字体的特点 视觉冲击力强&#xff1a;金属质感字体具有独特的光泽和质感&#xff0c;能够在视觉上…

使用ezuikit-js封装一个对接摄像头的组件

ezuikit-js 是一个基于 JavaScript 的视频播放库&#xff0c;主要用于在网页中嵌入实时视频流播放功能。它通常用于与支持 RTSP、RTMP、HLS 等协议的摄像头或视频流服务器进行交互&#xff0c;提供流畅的视频播放体验。 主要功能 多协议支持&#xff1a;支持 RTSP、RTMP、HLS …

PHP post 数据丢失问题

max_input_vars是PHP配置选项之一&#xff0c;用于设置一个请求中允许的最大输入变量数。它指定了在处理POST请求或者通过URL传递的参数时&#xff0c;PHP脚本能够接收和处理的最大变量数量。 max_input_vars的默认值是1000&#xff0c;意味着一个请求中最多可以包含1000个输入…

jenkins docker容器中安装python3.9环境

在运维过程中&#xff0c;不过避免的需要使用到python&#xff0c;在jenkins 的docker容器中&#xff0c;是没有python环境的&#xff0c;需要我们自己手动安装一下。 查看是否有工具apt-get 直接输入apt-get 然后回车&#xff0c;出现以下内容&#xff0c;表示支持apt-get命令…

《Spring实战》(第6版) 保护Spring

第1部分 Spring基础 第4章 使用非关系型数据 关系型数据库一直是首选&#xff0c;近年来"NoSQL"数据库提供了数据存储的不同概念和结构。 SpringData为很多NoSQL数据库提供了支持&#xff0c;包括MongoDB、Cassandra、Couchbase、Neo4j、Redis等&#xff0c;无论选…

SQLMesh 系列教程7- 详解 seed 模型

SQLMesh 是一个强大的数据建模和管道管理工具&#xff0c;允许用户通过 SQL 语句定义数据模型并进行版本控制。Seed 模型是 SQLMesh 中的一种特殊模型&#xff0c;主要用于初始化和填充基础数据集。它通常包含静态数据&#xff0c;如参考数据和配置数据&#xff0c;旨在为后续的…

【JavaEE】-- 多线程(初阶)2

文章目录 3.线程的状态3.1观察线程的所有状态3.2线程状态和状态转移的意义 4.多线程带来的的风险-线程安全 (重点)4.1观察线程不安全4.2 线程不安全的原因4.2.1 线程调度是随机的4.2.2 修改共享数据4.2.3 原子性4.2.4 内存可见性4.2.5 指令重排序 4.3解决之前的线程不安全问题 …

安卓系统远程控制电脑方法,手机远控教程,ToDesk工具

不知道大家有没有觉得手机、平板虽然很好用&#xff0c;却也仍存在有很多替代不了电脑的地方。就比如说撰写文档、做数据报表啥的就不如PC端操作般方便&#xff0c;就跟别说PS修图、AE视频剪辑等需高性能设备来带动才易用的了。 好在也是有对策可解决&#xff0c;装个ToDesk远程…

机器学习(李宏毅)——RL(强化学习)

一、前言 本文章作为学习2023年《李宏毅机器学习课程》的笔记&#xff0c;感谢台湾大学李宏毅教授的课程&#xff0c;respect&#xff01;&#xff01;&#xff01; 二、大纲 What is RL&#xff1f;Three steps in MLPolicy GradientActor-CriticReward Shaping 三、What …

【Go】Go wire 依赖注入

1. wire 简介 wire 是一个 Golang 的依赖注入框架&#xff08;类比 Spring 框架提供的依赖注入功能&#xff09; ⭐ 官方文档&#xff1a;https://github.com/google/wire 这里关乎到编程世界当中一条好用的设计原则&#xff1a;A用到了B&#xff0c;那么B一定是通过依赖注入的…

《动手学机器人学》笔记

目录 0.介绍1.概述&#xff5c;空间位置、姿态的描述&#xff08;33&#xff09;&#xff5c;《动手学机器人学》2.&#xff08;2&#xff09;-Robotics Toolbox①&#xff08;V10.4&#xff09;3.齐次坐标与变换矩阵4.一般形式的旋转变换矩阵5.&#xff08;轴角法&#xff09;…

【蓝桥杯单片机】第十三届省赛第二场

一、真题 二、模块构建 1.编写初始化函数(init.c) void Cls_Peripheral(void); 关闭led led对应的锁存器由Y4C控制关闭蜂鸣器和继电器 2.编写LED函数&#xff08;led.c&#xff09; void Led_Disp(unsigned char ucLed); 将ucLed取反的值赋给P0 开启锁存器 关闭锁存…

大语言模型基础

简介 AI大模型是“人工智能预训练大模型”的简称&#xff0c;包含了“预训练”和“大模型”两层含义&#xff0c;二者结合产生了一种新的人工智能模式&#xff0c;即模型在大规模数据集上完成了预训练后无需微调&#xff0c;或仅需要少量数据的微调&#xff0c;就能直接支撑各…