news 2026/9/2 21:04:14

Android USB HID通信Demo:USB Host模式数据收发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android USB HID通信Demo:USB Host模式数据收发

简介:一套基于安卓USB Host与OTG模式的HID通信Demo,面向需要实现手机与单片机双向数据交互的嵌入式及安卓开发人员。资源内含完整Android工程,覆盖UsbManager设备枚举、权限申请、UsbDeviceConnection建立连接、输入输出端点读写及HID报告解析等关键环节,Ground_Station示例展示了将安卓设备作为地面站与单片机通信的落地方式,可迁移至飞控、遥测等场景。压缩包共1055个文件,以XML布局配置、Java源码、JSON数据、PNG图片和协议原始文件为主,包含Gradle构建脚本及运行库,整体仅17.44MB,结构清晰便于按功能模块查阅。目前已有3752人学习下载,适合有一定安卓基础、希望快速上手USB HID通信的开发者作为参考模板。

1. 项目概述与整体思路拆解

做安卓 USB 外设开发,最绕不开的就是跟 HID 设备打交道。我这次要分享的“安卓 usb hid通信demo”,一句话说清楚就是:让 Android 设备通过 USB Host 模式,直接和 HID 协议的设备进行数据收发,不依赖任何第三方库,纯 Android SDK 原生 API 实现,Demo 代码可以直接拷到自己的项目里改改就用。

这个需求在真实场景里出现频率很高。比如你要做一个工业数据采集的安卓 App,设备端是一个 USB 键盘式的扫码枪;或者你手上有一块自定义 HID 协议的 USB 传感器板子,想用手机读取数据;再或者你想做一个安卓端的上位机,控制一个 USB HID 继电器模块。这些场景背后都是同一件事:Android 作为 USB Host,和 HID 外设建立通信通道,按 HID Report 格式收发数据。

在动手之前,先讲清楚几个选型层面的决策,这是很多人一上来就踩坑的地方。

第一个决策:为什么选 HID 而不是串口?安卓端做 USB 通信,最常见的有三条路:USB 转串口(CDC ACM)、USB HID、USB 自定义 Vendor 类。串口方案在工业场景很成熟,但有个硬伤——需要外设侧预先烧录 CDC 固件,而且很多 USB 转串口芯片(比如常见的 FT232R、CH340)在安卓上需要额外处理系统权限。HID 协议则不同,它是操作系统级别“免驱”的,安卓原生支持 HID Host,插上就能枚举,不需要 root,不用装驱动,这是它最大的优势。

第二个决策:通信模式选中断传输(Interrupt)还是控制传输(Control)?这取决于设备的实际配置。绝大多数 HID 键盘、扫码枪、自定义 HID 设备,数据通道都是中断传输端点(Interrupt OUT/IN),因为 HID 协议设计之初就是为低延迟、小数据量交互准备的。控制传输用于读取 HID 描述符和发送 Set_Report/Get_Report 请求,更多是初始化阶段用。我在 Demo 里两种都实现了,但主力走中断传输。

第三个决策:用官方 UsbManager 还是封装库?网上有人推 usb-serial-for-android 这类库,但那是针对 CDC 串口设备的。做 HID 通信,直接用系统 API 就够了,封装反而碍事。官方 API 的核心就四个对象:UsbManagerUsbDeviceUsbInterfaceUsbEndpoint,理解这四个对象,整个通信链路就打通了。

这个 Demo 适合谁看?已经会安卓基础开发、想进入 USB 外设领域的人,以及做嵌入式、手头有 HID 设备需要快速出一版安卓调试工具的工程师。对于完全没碰过安卓的小白,文中涉及 Activity 生命周期和异步线程的部分建议先补一下基础。

2. 开发环境准备与基础设施搭建

2.1 硬件选型与设备准备

我这次实测用的设备组合是:

  • 手机:红米 12C(安卓 11 系统,Type-C 口)
  • HID 设备:一个自制的 STM32 自定义 HID 设备,Report 长度 64 字节

