苏兮博客-记录学习经验 - 经验教程
type: channel
link: https://www.suxii.cn/category/jyjc/
items: Ubuntu Server 网络配置实战：Netplan 配置 WiFi 与静态 IP 详解
items: 刷 Linux 前置操作配置指南：MBR + 传统模式篇
items: 使用 AutoSSH 打造坚如磐石的内网穿透隧道
items: 记录几个flutter自定义组件
items: Flutter模板方法模式在多Tab列表中的应用
items: Flutter 实现流式消息接收
items: Flutter LayoutBuilder 原理深度解析
items: ESP32C3使用Slint渲染TFT屏幕
items: ESP32C3驱动TFT屏幕
items: ESP32C3使用Slint渲染OLED屏幕

Ubuntu Server 网络配置实战：Netplan 配置 WiFi 与静态 IP 详解
type: item
date: 2026-07-26T09:04:00+08:00
description: 在 Ubuntu Server（特别是 18.04/20.04/22.04+ 版本）中，传统的 /etc/network/interfaces 配置文件已逐渐被 Netplan 取代。对于使用 USB 无线网卡或需要固定服务器 IP 的场景，掌握 Netplan 的配置至关重要。本文将手把手教你如何通过命令行完成 WiFi 连接及静态 IP 设置，并深度解析配置文件中每一个参数的含义。第一步：确认无线网卡名称在开始之前，我们需要知道系统识别到的无线网卡接口名称。USB 网卡通常以 wlx 开头。打开终端，输入以下命令：ip link show输出示例分析：1: lo: <LOOPBACK,UP...> 2: enp1s0: <BROADCAST,MULTICAST...> <-- 这是有线网卡 3: wlx485f082e470d: <BROADCAST,MULTICAST...> <-- 这就是我们的 USB 无线网卡注意：请记下第 3 行的接口名称（例如 wlx485f082e470d），后续配置必须完全一致。第二步：编辑 Netplan 配置文件Ubuntu 的网络配置文件位于 /etc/netplan/ 目录下，通常是 .yaml 格式。1. 打开编辑器使用 nano 编辑器打开文件（文件名可能略有不同，常见为 01-network-manager-all.yaml 或 50-cloud-init.yaml）：sudo nano /etc/netplan/01-network-manager-all.yaml2. 写入配置代码清空原有内容，根据你的需求选择以下两种配置之一。方案 A：动态获取 IP (DHCP)适用于普通上网，IP 地址由路由器自动分配。network: version: 2 renderer: networkd wifis: wlx485f082e470d: # 替换为你的实际网卡名称 dhcp4: true access-points: "Your_WiFi_Name": # 替换为你的 WiFi 名称 (SSID) password: "Your_WiFi_Password" # 替换为你的 WiFi 密码方案 B：配置静态 IP (Static IP)适用于服务器环境，确保 IP 地址固定不变，方便远程管理。network: version: 2 renderer: networkd wifis: wlx485f082e470d: # 替换为你的实际网卡名称 dhcp4: no # 关闭自动获取 IP addresses: - 192.168.1.200/24 # 你想要设置的固定 IP 和子网掩码 routes: - to: default via: 192.168.1.1 # 你的路由器网关地址 nameservers: addresses: [8.8.8.8, 114.114.114.114] # DNS 服务器地址 access-points: "Your_WiFi_Name": password: "Your_WiFi_Password"3. 保存并退出按 Ctrl + O 然后回车保存。按 Ctrl + X 退出编辑器。第三步：应用配置配置写好后，需要让 Netplan 重新生成后端配置并应用：sudo netplan apply如果没有报错，说明配置成功。你可以使用 ping baidu.com 测试网络是否通畅，或使用 ip addr show 查看 IP 是否已变更。核心参数详解理解每个参数的含义，能让你在排查故障时事半功倍。1. 基础架构参数version: 2含义：指定 Netplan 配置文件的语法版本。目前 Ubuntu 主要使用 Version 2，Version 1 已被废弃。renderer: networkd含义：指定由哪个后端程序来管理网络。选项：networkd：即 systemd-networkd。Server 版（无图形界面）的标准选择，轻量高效。NetworkManager：Desktop 版（有图形界面）的标准选择。wifis:含义：定义无线网络接口块。如果是有线网络，则使用 ethernets:。2. 设备与连接参数**wlx485f082e470d** (接口名称)含义：具体的物理设备标识符。对于 USB 网卡，Ubuntu 倾向于使用基于 MAC 地址的可预测命名。这个名字必须和 ip link show 查出来的一模一样。**access-points**含义：定义要连接的 WiFi 热点列表。**"Your_WiFi_Name"** (SSID)细节：如果 WiFi 名称包含空格或特殊字符，必须加双引号。建议养成始终加引号的习惯。**pas…

