news 2026/9/19 21:58:01

BrewUI:给Homebrew套上可视化界面,让包管理不再依赖命令行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BrewUI:给Homebrew套上可视化界面,让包管理不再依赖命令行

MacOS 下用 Homebrew 管软件,用一年还行,用到第三年,brew list一刷就是上百行,想找一个包要靠 grep 来回筛;brew update每次刷屏刷得人眼花,也不知道卡在哪儿;更别提brew autoremove --dry-run这样的清理命令,想弄清楚它会删掉哪个依赖,得先把依赖树捋上一遍。命令行没问题,有问题的是人脑不擅长处理这种密集的文本流。BrewUI 就是冲着这个痛点来的——一个给 Homebrew 套上可视化界面的个人项目,能浏览、搜索、安装、卸载、升级软件包,也能直观看到依赖关系和磁盘占用,让不习惯终端操作的人也能轻松打理自己的 macOS 软件环境。

这篇博文我会从项目起因、技术选型、核心实现到常见坑位完整拆一遍。不管是打算自己动手给 Homebrew 写个前端,还是单纯想找一种更舒服的包管理姿势,这里面的思路和踩坑记录都值得参考。

1. 做这个项目的起因:命令行到底哪里让人难受

1.1 痛点拆解

Homebrew 本身的定位是「缺失的包管理器」,它做得足够好,但它的使用方式决定了它天然对新手不友好,也天然不适合「扫描」信息。

举个例子。你想看看系统里装了哪些跟 Python 相关的包,在终端里大概率会打出这样一串命令:

brew list | grep -i python

输出结果是一堆软件名,有的带版本号,有的带依赖关系标记,有的名字很像但实际是不同项目。你要靠人眼去区分python@3.11python-tk@3.9libpython这些包到底谁依赖谁,谁可以安全移除,谁动一下就可能导致其他工具崩掉。

另一个高频场景是升级。brew upgrade跑起来之后,终端里刷过的是源码包下载地址、校验哈希、编译日志。如果某个包的依赖在编译阶段失败,整条命令会卡在那里,你只能盯着光标发呆,完全不知道它是在编译、还在在下载,或者在等待网络超时。

第三个场景是磁盘清理。macOS 用户一般不太敢随便删/opt/homebrew目录下的文件,但brew cleanup --verbose --dry-run输出的内容又不够直观。哪个 formula 占了多少空间,旧版本打包文件有多大,有没有办法一键清理,这些信息应该在界面上用进度条和数字呈现,而不是丢给用户一个待解析的文本流。

1.2 为什么选择 GUI 而不是继续靠命令行技巧打补丁

我认识一些朋友,他们的方案是在 shell 里配置一堆 alias 和函数,比如alias brewup='brew update && brew upgrade && brew cleanup',再配合一些高亮插件把终端输出染色。这确实能解决一部分问题,但它只是给文本流加了层滤镜,没有改变信息组织方式。

我之所以选择写 GUI 项目,是因为这个问题本质上是信息结构问题:

  • 命令行是「流式」输出,GUI 是「结构化」展示
  • 命令行只能从上到下一个方向阅读,GUI 可以在列表、详情、关系图、日志之间自由跳转
  • 命令行执行任务是同步阻塞的,GUI 天然适合异步执行和并发反馈

说白了,GUI 不是要替代终端,而是把 Homebrew 背后庞大的状态空间展平到人的视觉能快速处理的程度。这是我坚持做 BrewUI 的根本原因。

2. 整体架构与设计思路

2.1 技术选型:SwiftUI 与 Electron 的取舍

做这个项目时我反复纠结过是用 Electron 还是 SwiftUI,身边好几个朋友也都提出了不同意见。这件事没有绝对对错,我把两个方案的关键差异列在下面,方便你们对照自己的情况判断:

维度SwiftUIElectron
内存占用低(约 60-100MB)较高(约 200-400MB)
安装包体积几 MB80-100MB 起步
与 macOS 集成度原生支持系统主题、键盘快捷键、菜单栏需要额外适配
开发上手成本需要熟悉 Swift 和 Xcode前端技术栈即可
跨平台能力仅 Apple 平台Windows / Linux / macOS
调用系统命令的便利性Process 直接调用Node child_process 调用
长期维护成本依赖系统框架升级依赖 Electron 大版本同步升级