为什么强调 64 字节?因为USB HID 协议规定中断传输的最大数据包长度不能超过 64 字节(全速设备)。如果你的设备一次要传超过 64 字节的数据,必须在驱动层做分包组包,这是 HID 通信一个绕不开的限制。实测下来,低速设备最大 8 字节,全速设备最大 64 字节,高速设备理论上可以更大,但绝大多数嵌入式 HID 设备都是全速模式,所以按 64 字节来设计数据帧是最稳妥的。

如果你手头没有 HID 设备,有两个替代方案可以先用起来:一个是 USB 键鼠(因为它们是标准 HID 设备),另一个是用 USB 转串口模块刷一个 HID 固件(注意这个只用于学习调试,不算完整方案)。代码逻辑是一样的,只要厂商 ID(Vendor ID)和产品 ID(Product ID)不冲突,系统就能识别。

2.2 动态权限申请与设备过滤声明

做 USB Host 通信,第一步是让系统知道“我的 App 要在特定设备插入时被拉起”。这个在AndroidManifest.xml里声明一个 intent-filter 就行。

<activity android:name=".MainActivity" android:launchMode="singleTop"> <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity>

同时需要在res/xml/device_filter.xml里配置设备过滤规则:

<?xml version="1.0" encoding="utf-8"?> <resources> <usb-device vendor-id="1155" product-id="22352" /> <!-- 这里是示例的 VID/PID,你要是用标准 HID 键盘,一般不需要在这里声明 --> </resources>

这里的 vendor-id 和 product-id 是十进制数,用lsusb命令或 Windows 设备管理器查到的十六进制值先转成十进制再填,别直接填十六进制,不然会踩一个很隐蔽的坑:明明设备插上却过滤不到,系统完全没有反应

注意:过滤声明写好后,如果设备没出现在列表里,优先检查 VID/PID 是否转换成了十进制,这是我在实际调试中遇到频率最高的问题之一。

权限申请这一步要区分两种情况:如果你的 App 是在设备“已插入”状态下通过 intent-filter 被拉起的,系统会自动带上权限,可以直接获取;但如果 App 是手动启动、设备已经在系统里,就必须用requestPermission()动态申请。

这个动态申请在真机上表现很直接:会弹一个系统对话框,问用户“是否允许此应用访问该 USB 设备?”,用户点了“确定”回调才会给权限。权限回调的结果是异步的,用广播接收器(BroadcastReceiver)监听ACTION_USB_PERMISSION。我最初写这个 Demo 时犯过一个错误,就是没注册这个广播就急着去 open 设备,结果拿到 null 设备时一脸懵。

3. 核心代码实现:从设备识别到数据收发

3.1 枚举设备与权限申请完整代码

核心逻辑放在MainActivity里,第一步是获取 UsbManager 并枚举设备。看到这里你会发现,安卓的 USB Host 架构其实就是“管理器”模式,所有对 USB 的操作都要过 UsbManager 这个口子。

