文章目录

  • 前言
  • 一、USB通信是什么?
  • 二、USB通信开发
    • 具体步骤:
      • 1.通过vendorId、productId过滤找到自己的USB设备
      • 2.找到此设备发送信息,接收信息的UsbEndpoint
      • 3.打开Usb设备通讯
      • 4.进行初始化、发送、接收消息等操作
      • 5.释放资源
  • 三.热敏打印机案例
    • 1.初始化
    • 2.打印
    • 3.切纸
    • 4,完整代码
  • 总结

前言

距离写上一篇《Android 串口通信开发总结和解析案例》已经过去有段时间了,终于最近有时间,来记录和温习一下。
这一篇文章将会结合热敏打印机的实例来学习关于USB通讯的知识。


一、USB通信是什么?

老规矩先上概念

USB通讯原理:USB是轮询总线,USB主机与设备之间的数据交换都是由主机发起的,设备端只能被动的响应。USB数据传入或传出 USB 设备中的端点。
USB是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。USB接口支持设备的即插即用和热插拔功能

简单说。 USB通讯跟串口一样,也是靠一种通讯协议来通信的。区别是支持即插即用,通讯时设备只能被动响应。通信的数据不是串口那样一位一位的传输。当然这种类型的开发最关键的还是知道怎么用。我们使用时是通过Android 提供的API来发送、接收数据。
进入正题

二、USB通信开发

具体步骤:

1.通过vendorId、productId过滤找到自己的USB设备

首先AndroidManifest中声明权限

    <uses-feature android:name="android.hardware.usb.host" />

声明变量

    private UsbDeviceConnection connection;
        UsbManager usbManager = getUsbManager();//创建和初始化用于存储厂家、和产品号的两个ListList<Integer> vendorIds = new ArrayList<>();List<Integer> productIds = new ArrayList<>();//下面的五组是我的使用场景中的五中可能出现的设备对应的厂家号和产品号。//由厂家提供给公司,按需要更改,要尽可能把设备会用到的USB设备信息都列进去//案例中只有一种Collections.addAll(vendorIds, 1155, 1157, 1659, 3544, 6790);Collections.addAll(productIds, 30016, 30017, 8965, 5120, 30084);//过滤USB设备,找到匹配的,那一个usb设备UsbDevice usbDevice = null;for (UsbDevice one : usbManager.getDeviceList().values()) {int vendorIdx = vendorIds.indexOf(one.getVendorId());int productIdx = productIds.indexOf(one.getProductId());if (vendorIdx == productIdx && vendorIdx != -1) {usbDevice = one;break;}}

2.找到此设备发送信息,接收信息的UsbEndpoint

 // 检查interfaceif (usbDevice == null || usbDevice.getInterfaceCount() != 1) {return;}// 检查此设备断点endpointUsbInterface usbInterface = usbDevice.getInterface(0);if (usbInterface.getEndpointCount() < 1) {return;}// 过滤此USB的端点。用来发送和接收数据UsbEndpoint usbEndpointOut = null;UsbEndpoint usbEndpointIn = null;for (int i = 0; i < usbInterface.getEndpointCount(); i++) {UsbEndpoint usbEndpoint = usbInterface.getEndpoint(i);int direction = usbEndpoint.getDirection();int type = usbEndpoint.getType();if (type != UsbConstants.USB_ENDPOINT_XFER_BULK) {//过滤不支持的USB协议类型continue;}if (direction == UsbConstants.USB_DIR_OUT) {usbEndpointOut = usbEndpoint;} else if (direction == UsbConstants.USB_DIR_IN) {usbEndpointIn = usbEndpoint;}if (usbEndpointIn != null && usbEndpointOut != null) {break;}}

3.打开Usb设备通讯

         //连接USB设备connection = usbManager.openDevice(usbDevice);

4.进行初始化、发送、接收消息等操作

