首页 文章 精选 留言 我的

精选列表

搜索[多模态文档智能],共10000篇文章
优秀的个人博客,低调大师

区块链全栈工程师视角:从智能合约到DApp前端的完整开发链路

作为一名区块链全栈工程师,日常工作的核心在于打通“链上逻辑”与“链下交互”之间的鸿沟。本文从工程实践角度,拆解一个完整DApp的开发链路,并给出可运行的关键代码。 ## 一、技术栈选型 典型全栈组合为:Solidity + Hardhat(合约层)、ethers.js v6 + React + wagmi(前端层)、The Graph 或自建索引器(数据层)。这套组合兼顾开发效率与生产可用性。 ## 二、合约层:可升级的NFT铸造合约 合约设计需考虑权限控制、重入防护与事件索引。以下是一个精简的ERC-721铸造合约: ``` // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract MintNFT is ERC721URIStorage, Ownable, ReentrancyGuard { uint256 public nextTokenId; uint256 public mintPrice = 0.01 ether; uint256 public maxSupply = 1000; mapping(address => bool) public whitelist; event Minted(address indexed to, uint256 indexed tokenId, string uri); constructor() ERC721("FullStackNFT", "FSN") Ownable(msg.sender) {} function setWhitelist(address[] calldata users, bool status) external onlyOwner { for (uint256 i = 0; i = mintPrice, "Insufficient fee"); if (!whitelist[msg.sender]) { require(msg.value >= mintPrice * 2, "Public price higher"); } uint256 tokenId = nextTokenId++; _safeMint(msg.sender, tokenId); _setTokenURI(tokenId, uri); emit Minted(msg.sender, tokenId, uri); } function withdraw() external onlyOwner { (bool ok, ) = payable(owner()).call{value: address(this).balance}(""); require(ok, "Withdraw failed"); } } ``` **工程要点**:使用OpenZeppelin成熟组件降低安全风险;`nonReentrant`防护铸造类攻击;事件携带`uri`便于链下索引;白名单与公开价格分层是常见商业逻辑。 ## 三、前端层:ethers.js v6 交互封装 前端需处理钱包连接、网络切换、交易签名与状态回显。以下是一个可复用的合约Hook: ``` import { BrowserProvider, Contract, parseEther } from "ethers"; import { useState, useCallback } from "react"; const ABI = [ "function mint(string uri) payable", "function nextTokenId() view returns (uint256)", "event Minted(address indexed to, uint256 indexed tokenId, string uri)" ]; const CONTRACT_ADDRESS = "0xYourContractAddress"; export function useMint() { const [loading, setLoading] = useState(false); const [txHash, setTxHash] = useState(); const mint = useCallback(async (uri: string) => { if (!window.ethereum) throw new Error("请安装MetaMask"); setLoading(true); try { const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const contract = new Contract(CONTRACT_ADDRESS, ABI, signer); const price = parseEther("0.02"); const tx = await contract.mint(uri, { value: price }); setTxHash(tx.hash); const receipt = await tx.wait(1); // 解析事件,获取tokenId const log = receipt.logs .map((l: any) => { try { return contract.interface.parseLog(l); } catch { return null; } }) .find((e: any) => e?.name === "Minted"); return { tokenId: log?.args?.tokenId?.toString(), hash: tx.hash }; } finally { setLoading(false); } }, []); return { mint, loading, txHash }; } ``` **工程要点**:v6中`BrowserProvider`替代了旧版`Web3Provider`;`tx.wait(1)`确保确认数;事件解析应做`try/catch`容错,因为交易日志中可能混入其他合约事件。 ## 四、链下数据层与常见陷阱 链上不适合存储大对象,token URI通常指向IPFS或中心化CDN。工程上需注意: 1. **RPC限流**:生产环境应使用Alchemy/Infura多节点轮询,前端避免高频`eth_call`。 1. **交易重放**:前端需维护`nonce`与`pending`状态,防止用户重复点击导致nonce冲突。 1. **事件索引**:`Minted`事件应被The Graph或自建服务消费,前端列表页从索引服务读取而非逐块扫描。 ## 五、安全与测试 合约必须经过Slither静态扫描与Foundry fuzz测试;前端需校验`chainId`,防止用户在错误网络上签名。上线前建议通过Tenderly做交易模拟,验证gas估算与失败路径。 全栈工程师的价值不在于单点技术深度,而在于让合约、前端、数据层形成闭环,并在每个接缝处做好错误处理与安全假设。以上代码可直接作为项目脚手架起点,按需扩展。

优秀的个人博客,低调大师

鸿蒙RS232驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】