public class MainActivity extends AppCompatActivity { private static final String TAG = "UsbHidDemo"; private static final String ACTION_USB_PERMISSION = "com.example.usbhid.USB_PERMISSION"; private UsbManager usbManager; private PendingIntent permissionIntent; private UsbDevice targetDevice; private UsbDeviceConnection usbConnection; private UsbInterface usbInterface; private UsbEndpoint outEndpoint; // 中断输出端点 private UsbEndpoint inEndpoint; // 中断输入端点 private final BroadcastReceiver usbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (device != null) { // 拿到权限,开始打开设备 openUsbDevice(device); } } else { Log.e(TAG, "用户拒绝了 USB 权限"); Toast.makeText(MainActivity.this, "USB权限被拒绝", Toast.LENGTH_SHORT).show(); } } } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { // 设备拔出,清理连接 closeUsbDevice(); } } }; }

这里的重点在于广播接收器的两个 action 缺一不可:一个是权限回调,一个是设备拔出通知。拔出的监听特别重要,因为不少 HID 设备存在热插拔场景,如果不做处理,App 很容易在设备拔出后崩溃。

然后是枚举和申请权限的代码:

private void findAndRequestDevice() { HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); if (deviceList.isEmpty()) { Log.e(TAG, "没有检测到 USB 设备"); return; } for (UsbDevice device : deviceList.values()) { Log.d(TAG, "发现设备: VID=" + device.getVendorId() + ", PID=" + device.getProductId() + ", DeviceClass=" + device.getDeviceClass()); // 判断是否是 HID 设备 if (device.getDeviceClass() == UsbConstants.USB_CLASS_HID) { targetDevice = device; break; } } if (targetDevice != null) { // 第一次连接需要动态申请权限 permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE); usbManager.requestPermission(targetDevice, permissionIntent); } else { // 这里很容易踩坑:很多设备 deviceClass 不是 HID,但 interfaceClass 才是 Log.w(TAG, "没有找到 HID 设备,尝试按 interface 匹配"); findHidDeviceByInterface(deviceList); } }

日志里那句提示是我后来加上的,因为很多自制的 HID 设备(比如 STM32 用 CubeMX 生成的例程)deviceClass 写的是0x00,但实际上它的接口(Interface Class)是 HID。写代码时不能只看设备类,还要往下看一层接口类,所以配套一个按 interface 匹配的函数是必须的。

private void findHidDeviceByInterface(HashMap<String, UsbDevice> deviceList) { for (UsbDevice device : deviceList.values()) { for (int i = 0; i < device.getInterfaceCount(); i++) { UsbInterface usbIf = device.getInterface(i); if (usbIf.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { targetDevice = device; usbInterface = usbIf; return; } } } }

3.2 打开设备、选择端点和数据收发

拿到权限后就可以打开设备了。这一步最核心的是选对端点和传输方向。一个 HID 接口通常有多个端点:中断输入端点(设备发给主机)、中断输出端点(主机发给设备)、控制端点 0。如果选错端点,读写都会失败,而且报错信息不一定直观,可能只是超时。

private void openUsbDevice(UsbDevice device) { UsbInterface targetInterface = null; for (int i = 0; i < device.getInterfaceCount(); i++) { UsbInterface usbIf = device.getInterface(i); if (usbIf.getInterfaceClass() == UsbConstants.USB_CLASS_HID) { targetInterface = usbIf; break; } } if (targetInterface == null) { Log.e(TAG, "未找到 HID 接口"); return; } usbConnection = usbManager.openDevice(device); if (usbConnection == null) { Log.e(TAG, "打开设备失败,请检查权限或设备是否被占用"); return; } // 声明独占接口 usbConnection.claimInterface(targetInterface, true); // 遍历端点,找到输入和输出端点 for (int i = 0; i < targetInterface.getEndpointCount(); i++) { UsbEndpoint ep = targetInterface.getEndpoint(i); if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT) { if (ep.getDirection() == UsbConstants.USB_DIR_IN) { inEndpoint = ep; Log.d(TAG, "找到中断输入端点,地址: " + ep.getAddress() + ", 包大小: " + ep.getMaxPacketSize()); } else if (ep.getDirection() == UsbConstants.USB_DIR_OUT) { outEndpoint = ep; Log.d(TAG, "找到中断输出端点,地址: " + ep.getAddress() + ", 包大小: " + ep.getMaxPacketSize()); } } } }

数据发送和接收就非常简洁了:

// 发送数据,注意 input 必须是 64 字节以内的 byte 数组 public boolean sendData(byte[] data) { if (outEndpoint == null || usbConnection == null) { Log.e(TAG, "输出端点或连接未就绪"); return false; } if (data.length > outEndpoint.getMaxPacketSize()) { Log.e(TAG, "数据长度超过端点最大包长"); return false; } int transferred = usbConnection.bulkTransfer(outEndpoint, data, data.length, 1000); return transferred >= 0; } // 接收数据,阻塞式读取,建议放在子线程 public byte[] receiveData() { if (inEndpoint == null || usbConnection == null) { return null; } byte[] buffer = new byte[inEndpoint.getMaxPacketSize()]; int received = usbConnection.bulkTransfer(inEndpoint, buffer, buffer.length, 1000); if (received > 0) { byte[] result = new byte[received]; System.arraycopy(buffer, 0, result, 0, received); return result; } return null; }

关于bulkTransfer这个方法,多说一句:它虽然名字带 bulk,但对中断端点同样适用。本质就是“同步阻塞式端点传输”,可以简单理解成“把数据丢进一个管道,数据到了就返回,不到就阻塞到超时时间为止”。它唯一的缺点是不能设置多个超时周期(不像 AOA 方案里可以通过别的方式做异步),但在 Demo 阶段完全够用。

提示:bulkTransfer最后一个参数是超时时间,单位毫秒。如果你设 0,表示无限等待,在 UI 线程调用必卡死,所以接收数据一定要放到子线程。我习惯用 1000ms 超时,既能保证及时性,又不会让线程长期挂起。

3.3 数据读取线程与 UI 更新

接收数据是阻塞的,不能放在主线程。我用一个HandlerThread循环读数据,读到就通过 Handler 回调到主线程更新 UI:

private HandlerThread readThread; private Handler readHandler; private Handler mainHandler = new Handler(Looper.getMainLooper()); private void startReading() { readThread = new HandlerThread("UsbReadThread"); readThread.start(); readHandler = new Handler(readThread.getLooper()); readHandler.post(new Runnable() { @Override public void run() { while (isReading) { byte[] data = receiveData(); if (data != null) { // 通过主线程 Handler 更新 UI mainHandler.post(() -> { String hexStr = bytesToHex(data); tvReceive.setText(hexStr); }); } else { // 超时或读取失败,这里不要 break,否则线程就死了 Log.d(TAG, "读取超时,继续等待"); } } } }); }

注意那个else分支,读取超时是正常的,不应该让它退出循环。很多新手在这里写成if (data != null) break;,导致只读了一次,后面再也收不到数据,排查半天还以为是设备问题。

发送数据,我测试的时候是用了一个简单的“定长帧”协议,比如前两位是命令字,后 62 位是数据,因为 HID 包固定 64 字节,发多长其实就是填满 64 字节,设备侧解析也方便:

// 封包:命令 0x01 代表查询状态 byte[] packet = new byte[64]; packet[0] = 0x01; packet[1] = 0x00; sendData(packet);

4. 实测过程与典型问题排查

4.1 一次完整的实测记录

我把这个 Demo 部署到红米 12C 上做了完整测试。设备是一个 STM32 模拟 HID 键盘的设备,插上电之后系统立刻弹了授权框,点允许后进入主界面,日志打印出找到的端点和包大小。实际读到了设备端返回的 64 字节数据包,内容为设备状态结构体,我按字节解析后显示在界面上,通信链路完全打通。

有一个细节值得注意:HID 设备插入时,系统不一定立刻弹出授权框,如果 App 是后台进程,授权框可能被系统窗口遮挡。所以我的 Demo 的设计是:App 启动时直接调用findAndRequestDevice(),主动要权限,这样用户一打开 App 就能看到授权弹窗,而不是傻等插入事件。

4.2 遇到的典型问题与解决思路

我把调试过程中遇到的高频问题整理成了表格,方便大家对照排查:

问题现象可能原因解决办法
设备插上后 App 没反应VID/PID 过滤没生效或没填对检查 device_filter.xml,确认 VID/PID 是否为十进制
设备能枚举但openDevice返回 null设备已被其他应用占用,或没有申请权限确认权限回调成功后再 open;检查是否有其他 App 持有设备
打开设备后读写超时端点选择错误,或claimInterface未执行成功打印所有端点信息,确认选的是中断输入/输出端点;claimInterface第二个参数传 true
读到的数据全是0x00读取缓冲区和设备发送的数据长度不匹配,或读取周期太快将缓冲区设为getMaxPacketSize(),检查解析逻辑
设备拔出后 App 闪退没有注册ACTION_USB_DEVICE_DETACHED监听注册广播,拔出时调用closeUsbDevice()释放连接
系统提示“该设备找不到足够资源可以使用。(代码 12)”设备的端点资源分配冲突,或 USB Host 控制器资源不足尝试重新插拔、重启手机、关掉占用 USB 的其他应用;如果设备是高速/超速设备,考虑换一个 USB HUB 供电

“代码 12”这个问题在 Windows 上最常见,在安卓上比较少见,但一旦出现大概率是 USB Host 控制器的资源被耗尽。多数情况下重启设备或者换一个 USB 口就能解决,不用太焦虑。

4.3 抓包验证:USB 通信看不到数据时的终极大法

如果代码看起来没问题但就是读不到数据,我强烈建议用Wireshark 抓一次 USB 通信,前提是你在 Linux 环境下运行抓包,或者使用安装了 USB 抓包插件的 Windows 系统。

在 Linux 下抓 USB 包,需要先加载 usbmon 模块:

sudo modprobe usbmon

然后用 Wireshark 选择 usbmon 网卡(比如 usbmon1、usbmon2,对应不同的 USB 总线),就能看到设备枚举、控制传输、中断传输的完整报文。这是排查 HID 设备通信问题最有效的手段,没有之一。它能让你一眼看出来:是设备根本没发数据,还是主机发了但设备没应答,还是数据被解析错了。抓包步骤虽然稍微复杂一点,但在“看不到数据”这种疑难杂症面前,比反复改代码猜问题高效太多了。

5. 实际调试中的坑点回顾与排查技巧

5.1 最容易忽略的细节

这些坑是在项目中真正踩过的,大多数时候不会直接报错,而是表现为“诡异行为”,我把它们单独列出来说。

第一,设备拔出后的清理顺序。很多人的closeUsbDevice()只做了 close 连接,忘了releaseInterface。如果你开发的 App 是常驻后台的,比如做数据采集的,那么这个释放步骤会直接影响下一次插拔的体验。正确顺序是:

private void closeUsbDevice() { isReading = false; if (usbConnection != null) { if (usbInterface != null) { usbConnection.releaseInterface(usbInterface); } usbConnection.close(); } usbConnection = null; usbInterface = null; inEndpoint = null; outEndpoint = null; }

第二,PendingIntent.FLAG_IMMUTABLE在安卓 12 及更高版本是强制要求。如果你 targetSdk 切到 31 以上,不加这个 flag 会直接崩。这个问题看起来小,但真的影响 App 启动。

第三,不要在 USB 权限回调里做耗时操作,比如打开数据库、初始化网络连接等。权限回调是跑在 Binder 线程上的,不是主线程,也不是专门的回调线程。你在里面做耗时操作,虽然短期内看不出问题,但一旦设备拔插频繁,就可能出现连不上或数据丢失。

第四个坑比较隐蔽:bulkTransfer的返回值不一定等于你传入的 data.length。它返回的是实际传输的字节数,可能小于请求长度。如果你的协议是定长帧,解析数据时一定要以received为准,不要用buffer.length或者固定的 64,否则会读到上次残留的旧数据。我在写 receiveData 时专门做了System.arraycopy裁剪,就是为了避免这个问题。

5.2 安卓 11 及以上版本的适配问题

安卓 11 开始,USB 权限相关的行为有一些微妙变化。我在红米 12C(安卓 11)上测试时发现几个问题:

  • 系统对“后台使用 USB 设备”的限制更严了。如果你的 App 退到后台,即使之前已经拿到权限,也可能收不到设备的数据。这是系统休眠策略导致的,不是你的代码问题。解决方案是让你的 App 在前台运行,或用前台服务保持进程活跃。
  • requestPermission返回的弹窗如果被用户取消,不会再弹出第二次。所以最好在 UI 上给用户一个“重新申请权限”的按钮,而不是只能重启 App。
  • 某些定制 ROM(尤其国产 ROM)对 USB 授权的弹窗显示策略不同,在个别设备上弹窗可能不出现。遇到这种情况,优先检查自己 App 的通知权限,因为系统把 USB 授权弹窗通过通知栏渠道展示,通知被关的话弹窗就会被吞掉。

5.3 进阶:HID Report Descriptor 与自定义协议解析

如果你的 HID 设备是自定义设备,比如自己用 STM32 实现的 HID 传感器,你还需要了解Report Descriptor这个概念。它描述了设备的报告格式,但其实在安卓侧我们可以不解析它,直接按固定长度收发原始数据即可。真正需要关心里面内容的时候,是你要逆向一个不熟悉的 HID 设备时,或者要实现 HID 键盘/鼠标模拟功能时。

Report Descriptor 的样子,以最简单的 64 字节自定义 HID 设备为例,核心部分大概长这样:

0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) 0x09, 0x01, // Usage (0x01) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (0x01) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8) 0x95, 0x40, // Report Count (64) 0x81, 0x02, // Input (Data, Var, Abs) 0xC0 // End Collection

这段描述符定义了 64 字节的输入报告。它在安卓侧的对应物就是UsbEndpointgetMaxPacketSize()返回 64。如果你要在电脑上好好调这类设备,建议配合 HID Descriptor Tool、USBlyzer 这类工具去看得更清楚。不过这些细节在安卓开发侧不是必需的——你只要遵守 64 字节以内的数据包长度,通信就能通。

6. 从 Demo 到产品级应用的几点建议

很多人做完 Demo 就完了,但真要放到产品里,这几个地方一定要提前规划。

通信协议设计。Demo 里简单发送定长 64 字节没问题,但产品化时我建议设计自己的帧格式,比如帧头(2 字节)+ 长度(1 字节)+ 命令字(1 字节)+ 数据区 + 校验(CRC16 或累加和)。这能大大减少排障难度。实测过很多工业 HID 设备,它们内部都是这种结构。如果只是裸发数据,一旦出现错位,基本没法定位。

多设备支持。如果你的 App 需要同时连接多个 HID 设备,要注意安卓系统对 USB Host 同一时刻只允许一个 App 独占权限,多个设备能接入,但每个设备都要单独申请权限。代码层面要把deviceList的遍历、权限申请、连接管理做成按设备维度的“会话”,不能像 Demo 里那样只存一个targetDevice

线程模型优化。bulkTransfer的阻塞模式虽然在 Demo 里够用,但产品里推荐用UsbRequest做异步,或者干脆用BlockingQueue加多个工作线程来解耦读写。特别是在高频率收数据(比如 1kHz 上报率的传感器)时,阻塞模式很容易丢包。

功耗管理。USB Host 模式下手机可以作为外设的供电方,如果你连接的是无外部供电的 HID 设备,要考虑手机电量消耗。实测下来,长时间 USB Host 通信对手机电量消耗在每小时 8%~12% 左右,这是一块不小的开销。产品里建议增加供电提示和低电量告警,必要时引导用户使用外部供电的 USB Hub。这个细节很多人忽略,但真的做了产品化你会发现它很重要。

7. 个人实操经验分享

最后分享一点我自己的体会。这个 HID 通信 Demo 看起来简单,但从“能通”到“稳定通”,中间隔着一堆细节。我最初做的时候,光在端点选择上就卡了两天,因为设备端和安卓端的理解不一致,设备认为自己是输出端点,安卓却以主机的视角去看这个端点。想明白“端点方向是相对于主机来说的”这件事之后,一切豁然开朗。

另外,如果你打算长期做 USB 相关开发,强烈建议在工位上常备一根 USB 转 TTL 的调试线,再备一个Wireshark 抓包环境。很多 USB 问题在代码层面根本看不出来,但是抓包数据一到手,设备波形、字节流全摆在那里,问题就清晰了。这比看日志、printf 高效太多了。

把这个 Demo 跑通之后,你在安卓端做 USB 键盘监听、HID 扫码枪接入、自定义 HID 设备通信就都有了地基。下一步可以考虑往 AOA(Android Open Accessory)协议或者 USB 音频方向扩展,但那是另一个话题了。先把 64 字节以内的中断传输玩明白,后面遇到再复杂的外设,思路都是相同的。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 21:03:57

pdf.js实战:从零实现前端PDF预览与交互

简介&#xff1a;面向需要在浏览器中集成 PDF 查看功能的开发者&#xff0c;这份资源以实际可运行的 demo 演示 PDF.js 的典型用法&#xff0c;帮助解决 Web 端 PDF 在线预览与交互控制的落地问题。压缩包仅 6 个文件、约 601KB&#xff0c;包含 3 个 HTML 示例页面、2 个核心 …

作者头像 李华
网站建设 2026/9/2 21:03:54

用pdf.js实现网页内PDF预览:从零构建可定制翻页缩放方案

简介&#xff1a;这份pdf.js使用demo是一份面向Web前端开发者的实践示例&#xff0c;以Mozilla团队开源的PDF.js为核心&#xff0c;演示如何在浏览器中嵌入PDF文档的解析与渲染&#xff0c;解决开发者不熟悉库集成与配置的痛点。压缩包解压后共6个文件&#xff1a;3个HTML示例页…

作者头像 李华
网站建设 2026/9/2 20:55:57

资源站源码选型与部署实战:PHP建站、SEO优化到长期运营

简介&#xff1a;这是一套基于ASP的完整免费资源站源码&#xff0c;面向网站开发初学者、快速建站用户及二次开发者&#xff0c;可帮助快速搭建并理解完整站点。压缩包内包含完整的前端页面、服务端脚本、数据库配置及样式交互文件&#xff0c;共包含1119个相关文件&#xff0c…

作者头像 李华
网站建设 2026/9/2 20:54:15

基于物理信息神经网络(PINN)的三维声波方程求解与MATLAB实现

简介&#xff1a;本资源是一套基于物理信息神经网络&#xff08;PINN&#xff09;求解三维声波波动方程的MATLAB实现方案&#xff0c;面向计算物理、声学仿真及深度学习交叉领域的研究者与高年级本科生/研究生&#xff0c;解决传统数值方法在复杂边界或高维场景下计算成本高、泛…

作者头像 李华
网站建设 2026/9/2 20:53:44

指弹吉他《Scarborough Fair》独奏资源与练习全攻略

这次直接看一份指弹吉他资源&#xff1a;冈崎伦典改编的《Scarborough Fair》指弹独奏版&#xff0c;配套吉他谱和演示音频。它不是教学视频&#xff0c;也不是原版弹唱谱&#xff0c;而是一份适合独奏练习的指弹改编资源。核心用途很清楚&#xff1a;看着谱面演奏&#xff0c;…

作者头像 李华
网站建设 2026/9/2 20:53:41

家居建材企业使用抓词GEO优化实战复盘:提升AI引用率实证分析

摘要:本文基于国内本土家居建材实体企业真实落地实操案例,遵循 Google EEAT 权威内容质量标准,围绕GEO 生成式引擎优化(行业也常称为 AEO,AI 生成式引擎优化),完整复盘家居企业从低 AI 收录、零 AI 引用,到被主流大模型稳定采信、高频引用的全流程优化路径。文中客观呈现基线监…

作者头像 李华