java接收串口数据
导入依赖
1.下载RXTXcomm.jar
地址: http://fizzed.com/oss/rxtx-for-java
这里的下载是根据jdk安装的位数下载,我之前下载的是W64的版本,电脑系统也是64的,但是代码跑不起来,后来才发现我电脑的JDK是32位的。
2.
下载完成后将 rxtxParallel.dll 、 rxtxSerial.dll 、文件拷贝到放入<JAVA_HOME>\jre\bin中
3.Maven导入方式
方式一:本地引入
<dependency>
<groupId>com.atian</groupId>
<artifactId>rxtxcomm</artifactId>
<version>2.2</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/java/com/atian/lib/RXTXcomm.jar</systemPath>//jar包存放的路径
</dependency>
方式二: maven引入
<dependency>
<groupId>org.bidib.jbidib.org.qbang.rxtx</groupId>
<artifactId>rxtxcomm</artifactId>
<version>2.2</version>
</dependency>
1.SerialPortManager (工具类)
package com.atian; import gnu.io.CommPortIdentifier; import gnu.io.SerialPort; import gnu.io.SerialPortEventListener; import gnu.io.CommPort; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.TooManyListenersException; public class SerialPortManager { /** * 16进制直接转换成为字符串(无需Unicode解码) * * @param hexStr * @return */ public static String hexStr2Str(String hexStr) { String str = "0123456789ABCDEF"; hexStr = hexStr.replace(" ", ""); char[] hexs = hexStr.toCharArray(); byte[] bytes = new byte[hexStr.length() / 2]; int n; for (int i = 0; i < bytes.length; i++) { n = str.indexOf(hexs[2 * i]) * 16; n += str.indexOf(hexs[2 * i + 1]); bytes[i] = (byte) (n & 0xff); } return new String(bytes); } /** * 将16进制转换为二进制 * * @param hexString * @return */ public static String hexString2binaryString(String hexString) { if (hexString == null || hexString.length() % 2 != 0) return null; String bString = "", tmp; for (int i = 0; i < hexString.length(); i++) { tmp = "0000" + Integer.toBinaryString(Integer.parseInt(hexString.substring(i, i + 1), 16)); bString += tmp.substring(tmp.length() - 4); } //字符串反转 return new StringBuilder(bString).reverse().toString(); } //Byte数组转十六进制 public static String byte2HexString(byte[] bytes) { String hex = ""; if (bytes != null) { for (Byte b : bytes) { hex += String.format("%02X", b.intValue() & 0xFF); } } return hex; } //十六进制转Byte数组 public static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; try { for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } } catch (Exception e) { // Log.d("", "Argument(s) for hexStringToByteArray(String s)"+ "was not a hex string"); } return data; } /** * 查找所有可用端口 * * @return 可用端口名称列表 * <p> * 判断端口类型是否为串口 * PORT_SERIAL = 1; 【串口】 * PORT_PARALLEL = 2; 【并口】 * PORT_I2C = 3; 【I2C】 * PORT_RS485 = 4; 【RS485】 * PORT_RAW = 5; 【RAW】 * if(portManager.getPortType() == CommPortIdentifier.PORT_SERIAL){}查看是不是串口类型具体查看SerialPortDataHandle类 */ @SuppressWarnings("unchecked") public static final ArrayList<String> findPort() { // 获得当前所有可用串口 Enumeration<CommPortIdentifier> portList = CommPortIdentifier .getPortIdentifiers(); ArrayList<String> portNameList = new ArrayList<String>(); // 将可用串口名添加到List并返回该List while (portList.hasMoreElements()) { String portName = portList.nextElement().getName(); portNameList.add(portName); } return portNameList; } /** * 打开串口 * * @param portName 端口名称 * @param baudrate 波特率 * @return 串口对象 * 设置串口参数:setSerialPortParams( int b, int d, int s, int p ) * b:波特率(baudrate) * d:数据位(datebits),SerialPort 支持 5,6,7,8 * s:停止位(stopbits),SerialPort 支持 1,2,3 * p:校验位 (parity),SerialPort 支持 0,1,2,3,4 * 如果参数设置错误,则抛出异常:gnu.io.UnsupportedCommOperationException: Invalid Parameter * 此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用 */ public static final SerialPort openPort(String portName, int baudrate, int DATABITS, int Parity) throws Exception { // 通过端口名识别端口 CommPortIdentifier portIdentifier = CommPortIdentifier .getPortIdentifier(portName); // 打开端口,设置端口名与timeout(打开操作的超时时间) CommPort commPort = portIdentifier.open(portName, 2000); // 判断是不是串口 if (commPort instanceof SerialPort) { SerialPort serialPort = (SerialPort) commPort; // 设置串口的波特率等参数 serialPort.setSerialPortParams(baudrate, DATABITS, SerialPort.STOPBITS_1, Parity); return serialPort; } return null; } /** * 关闭串口 * * @param */ public static void closePort(SerialPort serialPort) { if (serialPort != null) { serialPort.close(); } } /** * 向串口发送数据 * * @param serialPort 串口对象 * @param order 待发送数据 * 关闭串口对象的输出流出错 */ public static void sendToPort(SerialPort serialPort, byte[] order) throws Exception { OutputStream out = null; try { out = serialPort.getOutputStream(); out.write(order); out.flush(); Thread.sleep(100);//间隔20ms } catch (IOException e) { throw new Exception(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { throw new Exception(); } } } /** * 从串口读取数据 * * @param serialPort 当前已建立连接的SerialPort对象 * @return 读取到的数据 */ public static byte[] readFromPort(SerialPort serialPort) throws Exception { InputStream in = null; byte[] bytes = null; try { in = serialPort.getInputStream(); // 获取buffer里的数据长度 int bufflenth = in.available(); while (bufflenth != 0) { // 初始化byte数组为buffer中数据的长度 bytes = new byte[bufflenth]; in.read(bytes); bufflenth = in.available(); } } catch (IOException e) { throw new Exception(); } finally { try { if (in != null) { in.close(); } } catch (IOException e) { throw new Exception(); } } return bytes; } /** * 添加监听器 * * @param port 串口对象 * @param listener 串口监听器 * @throws */ public static void addListener(SerialPort port, SerialPortEventListener listener) throws Exception { try { // 给串口添加监听器 port.addEventListener(listener); // 设置当有数据到达时唤醒监听接收线程 port.notifyOnDataAvailable(true); // 设置当通信中断时唤醒中断线程 port.notifyOnBreakInterrupt(true); } catch (TooManyListenersException e) { throw new Exception(); } } }
Main(主方法)
package com.atian; import gnu.io.SerialPort; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; public class Main implements Runnable { //所有端口 static ArrayList<String> arrayList = null; //打开端口对象 static SerialPort serialPort; // 串口输入流引用 static InputStream inputStream; // 串口输出流引用 static OutputStream outputStream; static ChuanKouListener chuanKouListener; //初始化方法 public void init() { //1.查询串口信息 arrayList = SerialPortManager.findPort(); String s = "null"; // 选择串口,默认选择第一个串口 if(!arrayList.isEmpty()){ s = arrayList.get(0); } System.out.println("第一个串口为:" + s); //2.打开串口 try { if (serialPort == null) { serialPort = SerialPortManager.openPort(s, 115200, SerialPort.DATABITS_8, SerialPort.PARITY_NONE); System.out.println("已经打开串口对象!"); } // 3. 设置串口的输入输出流引用 try { inputStream = serialPort.getInputStream(); outputStream = serialPort.getOutputStream(); } catch (IOException e) { System.out.println("串口输入输出IO异常"); } } catch (Exception e) { e.printStackTrace(); System.out.println("串口使用异常!"); } //new 一个ChuanKouListener对象。 chuanKouListener = new ChuanKouListener(inputStream, outputStream); // 4.串口不为空时,设置串口监听器 try { if (serialPort != null) { SerialPortManager.addListener(serialPort, chuanKouListener); } } catch (Exception e) { e.printStackTrace(); } } public void run() { try { System.out.println("串口线程已运行"); while (true) { // 如果堵塞队列中存在数据就将其输出 if (ChuanKouListener.msgQueue != null && ChuanKouListener.msgQueue.size() > 0) { // take() 取走BlockingQueue里排在首位的对象 // 若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止 String msg = ChuanKouListener.msgQueue.take(); System.out.println("队列里拿出的信息:" + msg); //echo数据 // sendToPort(serialPort, msg.getBytes()); //从队列中拿出串口收到的信息! } } } catch (InterruptedException e) { // logger.error("线程执行异常", e); System.out.println(e); } } public static void main(String[] args) {//可以当作一个启动类。 Main main = new Main(); try { main.init(); } catch (Exception e) { e.printStackTrace(); } main.run(); } }
ChuanKouListener (串口事件监听类)
package com.atian; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class ChuanKouListener implements SerialPortEventListener { // 串口输入流引用 InputStream inputStream; // 串口输出流引用 OutputStream outputStream; public ChuanKouListener(InputStream inputStream, OutputStream outputStream) { this.inputStream = inputStream; this.outputStream = outputStream; } // 堵塞队列:用来存放串口发送到服务端的数据 public static BlockingQueue<String> msgQueue = new LinkedBlockingQueue(); public void serialEvent(SerialPortEvent serialPortEvent) { switch (serialPortEvent.getEventType()) { /* * SerialPortEvent.BI:/*Break interrupt,通讯中断 * SerialPortEvent.OE:/*Overrun error,溢位错误 * SerialPortEvent.FE:/*Framing error,传帧错误 * SerialPortEvent.PE:/*Parity error,校验错误 * SerialPortEvent.CD:/*Carrier detect,载波检测 * SerialPortEvent.CTS:/*Clear to send,清除发送 * SerialPortEvent.DSR:/*Data set ready,数据设备就绪 * SerialPortEvent.RI:/*Ring indicator,响铃指示 * SerialPortEvent.OUTPUT_BUFFER_EMPTY:/*Output buffer is empty,输出缓冲区清空 */ case SerialPortEvent.BI: case SerialPortEvent.OE: case SerialPortEvent.FE: case SerialPortEvent.PE: case SerialPortEvent.CD: case SerialPortEvent.CTS: case SerialPortEvent.DSR: case SerialPortEvent.RI: case SerialPortEvent.OUTPUT_BUFFER_EMPTY: break; // 当有可用数据时读取数据 case SerialPortEvent.DATA_AVAILABLE: // 数据接收缓冲容器 byte[] readBuffer = new byte[1024]; try { readBuffer = new byte[inputStream.available()]; } catch (IOException e) { e.printStackTrace(); } try { // 存储待接收读取字节数大小 int numBytes = 0; while (inputStream.available() > 0) { numBytes = inputStream.read(readBuffer); if (numBytes > 0) { msgQueue.add(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(new Date()) + " 收到的串口发送数据为:" + new String(readBuffer)); // 数据接收缓冲容器清空初始化 readBuffer = new byte[0]; } } } catch (IOException e) { System.out.println("00异常00"); } break; } } }
SerialPortDataHandle(参考运行类
package com.atian.util; import gnu.io.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Enumeration; import java.util.TooManyListenersException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; class SerialPortDataHandle extends Thread implements SerialPortEventListener { private static final Logger logger = LoggerFactory.getLogger(SerialPortDataHandle.class); // 通讯端口管理,控制对通信端口的访问的中心类 static CommPortIdentifier portManager; // 有效连接上的端口的枚举 static Enumeration<?> portList; // 串口输入流引用 static InputStream inputStream; // 串口输出流引用 static OutputStream outputStream; // 串口对象引用 static SerialPort serialPort; // 堵塞队列:用来存放串口发送到服务端的数据 private BlockingQueue<String> msgQueue = new LinkedBlockingQueue(); // 线程控制标识 private boolean flag = true; public void serialEvent(SerialPortEvent event) { switch (event.getEventType()) { /* * SerialPortEvent.BI:/*Break interrupt,通讯中断 * SerialPortEvent.OE:/*Overrun error,溢位错误 * SerialPortEvent.FE:/*Framing error,传帧错误 * SerialPortEvent.PE:/*Parity error,校验错误 * SerialPortEvent.CD:/*Carrier detect,载波检测 * SerialPortEvent.CTS:/*Clear to send,清除发送 * SerialPortEvent.DSR:/*Data set ready,数据设备就绪 * SerialPortEvent.RI:/*Ring indicator,响铃指示 * SerialPortEvent.OUTPUT_BUFFER_EMPTY:/*Output buffer is empty,输出缓冲区清空 */ case SerialPortEvent.BI: case SerialPortEvent.OE: case SerialPortEvent.FE: case SerialPortEvent.PE: case SerialPortEvent.CD: case SerialPortEvent.CTS: case SerialPortEvent.DSR: case SerialPortEvent.RI: case SerialPortEvent.OUTPUT_BUFFER_EMPTY: break; // 当有可用数据时读取数据 case SerialPortEvent.DATA_AVAILABLE: // 数据接收缓冲容器 byte[] readBuffer = new byte[1024]; try { readBuffer = new byte[inputStream.available()]; } catch (IOException e) { e.printStackTrace(); } try { // 存储待接收读取字节数大小 int numBytes = 0; while (inputStream.available() > 0) { numBytes = inputStream.read(readBuffer); if (numBytes > 0) { msgQueue.add(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(new Date()) + " 收到的串口发送数据为:" + new String(readBuffer)); // 数据接收缓冲容器清空初始化 readBuffer = new byte[0]; } } } catch (IOException e) { logger.error("IO异常", e); } break; } } public int init() { // 通过串口通信管理类获得当前连接上的端口列表 //(获取一个枚举对象,该CommPortIdentifier对象包含系统中每个端口的对象集[串口、并口]) portList = CommPortIdentifier.getPortIdentifiers(); while (portList.hasMoreElements()) { // 获取相应串口对象 portManager = (CommPortIdentifier) portList.nextElement(); /* * 判断端口类型是否为串口 * PORT_SERIAL = 1; 【串口】 * PORT_PARALLEL = 2; 【并口】 * PORT_I2C = 3; 【I2C】 * PORT_RS485 = 4; 【RS485】 * PORT_RAW = 5; 【RAW】 */ if (portManager.getPortType() == CommPortIdentifier.PORT_SERIAL) { logger.info("串口设备名称:" + portManager.getName()); // 判断模拟COM4串口存在,就打开该串口 if (portManager.getName().equals("COM4")) { logger.info("测试串口设备名称:" + portManager.getName()); try { if (serialPort == null) { // 打开串口,设置名字为COM_4(自定义),延迟阻塞时等待3000毫秒(赋值给预设的串口引用) serialPort = (SerialPort) portManager.open("COM4", 3000); logger.info("串口设备COM4已打开"); } } catch (PortInUseException e) { logger.error("串口使用异常", e); return 0; } // 在串口引用不为空时进行下述操作 if (serialPort != null) { // 1. 设置串口的输入输出流引用 try { inputStream = serialPort.getInputStream(); outputStream = serialPort.getOutputStream(); } catch (IOException e) { logger.error("串口输入输出IO异常", e); return 0; } // 2. 设置串口监听器 try { serialPort.addEventListener(this); } catch (TooManyListenersException e) { logger.error("串口监听器添加异常", e); return 0; } // 设置监听器在有数据时通知生效 serialPort.notifyOnDataAvailable(true); // 3. 设置串口相关读写参数 try { // 比特率、数据位、停止位、校验位 serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); } catch (UnsupportedCommOperationException e) { logger.error("串口设置操作异常", e); return 0; } return 1; } return 0; } } } return 0; } @Override public void run() { try { logger.info("串口线程已运行"); while (flag) { // 如果堵塞队列中存在数据就将其输出 if (msgQueue.size() > 0) { // take() 取走BlockingQueue里排在首位的对象 // 若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止 String msg = msgQueue.take(); logger.info(msg); //echo数据 sendToPort(serialPort, msg.getBytes()); } } } catch (InterruptedException e) { logger.error("线程执行异常", e); } } public void stopGetDataBySerialPort() { this.flag = false; } /** * 往串口发送数据 * * @param serialPort 串口对象 * @param data 待发送数据 */ public static void sendToPort(SerialPort serialPort, byte[] data) { OutputStream out = null; try { out = serialPort.getOutputStream(); out.write(data); out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) { SerialPortDataHandle handle = new SerialPortDataHandle(); int i = handle.init(); if (i == 1) { // 线程启动 handle.start(); } } }
import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import gnu.io.SerialPortEventListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.TooManyListenersException;
public class SerialPortManager {
/**
* 16进制直接转换成为字符串(无需Unicode解码)
*
* @param hexStr
* @return
*/
public static String hexStr2Str(String hexStr) {
String str = "0123456789ABCDEF";
hexStr = hexStr.replace(" ", "");
char[] hexs = hexStr.toCharArray();
byte[] bytes = new byte[hexStr.length() / 2];
int n;
for (int i = 0; i < bytes.length; i++) {
n = str.indexOf(hexs[2 * i]) * 16;
n += str.indexOf(hexs[2 * i + 1]);
bytes[i] = (byte) (n & 0xff);
}
return new String(bytes);
}
/**
* 将16进制转换为二进制
*
* @param hexString
* @return
*/
public static String hexString2binaryString(String hexString) {
if (hexString == null || hexString.length() % 2 != 0)
return null;
String bString = "", tmp;
for (int i = 0; i < hexString.length(); i++) {
tmp = "0000" + Integer.toBinaryString(Integer.parseInt(hexString.substring(i, i + 1), 16));
bString += tmp.substring(tmp.length() - 4);
}
//字符串反转
return new StringBuilder(bString).reverse().toString();
}
//Byte数组转十六进制
public static String byte2HexString(byte[] bytes) {
String hex = "";
if (bytes != null) {
for (Byte b : bytes) {
hex += String.format("%02X", b.intValue() & 0xFF);
}
}
return hex;
}
//十六进制转Byte数组
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
try {
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
} catch (Exception e) {
// Log.d("", "Argument(s) for hexStringToByteArray(String s)"+ "was not a hex string");
}
return data;
}
/**
* 查找所有可用端口
*
* @return 可用端口名称列表
* <p>
* 判断端口类型是否为串口
* PORT_SERIAL = 1; 【串口】
* PORT_PARALLEL = 2; 【并口】
* PORT_I2C = 3; 【I2C】
* PORT_RS485 = 4; 【RS485】
* PORT_RAW = 5; 【RAW】
* if(portManager.getPortType() == CommPortIdentifier.PORT_SERIAL){}查看是不是串口类型具体查看SerialPortDataHandle类
*/
@SuppressWarnings("unchecked")
public static final ArrayList<String> findPort() {
// 获得当前所有可用串口
Enumeration<CommPortIdentifier> portList = CommPortIdentifier
.getPortIdentifiers();
ArrayList<String> portNameList = new ArrayList<String>();
// 将可用串口名添加到List并返回该List
while (portList.hasMoreElements()) {
String portName = portList.nextElement().getName();
portNameList.add(portName);
}
return portNameList;
}
/**
* 打开串口
*
* @param portName 端口名称
* @param baudrate 波特率
* @return 串口对象
* 设置串口参数:setSerialPortParams( int b, int d, int s, int p )
* b:波特率(baudrate)
* d:数据位(datebits),SerialPort 支持 5,6,7,8
* s:停止位(stopbits),SerialPort 支持 1,2,3
* p:校验位 (parity),SerialPort 支持 0,1,2,3,4
* 如果参数设置错误,则抛出异常:gnu.io.UnsupportedCommOperationException: Invalid Parameter
* 此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用
*/
public static final SerialPort openPort(String portName, int baudrate, int DATABITS, int Parity)
throws Exception {
// 通过端口名识别端口
CommPortIdentifier portIdentifier = CommPortIdentifier
.getPortIdentifier(portName);
// 打开端口,设置端口名与timeout(打开操作的超时时间)
CommPort commPort = portIdentifier.open(portName, 2000);
// 判断是不是串口
if (commPort instanceof SerialPort) {
SerialPort serialPort = (SerialPort) commPort;
// 设置串口的波特率等参数
serialPort.setSerialPortParams(baudrate,
DATABITS, SerialPort.STOPBITS_1,
Parity);
return serialPort;
}
return null;
}
/**
* 关闭串口
*
* @param
*/
public static void closePort(SerialPort serialPort) {
if (serialPort != null) {
serialPort.close();
}
}
/**
* 向串口发送数据
*
* @param serialPort 串口对象
* @param order 待发送数据
* 关闭串口对象的输出流出错
*/
public static void sendToPort(SerialPort serialPort, byte[] order)
throws Exception {
OutputStream out = null;
try {
out = serialPort.getOutputStream();
out.write(order);
out.flush();
Thread.sleep(100);//间隔20ms
} catch (IOException e) {
throw new Exception();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
throw new Exception();
}
}
}
/**
* 从串口读取数据
*
* @param serialPort 当前已建立连接的SerialPort对象
* @return 读取到的数据
*/
public static byte[] readFromPort(SerialPort serialPort)
throws Exception {
InputStream in = null;
byte[] bytes = null;
try {
in = serialPort.getInputStream();
// 获取buffer里的数据长度
int bufflenth = in.available();
while (bufflenth != 0) {
// 初始化byte数组为buffer中数据的长度
bytes = new byte[bufflenth];
in.read(bytes);
bufflenth = in.available();
}
} catch (IOException e) {
throw new Exception();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
throw new Exception();
}
}
return bytes;
}
/**
* 添加监听器
*
* @param port 串口对象
* @param listener 串口监听器
* @throws
*/
public static void addListener(SerialPort port,
SerialPortEventListener listener) throws Exception {
try {
// 给串口添加监听器
port.addEventListener(listener);
// 设置当有数据到达时唤醒监听接收线程
port.notifyOnDataAvailable(true);
// 设置当通信中断时唤醒中断线程
port.notifyOnBreakInterrupt(true);
} catch (TooManyListenersException e) {
throw new Exception();
}
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 我与微信审核的“相爱相杀”看个人小程序副业
· DeepSeek “源神”启动!「GitHub 热点速览」
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库