串口节点会找了。今天第一颗外设是 RS232。发送脚曾被公版相机电源抢走,探测失败时设备节点都不会出现。 ![](https://developer.qcloudimg.com/http-save/yehe-12803057/bed1e222605c67f7fca0f0466d740d83.png) ![](https://developer.qcloudimg.com/http-save/yehe-12803057/e95f690471953497a66e19fa34d0e33d.png) ![](https://developer.qcloudimg.com/http-save/yehe-12803057/caa16970ff46e2cdd2ee32db2e69b827.png) `/dev/ttyS0` 在 `ls` 里看得到,`open` 也能返回一个 fd,`tcgetattr` 却报 `Not a tty`。或者更干净:`/proc/tty/driver/serial` 根本没有 `0:` 这一行。uart2、uart3、uart7 都好好的,只有 uart0 缺席。 这不是 RS232 电平转换芯片坏了。转换芯片还没轮到上场。SoC 这边 uart0 的 TX 脚已经被别人抢走,probe 返回 -22,驱动没有绑定,tty 层没有第 0 路。有人急了,`echo test > /dev/ttyS0`,当时节点还不存在,tmpfs 的 `/dev` 建了一个普通文件。从此 `ls` 永远看得到 ttyS0,它只是个假的。 UART 的打开方式、poll、termios,UART 那篇已经写过。这里不重复那份 `uart_tool.c`,只补:假文件、probe -22、抢脚的 `vcc_camera`、设备树怎么删 gpio、DB9 回环。 产品 `rk3568_evb`。uart0 基址 `0xFDD50000`,节点 `/dev/ttyS0`,主次设备号 `4, 64`。 --- ![](https://developer.qcloudimg.com/http-save/yehe-12803057/e8bbb00318278f679c9aced92f13bbc6.png) ## 1\. RS232 在这套硬件上是什么 SoC 的 uart0 吐的是 3.3 V TTL。PC、工控屏、不少仪表要的是 RS232:逻辑 1 是负电压,逻辑 0 是正电压,摆幅大约 ±5 V 到 ±15 V。中间必须有一颗电平转换芯片。模块侧是 DB9,板侧是 TTL 接到 SoC 的 UART0\_TX / UART0\_RX。 ```bash PC / 仪表 DB9 ←±12V→ 电平转换 ←3.3V TTL→ SoC uart0 → /dev/ttyS0 ``` TX、RX 交叉:模块的 TX 进 SoC 的 RX,模块的 RX 接 SoC 的 TX。地要共。DB9 常见是针 2 RX、针 3 TX、针 5 GND,以模块丝印为准,不要死记「公头一定是 2 收 3 发」。 应用层看起来和 ttyS7 没区别:8N1、原始模式、`poll` 读。波特率常见 9600 或 115200,跟对端商量。不要用 1500000——那是 ttyS2 调试口的速率。 uart0 的 pinctrl 组叫 `uart0_xfer`: | 信号 | 脚 | Linux gpio 编号 | | --- | --- | --- | | UART0\_RX | GPIO0\_C0 | 16 | | UART0\_TX | GPIO0\_C1 | 17 | GPIO0\_C1 = bank0 的 pin 17。记住 17,后面 dmesg 里出现的是 `gpio0-17`。 --- ## 2\. 先分清三种「没有 ttyS0」 **假文件。** `ls -l /dev/ttyS0` 第一位是 `-`,不是 `c`。`stat` 看到普通文件。`tcgetattr`:`Not a tty`。 ```bash hdc shell "ls -l /dev/ttyS0" hdc shell "cat /proc/tty/driver/serial | grep '^0:'" ``` 没有 `0:` 这一行,这个 `/dev/ttyS0` 就是垃圾。删掉: ```bash hdc shell "rm /dev/ttyS0" ``` 删完不会自动长出真节点。真节点只有驱动 probe 成功才会 `device_create`。假文件只是挡住你看清「驱动根本没起来」。 **节点不存在。** `ls` 报 No such file。驱动没绑。往下查 probe。 **节点是 `crw`,但应用打不开。** 权限 `600`。UART 那篇的开机 `chmod`。uart0 若给 HAP 用,同样要 `chmod 0666 /dev/ttyS0`。调试阶段 hdc shell 是 root,先别被 shell 能打开骗过去。 这三种里,本产品上最常见的是第一种叠第二种:probe 失败 → 没有真节点 → 有人 echo 出假文件 → 看起来「有 ttyS0 但不是串口」。 --- ## 3\. probe -22 的现场 设备树里 `&uart0 { status = "okay"; }` 已经写了。运行中的 dtb 也 okay: ```bash hdc shell "cat /proc/device-tree/serial@fdd50000/status" # okay ``` 驱动却没绑: ```bash hdc shell "ls -l /sys/bus/platform/devices/fdd50000.serial/driver" # 没有这一项,或 ls 失败 ``` 不在 deferred 列表里: ```bash hdc shell "cat /sys/kernel/debug/devices_deferred" ``` 空,或者没有 `fdd50000.serial`。意思是:probe 已经跑过,返回了**非 defer 的错误**,内核不会再试。defer 是「资源还没好,晚点再来」;-22(`-EINVAL`)是「我试了,不行」。 dmesg 可能已经被 audit 噪声冲掉。手动绑一次,错误会重新打出来。先看一路正常的 uart 绑的是哪个驱动: ```bash hdc shell "readlink /sys/bus/platform/devices/fe660000.serial/driver" # .../dw-apb-uart ``` 然后: ```bash hdc shell "echo fdd50000.serial > /sys/bus/platform/drivers/dw-apb-uart/bind" hdc shell "dmesg | tail -20" ``` 关键两行(原文大意,你板上的标点可能略有出入): ```bash pin gpio0-17 already requested by vcc-camera-regulator; cannot claim for fdd50000.serial could not request pin 17 (gpio0-17) from group uart0-xfer fdd50000.serial: probe of fdd50000.serial failed with error -22 ``` `gpio0-17` 就是 GPIO0\_C1,uart0 的 TX。占用者叫 `vcc-camera-regulator`。不是相机驱动,是一个 always-on 的 regulator 节点,把这根脚当成使能脚用了。 --- ## 4\. 谁在抢脚 公版 EVB 的 dtsi 里有一段类似: ```bash vcc_camera: vcc-camera-regulator { compatible = "regulator-fixed"; regulator-name = "vcc_camera"; regulator-always-on; regulator-boot-on; enable-active-high; gpio = ; pinctrl-names = "default"; pinctrl-0 = ; }; ``` `regulator-always-on` + `regulator-boot-on`:开机立刻申请那根 gpio。uart0 后 probe,pinctrl 要这根脚做 TX,申请失败,-22。 本产品的相机供电是硬连线的 `vcc_avdd` / `vcc_dovdd` / `vcc_dvdd`,相机节点的 `*-supply` 引用这三路,**根本不引用 `vcc_camera`**。这个 regulator 是 EVB 底板留下来的死遗留。删掉它的 gpio / pinctrl,相机照样亮。保留 regulator 节点本身无害,always-on 一个没有消费者的固定电源,只是名字吓人。 同类抢脚在这块板上出现过不止一次:某个 always-on 的电源节点、某个 disabled 得不彻底的 PWM,都会在 probe 阶段把 GPIO 先占上。排障口诀:`already requested by X` 里的 X,去设备树全局搜,看它是不是「开机就抢、实际没人用」。 --- ## 5\. 设备树怎么改 板级 dts,不要去改 SoC 的 dtsi。覆盖节点、删属性: ```bash /* device/board/rk/rk3568_evb/kernel/rk3568-evb-linux.dts */ &uart0 { status = "okay"; }; /* * uart0 TX = GPIO0_C1 = gpio0-17。 * 公版 vcc_camera 把这根脚当使能,regulator-always-on,开机即抢, * uart0 probe -22,/dev/ttyS0 不出。 * 本板相机走硬连线 avdd/dovdd/dvdd,不引用 vcc_camera 的 gpio。 */ &vcc_camera { /delete-property/ gpio; /delete-property/ pinctrl-names; /delete-property/ pinctrl-0; /delete-property/ enable-active-high; }; ``` `/delete-property/` 是 dtc 的覆盖语法,把基 dtsi 里已经写上的属性拿掉。只写 `gpio;` 空属性是不行的,那是声明,不是删除。 改完重打 dtb,刷进 **resource** 和带 dtb 的 `boot_linux`(生效的 dtb 在 resource 分区的 `rk-kernel.dtb`)。只刷 Image、不刷 resource,抢脚还在。 解包确认属性真的没了: ```bash debugfs -R 'dump /extlinux/toybrick.dtb /tmp/run.dtb' boot_linux.img dtc -I dtb -O dts /tmp/run.dtb 2>/dev/null | grep -A20 'vcc-camera-regulator' ``` `gpio = ` 不该再出现。`uart0` 那段应有 `pinctrl-0` 指向 `uart0-xfer`,`status = "okay"`。 板上运行时: ```bash hdc shell "cat /proc/device-tree/serial@fdd50000/status" hdc shell "ls /sys/bus/platform/devices/fdd50000.serial/driver" hdc shell "cat /proc/tty/driver/serial | grep '^0:'" hdc shell "ls -l /dev/ttyS0" ``` 期望: ```bash okay /sys/bus/platform/devices/fdd50000.serial/driver -> .../dw-apb-uart 0: uart:16550A mmio:0xFDD50000 irq:... tx:0 rx:0 crw-rw-rw- 1 root root 4, 64 ... /dev/ttyS0 ``` 主设备号 4、次设备号 64 是 ttyS0 的标准编号。不是 `4, 64` 就不是这路串口。 --- ## 6\. 回环 电平转换芯片、DB9、线,到这一步才值得测。TX 和 RX 在 DB9 侧短接:针 2 和针 3 用杜邦线或金属夹短在一起。短的是 RS232 侧,信号会走转换芯片打一个来回,再进 SoC 的 RX。只在 TTL 侧短 TX-RX,测的是 SoC 脚,绕过了转换芯片,模块坏了你测不出来。 两边都测也行:先 TTL 回环确认控制器,再 DB9 回环确认转换芯片。出问题的时候按这个分层,不要一上来换模块。 波特率 115200 8N1,和UART 那篇同一套 termios:`O_NONBLOCK`、`poll`、关流控。回环时顺序:先 flush,再 write,再 poll 读。不要在 write 之后立刻 flush 接收缓冲,把刚绕回来的字节冲掉。 完整工具 `uart0_loop.c`: ```bash /* uart0_loop.c — /dev/ttyS0 回环。短接 DB9 2/3。 * 32 位 musl。用法:uart0_loop /dev/ttyS0 115200 */ #include #include #include #include #include #include #include #include static speed_t baud_const(int b) { switch (b) { case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; case 57600: return B57600; case 115200: return B115200; default: return B115200; } } static int open_raw(const char *path, int baud) { int fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd 0 && (p.revents & POLLIN)) r = (int)read(fd, buf + used, (size_t)(bufsz - 1 - used)); if (r > 0) { used += r; idle = 0; } else { idle += 50; } } buf[used] = '\0'; return used; } int main(int argc, char **argv) { const char *dev = "/dev/ttyS0"; int baud = 115200; if (argc >= 2) dev = argv[1]; if (argc >= 3) baud = atoi(argv[2]); int fd = open_raw(dev, baud); if (fd { this.toggle(); }) Button('回环').margin({ left: 8 }).onClick(() => { this.loopback(); }) Button('读一次').margin({ left: 8 }).onClick(() => { this.readOnce(); }) } Text(this.msg).fontSize(14).margin({ top: 10 }).fontColor('#1565C0') Text(this.out).fontSize(13).margin({ top: 8 }) }.padding(16).width('100%') } toggle() { if (this.opened) { board.uartClose(); this.opened = false; this.msg = '已关闭'; return; } let rc: number = board.uartOpen('/dev/ttyS0', 115200); if (rc === 0) { this.opened = true; this.msg = '已打开 /dev/ttyS0 @115200'; } else { this.msg = '打开失败。ls -l 必须是 crw 4,64;serial 必须有 0: 16550A。'; } } loopback() { if (!this.opened) { this.msg = '先打开'; return; } let payload: string = 'RS232-LOOP-0123456789'; let n: number = board.uartWrite(payload, 0); let s: string = board.uartRead(400, 0); if (s.indexOf('RS232-LOOP') >= 0) { this.out = 'PASS write=' + n.toString() + ' read=' + s; } else { this.out = 'FAIL write=' + n.toString() + ' read=[' + s + '] 短接 DB9 2/3,查转换芯片供电'; } } readOnce() { if (!this.opened) { this.msg = '先打开'; return; } this.out = board.uartRead(600, 0); } } ``` NAPI 的 `UartWrite` 里有 `tcflush(TCIFLUSH)`。那是为了「发命令前丢掉模块主动上报的半帧」。回环不是那种场景:write 把数据送进 TX,几乎同时出现在 RX。如果 write 函数先 flush 接收缓冲,再 write,回环数据随后到达,read 还能接到。如果内核把回环字节在 `write()` 返回前就放进接收缓冲,flush 会把它们丢掉。现场若稳定 FAIL 且 read 为空、示波器上 TX 有波形,把回环留在 `uart0_loop.c` 里验硬件,ArkTS 只验「应用能打开 ttyS0、能写、对端 PC 能收到」。 --- ## 8\. 和对端 PC 对测 回环只证明自己能听见自己。真用还要对面一台会 RS232 的机器。 1. 板子 `uart_tool write /dev/ttyS0 115200 text hello` 2. PC 串口工具 115200 8N1 无流控,应出现 `hello` 3. PC 发 `world`,板子 `uart_tool read /dev/ttyS0 115200 text 1000` 应出现 `world` PC 侧常见坑: - 线是交叉还是直通。模块已经交叉过一次,再插交叉线等于直通,两边都在听自己。 - 波特率 9600 对 115200,两边都有「收到了」的错觉,内容是垃圾。hex 读一眼。 - PC 开了 RTS/CTS,板子没开。关掉 PC 的硬件流控。 - USB 转 RS232 芯片的驱动没装,设备管理器里是未知设备。 - Windows 串口工具勾「新行发送 CR+LF」。板子原始模式不会把 `\n` 翻译成 `\r\n`。PC 只发 LF、仪表要 CRLF,表现为「命令写了没反应」。 板上 `tx:` 计数在 `/proc/tty/driver/serial` 的 `0:` 那行,write 之后应该涨。`rx:` 在对端发送之后涨。两个计数都不动,问题在 pinctrl 或假节点,不在应用。 短线 115200 稳。十几米的 RS232 电缆、劣质转换芯片,115200 会偶发丢字节,降到 9600 再对一次。丢字节时 `/proc` 的 `rx:` 仍在涨,应用看到的是缺了几个字符的行——那是电气,不是 termios。 --- ## 9\. 权限和开机 HAP 要用 ttyS0,开机脚本加上: ```bash # device/board/rk/rk3568_evb/board_perm/board-perm.sh chmod 0666 /dev/ttyS0 2>/dev/null ``` 不要碰 ttyS2。`/dev` 是 tmpfs,重启重建。改脚本刷 vendor。 `init.rk30board.cfg` 里 once 服务,跟UART 那篇同一份脚本。`ohos.boot.hardware` 必须是 `rk30board`,写错后缀这份 cfg 不会加载,chmod 永远不跑。 SELinux enforcing 时还要 te 规则。bring-up 阶段 `enforcing=0`。 --- ## 10\. 改 dtb 之后仍然 -22 刷错分区。`/dev/block/by-name/resource` 指向 eMMC,你从 SD 启动,dd 到 by-name 等于写了另一块介质。`cat /proc/partitions`,认 mmcblk0 / mmcblk1,dd 用 `/dev/block/mmcblkXpY`。 dtb 没进 resource。只替换了 Image。 `vcc_camera` 的 gpio 写在别的 dtsi,板级 `/delete-property/` 没覆盖到那个节点名。`dtc -I dtb -O dts` 全局搜 `RK_PC1` 和 `camera_pwr`,看还有谁引用 GPIO0\_C1。 手动 bind 的错误变成别的 pin。那就不是这根脚,按新的 `already requested by` 再搜。 相机「修完 uart0 之后不亮」:先确认你删的只是 gpio / pinctrl,没有把 `vcc_avdd` 那些硬连线 regulator 关掉。uart0 抢的是 GPIO0\_C1,相机供电不是这根脚。 compatible 保持 SoC dtsi 里的 `snps,dw-apb-uart` 一类,不要改成 8250 的别名碰运气。时钟、dma 也别在板级乱动。这章只删 `vcc_camera` 的 gpio,不动 uart0 节点的 clocks。 --- ## 11\. pinctrl 和 gpio 编号 Linux gpio 编号 = bank × 32 + 组内偏移。RK 的组:A=0、B=8、C=16、D=24。GPIO0\_C1 = 0×32 + 16 + 1 = 17。dmesg 写 `gpio0-17`,sysfs 导出是 `17`。混用「C1」和「17」的时候,先换算再搜。 占用情况: ```bash hdc shell "cat /sys/kernel/debug/pinctrl/pinctrl-rockchip/pinmux-pins | grep -i 'gpio0-17\|pc1\|uart0'" hdc shell "cat /sys/kernel/debug/gpio | grep gpio-17" ``` 修好之前,gpio-17 的 owner 是 `vcc-camera-regulator` 或 `sysfs`。修好之后应是 uart0 的 pinctrl 组。还停在 camera regulator 上,dtb 没生效。 示波器:GPIO0\_C1 上电之后应能看到 uart0 的 TX 空闲高电平(TTL 3.3 V)。`write` 的时候有比特波形。一直 0 V:脚被当成 gpio 拉低,或没接到转换芯片。一直 3.3 V 且 write 后 `tx:` 计数涨、脚上没跳变:pinctrl 还是 gpio,不是 uart 功能。 转换芯片的 VCC 常见 3.3 V 或 5 V,以模块丝印为准。没供电的转换芯片,TTL 侧回环可能仍 PASS(你短在 SoC 脚上),DB9 侧一定 FAIL。分层测,不要两边一起怀疑。 uart0 和相机同时亮,才算这章收工。只亮串口、相机没了,是误删了 avdd 那组 regulator,把 `/delete-property/` 用过了头。 --- ## 12\. 验收 1. `ls -l /dev/ttyS0` → `crw`,`4, 64`。不是普通文件。 2. `/proc/tty/driver/serial` 有 `0: uart:16550A mmio:0xFDD50000`。 3. `/sys/bus/platform/devices/fdd50000.serial/driver` 链到 `dw-apb-uart`。 4. `dmesg` 不再出现 `gpio0-17 already requested by vcc-camera-regulator`。 5. DB9 2/3 短接,`uart0_loop` 打印 `PASS`。 6. 对端 PC 115200 8N1,板子 write 的文本能出现在 PC 窗口,反过来也行。 1 到 4 过了、5 失败:转换芯片、供电、短接针。1 到 4 不过:还在抢脚或假文件,不要买新模块。 --- ## 13\. 现场记录(一次真实的下午) dtb 反编译确认 uart0 okay、pinctrl 指向 uart0-xfer。`/proc/device-tree` 同样 okay。`/dev/ttyS0` 存在。应用报 Not a tty。 `ls -l` 第一位是 `-`。假文件。删掉。节点不再出现。 `ls driver` 对 `fdd50000.serial` 失败。`devices_deferred` 没有它。手动 bind,dmesg 打出 gpio0-17 被 `vcc-camera-regulator` 占用。 搜板级 dts,没有 vcc\_camera。搜基 dtsi,有,always-on,gpio 就是 RK\_PC1。板级加 `/delete-property/` 四条。重打 dtb,刷 resource + boot\_linux。 重启。`0: uart:16550A`。`crw 4,64`。TTL 侧回环 PASS。DB9 侧第一次 FAIL:短接了针 2 和针 5(地)。改短 2 和 3,PASS。 相机预览还在。删 gpio 没有动 avdd。 这套步骤可以原样用在下一次「某路 UART 就是不出节点」:先看是不是假文件,再看 deferred 还是硬错误,再手动 bind 抓 `already requested by`。 --- ## 路径 | 路径 | 用途 | | --- | --- | | \`device/board/rk/rk3568\_evb/kernel/rk3568-evb-linux.dts\` | \`&uart0\`、\`&vcc\_camera\` 删 gpio | | 公版 dtsi 里的 \`vcc\_camera\` | 抢脚来源,不要改 dtsi,用板级覆盖 | | \`/dev/ttyS0\` | 用户态节点,必须是 \`c 4,64\` | | \`/proc/tty/driver/serial\` | \`0: uart:16550A mmio:0xFDD50000\` | | \`/sys/bus/platform/devices/fdd50000.serial/driver\` | 是否绑上 dw-apb-uart | | \`/sys/kernel/debug/devices\_deferred\` | 区分 defer 和硬错误 | | \`/sys/bus/platform/drivers/dw-apb-uart/bind\` | 手动 bind 抓 dmesg | | \`uart0\_loop.c\` | DB9 回环 | | \`entry/src/main/cpp/napi\_init.cpp\` | 复用 uartOpen 那套 termios | | \`device/board/rk/rk3568\_evb/board\_perm/board-perm.sh\` | \`chmod 0666 /dev/ttyS0\` | GPIO0\_C1 还给 uart0 之后,这路 RS232 就只剩电平转换和线序。室内没经纬度是 GPS 的事,不要拿到这里来改 probe。 --- ## DB9 针脚和三种线 DE-9 公头正视、缺口向上,常用针: | 针 | 信号 | 回环时 | | --- | --- | --- | | 2 | RXD | 与 3 短接 | | 3 | TXD | 与 2 短接 | | 5 | GND | 不要和 2 短,第一次 FAIL 经常短到这里 | 模块是 DCE 还是 DTE,会把 2/3 对调。TTL 侧回环(转换芯片靠 SoC 一侧把 TX 焊到 RX)能过、DB9 不过:转换芯片或它的供电。DB9 能过、对端 PC 不过:交叉线还是直通、PC 口不是 115200 8N1、流控勾了 RTS/CTS。板子 termios 已关 CRTSCTS,PC 端也要关。 波特率这路默认 **115200**,不是调试口的 1500000,也不是 GPS 的 9600。三个数字混用,窗口里全是 0x80 以上的脏字节。文本模式若不过滤非 ASCII,ArkTS 一 `split` 就崩,看起来像「RS232 把应用打挂了」。过滤在UART 那篇的 `uartRead` 里,这路同样要走。 假文件再强调一次:`ls -l /dev/ttyS0` 第一位必须是 `c`,主次设备号 `4, 64`。第一位是 `-`,那是有人 `echo > /dev/ttyS0` 在 tmpfs 上建的普通文件,删掉重开,不要在它上面调 termios。 手动 bind 的命令不要记错控制器名。uart2 能用时先抄它的驱动名: ```bash readlink /sys/bus/platform/devices/fe660000.serial/driver # .../dw-apb-uart echo fdd50000.serial > /sys/bus/platform/drivers/dw-apb-uart/bind dmesg | tail -20 ``` `already requested by vcc-camera-regulator` 出现,就不要再怀疑电平转换芯片。脚还在别人手里,转换芯片连信号都收不到。板级 dts 覆盖之后,这两条命令应当 bind 成功,`/proc/tty/driver/serial` 出现 `0: uart:16550A mmio:0xFDD50000`。 ArkTS 复用UART 那篇串口页,设备填 `/dev/ttyS0`,波特率 115200,文本模式。打开失败先 `ls -l`,不要在页面里重试一百次。回环 PASS 之后再接对端。对端乱码先对波特率,再对 2/3 交叉。 `ttyS0` 也要进 `board-perm.sh` 的 `chmod 0666`。HAP 不是 root,节点默认 0600 时应用 open 直接 -1,看起来又像「驱动没绑上」。root 的 hdc 工具能通、应用不通,先看权限这一层。 ```bash hdc shell "cat /proc/partitions" hdc shell "ls -l /dev/i2c-1 /dev/ttyS0" hdc shell "dmesg | grep -E 'i2c|mlx|hdc|uart0|vcc-camera' | tail -30" ``` 上面三条在换模块、改 dts、刷完分区之后各跑一次,比在应用里加 log 便宜。输出贴进记录,下一轮对照。 --- 系列第 42 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10

优秀的个人博客,低调大师

智能仓储 WMS 前端系统 | 计算机视觉集成:摄像头视频流 + AI 识别结果叠加

# 引言 仓库摄像头不是"看得见"就够用。当 AI 在另一端识别到破损、错贴面单、托盘塌垛、货位错放时,如果前端只弹一条文字告警,拣货员要做的第一件事是**把画面切回去找那条边**。真正的视觉集成,是把识别结果钉在它出现的那一帧、那一块像素上——框跟着物体走,标签跟着框走,告警跟着任务走。 这件事的技术难点全在同步:视频帧在编码器里排队、AI 推理在云端排队、网络有抖动、设备有丢帧。任何一个环节脱节,框就会画在错的位置上,比没有框更危险。本文落地一套能扛住这些现实的视频-AI 叠加方案:摄像头接入、带时间戳的推理结果流、坐标系变换、按 PTS 对齐的渲染循环,以及离线与降级策略。 --- # 一、接入层:四种视频源,四条不同的路 仓库现场的设备差异极大:USB 工业相机、H.265 网络摄像头、无头闸机的 RTSP 流、PDA 端的前后置摄像头。把它们统一成同一个 `MediaStream`,是这一层唯一的目标。 ```javascript // src/vision/stream.js export async function acquireVideoSource(kind, constraints = {}) { if (kind === 'usb') { const devices = await navigator.mediaDevices.enumerateDevices(); const cam = devices.find((d) => d.kind === 'videoinput' && d.deviceId); return await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: cam.deviceId }, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false, }); } if (kind === 'ip') { // 浏览器无法直接解析 RTSP/H.265,必须由边缘网关转成 WebRTC 或 MSE return await connectGateway(constraints.gatewayUrl); } if (kind === 'pda') { return await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } }, // 后置摄像头 audio: false, }); } } async function connectGateway(url) { const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); const stream = new MediaStream(); pc.ontrack = (e) => e.streams[0].getTracks().forEach((t) => stream.addTrack(t)); const offer = await (await fetch(`${url}/offer`, { method: 'POST' })).json(); await pc.setRemoteDescription(offer); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); await fetch(`${url}/answer`, { method: 'POST', body: JSON.stringify(answer) }); return stream; } ``` 逐段解读: - `enumerateDevices()` 在**未授权前返回的设备名为空字符串**。所以初始化流程必须是"先请求权限,再枚举选设备",不能反着来。这是一个极其常见的坑,会导致明明有摄像头却拿到空列表。 - `audio: false` 必须显式写。移动端 Safari 上,含音频轨道的流在后台会被整条挂起,画面随之冻结;纯视频轨道则不会。 - `width: { ideal: 1920 }` 用 `ideal` 而非 `exact`。`exact` 在设备不支持时会直接抛 `OverconstrainedError`;`ideal` 让浏览器在能力范围内尽量给,拿不到时降级到 1280×720 而不报错。工业相机常见的 30fps 与 1080p,应在能力查询 `getSupportedConstraints()` 后按设备能力设定。 - RTSP/H.265 的处理是关键判断:**浏览器无法原生解析**。H.265 的专利授权问题使主流浏览器至今不支持硬解,RTSP 也不是 Web 标准。必须靠边缘网关(如带有 WebRTC 网关的 NVR)转码或转协议。MSE(Media Source Extensions)只能处理 fragmented MP4/WebM,也绕不开编码格式问题。因此网关选型在架构阶段就要定,不能留到联调。 --- # 二、推理通道:把"帧编号"变成"时间锚" AI 服务返回的不是一个框,而是一批带**时间戳**的框。时间戳是整套系统能正确叠加的唯一依据。 ```javascript // src/vision/inference.js export function createInferenceChannel({ wsUrl, video, codec = 'image/jpeg' }) { const ws = new WebSocket(wsUrl); const pending = new Map(); // 帧编号 → 时间戳,用于把结果对回原始帧 let seq = 0; ws.onopen = () => { video.requestVideoFrameCallback?.(function loop(now, meta) { const id = ++seq; pending.set(id, { expectedTime: now / 1000, pts: meta.mediaTime }); ws.send(JSON.stringify({ id, pts: meta.mediaTime })); // 实际抓帧通过 canvas.captureStream 或 ImageCapture,此处仅发触发信号 video.requestVideoFrameCallback(loop); }); }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); // { id, boxes: [{x,y,w,h,label,confidence}], latency } const ctx = pending.get(msg.id); if (!ctx) return; // 过期结果直接丢弃 pending.delete(msg.id); dispatchResult({ ...msg, clientTime: ctx.expectedTime }); }; return { close() { ws.close(); pending.clear(); }, }; } ``` 逐段解读: - `requestVideoFrameCallback` 是这里的王牌 API。它与 `requestAnimationFrame` 不同:rAF 只告诉你"浏览器准备画下一帧",而 rVFC **在每一帧合成完成后立即回调,并给出该帧的 **`mediaTime`**(即 PTS)**。用它做时间戳锚点,框的偏移可以控制在亚帧级别。 - `pending` 是一个"发出即忘"的回执表。发出请求时登记编号与时间,结果回来时查表配对。**过期结果必须丢弃**——如果一个推理结果晚到了 3 秒,用它画框只会误导操作员。配对失败直接 return,不留兜底。 - `meta.expectedDisplayTime` 才是该帧**预计呈现时间**,`mediaTime` 是媒体时间戳。配对时两个值各有用途:用 `mediaTime` 对回原始帧,用 `expectedDisplayTime` 判断该不该画。 - `latency` 由服务端返回。它是判断通道是否健康的唯一客观指标:超过 800ms 就该告警"AI 通道拥塞",而不是默默让框漂移。 ![](https://developer.qcloudimg.com/http-save/audit-10133498/a5349daf0808f7034b23788229b55a72.png) --- # 三、坐标系变换:从归一化到屏幕像素 AI 模型输出的坐标几乎总是**归一化值**(0~1 的相对比例),而画布需要的是像素。此外视频有横竖屏、镜像、裁剪,任一项不处理,框就会系统性偏移。 ```javascript // src/vision/overlay.js export function createOverlay(video, canvas, { mirror = false } = {}) { const ctx = canvas.getContext('2d'); function syncSize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = video.videoWidth * dpr; canvas.height = video.videoHeight * dpr; canvas.style.width = `${video.clientWidth}px`; canvas.style.height = `${video.clientHeight}px`; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 逻辑坐标 = CSS 像素 } function drawBoxes(boxes, now) { const vw = video.clientWidth; const vh = video.clientHeight; const sx = vw / video.videoWidth; // 视频像素 → 显示像素 const sy = vh / video.videoHeight; ctx.clearRect(0, 0, vw, vh); ctx.save(); if (mirror) { ctx.translate(vw, 0); ctx.scale(-1, 1); } for (const b of boxes) { const x = b.x * video.videoWidth * sx; const y = b.y * video.videoHeight * sy; const w = b.w * video.videoWidth * sx; const h = b.h * video.videoHeight * sy; const age = now - b.clientTime; const stale = age > 1500; // 超过 1.5 秒判定为过期 ctx.strokeStyle = stale ? '#94a3b8' : colorFor(b.label); ctx.lineWidth = 2; ctx.setLineDash(stale ? [6, 4] : []); ctx.strokeRect(x, y, w, h); // 标签背景 + 文字:左上角锚定,不随框位置抖动 const label = `${b.label} ${Math.round(b.confidence * 100)}%`; ctx.font = '12px system-ui'; const tw = ctx.measureText(label).width; ctx.fillStyle = ctx.strokeStyle; ctx.fillRect(x, y - 18, tw + 8, 18); ctx.fillStyle = '#fff'; ctx.fillText(label, x + 4, y - 5); } ctx.restore(); } return { syncSize, drawBoxes }; } function colorFor(label) { const map = { damage: '#ef4444', // 破损:红 wrong_label: '#f59e0b', // 面单异常:橙 collapse: '#dc2626', // 塌垛:深红 misplace: '#8b5cf6', // 错放:紫 }; return map[label] || '#3b82f6'; } ``` 逐段解读: - `syncSize` 是容器尺寸与视频尺寸不一致时的桥梁。摄像头常是 16:9,UI 容器可能是卡片比例,直接把 canvas 设成 `videoWidth` 会造成拉伸。**正确做法是 canvas 匹配视频真实分辨率、CSS 尺寸匹配容器**,再用 `sx/sy` 缩放系数做映射。 - `setTransform(dpr, ...)` 之后,所有绘图调用都用 CSS 像素书写,心智负担大幅降低。这是 Canvas 开发的统一规范,不是可选项。 - `mirror` 只做**坐标镜像**,不改变 `x/y` 语义。移动端自拍预览镜像但识别坐标不镜像,会导致框画在物体反方向——这是 PDA 视觉集成的头号 Bug。`scale(-1, 1)` + `translate(vw, 0)` 的组合是标准做法。 - **过期判定是安全机制**。1.5 秒没有新结果就把框变灰虚线,明确告诉操作员"这是旧数据"。没有这层处理,最危险的场景是:物体已移走,框还钉在原地。 - 标签放在框**左上角外部**而非内部。放在内部会被物体遮挡,放在右下角容易超出画布被裁。锚定外部是视觉叠加的通用约定。 --- # 四、渲染循环:按时间戳对齐,而不是按"最新"画 最简单也最错的写法:`onmessage` 里收到框就画。问题是一帧可能收到多个结果,下一帧可能一个都没收到,画布会闪烁且框会跳。 ```javascript // src/vision/renderLoop.js export function startOverlayLoop(overlay, video, resultQueue) { let raf = 0; function frame() { const now = performance.now(); const latest = resultQueue.consumeLatest(now); // 取 pts ≤ now 的最新结果 if (latest) { // 用 drawImage 直接绘制 ,零拷贝,避免再次 captureStream overlay.drawVideo(video); overlay.drawBoxes(latest.boxes, now); } else { overlay.drawVideo(video); // 无结果时也维持画面,避免黑屏闪烁 } raf = requestAnimationFrame(frame); } raf = requestAnimationFrame(frame); return () => cancelAnimationFrame(raf); } ``` ```javascript // src/vision/resultQueue.js export function createResultQueue({ maxAge = 2000 } = {}) { let buffer = []; return { push(result) { buffer.push(result); buffer.sort((a, b) => a.clientTime - b.clientTime); // 只保留窗口内的结果,防止内存膨胀 const cutoff = performance.now() - maxAge; buffer = buffer.filter((r) => r.clientTime >= cutoff); }, consumeLatest(now) { // 取 pts ≤ now 的最后一条:丢弃未来帧,避免"预测性"错位 let latest = null; for (const r of buffer) { if (r.clientTime ` 元素直接当 `CanvasImageSource` 绘制,是**零拷贝**操作,浏览器直接读取视频帧缓冲。不要用 `captureStream()` 再取一遍,那是给推流用的。 - `consumeLatest` 的关键词是**"≤ now"**。它主动丢弃未来的帧。这意味着:如果某条推理结果的时间戳晚于当前渲染时刻(比如时钟偏差、乱序到达),就不画——宁可暂时空框,也不画预测性的错位。 - 无结果时仍画视频帧。否则缓冲区空一瞬间画布就清空成黑屏,视觉上是严重闪烁。 - `maxAge` 窗口淘汰。一个忘记清理的结果队列会持续增长,长时间运行后排序和过滤的 O(n log n) 会变成可感知的卡顿。 --- # 五、性能:两个独立层级,各有各的预算 视频叠加的帧预算是 16.6ms(60fps)。这 16.6ms 里,`drawImage` 占大头,绘制几十个框通常不到 1ms,文本是相对昂贵的操作。 ![](https://developer.qcloudimg.com/http-save/audit-10133498/d8e9bb21dfc9a5b49b944540557dc2a9.png) --- 图表是经验量级:1080p 的 `drawImage` 是绝对主力,文本填充次之。**优化顺序应该反着来**——先砍最贵的是 `drawImage` 本身:DPR 封顶到 2、检测区域不要求全屏高分辨率时降采样视频源。文本无法避免,但可以合并:同 SKU 的多个框合并成一个标签。 三条通用规则: - `willReadFrequently`** 不要开**。我们的场景只写不读,开了反而触发 CPU 回读路径,性能下降明显。只有做像素级分析(比如局部截图送 OCR)才需要它。 - **离屏缓冲复用**。标签背景、置信度文字如果需要频繁重绘,预渲染到离屏 Canvas 再 `drawImage` 上去,比反复 `fillRect + fillText` 更快。 - **分层画布**。视频层一块、框层一块、UI 控件层一块。框只在有新结果时重绘,视频层单独循环,减少无效重绘。这是瓦片式地图的同源思路。 --- # 六、告警与交互:识别结果要能触发动作 框画出来只是第一步。真正的价值是**把视觉发现接进 WMS 作业流**:破损要拦截出库,错放要触发移库,面单异常要挂起该单复核。 ```javascript // src/vision/alerts.js const SEVERITY = { damage: 'critical', // 破损:阻断当前出库任务 collapse: 'critical', // 塌垛:要求立即处理 wrong_label: 'warning', // 面单异常:挂起订单 misplace: 'warning', // 错放:触发移库建议 }; export function handleDetections(detections, { onCritical, onWarning } { const byLevel = detections.reduce((acc, d) => { acc[SEVERITY[d.label] || 'info'].push(d); return acc; }, { critical: [], warning: [], info: [] }); if (byLevel.critical.length) { onCritical(byLevel.critical); // 视觉层:屏幕边缘红色脉冲 + 蜂鸣(Web Audio 短促提示音) flashEdge('critical'); } if (byLevel.warning.length) onWarning(byLevel.warning); return byLevel; } let flashTimer = null; function flashEdge(level) { document.body.dataset.visionAlert = level; clearTimeout(flashTimer); flashTimer = setTimeout(() => { delete document.body.dataset.visionAlert; }, 1200); } ``` 逐段解读: - 严重级别在前端**再判定一次**。服务端返回的是标签,最终该阻断还是仅告警,由前端按业务规则落定,方便不发包快速调整。但关键规则(如是否阻断出库)应在服务端二次校验,防止被绕过。 - `flashEdge` 用 `data-*` 属性驱动 CSS 动画。屏幕边缘红色脉冲是最不打断作业的告警方式——拣货员视线本就在画面上,边缘闪烁不会遮挡内容。 - 告警**必须可确认、可撤销**。操作员确认"这是误报"后,该 SKU + 该标签类型应进入本班次的抑制名单,避免同一处反复告警导致"告警疲劳"而错过真正的异常。 --- # 七、降级策略:离线、丢帧、弱网都不能崩 仓库现场的网络是不可靠的。降级不是"失败了就报错",而是**在能力边界内继续工作**。 ```javascript // src/vision/fallback.js export function pickStrategy({ rtcSupported, wsLatency, online }) { if (!online) { return { mode: 'offline', draw: (canvas, video) => drawFrozenFrame(canvas, video), // 缓存最后一帧 + 提示 hint: '网络离线,AI 识别已暂停,可继续本地复核', }; } if (!rtcSupported) { return { mode: 'mse', src: '/api/vision/stream.m3u8', // 走 HLS 降级流 hint: '当前使用兼容流,画质已降低', }; } if (wsLatency > 800) { return { mode: 'degraded', interval: 2000, // 降为 2 秒抓帧一次 hint: 'AI 通道拥塞,识别频率已降低', }; } return { mode: 'realtime', interval: 200 }; } ``` 四条降级路径: |场景|策略|代价|用户体验| | --- | --- | --- | --- | |完全离线|缓存最后一帧 + 本地提示|无实时识别|可继续人工复核| |WebRTC 不支持|切 HLS/MSE 兼容流|延迟升高 2–5 秒|画面可用,框略有滞后| |推理通道拥塞|降低抓帧频率 5–10 倍|实时性下降|仍有关键帧检测| |单帧超时|标记过期变灰虚线|无|明确区分新旧数据| 权限边界也要写清楚:摄像头属于敏感权限,首次授权需由明确的用户手势触发(按钮点击),`getUserMedia` 不能在页面加载时自动调用。PDA 端要处理前后摄像头切换和锁屏后流释放;工业相机场景则要提示用户检查 HTTPS——`getUserMedia` 在非安全上下文里会静默失败。 --- # 结语 本文交付了一套能在实际仓库里跑起来的视频-AI 叠加方案。四条核心结论: **第一,先解决协议,再解决画面。** 浏览器无法解析 RTSP 与 H.265,摄像头接入必须依赖边缘网关转 WebRTC 或 MSE。网关选型是架构决策,不是联调细节;`enumerateDevices` 必须在授权后调用,否则拿到空列表。 **第二,时间戳是唯一的对齐依据。**`requestVideoFrameCallback` 给出的 `mediaTime` 和 `expectedDisplayTime` 让结果能对回原始帧。结果队列按 `pts ≤ now` 消费、主动丢弃未来帧,宁可空框也不画错位——这是安全底线。 **第三,坐标系变换是精度的全部。** 视频像素与显示像素之间的 `sx/sy`、镜像处理、DPR 归一化,三处任一缺失都会让框系统性偏移。过期判定(1.5 秒变灰虚线)则让"旧数据"与"实时数据"在视觉上不可混淆。 **第四,降级决定可用性。** 离线缓存末帧、WebRTC 不支持切 HLS、通道拥塞降抓帧频率、单帧超时标灰——四条路径让系统在网络不可靠的仓库里仍能工作。告警要可确认可撤销,否则告警疲劳会让真正的异常被淹没。 这套方案的价值不在"框得准",而在于**框得可信**——操作员看到框的瞬间知道它是新的还是旧的、该信还是该确认。可信,才是视觉 AI 能进入生产作业流的前提。

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册