刷 Linux 前置操作配置指南：MBR + 传统模式篇
type: item
date: 2026-07-26T08:43:00+08:00
description: 在折腾各类“纯血”Linux 发行版之前，理清主板固件的底层逻辑是避免安装失败、系统崩溃的第一步。针对当前主流的“MBR 分区表 + 传统 BIOS 引导”老旧硬件或特定需求场景，前置配置的容错率极低。以下是为你整理的标准前置操作配置指南，帮助你顺利跨越这道门槛。一、 核心 BIOS 设置项与操作指南1. CSM（兼容性支持模块）：必须保持“启动”在 MBR + 传统模式下，CSM 是主板模拟传统 BIOS 中断调用、识别 MBR 引导记录的“生命线”。切勿听信“安装 Linux 必须关闭 CSM”的绝对化言论，那是针对 UEFI + GPT 环境的建议。一旦关闭 CSM，主板将仅支持 UEFI 引导，你的 MBR 硬盘将直接“隐身”，导致无法安装或无法启动。2. SATA 模式：从 RST 切换至 AHCI这是安装 Linux 最容易踩坑的环节。Intel RST（快速存储技术）属于专有协议，绝大多数 Linux 发行版的内核默认不包含 RST 驱动，这会导致安装程序在分区界面提示“找不到磁盘”。必须在 BIOS 中将存储模式修改为通用的 AHCI 标准，Linux 才能顺利接管硬盘读写。3. Secure Boot（安全启动）：无需手动干预在 CSM 开启的传统模式下，Secure Boot 会被主板自动隐藏并强制处于“禁用”状态。因此，你不需要像 UEFI 环境那样刻意去寻找并关闭它，它在此场景下对 Linux 安装没有任何阻碍。4. 启动介质选择：认准“无 UEFI 前缀”选项制作好 Linux 启动 U 盘后，在 BIOS 的启动顺序（Boot Priority）或快捷启动菜单（F12/F8）中，务必选择不带“UEFI:”前缀的 U 盘选项。带有 UEFI 前缀的选项会强制主板尝试以 GPT 模式引导，这在 MBR 环境下会导致启动失败或进入错误的安装流程。二、 底层逻辑：为什么要这样配置？MBR 与 CSM 的强绑定MBR（主引导记录）是一种古老的分区表格式，最大仅支持 2TB 磁盘。它依赖传统 BIOS 的 16 位实模式中断来加载操作系统的引导加载程序（如 GRUB）。CSM 正是提供这一模拟环境的模块，没有它，现代主板就无法理解 MBR 的引导指令。AHCI 是 Linux 的通用通行证Linux 内核的开源社区对通用标准的支持极为完善。AHCI 作为 SATA 控制器的开放标准，拥有完美的内核级驱动支持。放弃专有的 RST 转向 AHCI，本质上是为了让操作系统能够直接与存储硬件进行标准化通信，从而避免“无盘可装”的尴尬。三、 核心关键词速记核心操作：CSM 保持启动、SATA 改 AHCI、Legacy 引导避坑要点：不关 CSM、无视 Secure Boot、选对非 UEFI 启动项技术原理：MBR 依赖 CSM 模拟、Linux 依赖 AHCI 驱动、传统模式绕过安全启动掌握以上前置配置，你的 MBR 传统模式主机就已经具备了安装主流 Linux 发行版的完美土壤。接下来，只需准备好对应的 Legacy 引导镜像，即可开始你的开源之旅。

