news 2026/10/15 17:10:49

tracing 结构化诊断实践指南:Span、Event、Subscriber 与异步插桩全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
tracing 结构化诊断实践指南:Span、Event、Subscriber 与异步插桩全解析
  • 可观测性
  • 开发工具

【免费下载链接】tracing

Application level tracing for Rust.

项目地址:https://gitcode.com/gh_mirrors/tr/tracing
点击查看免费下载

tracing是 Tokio 项目维护的 Rust 应用级结构化诊断(tracing)框架,用于采集结构化、事件驱动的诊断信息,且不强制要求使用tokio运行时。本文基于仓库根目录 README.md 与 tracing/src/lib.rs 的完整文档,结合tracing、tracing-subscriber、tracing-attributes等 crate 的源码与示例,系统讲解 Span / Event / Subscriber 三大核心概念、宏语法与字段记录规则、应用与库两种插桩模式,以及异步代码中 span 的正确使用方法,帮助你从零开始把 Rust 程序的执行流变成可查询、可过滤、可聚合的结构化诊断数据。

核心概念:Span、Event 与 Subscriber

tracing的 API 由三个核心构件组成,理解它们之间的关系是使用该框架的前提(参见 tracing/src/lib.rs 中的 "Core Concepts" 一节)。

Span:表示一段时间

与一条日志记录代表"某个时刻"不同,Span 代表一段有开始和结束的时间区间。当程序开始在某上下文中执行某项工作时,它进入(enter)该 span;当执行离开该上下文时,它退出(exit)该 span。当前线程正在执行的 span 称为该线程的当前 span(current span)。

use tracing::{span, Level}; let span = span!(Level::TRACE, "my_span"); // `enter` 返回一个 RAII guard,guard 被 drop 时即退出 span, // 表明在当前词法作用域内我们处于该 span 之中。 let _enter = span.enter(); // 在 `my_span` 的上下文中执行一些工作...

Span::enter的具体实现在 tracing/src/span.rs:它调用内部的do_enter()并返回Enteredguard,drop 时自动调用Subscriber::exit。此外还有消费式变体Span::entered()(tracing/src/span.rs),允许用单个表达式创建并进入 span,例如let _span = info_span!("something_interesting").entered();。

Event:表示一个时刻

Event 代表时间轴上的一个瞬间,表示"trace 记录过程中发生了某事",与无结构日志系统发出的日志记录相当;但与普通日志行不同的是,Event 可以发生在某个 Span 的上下文之内。

use tracing::{event, span, Level}; // 在任意 span 上下文之外记录一个事件: event!(Level::INFO, "something happened"); let span = span!(Level::INFO, "my_span"); let _guard = span.enter(); // 在 "my_span" 内记录一个事件。 event!(Level::DEBUG, "something happened inside my_span");

一般来说,Event 应被用来表示span 内部的时间点——例如"请求以某状态码返回""从队列取走了 n 个新条目"等。

Subscriber:收集与过滤

当 Span 和 Event 发生时,由实现了 [Subscriber] trait 的对象负责记录或聚合。Subscriber 通过以下 trait 方法获得通知:

  • event:当 Event 发生时被调用;
  • enter:当执行进入某个 Span 时被调用;
  • exit:当执行退出某个 Span 时被调用。

Subscriber 还可以实现enabled函数,基于描述每个 Span 或 Event 的元数据(metadata)来过滤它收到的通知。若某个 Subscriber 对一组元数据返回false,则对应的 Span 或 Event 根本不会通知到它;出于性能考虑,如果当前没有任何活跃的 Subscriber 对某组元数据感兴趣,对应的 Span 或 Event 将永远不会被构造——这就是tracing能保持"零成本禁用"的关键机制。

在应用程序中使用:安装一个 Subscriber

要记录 trace 事件,可执行程序必须使用与tracing兼容的Subscriber实现。Subscriber定义了收集 trace 数据的方式(例如输出到标准输出)。[tracing-subscriber] 的 [fmt模块]提供了一个带合理默认值的日志型 subscriber;此外它还能消费由log插桩的库和模块发出的消息。

添加依赖

在Cargo.toml中加入:

[dependencies] tracing = "0.1" tracing-subscriber = "0.3"

最简单的做法:fmt::init()

然后创建并安装一个 Subscriber,例如使用init():