发送byte[]内容视厂家提供文档而定
比如这个 初始化的文档中对初始化的描述是这样

        if (connection != null) {connection.claimInterface(usbInterface, true);byte[] bytes;//初始化打印机bytes = new byte[]{0x1B, 0x40};if (connection.bulkTransfer(usbEndpointOut, bytes, bytes.length, 1500) < 0) {Logger.e("初始化打印机异常");}} else {Logger.e("未连接到打印机");}

接收的代码:

        status = connection.bulkTransfer(usbEndpointIn, receiveBytes, receiveBytes.length, 1500);

发现发送和接收信息都是调用了connection的同一个方法,区别在于传递的参数不一样。来看一下这个方法的参数说明。

/*** Performs a bulk transaction on the given endpoint.* The direction of the transfer is determined by the direction of the endpoint.* <p>* This method transfers data starting from index 0 in the buffer.* To specify a different offset, use* {@link #bulkTransfer(UsbEndpoint, byte[], int, int, int)}.* </p>** @param endpoint the endpoint for this transaction* @param buffer buffer for data to send or receive; can be {@code null} to wait for next*               transaction without reading data* @param length the length of the data to send or receive. Before*               {@value Build.VERSION_CODES#P}, a value larger than 16384 bytes*               would be truncated down to 16384. In API {@value Build.VERSION_CODES#P}*               and after, any value of length is valid.* @param timeout in milliseconds, 0 is infinite* @return length of data transferred (or zero) for success,* or negative value for failure*/public int bulkTransfer(UsbEndpoint endpoint,byte[] buffer, int length, int timeout) {return bulkTransfer(endpoint, buffer, 0, length, timeout);}

翻译在这里

在给定端点上执行批量事务。
传输的方向由端点的方向决定。
<p>此方法从缓冲区中的索引 0 开始传输数据。要指定不同的偏移量,请使用 {@link bulkTransfer(UsbEndpoint, byte[], int, int, int)}。
<p>
@param endpoint 此事务的端点
@param buffer 用于发送或接收数据的缓冲区;可以 {@code null} 等待下一个事务而不读取数据
@param length 发送或接收数据的长度。在 {@value Build.VERSION_CODESP} 之前,大于 16384 字节的值将被截断为 16384。在 API {@value Build.VERSION_CODESP} 及之后,任何长度值都是有效的。
@param 超时,以毫秒为单位,0 是无限的 @return 成功传输的数据长度(或零),或负值表示失败

很容易理解把,唯一需要说明一下的是,endpoint ,收和发不可以是同一个,而是设备本身就定义好的,就是在上一步中我们在设备里面过滤找到的那两个。

5.释放资源

        if (connection != null) {connection.close();}

三.热敏打印机案例

看过很多热敏打印机的文档,发现不同的厂商,大部分热敏打印机的开发文档这一块,发送的数据很多都是相同的,
比如初始化、切纸之类的,我估计他们底层也是根据一个规范来的,只不过对一部分进行了自己的定制。
不过具体还是要根据文档来的,省的发现问题再去看浪费时间。

1.初始化

这里贴一下完整的初始化的代码。

 /*** 初始化热敏打印机*/private void initPrinter() {UsbManager usbManager = getUsbManager();List<Integer> vendorIds = new ArrayList<>();List<Integer> productIds = new ArrayList<>();Collections.addAll(vendorIds, 1155, 1157, 1659, 3544, 6790);Collections.addAll(productIds, 30016, 30017, 8965, 5120, 30084);// 过滤USB设备UsbDevice usbDevice = null;for (UsbDevice one : usbManager.getDeviceList().values()) {int vendorIdx = vendorIds.indexOf(one.getVendorId());int productIdx = productIds.indexOf(one.getProductId());if (vendorIdx == productIdx && vendorIdx != -1) {usbDevice = one;break;}}// 检查interfaceif (usbDevice == null || usbDevice.getInterfaceCount() != 1) {return;}// 检查此设备断点endpointUsbInterface usbInterface = usbDevice.getInterface(0);if (usbInterface.getEndpointCount() < 1) {return;}// 过滤此USB的端点。用来发送和接收数据UsbEndpoint usbEndpointOut = null;UsbEndpoint usbEndpointIn = null;for (int i = 0; i < usbInterface.getEndpointCount(); i++) {UsbEndpoint usbEndpoint = usbInterface.getEndpoint(i);int direction = usbEndpoint.getDirection();int type = usbEndpoint.getType();if (type != UsbConstants.USB_ENDPOINT_XFER_BULK) {//过滤不支持的USB协议类型continue;}if (direction == UsbConstants.USB_DIR_OUT) {usbEndpointOut = usbEndpoint;} else if (direction == UsbConstants.USB_DIR_IN) {usbEndpointIn = usbEndpoint;}if (usbEndpointIn != null && usbEndpointOut != null) {break;}}//连接USB设备connection = usbManager.openDevice(usbDevice);if (connection != null) {connection.claimInterface(usbInterface, true);byte[] bytes;//初始化打印机bytes = new byte[]{0x1B, 0x40};if (connection.bulkTransfer(usbEndpointOut, bytes, bytes.length, 1500) < 0) {Logger.e("初始化打印机异常");printerInitStatus = false;}} else {Logger.e("未连接到打印机");printerInitStatus = false;}PrintHelper.getInstance().init(usbEndpointIn, usbEndpointOut, connection);}

相比上面 多了一句这,我封装的类,下面会把带注释的贴在下面

PrintHelper.getInstance().init(usbEndpointIn, usbEndpointOut, connection);

2.打印

/**** @param text         打印内容* @param needCut      是否切纸* @param bottomMargin 底边距 bottomMargin>1,底边距=bottomMargin*0.5mm 默认4* @param fullCut      是否全切 默认半切* @return             执行状态    -1异常*/@Overridepublic int print(String text, boolean needCut, int bottomMargin, boolean fullCut) {if (!printerInitStatus || connection == null) {Logger.e("打印机未完成初始化");return Const.Error.PRINT_ERROR;}if (!TextUtils.isEmpty(text)) {Logger.i("打印数据:%s", text);}try {int paperBoxStatus;int ret;ret = PrintHelper.getInstance().checkPaperStatus();if (ret == Const.Error.PRINT_ERROR) {return ret;} else {paperBoxStatus = ret;if (TextUtils.isEmpty(text)) {return paperBoxStatus;}}ret = PrintHelper.getInstance().print(text);if (ret == Const.Error.PRINT_ERROR) {return ret;} else {//打印正常return PrintHelper.getInstance().cutPaper(bottomMargin, needCut, fullCut, paperBoxStatus);}} catch (Exception e) {Logger.e(e, "捕获到异常");return Const.Error.PRINT_ERROR;}}

3.切纸

这里补充一个概念,全切和半切
全切:一刀切到底,纸会掉下来
半切:切的不彻底,打印纸需要稍微用力才会掉下来

这个方法的参数注释看上一个方法就是了,本来也是在上面的方法里面调用的

 public int cutPaper(int bottomMargin, boolean needCut, boolean fullCut, int paperBoxStatus) {byte[] bytes;//发送//获取下边距 默认最小4if (bottomMargin < 4 || bottomMargin == 0) {bytes = new byte[]{0x1B, 0x64, 0x04};} else {bytes = new byte[]{0x1B, 0x64, (byte) (Integer.parseInt(String.valueOf(bottomMargin), 16))};}//进纸空行 为了边距if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 1500) < 0) {Logger.e("打印进纸空行异常");//打印输出打印缓冲区中的数据,并进纸 n 行return Const.Error.PRINT_ERROR;}// 切纸if (needCut) {if (fullCut) {//全切bytes = new byte[]{0x1B, 0x69};} else {//半切bytes = new byte[]{0x1B, 0x6d};}if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 1500) < 0) {return Const.Error.PRINT_ERROR;}}return paperBoxStatus;}

4,完整代码

这里贴出PrintHelper。
这里再次重申一下,不同的设备,发送的数据和接收的数据有些是不同的,但是步骤都是一样的,你只需要根据文档改改参数而已。

package com.qinze.multimedia.client.util;import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.text.TextUtils;import com.google.gson.reflect.TypeToken;
import com.orhanobut.logger.Logger;
import com.qinze.multimedia.client.MyApp;
import com.qinze.multimedia.client.config.Const;
import com.qinze.multimedia.client.entity.PrintBody;import java.io.UnsupportedEncodingException;
import java.util.List;public class PrintHelper {public static PrintHelper instance;private UsbEndpoint usbEndpointOut;private UsbEndpoint usbEndpointIn;private UsbDeviceConnection connection;public void init(UsbEndpoint usbEndpointIn, UsbEndpoint usbEndpointOut, UsbDeviceConnection connection) {this.usbEndpointIn = usbEndpointIn;this.usbEndpointOut = usbEndpointOut;this.connection = connection;}public static PrintHelper getInstance() {if (instance == null) {instance = new PrintHelper();}return instance;}public int checkPaperStatus() {if (connection == null) {Logger.e("连接USB设备异常");return Const.Error.PRINT_ERROR;}byte[] bytes;//发送byte[] receiveBytes = new byte[10];//接收//检查连续用纸传感器状态bytes = new byte[]{0x10, 0x04, 0x04};int status;status = connection.bulkTransfer(usbEndpointOut, bytes, bytes.length, 1500);if (status < 0) {Logger.e("发送连续用纸传感器状态异常");return Const.Error.PRINT_ERROR;}//打印机接收到的信息字节status = connection.bulkTransfer(usbEndpointIn, receiveBytes, receiveBytes.length, 1500);if (status < 0) {Logger.i("接收连续用纸传感器状态检查错误", status);return Const.Error.PRINT_ERROR;} else {//返回纸盒状态return Integer.parseInt(String.valueOf(receiveBytes[0]));}}public int print(String text) throws UnsupportedEncodingException {byte[] bytes;//发送List<PrintBody> printBodyList = GsonUtils.fromJson(text, new TypeToken<List<PrintBody>>() {}.getType());for (int i = 0; i < printBodyList.size(); i++) {PrintBody printBody = printBodyList.get(i);String content = printBody.getContent();int fontSize = printBody.getTextSize();int contentType = printBody.getContentType();//设置字号String cmdStr = cmdFontSize(fontSize);if (TextUtils.isEmpty(cmdStr)) {Logger.e("数据非法:字号为空");return Const.Error.PRINT_ERROR;}// 设置字号if (connection.bulkTransfer(this.usbEndpointOut, cmdStr.getBytes(), cmdStr.getBytes().length, 1500) < 0) {Logger.e("设置字号异常");return Const.Error.PRINT_ERROR;}if (contentType == 1) {//文本bytes = new byte[]{0x1b, 0x61, 0x00};//左对齐if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {return Const.Error.PRINT_ERROR;}bytes = content.getBytes("GB2312"); // 需转换为GB2312字符集 否则汉字会乱码if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {Logger.e("获取字符异常");return Const.Error.PRINT_ERROR;}String lineSpacingExtra = printBody.getLineSpacingExtra();if (!TextUtils.isEmpty(lineSpacingExtra)) {bytes = new byte[]{0x1B, 0x64, (byte) Integer.parseInt(lineSpacingExtra, 16)};} else {bytes = new byte[]{0x1B, 0x64, 0x02};}//行边距if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 1500) < 0) {Logger.e("行边距设置异常");//打印输出打印缓冲区中的数据,并进纸 n 行return Const.Error.PRINT_ERROR;}} else if (contentType == 2) {//二维码bytes = new byte[]{0x1b, 0x61, 0x01, 0x49};//居中if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {return Const.Error.PRINT_ERROR;}bytes = new byte[]{0x1d, 0x28, 0x6b, 0x04, 0x00, 0x31, 0x41, 0x31, 0x00};if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {Logger.e("选择QR码类型异常");return Const.Error.PRINT_ERROR;}bytes = new byte[]{0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, 0x10};if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {Logger.e("设置QR条码的模块大小异常");return Const.Error.PRINT_ERROR;}bytes = new byte[]{0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, 0x30};if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {Logger.e("选择QR条码的纠错等级异常");return Const.Error.PRINT_ERROR;}byte[] qrCodeBytes = content.getBytes("GB2312");byte[] printQrBytes = new byte[8 + qrCodeBytes.length];printQrBytes[0] = 0x1d;printQrBytes[1] = 0x28;printQrBytes[2] = 0x6b;int len = qrCodeBytes.length + 3;printQrBytes[3] = ((byte) (len % 256));printQrBytes[4] = ((byte) (len / 256));printQrBytes[5] = 0x31;printQrBytes[6] = 0x50;printQrBytes[7] = 0x30;for (int pos = 0; pos < qrCodeBytes.length; pos++) {printQrBytes[8 + pos] = qrCodeBytes[pos];}if (connection.bulkTransfer(this.usbEndpointOut, printQrBytes, printQrBytes.length, 5000) < 0) {Logger.e("下载QR码数据到符号存储区");return Const.Error.PRINT_ERROR;}bytes = new byte[]{0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30};if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 5000) < 0) {Logger.e("打印存储区二维码异常"); //打印QR条码存储区中的符号数据return Const.Error.PRINT_ERROR;}}}return 0;}public int cutPaper(int bottomMargin, boolean needCut, boolean fullCut, int paperBoxStatus) {byte[] bytes;//发送//获取下边距 默认最小4if (bottomMargin < 4 || bottomMargin == 0) {bytes = new byte[]{0x1B, 0x64, 0x04};} else {bytes = new byte[]{0x1B, 0x64, (byte) (Integer.parseInt(String.valueOf(bottomMargin), 16))};}//进纸空行 为了边距if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 1500) < 0) {Logger.e("打印进纸空行异常");//打印输出打印缓冲区中的数据,并进纸 n 行return Const.Error.PRINT_ERROR;}// 切纸if (needCut) {if (fullCut) {//全切bytes = new byte[]{0x1B, 0x69};} else {//半切bytes = new byte[]{0x1B, 0x6d};}if (connection.bulkTransfer(this.usbEndpointOut, bytes, bytes.length, 1500) < 0) {return Const.Error.PRINT_ERROR;}}return paperBoxStatus;}// 字体的大小// 0:正常大小 1:两倍高 2:两倍宽 3:两倍大小 4:三倍高 5:三倍宽 6:三倍大小// 7:四倍高 8:四倍宽 9:四倍大小 10:五倍高 11:五倍宽 12:五倍大小public String cmdFontSize(int fantasize) {String cmdStr = "";// 设置字体大小switch (fantasize) {case 0:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 0).toString();// 29 33break;case 1:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 1).toString();break;case 2:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 16).toString();break;case 3:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 17).toString();break;case 4:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 2).toString();break;case 5:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 32).toString();break;case 6:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 34).toString();break;case 7:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 3).toString();break;case 8:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 48).toString();break;case 9:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 51).toString();break;case 10:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 4).toString();break;case 11:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 64).toString();break;case 12:cmdStr = new StringBuffer().append((char) 29).append((char) 33).append((char) 68).toString();break;}return cmdStr;}
}

总结

关于USB通讯,跟串口一样,还是一通百通的,步骤都是一样的,就像上面分的那几个步骤,开始不要忘记加权限,最后不要忘记关闭。 一般USB比串口的解析简单一点。
于热敏打印机,步骤也都是一样的,初始化,设置打印的参数(字符集,内容,对齐方式,间距,加粗等),切纸。
是不是很简单。
对了,最后说一下为什么我没做关于USB设备插入,拔出的监听?因为我觉得没必要。这种打印机的使用场景,谁没事会不断电谢拆主机拔掉打印机呢。需要的话网上有很多。
最后再说一次,通信的数据需要根据厂家文档做出修改,没有就想办法去找,别不改硬试!!!!!

Android USB通信开发总结和热敏打印机开发实例解析相关推荐

  1. 基于AOA协议的android USB通信

    摘 要:AOA协议是Google公司推出的用于实现Android设备与外围设备之间USB通信的协议.该协议拓展了Android设备USB接口的功能,为基于Android系统的智能设备应用于数据采集和设 ...

  2. android与usb通信,android USB通信

    USB模式 支持USB accessory模式和USB host模式.通过这两种模式,android支持各种各样的USB 外围设备和USB 配件(硬件需要实现android配件协议). USB acc ...

  3. Android开发之IPC进程间通信-AIDL介绍及实例解析

    一.IPC进程间通信 IPC是进程间通信方法的统称,Linux IPC包括以下方法,Android的进程间通信主要采用是哪些方法呢? 1. 管道(Pipe)及有名管道(named pipe):管道可用 ...

  4. Linux 设备驱动开发 —— platform设备驱动应用实例解析

    前面我们已经学习了platform设备的理论知识Linux 设备驱动开发 -- platform 设备驱动 ,下面将通过一个实例来深入我们的学习. 一.platform 驱动的工作过程 platfor ...

  5. Android开发框架模式(MVC、MVP、MVVM)实例解析

    Android项目中,尤其是比较大型的项目开发中,模块内部的高聚合和模块间的低耦合性就显得尤为重要了.所以我们一般情况下需要为项目设计一种框架模式,通常情况下我们一般用到的三种MVC.MVP.MVVM ...

  6. Android NFC开发详解 总结和NFC读卡实例解析

    文章目录 前言 一.什么是NFC? 二.基础知识 1.什么是NDEF? 2.NFC技术的操作模式 3.标签的技术类型 4.实现方式的分类 5.流程 三.获取标签内容 1.检查环境 2.获取NFC标签 ...

  7. Android USB 串口通信

    公司要求安卓PAD对接一台Windows的设备,实现双向数据传输. 是通过Windows设备的一根数据线进行数据传输的,涉及到的技术就是USB转串口通信,网上讲原理的一大堆,我就不讲了,直接上demo ...

  8. Android USB的AOA协议设备端(主机模式,配件模式),ADB连接

    USB的ADB/AOA协议(一种是ADB模式,一种是AOA模式).AOA协议是Google公司推出的用于实现Android设备与外围设备之间USB通信的协议. ADK中与USB配件模式相关的两个类是U ...

  9. Android USB 开发详解

    Android USB 开发详解 先附上 Android USB 官方文档 Android通过两种模式支持各种 USB 外设和 Android USB 附件(实现Android附件协议的硬件):USB ...

  10. Android USB开发小结:host模式与accessory模式

    很早之前就想对Android USB的两种模式作个小结,但是一直没有空去搞,毕竟USB这块应该属于冷门方向,并且应用层能够做的比较少也很简单.最近刚好在做大疆无人机的二次开发,想着对USB连接检测这块 ...

最新文章

  1. ---Pcie基本概念普及(扫盲篇--巨适合新手)
  2. ASM(active shape models)算法介绍
  3. What's NEW in C++/CLI Language
  4. 如何在UI设计中制作完美阴影
  5. Android学习笔记-判断手机外部存储是否可读写
  6. C++中友元函数,友元类数详解
  7. 【进阶2-3期】JavaScript深入之闭包面试题解
  8. android app打开另一个app并触发按钮_Android进程调度:Low memory killer(4)修改版
  9. python输入一个区间_Python 学习笔记:根据输入年月区间,返回期间所有的月份...
  10. 钓鱼网站 (搬运自common craft )
  11. tuxedo中间件tmadmin的命令使用
  12. cat6 万兆_千兆网线和万兆网线有什么区别?
  13. SSH注册通过邮箱激活
  14. 报刊订阅管理系统数据库
  15. 机器学习--SVM支持向量机
  16. 如何让同步/刷新的图标(el-icon-refresh)旋转起来
  17. SwiftUI iOS 完整项目之基于CoreData构建购物计划App(教程含源码App Store上线app)
  18. 如何使用html实现在线秒表,请使用js实现一个秒表计时器的程序
  19. python 内置函数——排序
  20. 指纹锁—AS608指纹模块

热门文章

  1. SSM社区医院卫生所病人患者随访信息管理javaweb网站系统设计与实现
  2. RTF(rich textformat)富文本格式
  3. 斐讯 K2 路由器 无线中继 无线扩展设置教程图文
  4. 计算机初级程序员哪里颁发的,初级程序员证书怎么考_初级程序员证书考什么_上学吧...
  5. 微信小程序如何跳转视频号直播间
  6. java 合并两个有序链表
  7. 期刊论文公式编号、居中技巧
  8. 植物大战僵尸修改数据
  9. Python编程 whl文件安装库
  10. 【产品宣传广告片制作软件】Focusky教程 | 封面设计