使用 AutoSSH 打造坚如磐石的内网穿透隧道
type: item
date: 2026-07-26T08:32:00+08:00
description: 在远程运维和开发场景中，我们经常面临一个痛点：家里的电脑或公司的内网服务器没有公网 IP，想要从外部随时访问该怎么办？传统的 DDNS + 端口映射不仅配置繁琐，还会将内网端口直接暴露在公网，极易招致恶意扫描和攻击。最安全、最优雅的方案，莫过于利用一台公网 VPS 作为跳板机，通过 SSH 反向隧道实现“隐形”的内网穿透。但普通的 SSH 隧道有一个致命弱点：一旦遇到网络波动，连接就会“假死”，进程还在但数据不通。今天，我们就来聊聊如何使用 autossh 打造一个“打不死”的 SSH 隧道，实现稳定、安全的内网穿透。架构拓扑：打造安全的“隐形通道”我们的整体架构非常清晰：[外部设备] --(SSH加密连接)--> [公网跳板机] --(本地回环转发)--> [内网目标主机]这种架构有两个核心优势：隐蔽性：公网跳板机对外只开放常规的 SSH 端口，不暴露任何额外的业务端口。安全性：内网目标主机的服务只监听本地回环地址（127.0.0.1），对局域网内的其他设备完全不可见，所有流量都必须经过加密的 SSH 隧道。第一步：环境准备与密钥生成假设我们已经拥有一台公网跳板机（IP为 <公网跳板机IP>，SSH端口为 <跳板机SSH端口>），以及一台位于内网的目标主机（用户名为 <内网主机用户名>）。首先，我们需要在内网目标主机上安装必要的软件，并生成一把专门用于建立隧道的 SSH 密钥（不要与日常登录的密钥混用）：# 更新软件源并安装 openssh-server 和 autossh sudo apt update sudo apt install openssh-server autossh # 生成专用的隧道密钥（一路回车，不设置密码） ssh-keygen -t ed25519 -f ~/.ssh/id_tunnel -N "" -C "autossh-tunnel"接下来，将这把密钥的公钥上传到公网跳板机：ssh-copy-id -i ~/.ssh/id_tunnel.pub -p <跳板机SSH端口> <跳板机用户名>@<公网跳板机IP>第二步：配置 Systemd 守护进程为了让隧道在系统开机时自动启动，并在意外断开时自动重连，我们使用 systemd 来托管 autossh。在内网目标主机上创建服务配置文件：sudo nano /etc/systemd/system/autossh-tunnel.service将以下内容写入文件中（注意替换对应的参数）：[Unit] Description=AutoSSH Reverse Tunnel After=network-online.target ssh.service Wants=network-online.target [Service] User=<内网主机用户名> Type=simple # 核心参数解析： # -M 0: 禁用 autossh 自身的监控端口，改用 SSH 原生的心跳保活机制 # -N: 不执行远程命令，仅建立隧道 # -R 127.0.0.1:<跳板机映射端口>:localhost:22: # 将跳板机的指定端口绑定在 127.0.0.1，确保公网无法直接扫描 Environment="AUTOSSH_GATETIME=0" ExecStart=/usr/bin/autossh -M 0 -N \ -o "ServerAliveInterval=30" \ -o "ServerAliveCountMax=3" \ -o "ExitOnForwardFailure=yes" \ -o "StrictHostKeyChecking=no" \ -p <跳板机SSH端口> \ -i /home/<内网主机用户名>/.ssh/id_tunnel \ <跳板机用户名>@<公网跳板机IP> # 崩溃或断开后自动重启 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target第三步：启动服务与验证配置完成后，启动服务并设置开机自启：sudo systemctl enable --now autossh-tunnel.service查看服务状态，确认是否正常运行：systemctl status autossh-tunnel.service如果看到 Active: active (running) 且没有 Permission denied 等报错，说明隧道已经成功建立。此时，隧道已经将内网目标主机的 22 端口，安全地投射到了公网跳板机的 <跳板机映射端口> 上。第四步：如何从外部连接？现在，无论你身处何地（公司、咖啡厅或旅途中），只要先通过 SSH 登录到公网跳板机，就可以轻松跳转到内网主…