use tracing::info; use tracing_subscriber; fn main() { // 安装一个根据 RUST_LOG 环境变量配置的全局 subscriber。 tracing_subscriber::fmt::init(); let number_of_yaks = 3; // 在任意 span 之外创建一个新事件。 info!(number_of_yaks, "preparing to shave yaks"); let number_shaved = yak_shave::shave_all(number_of_yaks); info!( all_yaks_shaved = number_shaved == number_of_yaks, "yak shaving completed." ); }

调用init()等价于调用 [set_global_default()],此后该 subscriber 将在程序余下的生命周期内、所有线程中作为默认值使用——这与logcrate 设置 logger 的方式类似。

从源码看,tracing_subscriber::fmt::init()(tracing-subscriber/src/fmt/mod.rs)内部调用try_init()并expect;而try_init()(tracing-subscriber/src/fmt/mod.rs)在启用env-filter特性时等价于fmt().with_env_filter(EnvFilter::from_default_env()).init(),即默认基于RUST_LOG环境变量过滤;未启用env-filter时则会回退到Targets过滤器读取RUST_LOG。若安装失败(例如全局 subscriber 已存在),init()会 panic,而try_init()返回Result。

仓库中的完整可运行示例见 examples/examples/fmt.rs,它演示了tracing_subscriber::fmt().with_max_level(Level::TRACE).init()与 yak shaving 插桩的组合。

更多控制:分段构建与局部覆盖

如需更多控制,可以分阶段构建 subscriber,并且不设置为全局,而是用它局部覆盖默认 subscriber:

use tracing::{info, Level}; use tracing_subscriber; fn main() { let subscriber = tracing_subscriber::fmt() // 过滤级别为 TRACE 或更高的 span/event。 .with_max_level(Level::TRACE) // 构建但不安装 subscriber。 .finish(); tracing::subscriber::with_default(subscriber, || { info!("This will be logged to stdout"); }); info!("This will _not_ be logged to stdout"); }

任何在 subscriber 上下文之外产生的 trace 事件都不会被收集。这种方式允许程序在不同上下文内由多个 subscriber 收集 trace 数据。注意:with_default的覆盖只作用于当前执行线程,其他线程看不到这一改变。

构建器还提供一系列链式配置方法(见 tracing-subscriber/src/fmt/mod.rs):

  • with_env_filter(filter):以EnvFilter或RUST_LOG风格字符串作为过滤规则(需要env-filter特性);
  • with_max_level(LevelFilter):设定最大启用的详细级别,替换此前的过滤配置;
  • with_writer(make_writer):更换输出 Writer,例如with_writer(io::stderr)输出到 stderr;
  • with_timer(timer)/without_time():配置或关闭日志时间戳(启用time特性可获得UtcTime、LocalTime等更复杂的时间格式);
  • with_span_events(FmtSpan):决定是否在 span 生命周期(创建 NEW / 进入 ENTER / 退出 EXIT / 关闭 CLOSE,可组合,FmtSpan::FULL为全部)时合成日志事件,启用时间戳时 CLOSE 事件还会附带 span 的 busy time(被进入的总时长)与 idle time(存在但未进入的总时长);
  • event_format(fmt_event):自定义事件格式化器,例如format().compact()。

一旦 subscriber 被设置,就可以使用tracingcrate 的宏向可执行程序添加插桩点。

使用宏记录结构化数据

Span 和 Event 都通过宏记录。span!宏展开为一个 [Span] 结构体;Span::enter记录该 span 已进入并返回 RAII guard。event!宏用于记录 Event:

use tracing::{event, Level}; event!(Level::INFO, "something has happened!");

级别、target、parent 与 name

span!与event!宏(以及#[instrument]属性)使用相近的语法。两者都要求一个Level指定详细程度;可选地,可以覆盖target(默认是宏调用处的模块路径)和parent span(默认是 subscriber 决定的当前 span):

use tracing::{span, event, Level}; span!(target: "app_spans", Level::TRACE, "my span"); event!(target: "app_events", Level::INFO, "something has happened!"); let span = span!(Level::TRACE, "my span"); event!(parent: &span, Level::INFO, "something has happened!");

span 宏在 Level 之后接受一个字符串字面量作为 span 名称;event 宏则可用name:覆盖事件名称(默认为event file:line):

span!(Level::TRACE, "my span"); event!(name: "some_info", Level::INFO, "something has happened!");

结构化字段的记录语法

结构化字段使用field_name = field_value语法,字段之间用逗号分隔:

use tracing::{event, Level}; // 记录一个带两个字段的事件: // - "answer",值为 42 // - "question",值为 "life, the universe and everything" event!(Level::INFO, answer = 42, question = "life, the universe, and everything");

作为简写,局部变量可以直接作为字段值(类似结构体初始化):

let user = "ferris"; span!(Level::TRACE, "login", user); // 等价于: span!(Level::TRACE, "login", user = user);

字段名可以包含点号(但不能以点号结尾),因此结构体的字段可通过简写直接使用:

let user = User { name: "ferris", email: "ferris@rust-lang.org" }; // span 将包含字段 `user.name = "ferris"` 和 `user.email = "ferris@rust-lang.org"`。 span!(Level::TRACE, "login", user.name, user.email);

对于非 Rust 标识符或保留字作字段名的情况,可使用带引号的字符串字面量(不能与局部变量简写混用):

span!(Level::TRACE, "api", "guid:x-request-id" = "abcdef", "type" = "request");

常量表达式也可以作为字段名,需用花括号包裹以表示取常量的值作为字段名:

const RESOURCE_NAME: &str = "foo"; // 该 span 将包含字段 `foo = "some_id"` span!(Level::TRACE, "get", { RESOURCE_NAME } = "some_id");

?与%符号:Debug / Display 记录

?是简写,指定字段使用其fmt::Debug实现记录;%类似,但使用fmt::Display:

#[derive(Debug)] struct MyStruct { field: &'static str } let my_struct = MyStruct { field: "Hello world!" }; // `my_struct` 将使用其 `fmt::Debug` 实现记录。 event!(Level::TRACE, greeting = ?my_struct); // 等价于: event!(Level::TRACE, greeting = tracing::field::debug(&my_struct)); // `my_struct.field` 将使用其 `fmt::Display` 实现记录。 event!(Level::TRACE, greeting = %my_struct.field); // 等价于: event!(Level::TRACE, greeting = tracing::field::display(&my_struct.field));

%与?也可以与局部变量简写配合使用:event!(Level::TRACE, %my_struct.field);。

延迟记录:field::Empty

Span 可以声明值为特殊值 [Empty] 的字段,表示该值当前尚不存在、但稍后可能被记录:

use tracing::{trace_span, field}; // 创建包含两个字段的 span:`greeting`(值为 "hello world")与 `parting`(暂无值)。 let span = trace_span!("my_span", greeting = "hello world", parting = field::Empty); // ... // 现在也为 parting 记录一个值。 span.record("parting", &"goodbye world!");

人类可读消息:message字段

Event 还可以在键值字段之后附带人类可读的格式字符串(及其可选参数)。若提供了格式串与参数,它们会隐式创建一个名为message的字段:

let question = "the ultimate question of life, the universe, and everything"; let answer = 42; // 记录一个包含以下字段的事件: // - `question.answer`,值为 42 // - `question.tricky`,值为 true // - "message",值为 "the answer to the ultimate question of life, the universe, and everything is 42." event!( Level::DEBUG, question.answer = answer, question.tricky = true, "the answer to {} is {}.", question, answer );

值得注意的是,以这种方式指定格式化消息默认不会产生堆分配。

预设级别的简写宏

tracing还提供一系列预设级别的宏:trace!、debug!、info!、warn!、error!与event!行为一致但已指定Level;对应的trace_span!、debug_span!、info_span!、warn_span!、error_span!则对应span!。这些宏既作为简写,也用于与logcrate 兼容(见下文)。

在库中插桩:只依赖 tracing,不安装 Subscriber

库只应依赖tracingcrate,使用其提供的宏与类型,采集对下游消费者可能有用的信息:

use std::{error::Error, io}; use tracing::{debug, error, info, span, warn, Level}; // `#[tracing::instrument]` 属性在每次调用该函数时创建并进入一个 span。 // span 以函数/方法命名,传入的参数会被记录为字段。 #[tracing::instrument] pub fn shave(yak: usize) -> Result<(), Box<dyn Error + 'static>> { // 创建一个 DEBUG 级别、带两个字段的事件: // - `excitement`,键为 "excitement",值为 "yay!" // - `message`,键为 "message",值为 "hello! I'm gonna shave a yak." // // 与其他字段不同,`message` 的简写初始化直接就是字符串本身。 debug!(excitement = "yay!", "hello! I'm gonna shave a yak."); if yak == 3 { warn!("could not locate yak!"); // 注意:这里仅用于演示 `tracing` 的功能,而非惯用的错误处理方式! // 在库或应用中,应考虑返回专门的 `YakError`,snafu 或 thiserror 可简化此过程。 return Err(io::Error::new(io::ErrorKind::Other, "shaving yak failed!").into()); } else { debug!("yak shaved successfully"); } Ok(()) } pub fn shave_all(yaks: usize) -> usize { // 构造名为 "shaving_yaks"、TRACE 级别、带键 "yaks" 字段的新 span。 // 等价于写作: // let span = span!(Level::TRACE, "shaving_yaks", yaks = yaks); // // 局部变量(`yaks`)可以不赋值直接作为字段值,类似结构体初始化。 let span = span!(Level::TRACE, "shaving_yaks", yaks); let _enter = span.enter(); info!("shaving yaks"); let mut yaks_shaved = 0; for yak in 1..=yaks { let res = shave(yak); debug!(yak, shaved = res.is_ok()); if let Err(ref error) = res { // 与 span 一样,事件也可以使用字段初始化简写。 // 此处 `yak` 是被初始化的字段。 error!(yak, error = error.as_ref(), "failed to shave yak!"); } else { yaks_shaved += 1; } debug!(yaks_shaved); } yaks_shaved }

对应依赖只需要:

[dependencies] tracing = "0.1"

重要警告:库绝不应当通过调用 [set_global_default()] 之类的方法安装 subscriber,否则当可执行程序稍后尝试设置默认值时会引发冲突。库只应记录数据,由最终的可执行程序决定收集方式。库内tracing/src/lib.rs的文档也以醒目警告重申了这一点(tracing/src/lib.rs)。

#[instrument]属性由 tracing-attributes crate 实现(对应attributes特性,默认开启)。从 tracing-attributes/src/expand.rs 的实现看,span 名称默认取被插桩函数名,但可用#[instrument(name = "...")]覆盖;同时支持level、target、fields、skip/skip_all等参数,参数默认以Debug记录为字段。完整可运行示例见 examples/examples/fmt/yak_shave.rs。

异步代码中的追踪

追踪async fn的首选方式是使用#[instrument]属性:

use tracing::{info, instrument}; use tokio::{io::AsyncWriteExt, net::TcpStream}; use std::io; #[instrument] async fn write(stream: &mut TcpStream) -> io::Result<usize> { let result = stream.write(b"hello world\n").await; info!("wrote to stream; success={:?}", result.is_ok()); result }

仓库中的 examples/examples/async-fn.rs 给出了完整的可运行版本(基于 tokio 的hello_world示例):用ncat -l 6142监听端口,再运行cargo run --example async-fn即可观察#[instrument]为connect、write两个异步函数生成的 span 与字段。

为什么span.enter()不能跨 await?

对于使用std::future::Future或async/await代码块的通用场景需要特殊处理,因为下面这种写法不会按预期工作:

async { let _s = span.enter(); // ... }

span guard_s在async代码块生成的 future 完成之前不会退出。由于 future 和 span 可以在未完成的情况下被多次进入和退出,该 span 会在 future 存在的整个期间保持进入状态,而不是仅在它被 poll 时进入,导致非常混乱和错误的输出。tracing的文档(tracing/src/lib.rs)同样对异步代码中跨 await 持有 guard 的情况给出了明确警告。

正确做法:Future::instrument

这个问题可以通过Future::instrument组合子解决:

use tracing::Instrument; let my_future = async { // ... }; my_future .instrument(tracing::info_span!("my_future")) .await

Future::instrument把 span 附着到 future 上,保证 span 的生命周期与 future 的生命周期一致。其实现见 tracing/src/instrument.rs:instrument返回Instrumented包装类型,每次该 future 被 poll 或 drop 时都会进入附着的 span。

在底层,#[instrument]宏执行的正是Future::instrument所做的显式 span 附着。同模块还提供了in_current_span()(用当前 span 附着 future,便于在派生新任务时传播上下文,tracing/src/instrument.rs)以及Span::or_current()组合子(若传给instrument的 span 被禁用,则退而使用当前 span,保证任务始终有 span 上下文)。

与logcrate 的互操作

logcrate 提供了 Rust 简单轻量的日志门面。虽然tracing在log的基础之上提供了更丰富的结构化诊断数据,但log的简洁与普及使其成为 Rust 文本日志的"最低公分母"——绝大多数 Rust 库与应用都在发射或消费log记录。因此tracing提供了双向互操作:

发射log记录:tracing提供log与log-always两个特性。启用log特性后,若没有活跃的tracingSubscriber,调用事件宏或创建带字段的 span 会额外发射一条log记录——这主要面向"希望诊断既能被tracing应用消费、也能被log应用消费"的库,避免两种形式都发射带来的额外开销。启用log-always后,即使已设置tracingSubscriber 也会发射log记录,适用于"用loglogger 记录文本日志、用tracing记录指标/剖析/分布式追踪等其他诊断"的应用;库一般不应启用log-always,否则应用将无法选择关闭这些log记录。生成的log记录消息是 span/event 字段的字符串表示,target、级别、模块路径、文件与行号等元数据也会填充;span 的进入、退出、关闭生命周期日志统一在Trace级别、归入独立的tracing::span(及子 targettracing::span::active)target,可与其他log记录分别开关。

消费log记录:tracing-logcrate 提供兼容层,使tracingSubscriber 能把log记录当作tracingEvent 消费,让使用tracing的应用把依赖库(使用log)的日志记录进应用的 trace 树上下文中。若使用tracing-subscriber的FmtSubscriber,则无需直接依赖tracing-log(tracing-subscriber已通过其tracing-log特性集成了LogTracer)。

支持的 Rust 版本

tracing基于最新稳定版构建,最低支持版本为Rust 1.65,不保证在更早版本上构建(见 tracing/Cargo.toml 中rust-version = "1.65.0"与 README.md 的 "Supported Rust Versions" 一节)。tracing遵循 Tokio 项目的编译器支持策略:始终支持当前稳定编译器及其之前三个最近的次版本;在该策略范围内提高最低编译器版本不被视为 semver 破坏性变更。

特性开关(Feature Flags)

tracingcrate 提供如下特性(详见 tracing/src/lib.rs 与 tracing/Cargo.toml):

  • 静态级别控制:max_level_off、max_level_error、max_level_warn、max_level_info、max_level_debug、max_level_trace,以及对应的release_max_level_*系列,可在编译期将超过某级别的插桩点完全剔除;
  • log:未设置默认tracingsubscriber 时,trace 插桩点同时发射log记录;
  • log-always:即使已设置 subscriber 也发射log记录;
  • attributes:支持#[instrument]属性(默认开启;会引入syn依赖,可能增加未使用它的 crate 的编译时间);
  • std:依赖 Rust 标准库(默认开启)。no_std用户可用default-features = false关闭:
    [dependencies] tracing = { version = "0.1.38", default-features = false }

    注意no_std支持要求liballoc。

另有不稳定特性valuable(支持使用valuablecrate 记录字段值),需要传入--cfg tracing_unstable启用:

RUSTFLAGS="--cfg tracing_unstable" cargo build

或在项目.cargo/config中写入[build] rustflags = ["--cfg", "tracing_unstable"]。

仓库结构与生态

仓库以tracingcrate 为主体:它包含主要的插桩API,用于在库与应用中打点发射 trace 数据;tracing-core包含其余 crate 所基于的核心API 原语,trace subscriber 的作者可以依赖tracing-core(其稳定性保证更高)。此外,仓库还包含一系列兼容与工具库,其中部分处于预发布状态、稳定性低于tracing与tracing-core:

  • tracing-futures:为 futures 提供插桩工具;
  • tracing-macros:发射 trace 事件的实验性宏(不稳定);
  • tracing-attributes:自动插桩函数的 proc-macro 属性;
  • tracing-log:与logcrate 的兼容层(不稳定);
  • tracing-serde:用 serde 序列化 trace 数据的兼容层(不稳定);
  • tracing-subscriber:Subscriber 实现,以及实现与组合 Subscriber 的工具;
  • tracing-tower:与 tower 生态的兼容(不稳定);
  • tracing-appender:输出 tracing 数据的工具,含文件 appender 与非阻塞 writer;
  • tracing-error:提供SpanTrace,用 tracing span 插桩错误;
  • tracing-flame:基于 span 进入/退出事件生成火焰图的 Layer;
  • tracing-journald:把事件记录到 Linuxjournald服务并保留结构化数据的 Layer。

除此之外,仓库 README 还收录了大量由第三方维护的生态 crate(不在本仓库内),例如基于tracing实现事件间计时指标的tracing-timing、面向 OpenTelemetry 的发射层、面向 Sentry/Grafana Loki/Elastic APM/Windows ETW 等后端的 Layer、面向 actix-web / axum / reqwest / tide 等框架的集成、浏览器端console.*与 User Timing API 的 wasm 实现、测试辅助的test-log、以及tracing-chrome(导出可在chrome://tracing查看的数据)等。其中部分生态 crate 尚未发布或处于活跃开发中,稳定性可能低于tracing与tracing-core。这些第三方 crate 的功能与场景覆盖了指标、分布式追踪、火焰图、日志平台对接等方向,可作为扩展选型的参考。

获取帮助与参与贡献

根据 README 的指引:遇到问题时,首先查阅各 crate 的 API 文档;若未找到答案,可以前往 Tracing 项目的 Discord 频道询问;最后还可以通过提交 issue 提问。项目提供了 CONTRIBUTING.md 贡献指南帮助新贡献者参与。Tracing 项目以 MIT 许可证 发布;除非另行明确声明,你有意为 Tracing 提交的任何贡献均按 MIT 许可(无附加条款)授权。

提示:README 的 "External Resources" 一节还整理了关于 Tracing 的博客文章与会议演讲清单(如 Tokio 博客的 "Diagnostics with Tracing"、RustConf 2019 演讲、Jon Gjengset 的 "Decrusting the tracing crate" 等),并欢迎通过 pull request 扩充该列表,适合作为进一步学习的资料索引。

小结

tracing把"日志"提升为"结构化、带时序与因果关系的诊断数据":用Span描述执行区间、用Event描述瞬间、用Subscriber负责收集过滤;宏语法让字段记录、延迟填充、Debug/Display 格式化等操作简洁而低开销。应用侧通过tracing-subscriber的fmt::init()/with_default安装收集器(默认读取RUST_LOG),库侧只打点不安装,异步代码则使用#[instrument]或Future::instrument正确绑定 span 生命周期。无论你的目标是可读的终端日志、JSON 结构化输出,还是接入 OpenTelemetry、Loki、Sentry 等外部系统,tracing都提供了统一的插桩基础。

  • 可观测性
  • 开发工具

【免费下载链接】tracing

Application level tracing for Rust.

项目地址:https://gitcode.com/gh_mirrors/tr/tracing
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

Windows HDR开启后画面发灰?从硬件到设置的完整排查调优指南

先从大家最常问的一个问题说起&#xff1a;明明自己的显示器支持HDR&#xff0c;系统设置里也把“使用HDR”打开了&#xff0c;为什么画面反而变得灰蒙蒙的&#xff0c;颜色又淡又白&#xff0c;甚至还不如没开之前舒服&#xff1f;很多人的第一反应是显示器坏了&#xff0c;或…

作者头像 李华
网站建设 2026/10/15 17:09:39

NRG355MC蓝牙例程开发实战:编译、广播、GATT与低功耗避坑

简介&#xff1a;ST公司BlueNRG-355MC蓝牙SoC的完整开发例程包&#xff0c;面向低功耗蓝牙与物联网开发者&#xff0c;解决BLE设备从固件移植到协议栈二次开发的核心难题。例程覆盖芯片初始化、射频参数配置、BLE协议栈分层调用、连接管理与GATT数据交换&#xff0c;并针对休眠…

作者头像 李华
网站建设 2026/10/15 17:09:27

基于STM32 Nucleo的压电MIDI鼓模块制作指南

简介&#xff1a;基于STM32 Nucleo的MIDI鼓模块工程&#xff0c;面向嵌入式开发者与电子音乐DIY爱好者&#xff0c;完整实现了18路输入信号的鼓触发与MIDI输出。项目基于STM32 G431核板&#xff0c;采用CubeIDE v1.6.0开发&#xff0c;将PA11/PA12的USB端口用作MIDI输出&#x…

作者头像 李华
网站建设 2026/10/15 17:08:30

从零复现 style_transformer:PyTorch 实现风格转换模型的完整项目实例

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 17:08:25

什么是 AI 工作流

AI 工作流自动化 大家应该都碰到过这样的经历&#xff1a; 每天早上到公司&#xff0c;先花半小时看邮件&#xff0c;筛选出重要的&#xff0c;然后一一回复。 收到客户反馈后&#xff0c;要手动整理、分类&#xff0c;再发给相应的负责人。 社交媒体要定期更新&#xff0c;…

作者头像 李华
网站建设 2026/10/15 17:07:59

java八股面试(3)

1.什么是RPC和HTTP&#xff0c;有什么区别一、 知识点讲解RPC 的全称是 Remote Procedure Call&#xff08;远程过程调用&#xff09;。它的核心设计目标是&#xff1a;让程序员像调用本地函数一样&#xff0c;去调用远程服务器上的函数。你写代码时是 result service.add(a, …

作者头像 李华