最终我选了 SwiftUI。原因很直接:BrewUI 定位就是 macOS 专属工具,不需要跨平台,Electron 的体积和内存占用对我来说是实打实的负担。还有一点很重要,SwiftUI 的ListSearchable组件对实现包管理界面几乎是量身定做,写出来的代码量比 Electron 那几个框架少得多。

这里有个常见误区:很多人以为 GUI 工具必须用脚本语言写才方便,其实 Swift 调用外部进程的Process类很成熟,配合async/await做异步回调也很顺手。我从头到尾没有遇到「进程管理难搞」这类问题,真正难的在后面讲到的数据解析。

2.2 核心模块划分

BrewUI 的源码组织采用了一个比较规整的四层结构,每一层只跟相邻层通信,避免逻辑纠缠:

  • 界面层(View):负责列表展示、搜索输入、按钮状态变化、日志滚动显示,里面不写任何 brew 命令字符串
  • 命令执行层(Executer):统一封装所有对 Homebrew 的调用,包括Process启动、标准输出读取、进程终止、退出码判断,对外提供install(package:)upgradeAll()search(keyword:)这样的异步接口
  • 数据解析层(Parser):把命令输出的文本或 JSON 转换成 Swift 模型,比如PackageInfoDependencyNodeOutdatedPackage
  • 缓存层(Cache):缓存搜索结果、已安装列表、可升级列表,避免每次切页都去跑一次慢速命令

这样分层不是故意为了优雅,而是实际踩坑后形成的结构。最开始我图省事,直接在 SwiftUI 的 View 里拼接brew list命令然后解析,后面每加一个新功能都要改一堆 View,错误率直线上升。后来老老实实把命令执行和数据解析抽出来,整个项目立刻好维护多了。

2.3 界面设计原则:默认不让你看到不该看的东西

BrewUI 的界面遵循一个核心原则:默认视图只显示用户当前需要决策的信息,把技术细节折叠到次级页面。

比如首页是「已安装包」列表,每行显示名称、版本号、占用的磁盘空间、是否被其他多个包依赖。点击任意一项进入详情页,才能看到该包的完整描述、依赖列表、反向依赖、安装日期和所有文件路径。这样设计是为了避免刚打开工具就被一堆术语糊脸。

另一个设计细节是「危险操作二次确认」。卸载包、清理缓存这类操作,界面层一定会弹出一个包含影响范围的确认面板,面板上直接列出「这个包还被以下应用依赖」或者「将要释放约多少 MB 空间」。让用户在确认页面就做好决策,而不是先执行再后悔。

3. 关键实现细节:让 brew 乖乖听 UI 的话

3.1 调用 Homebrew 命令的执行层封装

Homebrew 就是一个外面包着 Ruby 逻辑的命令行程序。BrewUI 本质上做的事情就是帮你调用它并解析结果,所以第一步要把「启动外部进程」这件事封装得可靠。

在 Swift 里最常用的是Process类。我封装的一个核心执行函数大概是这样的:

struct BrewOutput { let stdout: String let stderr: String let exitCode: Int32 } func runBrew(arguments: [String]) async throws -> BrewOutput { let brewPath = findBrewExecutable() // 需要预先探测路径 let process = Process() let stdoutPipe = Pipe() let stderrPipe = Pipe() process.executableURL = URL(fileURLWithPath: brewPath) process.arguments = arguments process.standardOutput = stdoutPipe process.standardError = stderrPipe // 关键:必须把 PATH 环境变量拼接完整 var environment = ProcessInfo.processInfo.environment let commonPaths = [ "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin" ] if let existingPath = environment["PATH"] { environment["PATH"] = commonPaths.joined(separator: ":") + ":" + existingPath } else { environment["PATH"] = commonPaths.joined(separator: ":") } process.environment = environment return try await withCheckedThrowingContinuation { continuation in process.terminationHandler = { proc in let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" continuation.resume(returning: BrewOutput( stdout: stdout, stderr: stderr, exitCode: proc.terminationStatus )) } do { try process.run() } catch { continuation.resume(throwing: error) } } }

3.2 解析 brew 返回的数据

Homebrew 本身提供了 JSON 输出能力,这是整个项目能顺利实现的关键。常用的是brew info --json=v2,它会输出一个包含全部 formula 和 cask 信息的大对象,而不是给人看的文本。

为了不每次都跑这种重量级命令,我把 JSON 结果缓存到本地一个 SQLite 表里,只在用户手动刷新或数据源版本变更时重新拉取。下面是模型定义的简化示例:

struct BrewPackage: Codable, Identifiable { let name: String let fullName: String let versions: Versions let desc: String? let size: Measurement<UnitInformationStorage>? let dependencies: [String] let installedAsDependency: Bool var id: String { fullName } } struct Versions: Codable { let stable: String? let head: String? let installed: [InstalledVersion] } struct InstalledVersion: Codable { let version: String let installedOnRequest: Bool let installedAsDependency: Bool }

从 JSON 解析出模型后,界面层只需绑定一个数组就能完成列表渲染。这个过程中我踩过的最大坑是:不同版本的 Homebrew 输出的 JSON 字段有些差异,有些老版本没有installedAsDependency这个字段。应对方案是解析时使用decodeIfPresent全部做可选处理,并且跑一个异常防御性的判断,而不是让整个工具崩溃。

3.3 安装、卸载、升级的真实处程

安装包的场景下,用户点击「安装」按钮后,界面上会立即出现一个日志面板,实时显示命令输出。实现实时输出需要读取管道的availableData

stdoutPipe.fileHandleForReading.readabilityHandler = { handle in let data = handle.availableData if data.isEmpty { return } let text = String(data: data, encoding: .utf8) ?? "" DispatchQueue.main.async { self.logOutput.append(text) self.scrollToBottom() } }

这个机制帮我在界面上还原了终端滚动日志的体验。还有一个细节:当用户点击「取消」时,不能只隐藏界面上的日志面板,必须调用process.terminate()把真正的后台进程杀掉,否则安装进程还在跑,下次操作就会遇到 brew 锁。

卸载包的流程比安装更需要注意依赖。在真正执行brew uninstall之前,界面层会先反向调用一次brew uses --installed <packageName>,把依赖它的包列出来给用户看。如果有重要项目依赖它,用户大概率会取消操作,这一步帮我们避免了很多次翻车。

3.4 依赖关系图的呈现方式

依赖关系是 brew 命令提示词里最容易让人懵掉的部分。我试过解析brew deps --tree的树状文本输出,但那是纯文本缩进,渲染到 GUI 里很难看。最后我的方案是解析brew deps --json生成的邻接表,然后用 SwiftUI 的递归视图绘制出可折叠的依赖树。

每个依赖节点都显示当前是否已安装、版本是否符合要求。如果是过期版本,节点会标一个黄色图标;如果依赖缺失,标红色提示。这样用户一眼能看出某个包「为什么装不上」或者「升级会牵扯到哪些包」。

这里有个经验:千万不要试图用第三方图表库重绘漂亮的拓扑图,因为 brew 依赖图通常有几十个节点,图形布局非常难做,处理不好 UI 会很卡。树形列表虽然朴素,但它符合人的阅读顺序,也容易折叠展开,实际体验反而更好。

4. 使用 BrewUI 的完整实操流程

4.1 安装与首次配置

BrewUI 目前通过 GitHub Releases 分发.dmg安装包,首次启动时它会自动探测 brew 可执行文件位置。探测顺序是:

  1. 从当前环境变量PATH中查找brew
  2. 检查默认路径/opt/homebrew/bin/brew
  3. 检查 Intel Mac 下的/usr/local/bin/brew
  4. 如果都找不到,弹窗让用户手动指定

首次启动还会做一次「环境自检」,检查 Homebrew 目录是否可写、是否有其他进程正在使用 brew、上次 brew 命令运行是否正常退出。如果检测到/opt/homebrew/var/homebrew/locks目录下有残留锁文件,会自动提示用户清理。

整个初始化过程大约 5 秒钟,因为没有任何重量级操作,就是几个快速命令的探测。

4.2 日常管理:搜索并安装一个新包

举个例子。我想装一个处理 JSON 的命令行工具jq

打开 BrewUI,在顶部搜索框输入jq,底部列表会实时展示搜索结果。这个搜索走的是 brew 的模糊匹配,同时还会展示名字里包含jq的其他包,比如jq本体、jq-lang,还有一个 jQuery 相关的东西。搜索结果右侧会标注「已安装版本」或「未安装」,方便快速判断。

点击jq进入详情页,能看到它的仓库地址、维护者信息、依赖列表和首次发布年份。确认没问题后,点右上角的「安装」按钮。

接下来界面进入执行态:顶部显示安装进度条,下方是一个日志面板,能看到brew install jq的运行输出。这时候我正常切到浏览器刷网页,安装完成后菜单栏图标会弹通知,不需要一直盯着日志。

整个安装过程差不多 20 秒到 2 分钟,取决于当前网络状况和是否要编译源包。安装完成后,详情页会自动刷新,jq的当前版本变为已安装状态,磁盘占用字段也更新了。

4.3 批量升级与清理

升级场景是 BrewUI 最让人舒服的地方。

点击侧边栏的「可升级」标签,系统会显示所有有新版可用的 formula 和 cask 列表,条目上直接写着当前版本和目标版本,以及这个包占用的磁盘空间。我可以勾选其中几个,「升级所选」,也可以直接「全部升级」。

执行升级时,界面显示一个聚合的进度视图,每个包独立显示状态,分为「等待中」「下载中」「正在编译」「已完成」「失败」五种。这种方式比终端里一堆滚动文本清晰得多,尤其是某个包编译失败的时候,我能立刻看到失败的是哪个,日志面板里定位到对应的错误行,而不是在几千行输出里翻找。

升级完成后再去「磁盘清理」页,工具会先执行brew cleanup --dry-run计算出本次清理能释放多少空间,然后展示一个「可清理旧版本」列表。用户确认后点击「执行清理」,一次性搞定。我实测过一次,清理完释放了大约 1.8GB 空间,过程完全可视化。

4.4 用状态栏插件提升使用频次

后来我给 BrewUI 加了一个菜单栏辅助插件,常驻在 macOS 顶部菜单栏里,显示当前可升级包数量。点开菜单可以直接展开升级列表,点击任意包可以单独升级,不需要打开主窗口。

这个功能做起来不难,但使用频率最高。它让 BrewUI 从一个「打开的时候才想起用的工具」变成了「随时能瞄一眼有没有更新」的状态。应用商店里那些做一个菜单栏图标的工具不少,但能顺带调 brew 的还是不多,算是一个很实用的差异化功能。

5. 常见问题与排查技巧实录

5.1 GUI 应用提示「brew: command not found」

这个问题的根源是 macOS 的 GUI 应用启动环境不同于终端 shell。你在终端里能直接敲brew,是因为 shell 启动时加载了你的配置文件,把/opt/homebrew/bin加进了PATH。但双击打开 GUI 应用时,它的环境变量来自 launchd,不是从你的 shell 继承。

表现就是 BrewUI 里运行任何 brew 命令都会报找不到可执行文件。排查时,我先在代码里输出当前进程的PATH环境变量确认,然后再做两件事:

  • runBrew执行前显式拼接/opt/homebrew/bin/usr/local/bin
  • 提供一个设置项,允许用户手动指定 brew 路径

如果你的 GUI 项目也遇到这种情况,优先做第一点,因为默认拼接公共路径就能覆盖绝大多数用户。

5.2 提示「Another active Homebrew process is already in progress」

Homebrew 自带锁机制,防止两个进程同时修改同一份数据库。但这对 GUI 工具来说有点尴尬,因为用户在终端里跑了一个长任务,又在 BrewUI 里点了个安装,后一个命令会一直等待锁释放,表现就是卡住不动。

我的解决思路是不去绕过 brew 的锁,而是提前检测。BrewUI 执行任何写操作之前,先检查/opt/homebrew/var/homebrew/locks目录下是否有残留锁文件,同时读取锁文件的创建时间。如果发现锁文件存在且创建时间在 3 分钟以内,就提示用户「当前 Homebrew 正被其他进程占用,是否等待?」;如果锁文件特别老,极有可能是上次意外中断留下的,就提示用户清理后再继续。

这个方案不够优雅,但安全第一。至少让用户知道发生了什么,而不是瞎等。

5.3 brew update 一直卡住,数据源刷新失败

使用过程中最常见的卡点是brew update。它在更新 Homebrew 仓库时,如果网络状况不理想,或仓库缓存较大,可能长时间停留在某个阶段没有反馈。

BrewUI 的做法是把 update 单独做成一个可取消的任务,并且把更新解耦成两档:

  • 轻量刷新:只刷新已安装包的信息,不更新 formula 仓库本身,速度快,用于显示「可升级」列表
  • 完整更新:调用brew update,这个过程最长可能要几分钟,界面里显示一个不确定进度条,并允许随时取消

在 UI 上我不会隐藏这个延迟,反而会明确告诉用户「正在同步 Homebrew 仓库数据,此操作可能耗时较长」,把这个预期管理好。很多用户以为卡死了,其实它只是慢,有明确提示就好很多。

5.4 卸载包时误伤其他软件

这是命令行时代最容易出的坑。brew uninstall xxx只会卸载目标包,不会自动处理它的依赖,但如果你卸载的是某个大型依赖链上的共同依赖,可能会让很多上层包失效。

BrewUI 里每次卸载操作前都会做反向依赖分析,用的命令是:

brew uses --installed <packageName>

如果结果不为空,界面会弹一个推荐提示。我会在明细里标出「建议先查看依赖关系」并列出相关包名。实际使用中这个功能拦住过我很多次,有一次我想卸载openssl@3,反查结果显示有十几个包依赖它,我立刻取消了操作。

如果你自己实现 bash 脚本做批量卸载,强烈建议先跑一遍这个分析,不要直接brew uninstall后追悔莫及。

5.5 日志面板卡顿或内存占用过高

brew install在编译大型软件包时会输出大量日志,如果把这些文本全量追加到 SwiftUI 的ListText里,界面会越来越卡,内存也会明显上升。

解决方案是加一个滚动缓冲区,最多只保留最近 500 行日志。每次收到新输出时就立即裁剪掉旧的,确保 UI 渲染的数据量可控。另外,日志文本在追加前先按行切分,再用LazyVStack渲染,避免一次性创建大量视图对象。

这个优化做完之后,BrewUI 即使在编译 Ruby 或 Node 这类输出大户时也没有卡顿感,内存占用一直维持在 150MB 以内。

6. 关于权限、安全与分发的一些经验

6.1 不要用 Root 权限运行 GUI 工具

有朋友建议我直接把整个 App 提权运行,这样 brew 就不会因为目录权限报错。我坚决不同意。给 GUI 工具开 root 是非常危险的操作,一旦界面层存在缺陷,比如误格式化目录或删除缓存,root 权限会把事故放大到无法挽回。

BrewUI 的做法是默认使用当前用户权限运行,遇到需要写入/Library/usr/local的场景时,才单独针对相关命令申请授权。比如执行brew install --cask某些需要写入系统目录的应用时,才触发密码授权弹窗。平时浏览、查询、卸载常规包都用不上 root。

6.2 沙箱与签名要注意

如果打算把工具分发给其他人使用,有一件事最好提前确认:macOS 对未签名应用的拦截策略。

  • 没有开发者 ID 签名的应用,用户第一次运行要右键选择「打开」
  • 通过 App Store 分发的应用必须开沙箱,这会导致无法启动外部进程,所以不适合 BrewUI
  • 开发者 ID 签名 + 公证(notarization)可以让用户直接双击打开,是最平滑的分发方式

建议项目开发初期就申请 Apple Developer 账号,一块开发者 ID 证书大约一年 99 美元,但对于一个要被别人使用的项目来说,这点成本非常值。不然每次都引导用户去「系统设置 - 隐私与安全性」里点信任,交互体验差一大截。

6.3 数据安全:别在 GUI 里做不可逆操作

我给自己定了一条规矩:BrewUI 中任何不可逆操作,默认要求用户二次确认,并且在执行完成后留下日志文件。brew uninstallbrew cleanup --prune=all这类操作都要做到这一步。

日志文件路径放在~/Library/Logs/BrewUI/下面,按日期归档。这样万一用户误操作卸载了某个包,还能从日志里找到名字和版本号,可以快速恢复。我建议每个做类似工具的人都要加上这个日志机制,它可能一年都用不上,但用上一次就值回全部成本。

7. 项目后续可扩展的方向

BrewUI 做到现在这个程度,日常使用已经非常顺了,但我也在持续整理几个扩展方向。如果你自己动手做个类似的工具,这些方向大概率也会对你有参考价值:

  • 配置导出与同步:把你安装的所有包列表导出一个Brewfile,换新电脑后一键恢复,这本质上是把 Bundle 迁移流程图形化
  • 依赖冲突可视化:目前只能展示依赖树,还没有做到「如果同时安装这两个包,会在哪个节点发生冲突」的深度分析
  • 多源管理:不同软件源的切换目前还是要靠配置文件,未来界面层可以直接操作
  • 通知中心集成:升级任务完成后,通过通知中心发送结果摘要,比菜单栏弹窗更不打断人

我个人现阶段投入最多的是「依赖冲突可视化」。因为随着装的包变多,最让人恐慌的时刻就是升级时某个包编译失败,而你不知道它的失败会影响哪些下游项目。如果能把这个分析做好,BrewUI 的价值会再上一个台阶。

8. 几个给同类型项目的小建议

如果读到这里,你也打算给某个命令行工具做一个 GUI 封装,我最后分享几个不是从文档里能学到的细节:

一是先花一天时间把命令行工具的所有输出模式都摸清楚。是纯文本?还是支持 JSON?不同子命令之间有没有输出版本差异?这一步直接决定解析层的难度。

二是把「执行命令」和「展示结果」彻底分开。不要因为 SwiftUI 很方便就在 View 里频繁调用Process,时间久了你会被各种奇怪的状态问题折磨到怀疑人生。

三是一定要处理「部分成功」的状态。批量升级时,可能 8 个包成功了,2 个失败了,这个结果不能简单地用一个布尔值表示,最好设计一个枚举状态(succeededfailedcanceledpartiallySucceeded)来承载完整结果。

四是永远给用户一个「取消」按钮。命令行里按Ctrl+C很自然,但 GUI 里如果找不到取消按钮,用户只能干等,那种体验非常糟糕。执行任务时至少要支持「优雅终止」——杀死子进程但保持 App 本身不崩溃,然后让用户回到一个干净的状态。

做 BrewUI 这个项目,表面上是在写 UI,实际上是在处理大量边界状态:命令超时、部分成功、锁冲突、权限异常、数据源卡顿。它让我对「给命令行工具做 GUI」这件事有了很具体的认知——命令行工具的内核再强大,想要让界面真正好用,解的不是技术问题,而是信息组织和情绪预期的问题。现在每次在 Dock 里点开 BrewUI,看到界面上清晰地列出所有可升级的包,我都觉得当初把它做出来是对的。

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

Windows开机黑屏蓝屏排查指南:从BIOS到系统引导修复

1. 开机黑屏蓝屏这件事&#xff0c;先别急着送修电脑按下电源键&#xff0c;风扇转了、灯亮了&#xff0c;但屏幕一片漆黑&#xff0c;或者刚看到Windows徽标就蓝屏重启——这种场景我遇到过太多次了。身边朋友第一反应往往是"主板烧了"或者"硬盘挂了"&…

作者头像 李华
网站建设 2026/9/19 21:52:50

正则化全面解析:从L1/L2到软阈值与深度学习实践

1. 先从一个反直觉的结论说起&#xff1a;惩罚参数为什么能提升泛化能力&#xff1f;我第一次接触机器学习时&#xff0c;怎么也想不通一件事&#xff1a;为什么要在损失函数后面加一个"惩罚项"去主动降低模型的表现&#xff1f;训练模型不就是为了让损失尽量小吗&am…

作者头像 李华
网站建设 2026/9/19 21:52:41

基于STM32和微信小程序的宠物自动喂养系统设计

简介&#xff1a;设计资料围绕基于STM32的猫狗宠物喂养系统展开&#xff0c;面向物联网嵌入式学习者、电子设计竞赛参赛者及智能宠物硬件开发者&#xff0c;完整呈现从需求分析到系统落地的全流程方案。内容基于STM32F103RCT6主控&#xff0c;结合ESP8266 WiFi模块、28BYJ4步进…

作者头像 李华