记录几个flutter自定义组件
type: item
date: 2026-06-18T16:44:00+08:00
description: 波浪效果 class SineWaveWidget extends StatefulWidget { final double amplitude; // 振幅（波浪的高度） final double frequency; // 频率（波浪的密集程度） final Color color; // 波浪颜色 final Duration duration; // 流动一圈的周期 const SineWaveWidget({ super.key, this.amplitude = 20.0, this.frequency = 2.0, this.color = Colors.blueAccent, this.duration = const Duration(seconds: 1), }); @override State<SineWaveWidget> createState() => _SineWaveWidgetState(); } class _SineWaveWidgetState extends State<SineWaveWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; bool _isForward = true; @override void initState() { super.initState(); // 创建一个无限循环的动画控制器，用于驱动波浪的相位变化 _controller = AnimationController( vsync: this, duration: widget.duration, lowerBound: 0.0, upperBound: 2 * pi, )..repeat(); // 自动无限循环 } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { _isForward = !_isForward; }, child: AnimatedBuilder( animation: _controller /*.drive(Tween(begin: 0, end: 2 * pi))*/, // 将 0~1 的进度映射为 0~2π 的相位 builder: (context, child) { return CustomPaint( size: Size.infinite, // 填满父容器 painter: SineWavePainter( amplitude: widget.amplitude, frequency: widget.frequency, color: widget.color, phase: _controller.value * (_isForward ? 1 : -1), ), ); }, ), ); } } class SineWavePainter extends CustomPainter { final double amplitude; final double frequency; final Color color; final double phase; SineWavePainter({ required this.amplitude, required this.frequency, required this.color, required this.phase, }); @override void paint(Canvas canvas, Size size) { // final paint = Paint() // ..color = color // ..style = PaintingStyle.fill // ..blendMode = BlendMode.xor; // // final path = Path(); // // // 将波浪的基准线设置在垂直中心 // final centerY = size.height / 2; // // // 1. 起点：从左下角开始 // path.moveTo(0, size.height); // // // 2. 绘制正弦曲线 // // 遍历画布的每一个像素宽度 // for (double x = 0; x <= size.width; x++) { // // 核心公式：y = A * sin(ω * x…

Flutter模板方法模式在多Tab列表中的应用
type: item
date: 2026-01-16T15:18:25+08:00
description: 项目背景与需求在Flutter开发中，我们经常遇到需要实现多Tab栏列表的场景，每个Tab具有相似的结构（下拉刷新、上拉加载、列表展示），但数据类型和UI展示各不相同。本文将介绍如何使用模板方法模式来优雅地解决这个问题。模板方法模式简介模板方法模式是一种行为设计模式，它在超类中定义了一个算法的框架，允许子类在不修改结构的情况下重写特定步骤。在我们的场景中，每个Tab的加载流程（初始化→刷新→加载更多）是相同的，但具体的数据加载和UI展示是不同的。代码实现解析1. 抽象基类定义const int defaultPageSize = 10; abstract class TabData<T> { final String name; // tab名称 int total; // 数据数量 bool isSelected; // 选中状态 bool hasMore; // 是否有更多数据 List<T> items; // 数据列表 bool _isInited = false; // 是否初始化完成 final RefreshController controller; final Widget Function(BuildContext context,T item) itemBuilder; TabData({ required this.name, required this.itemBuilder, this.total = 0, this.isSelected = false, this.hasMore = true, this.items = const [] }) : controller = RefreshController(); int get pageSize => defaultPageSize; Future<List<T>> loadData(int page); void updateItems(List<T> newItems, bool isRefresh); Future<bool> init() async{ if (_isInited) { return true; } _isInited = true; await onRefresh(); return false; } Future<void> onRefresh() async { try { final data = await loadData(1); updateItems(data, true); hasMore = data.length >= pageSize; controller.refreshCompleted(); } catch (e) { controller.refreshFailed(); rethrow; } } Future<void> onLoadMore() async { if (!hasMore) { controller.loadNoData(); return; } try { final currentPage = (items.length / pageSize).ceil() + 1; final data = await loadData(currentPage); updateItems(data, false); hasMore = data.length >= pageSize; controller.loadComplete(); } catch (e) { controller.refreshFailed(); rethrow; } } void dispose() { controller.dispose(); } }设计要点：定义了完整的生命周期方法（init、onRefresh、onLoadMore）使用泛型T支持不同的数据类型封装了刷新控制器和分页逻辑提供itemBuilder回调实现UI定制化2. 具体子类实现点赞Tab实现：class LikeTabData extends TabData<LikeTabDataEntity>{ LikeTabData():super(name: "点赞",itemBuilder: (context,item){ return Text(item.name); }); @override Future<List<LikeTabDataEntity>> loadData(int page) async{ await Future.delayed(const Duration(seconds: 1)); return List.generate(pageSize, (index)=> LikeTabDataEntity(name: …

Flutter 实现流式消息接收
type: item
date: 2026-01-07T14:52:57+08:00
description: 我们通过 Dio 发起一个 ResponseType.stream 请求，消费后端逐字符返回的 HTTP 流。核心代码如下：void sendMessage() => Dio().get( "http://192.168.149.178:8080/stream/covert?url=${state.text}", options: Options(responseType: ResponseType.stream), cancelToken: CancelToken(), ).then((response) { final data = response.data; if (data is! ResponseBody) return; state.data = ""; update(); data.stream.listen((chunk) { final res = utf8.decoder.convert(chunk); state.data += res; update(); }); });UI 部分使用 Expanded + SingleChildScrollView 展示实时追加的文本：Column( children: [ TextField(controller: logic.text), TextButton(onPressed: logic.sendMessage, child: Text("发送消息")), Expanded( child: SelectionArea( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [Text(state.data)], ), ), ), ), ], )后端使用 Spring WebFlux 将目标网页内容逐字符延迟返回：@GetMapping("/covert") public Flux<String> covert(@RequestParam String url) { return Mono.fromCallable(() -> { // 同步抓取网页内容（在非 Netty 线程执行） var request = new Request.Builder().url(url).build(); try (var resp = client.newCall(request).execute()) { return resp.body().string(); } }) .flux() .flatMap(text -> Flux.fromStream(text.chars().mapToObj(c -> String.valueOf((char) c))) .delayElements(Duration.ofMillis(10)) // 每字符间隔 10ms ); }该方案无需 SSE 或 WebSocket，仅依赖标准 HTTP 分块传输，即可实现“打字机式”流式响应，适用于 AI 对话、实时日志等场景。

Flutter LayoutBuilder 原理深度解析
type: item
date: 2025-12-25T16:13:54+08:00
description: 引言在 Flutter 响应式布局体系中，LayoutBuilder 是一个强大而特殊的组件。与大多数在构建阶段（build phase）确定其子树的组件不同，LayoutBuilder 将子组件的构建推迟到布局阶段（layout phase），从而能够根据父级传递的实际布局约束动态构建 UI。本文将深入源码层面，全面剖析 LayoutBuilder 的工作原理。核心架构概览类继承层次Widget ├── RenderObjectWidget │ └── ConstrainedLayoutBuilder<BoxConstraints> │ └── LayoutBuilder (具体实现)主要角色分工ConstrainedLayoutBuilder<T> - 抽象基类，定义约束构建器的通用协议LayoutBuilder - 面向 BoxConstraints 的具体实现_LayoutBuilderElement - 连接 Widget 与 RenderObject 的桥梁_RenderLayoutBuilder - 实际执行布局的 RenderObject源码深度解析1. Element 的初始化与绑定// _LayoutBuilderElement.mount 方法 @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); // 1. 创建对应的 RenderObject renderObject.updateCallback(_rebuildWithConstraints); // 2. 绑定重建回调 }关键点分析：super.mount() 会创建 _RenderLayoutBuilder 实例updateCallback 将 _rebuildWithConstraints 方法注册到 RenderObject此时回调并未立即执行，只是建立了调用关系2. 布局阶段的核心调用链// _RenderLayoutBuilder.performLayout 方法 @override void performLayout() { final BoxConstraints constraints = this.constraints; // 1. 获取父级约束 rebuildIfNecessary(); // 2. 触发约束重建 if (child != null) { child!.layout(constraints, parentUsesSize: true); // 3. 布局子节点 size = constraints.constrain(child!.size); // 4. 确定自身大小 } else { size = constraints.biggest; } }2.1 约束的来源追踪约束的传递遵循 Flutter 渲染管线的标准流程：// RenderObject 层级传递 void layout(Constraints constraints, { bool parentUsesSize = false }) { this.constraints = constraints; // 父组件传递约束 // ... 后续布局计算 }this.constraints 在 performLayout 中通过 this.constraints 访问，这是 RenderObject 的标准属性。2.2 重建触发机制// RenderConstrainedLayoutBuilder 混入类 void rebuildIfNecessary() { assert(_callback != null); invokeLayoutCallback(_callback!); // 调用注册的回调 } // RenderObject 中的回调执行 void invokeLayoutCallback<T extends Constraints>(LayoutCallback<T> callback) { owner!._enableMutationsToDirtySubtrees(() { callback(constraints as T); // 关键：传递当前约束 }); }3. 安全的回调执行环境_enableMutationsToDirtySubtrees 是保证布局阶段安全修改组件树的关键：void _enableMutationsToDirtySubtrees(VoidCallback callback) { assert(_debugDoingLayout); bool? oldState; assert(()…

ESP32C3使用Slint渲染TFT屏幕
type: item
date: 2025-05-10T20:02:12+08:00
description: 这个教程算是在st7789屏幕没有买之前就已经开始打算了，之前的使用slint驱动oled也算是为这个打下基础吧创建新项目可以参考 ESP32C3使用Slint渲染OLED屏幕 引入slint依赖，创建跟配置slint跟这篇文章一样，引入的依赖基本上大差不差，只不过这个项目还需要引入st7789屏幕的相关依赖。然后在Cargo.toml中添加依赖 [dependencies] # 基础依赖 critical-section = "1.2.0" esp-hal = { version = "1.0.0-beta.0", features = ["esp32c3","unstable"] } esp-alloc = "0.7.0" esp-println = { version = "0.13.1",features = ["esp32c3","log"] } log = "0.4.27" esp-backtrace = { version = "0.15.1" ,features = ["esp32c3","println","panic-handler"]} # 图形相关 embedded-hal = "1.0.0" embedded-graphics-core = "0.4.0" embedded-graphics = "0.8.1" embedded-graphics-framebuf = "0.5.0" display-interface = "0.5.0" display-interface-spi = "0.5.0" mipidsi = "0.9.0" embedded-hal-bus = "0.3.0" # 异步 esp-hal-embassy = { version = "0.7.0", features = ["esp32c3"] } embassy-time = { version = "0.4.0", features = ["generic-queue-8"] } embassy-executor = { version = "0.7.0",features = ["task-arena-size-20480","executor-thread"] } #embassy-futures = "0.1.1" static_cell = { version = "2.1.0", features = ["nightly"] } tinygif = "0.0.4" [dependencies.slint] git = "https://github.com/slint-ui/slint" rev = "29168bc89270798f6075a0a729c14a3f011ceb4f" # 修复提交的哈希 default-features = false features = [ "compat-1-2", "unsafe-single-threaded", # 启用单线程模式 "libm", "renderer-software", ] [build-dependencies] slint-build = {git = "https://github.com/slint-ui/slint",rev = "29168bc89270798f6075a0a729c14a3f011ceb4f"} 创建ui文件创建 ui/main.slint 文件，作为程序的入口文件import { System,LocalTime,Route , Setting} from "./global.slint"; import { HomePage } from "./page/home_page.slint"; import { SettingPage } from "./page/setting_page.slint"; // 导出给rust代码调用 export { System,LocalTime,Setting } export component MainView inherits Window { background: black; width: System.screen_width; height: System.screen_height; default-font-family: "黑体"; // 简单实现路由 if System.route==Route.HomePage: HomePage{} if System.route==Route.SettingPage: SettingPage{} // 显示帧率 if Setting.fps == true:Rectangle { padding: 2px; x:0…

ESP32C3驱动TFT屏幕
type: item
date: 2025-05-09T14:56:00+08:00
description: 因为使用的是新版esp-generate创建的项目，很多api的使用都大改了，网上能搜到的类似的教程也都完全没法用，这个教程是使用最新版esp-hal基于rust开发esp32c3驱动st7789屏幕。看网上有些项目叫MOSI，MISO的，也有叫SDI，SDO，这两个本质就是一个东西，在不同协议叫法不同。SPI 协议中大家都喜欢叫 MOSI / MISO，有些芯片厂商文档上写的是 SDI / SDO；MOSI == SDI（对于从机）：表示“数据输入”MISO == SDO（对于从机）：表示“数据输出”项目创建在 rust裸机开发esp32c3 的基础上，先新建一个最基础的环境引入依赖[dependencies] critical-section = "1.2.0" esp-hal = { version = "1.0.0-beta.0", features = ["esp32c3","unstable"] } esp-println = { version = "0.13.1",features = ["esp32c3","log"] } log = "0.4.27" # 图形相关 embedded-hal = "1.0.0" embedded-graphics-core = "0.4.0" embedded-graphics = "0.8.1" embedded-graphics-framebuf = "0.5.0" display-interface = "0.5.0" display-interface-spi = "0.5.0" mipidsi = "0.9.0" embedded-hal-bus = "0.3.0" 编写main.rs代码#![no_std] #![no_main] use embedded_graphics::Drawable; use embedded_graphics::image::{Image, ImageRawLE}; use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::prelude::{DrawTarget, Point, Primitive, RgbColor}; use embedded_graphics::primitives::PrimitiveStyle; use embedded_graphics_core::prelude::Size; use embedded_graphics_core::primitives::Rectangle; use embedded_hal::delay::DelayNs; use embedded_hal::digital::ErrorType; use embedded_hal_bus::spi::ExclusiveDevice; use esp_hal::clock::CpuClock; use esp_hal::delay::Delay; use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::main; use esp_hal::spi::master::{Config, Spi}; use esp_hal::spi::Mode; use esp_hal::time::{Duration, Instant, Rate}; use esp_println::logger::init_logger; use esp_println::println; use log::{error, LevelFilter}; use mipidsi::_mock::MockSpi; use mipidsi::models::ST7789; use mipidsi::Builder; use mipidsi::interface::SpiInterface; use mipidsi::options::Orientation; #[panic_handler] fn panic(e: &core::panic::PanicInfo) -> ! { println!("{}",e); loop { } } #[main] fn main() -> ! { // generator version: 0.3.1 let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); init_logger(LevelFilter::Debug); let peripherals = esp_hal::init(config); let…

ESP32C3使用Slint渲染OLED屏幕
type: item
date: 2025-05-05T18:59:05+08:00
description: 前言：说一下为什么要用slint，既然都选择用rust开发嵌入式了避免不了要折腾，也是为后续驱动st7789做铺垫吧。而其实对应OLED屏幕已经有一个很好用的库了embedded-graphics，几乎能满足OLED屏幕的全部需求了，但是slint有动画，定时器，声明式，这些优势，再嵌入式开发再合适不过了。在写这个的时候也遇到不少坑，所以记录一下吧创建项目引入库编译问题已经在 ESP32C3引入Slint 文章解决了先创建一个最基础的esp32c3的项目然后再Cargo.toml中添加依赖# 基础依赖 critical-section = "1.2.0" esp-hal = { version = "1.0.0-beta.0", features = ["esp32c3","unstable"] } esp-alloc = "0.7.0" esp-println = { version = "0.13.1",features = ["esp32c3","log"] } log = "0.4.27" esp-backtrace = { version = "0.15.1" ,features = ["esp32c3","println","panic-handler"]} # 图形相关 embedded-hal = "0.2.7" embedded-graphics-core = "0.4.0" embedded-graphics = "0.8.1" embedded-graphics-framebuf = "0.5.0" ssd1306 = { version = "0.10.0" ,features = ["graphics"]} # 异步 esp-hal-embassy = { version = "0.7.0", features = ["esp32c3"] } embassy-time = { version = "0.4.0", features = ["generic-queue-8"] } embassy-executor = { version = "0.7.0",features = ["task-arena-size-20480"] } static_cell = { version = "2.1.0", features = ["nightly"] } [dependencies.slint] git = "https://github.com/slint-ui/slint" rev = "29168bc89270798f6075a0a729c14a3f011ceb4f" # 修复提交的哈希 default-features = false features = [ "compat-1-2", "unsafe-single-threaded", # 启用单线程模式 "libm", "renderer-software", ] [build-dependencies] slint-build = {git = "https://github.com/slint-ui/slint",rev = "29168bc89270798f6075a0a729c14a3f011ceb4f"} 配置Slint我们使用slint的ui跟代码分离的模式，修改build.rs新增一个build_slint函数，然后在main函数中调用它 fn main(){ build_slint(); ... } fn build_slint(){ slint_build::compile_with_config( "ui/MainView.slint", slint_build::CompilerConfiguration::new() .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer) ).unwrap() }创建UI文件在项目根目录下创建 ui/MainView.slint 文件，定义 UI 界面// 由于是在no_std的环境下，没有默认字体，使用需要引入自定义的字体 import "./fonts/simhei.ttf"; // 这里定义了一个全局的状态，用于在rust代码里进行修改ui状态 export global MyState { in-out property <bool> is_show : true; } export component MainView inherits Window { default-font-family: "黑体"; width: 128px; height: 64px; Rectangle …