Protocol Buffers在MCU上的nanopb介绍及使用详解
在嵌入式系统和资源受限的环境中,传统的Protocol Buffers 可能显得过于庞大。因此,nanopb 应运而生,它是一个轻量级的 Protocol Buffers 生成器,专为嵌入式系统设计c语言设计。本文将介绍如何安装和使用 nanopb,以及通过一个简单的例子来展示它的基本用法。
网上的大多数文章都是只讲如何使用。其实新手刚拿到后,很重要的一点是如何用起来?如何安装环境?protoc工具在哪里搞到?这里从环境介绍到详细使用做个总结,留作备忘。
什么是Protocol Buffers
Protocol Buffer是谷歌推出的,和开发语言无关、平台无关、可扩展的机制,用于序列化结构化数据——像XML,但它更小、更快、更简单。
关键特点:
1、跨平台。
2、可扩展。
3、序列化结构化数据。
4、使用简单。
官方网址:https://developers.google.com/protocol-buffers
支持最常使用的语言
1、C++
2、C#
3、Java
4、Python
什么是 nanopb
nanopb 是一个非常轻量级的 C 库,用于 Protocol Buffers 的序列化和反序列化。它专为嵌入式系统设计,可以运行在内存和存储空间有限的环境中。nanopb 支持 Protocol Buffers 2.3 和 3.0 版本的标准,因此可以用于大多数现有的 Protocol Buffers 定义文件。
仓库地址:https://gitcode.com/gh_mirrors/na/nanopb
github仓:https://github.com/nanopb/nanopb
安装 nanopb
这个很重要。很多人拿到仓库后,苦于找不到protoc工具,不知道如何生成pb.c文件。其实原因就出在这里,没有安装环境。而官方在这里的介绍简单了些。nanopb 的安装可以通过 pip3 和从源码编译两种方式进行。本文推荐使用 pip3 安装,因为它更加简便快捷。
首先,确保你的开发环境中已经安装了 Python3 和 pip3。如果还没有安装,可以通过以下命令安装:
sudo apt-get update
sudo apt-get install python3 python3-pip
接下来,使用 pip3 安装 nanopb 的相关工具。这里我们使用阿里云的镜像源来加速安装:
pip3 install protobuf grpcio-tools -i https://mirrors.aliyun.com/pypi/simple/
安装完成后,你需要确保 nanopb 的生成器工具 protoc
和 nanopb_generator
可用。上面安装完依赖后,这两个工具自然可用。可以将/root/test/c/nanopb/generator/加入搭排环境变量里,linux下方便使用protoc。 你可以通过以下命令来生成 .pb.c
和 .pb.h
文件:
../../generator/protoc --nanopb_out=. simple.proto
# 或者
nanopb_generator simple.proto
这里 simple.proto
是你的 Protocol Buffers 定义文件。生成器会根据这个文件生成对应的 C 代码文件。
使用 nanopb
要使用 Nanopb 库,您需要执行以下两个步骤:
- 使用 protoc 编译您的 .proto 文件以生成适用于 Nanopb 的文件。
- 在项目中包含 pb_encode.c、pb_decode.c 和 pb_common.c。
开始学习的最佳方式是研究 “examples/simple” 目录中的示例项目。它包含了一个 Makefile,在大多数 Linux 系统上可以直接工作。然而,对于其他类型的构建系统,可以参考该目录下 README.txt 中的手动步骤。
下面,我们将通过一个简单的例子来展示如何使用 nanopb。
假设我们有一个简单的 Protocol Buffers 定义文件 simple.proto
,
内容如下:
syntax = "proto2";message SimpleMessage {required int32 lucky_number = 1;
}
这个定义文件中只包含一个消息定义 SimpleMessage
,它有一个 int32
类型的字段 lucky_number
。
接下来,我们将编写 C 代码来处理这个消息。示例代码如下:
#include <stdio.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "simple.pb.h"int main()
{/* This is the buffer where we will store our message. */uint8_t buffer[128];size_t message_length;bool status;/* Encode our message */{/* Allocate space on the stack to store the message data.** Nanopb generates simple struct definitions for all the messages.* - check out the contents of simple.pb.h!* It is a good idea to always initialize your structures* so that you do not have garbage data from RAM in there.*/SimpleMessage message = SimpleMessage_init_zero;/* Create a stream that will write to our buffer. */pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));/* Fill in the lucky number */message.lucky_number = 13;/* Now we are ready to encode the message! */status = pb_encode(&stream, SimpleMessage_fields, &message);message_length = stream.bytes_written;/* Then just check for any errors.. */if (!status){printf("Encoding failed: %s\n", PB_GET_ERROR(&stream));return 1;}}/* Now we could transmit the message over network, store it in a file or* wrap it to a pigeon's leg.*//* But because we are lazy, we will just decode it immediately. */{/* Allocate space for the decoded message. */SimpleMessage message = SimpleMessage_init_zero;/* Create a stream that reads from the buffer. */pb_istream_t stream = pb_istream_from_buffer(buffer, message_length);/* Now we are ready to decode the message. */status = pb_decode(&stream, SimpleMessage_fields, &message);/* Check for errors... */if (!status){printf("Decoding failed: %s\n", PB_GET_ERROR(&stream));return 1;}/* Print the data contained in the message. */printf("Your lucky number was %d!\n", (int)message.lucky_number);}return 0;
}
这段代码首先初始化了一个 SimpleMessage
结构体,然后使用 pb_encode
函数将这个结构体编码到一个字节缓冲区中。接着,它又将这个缓冲区中的数据解码回一个 SimpleMessage
结构体,并输出其中的 lucky_number
字段。
Makefile 文件
为了简化编译过程,我们可以使用 Makefile 文件来管理编译规则。以下是示例 Makefile 文件的内容:
# Include the nanopb provided Makefile rules
include ../../extra/nanopb.mk# Compiler flags to enable all warnings & debug info
CFLAGS = -Wall -Werror -g -O0
CFLAGS += -I$(NANOPB_DIR)# C source code files that are required
CSRC = simple.c # The main program
CSRC += simple.pb.c # The compiled protocol definition
CSRC += $(NANOPB_DIR)/pb_encode.c # The nanopb encoder
CSRC += $(NANOPB_DIR)/pb_decode.c # The nanopb decoder
CSRC += $(NANOPB_DIR)/pb_common.c # The nanopb common parts# Build rule for the main program
simple: $(CSRC)$(CC) $(CFLAGS) -osimple $(CSRC)# Build rule for the protocol
simple.pb.c: simple.proto$(PROTOC) $(PROTOC_OPTS) --nanopb_out=. $<
这个 Makefile 文件首先包含了 nanopb 提供的 Makefile 规则 nanopb.mk
。然后,它定义了一些编译器标志 CFLAGS
,用于在编译过程中启用所有警告并包含调试信息。CSRC
变量列出了所有需要编译的 C 源代码文件,包括主程序文件 simple.c
、编译后的 Protocol Buffers 定义文件 simple.pb.c
以及 nanopb 库的核心文件。
最后,Makefile 文件定义了生成最终可执行文件 simple
的规则,以及从 Protocol Buffers 定义文件 simple.proto
生成 C 源代码文件 simple.pb.c
的规则。
nanopb.mk 文件
nanopb.mk
文件包含了 nanopb 提供的 Makefile 规则,用于生成 .pb.c
和 .pb.h
文件以及定义 nanopb 库的核心文件路径。以下是 nanopb.mk
文件的内容:
# This is an include file for Makefiles. It provides rules for building
# .pb.c and .pb.h files out of .proto, as well the path to nanopb core.# Path to the nanopb root directory
NANOPB_DIR := $(patsubst %/,%,$(dir $(patsubst %/,%,$(dir $(lastword $(MAKEFILE_LIST)))))))# Files for the nanopb core
NANOPB_CORE = $(NANOPB_DIR)/pb_encode.c $(NANOPB_DIR)/pb_decode.c $(NANOPB_DIR)/pb_common.c# Check if we are running on Windows
ifdef windir
WINDOWS = 1
endif
ifdef WINDIR
WINDOWS = 1
endif# Check whether to use binary version of nanopb_generator or the
# system-supplied python interpreter.
ifneq "$(wildcard $(NANOPB_DIR)/generator-bin)" ""# Binary packagePROTOC = $(NANOPB_DIR)/generator-bin/protocPROTOC_OPTS =
else# Source only or git checkoutPROTOC_OPTS =ifdef WINDOWSPROTOC = python $(NANOPB_DIR)/generator/protocelsePROTOC = $(NANOPB_DIR)/generator/protocendif
endif# Rule for building .pb.c and .pb.h
%.pb.c %.pb.h: %.proto %.options$(PROTOC) $(PROTOC_OPTS) --nanopb_out=. $<%.pb.c %.pb.h: %.proto$(PROTOC) $(PROTOC_OPTS) --nanopb_out=. $<
这个文件首先定义了 nanopb 的根目录路径 NANOPB_DIR
,然后列出了 nanopb 库的核心文件路径 NANOPB_CORE
。接着,它检查当前操作系统是否为 Windows,并根据操作系统的不同来设置 PROTOC
变量,以指向正确的 protoc
生成器工具。最后,它定义了生成 .pb.c
和 .pb.h
文件的规则。
稍复杂的使用示例
接下来做一个稍复杂的使用,proto文件定义了两个message:KeyValue和DeviceConfig。其中,DeviceConfig包含一些基本类型的字段和一个重复的KeyValue字段。生成的nanopb头文件中对于字符串和重复字段,使用了pb_callback_t类型,这意味着这些字段需要通过回调函数来处理,或者在生成时可能没有设置相应的选项来优化为静态数组。
假如有以下proto文件定义:
syntax = "proto2";package example;message KeyValue {required string key = 1;required int32 value = 2;
}message DeviceConfig {required int32 BrdNum = 1;required int32 address = 2;required string type = 3;required int32 priority = 4;required int32 accessTime = 5;required string brdLocation = 6;repeated KeyValue bitMean = 7;
}
编码(序列化)的过程大致是:初始化消息结构体,填充数据,然后调用pb_encode函数。对于回调处理的字段(如字符串和重复字段),需要设置回调函数或者在结构体中正确填充数据。例如,pb_callback_t类型的字段需要用户提供函数来处理数据的编码,或者在结构体中直接设置对应的数据指针和大小。
在提供的DeviceConfig结构体中,type、brdLocation和bitMean都是回调类型。对于字符串字段,通常可以使用pb_callback_t的简单方式,例如直接指定字符串的指针和长度,或者设置一个encode函数。而bitMean是repeated的KeyValue,需要处理为重复字段,可能使用多次回调或者预分配数组。但根据生成的代码,这里可能还是需要使用回调来处理每个条目。
因此在以下代码中,处理这些回调字段是关键。对于编码,需要为每个pb_callback_t字段设置对应的函数,或者直接填充数据。例如,对于字符串字段,可能可以使用pb_ostream_from_buffer来创建一个输出流,然后使用pb_encode函数来编码数据。
以下为编码和解码的使用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pb_encode.h>
#include <pb_decode.h>
#include "simple.pb.h"// 编码回调 ---------------------------------------------------
bool encode_string(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) {const char *str = *(const char **)*arg;if (!pb_encode_tag_for_field(stream, field))return false;return pb_encode_string(stream, (uint8_t*)str, strlen(str));
}bool encode_bitMean(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) {const example_KeyValue *items = (const example_KeyValue *)*arg;for (size_t i = 0; i < 2; i++) {if (!pb_encode_tag_for_field(stream, field))return false;if (!pb_encode_submessage(stream, example_KeyValue_fields, &items[i]))return false;}return true;
}// 解码回调 ---------------------------------------------------
typedef struct {char *key;int32_t value;
} KeyValueData;bool decode_string(pb_istream_t *stream, const pb_field_t *field, void **arg) {char **strptr = (char **)*arg;size_t len = stream->bytes_left;*strptr = malloc(len + 1);if (!pb_read(stream, (uint8_t*)*strptr, len)) {free(*strptr);return false;}(*strptr)[len] = '\0';return true;
}bool decode_bitMean(pb_istream_t *stream, const pb_field_t *field, void **arg) {KeyValueData **items = (KeyValueData **)*arg;size_t count = (*items == NULL) ? 0 : (*items)[0].value;// 扩展数组空间(使用[0]存储计数)KeyValueData *new_items = realloc(*items, (count + 1 + 1) * sizeof(KeyValueData));if (!new_items) return false;*items = new_items;(*items)[0].value = count + 1; // 更新计数KeyValueData *item = &(*items)[count + 1]; // 数据从[1]开始item->key = NULL;example_KeyValue kv = example_KeyValue_init_zero;kv.key.arg = &item->key;kv.key.funcs.decode = decode_string;if (!pb_decode(stream, example_KeyValue_fields, &kv)) {return false;}item->value = kv.value;return true;
}int main() {/**************** 编码阶段 ****************/example_DeviceConfig encode_config = example_DeviceConfig_init_default;uint8_t buffer[256];// 配置编码参数encode_config.BrdNum = 123;encode_config.address = 456;encode_config.priority = 1;encode_config.accessTime = 999;const char *type_str = "temperature";encode_config.type.arg = &type_str;encode_config.type.funcs.encode = encode_string;const char *loc_str = "Room 101";encode_config.brdLocation.arg = &loc_str;encode_config.brdLocation.funcs.encode = encode_string;example_KeyValue bit_means[2] = {{.key.arg = (void*)&(const char*[]){"status"}, .key.funcs.encode = encode_string, .value = 100},{.key.arg = (void*)&(const char*[]){"error"}, .key.funcs.encode = encode_string, .value = 0}};encode_config.bitMean.arg = bit_means;encode_config.bitMean.funcs.encode = encode_bitMean;pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));if (!pb_encode(&ostream, example_DeviceConfig_fields, &encode_config)) {fprintf(stderr, "Encode error: %s\n", PB_GET_ERROR(&ostream));return 1;}/**************** 解码阶段 ****************/example_DeviceConfig decode_config = example_DeviceConfig_init_zero;KeyValueData *bit_means_decoded = NULL;char *decoded_type = NULL;char *decoded_loc = NULL;// 配置解码回调decode_config.type.arg = &decoded_type;decode_config.type.funcs.decode = decode_string;decode_config.brdLocation.arg = &decoded_loc;decode_config.brdLocation.funcs.decode = decode_string;decode_config.bitMean.arg = &bit_means_decoded;decode_config.bitMean.funcs.decode = decode_bitMean;pb_istream_t istream = pb_istream_from_buffer(buffer, ostream.bytes_written); // 关键修正点if (!pb_decode(&istream, example_DeviceConfig_fields, &decode_config)) {fprintf(stderr, "Decode error: %s\n", PB_GET_ERROR(&istream));free(decoded_type);free(decoded_loc);if (bit_means_decoded) {for (size_t i = 1; i <= bit_means_decoded[0].value; i++)free(bit_means_decoded[i].key);free(bit_means_decoded);}return 1;}/**************** 输出结果 ****************/printf("Decoded config:\n""Type: %s\n""Location: %s\n""BitMeans count: %d\n",decoded_type, decoded_loc, bit_means_decoded ? bit_means_decoded[0].value : 0);if (bit_means_decoded) {for (int i = 1; i <= bit_means_decoded[0].value; i++) {printf(" [%d] %s = %d\n", i, bit_means_decoded[i].key, bit_means_decoded[i].value);}}/**************** 清理资源 ****************/free(decoded_type);free(decoded_loc);if (bit_means_decoded) {for (int i = 1; i <= bit_means_decoded[0].value; i++) {free(bit_means_decoded[i].key);}free(bit_means_decoded);}return 0;
}
优化使用示例
上述示例,使用了回调和动态内存分配,稍显复杂,改为以下方式则代码更简洁、内存管理更安全。
以下是通过添加nanopb选项优化proto定义后的使用示例:
1. 修改后的proto文件
syntax = "proto2";
import "nanopb.proto"; // 需要nanopb库中的选项定义package example;message KeyValue {required string key = 1 [(nanopb).max_size = 64]; // 限制key最大64字节required int32 value = 2;
}message DeviceConfig {required int32 BrdNum = 1;required int32 address = 2;required string type = 3 [(nanopb).max_size = 64]; // 限制type长度required int32 priority = 4;required int32 accessTime = 5;required string brdLocation = 6 [(nanopb).max_size = 128]; // 限制位置字符串repeated KeyValue bitMean = 7 [(nanopb).max_count = 10]; // 最多10个元素
}
2. 生成新的头文件
使用nanopb生成器命令:
protoc --nanopb_out=. simple.proto
3. 优化后的使用示例
#include "simple.pb.h"
#include <pb_encode.h>
#include <pb_decode.h>
#include <stdio.h>
#include <string.h>int main() {/**************** 编码示例 ****************/example_DeviceConfig config = example_DeviceConfig_init_default;// 直接赋值字符串(不再需要回调)strncpy(config.type, "temperature", sizeof(config.type));strncpy(config.brdLocation, "Room 101", sizeof(config.brdLocation));// 设置基本数值字段config.BrdNum = 123;config.address = 456;config.priority = 1;config.accessTime = 999;// 填充重复字段config.bitMean_count = 2; // 自动生成的数组长度字段strncpy(config.bitMean[0].key, "status", sizeof(config.bitMean[0].key));config.bitMean[0].value = 100;strncpy(config.bitMean[1].key, "error", sizeof(config.bitMean[1].key));config.bitMean[1].value = 0;// 编码缓冲区uint8_t buffer[256];pb_ostream_t ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));if (!pb_encode(&ostream, example_DeviceConfig_fields, &config)) {fprintf(stderr, "Encode failed: %s\n", PB_GET_ERROR(&ostream));return 1;}/**************** 解码示例 ****************/example_DeviceConfig decoded = example_DeviceConfig_init_default;pb_istream_t istream = pb_istream_from_buffer(buffer, ostream.bytes_written);if (!pb_decode(&istream, example_DeviceConfig_fields, &decoded)) {fprintf(stderr, "Decode failed: %s\n", PB_GET_ERROR(&istream));return 1;}/**************** 输出结果 ****************/printf("Decoded config:\n""Type: %s\n""Location: %s\n""BitMeans count: %d\n",decoded.type,decoded.brdLocation,decoded.bitMean_count);for (int i = 0; i < decoded.bitMean_count; i++) {printf(" [%d] %s = %d\n", i, decoded.bitMean[i].key, decoded.bitMean[i].value);}return 0;
}
优化后代码的主要变化
-
数据结构变化:
// 原始回调方式 pb_callback_t type;// 优化后静态分配 char type[64]; size_t type_size;
-
重复字段处理:
// 原始方式 pb_callback_t bitMean;// 优化后静态数组 example_KeyValue bitMean[10]; size_t bitMean_count;
-
字符串处理:
// 之前需要回调 config.type.funcs.encode = encode_string;// 现在直接操作 strncpy(config.type, "text", sizeof(config.type));
关键优势说明
-
内存管理简化:
- 所有字符串字段变为固定长度char数组
- 重复字段变为固定大小的数组+count计数器
- 不再需要手动malloc/free
-
性能提升:
- 消除回调函数开销
- 数据内存连续,提高缓存命中率
-
代码可读性增强:
- 字段访问方式与常规结构体一致
- 减少约60%的样板代码
注意事项
-
字段长度限制:
- 超出max_size的字符串会被截断
- 超过max_count的数组元素会被丢弃
-
默认值初始化:
example_DeviceConfig_init_default // 会清零所有字段 example_DeviceConfig_init_zero // 同上,两者等效
-
字符串处理建议:
// 使用strncpy防止溢出 strncpy(config.type, src, sizeof(config.type)); config.type[sizeof(config.type)-1] = '\0'; // 确保终止符
-
协议兼容性:
- 修改后的proto仍然与标准protobuf兼容
- nanopb选项仅影响生成代码的实现方式
推荐使用场景
- 嵌入式系统(内存受限环境)
- 需要确定性内存分配的场景
- 对性能要求较高的实时系统
- 希望简化代码逻辑的项目
通过这种优化方式,代码复杂度显著降低,同时保持了协议的高效性。建议根据实际字段的预期最大长度来设置合理的max_size
和max_count
值,在内存使用和灵活性之间取得平衡。
总结
通过本文,我们了解了什么是 nanopb,及其在嵌入式系统中的应用场景。我们还学习了如何安装和使用 nanopb,包括编写 Protocol Buffers 定义文件、C 代码文件以及 Makefile 文件。通过这些步骤,你可以轻松地在你的项目中集成 nanopb,以便更高效地进行消息的序列化和反序列化。
相关文章:
Protocol Buffers在MCU上的nanopb介绍及使用详解
在嵌入式系统和资源受限的环境中,传统的Protocol Buffers 可能显得过于庞大。因此,nanopb 应运而生,它是一个轻量级的 Protocol Buffers 生成器,专为嵌入式系统设计c语言设计。本文将介绍如何安装和使用 nanopb,以及通…...
leetcode日记(74)扰乱字符串
很有难度的一题,一开始真的绕了很多思维上的弯路。 最开始的想法是递归,看到题目的时候想到动态规划但是完全没有思路应该怎么用,结果确实是递归动态规划。 最开始的想法是构建树,每一层包含这一步划分的方法(实际会…...
Blazor-根级别级联值
根级别级联值注册 using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;namespace BlazorApp1 {public class Program{public static async Task Main(string[] args){var builder WebAssemblyHostBuilder.CreateDefault…...
懒加载能够解决Spring循环依赖吗
懒加载本身并不能直接解决 Spring 循环依赖问题,但它可以在一定程度上缓解或绕过循环依赖带来的问题,下面详细分析: 1. 什么是 Spring 循环依赖 循环依赖指的是两个或多个 Bean 之间相互依赖,形成一个闭环。例如,Bea…...
Matlab中使用GUIDE工具开发图形用户界面(GUI)
文章目录 1. 初识GUIDE工具1.1 .m 和 .fig的区别和联系1.2 GUIDE工具的详细介绍1.3 GUI控件的属性1.4 自动生成的 .m 文件1.5 回调函数 2. GUI中常见的函数2.1 get 和 set 函数2.2 handles.Tag2.3 OpeningFcn 和 OutputFcn2.4 Callback2.5 CreateFcn 和 DeleteFcn2.6 ButtonDow…...
[通俗易懂C++]:引用返回和地址返回
在之前的文章中已经提到过,当使用按值传递时会创建参数的一个副本到函数中。对于基本类型(复制成本较低),这是可以的。但对于类类型(如 std::string ),复制通常成本较高。我们可以通过使用(const)引用传递(或按地址传递)来避免进行昂贵的复制。 这篇文章主要介绍一些…...
基于 MySQL 数据库对三级视图(用户视图、DBA视图、内部视图)的详细解释
基于 MySQL 数据库对三级视图(用户视图、DBA视图、内部视图)的详细解释,结合理论与实际操作说明: 一、三级视图核心概念 数据库的三级视图是 ANSI/SPARC 体系结构的核心思想,MySQL 的实现逻辑如下: …...
LLM - Attention Is All You Need 的理解
一:概述 当前主流的序列转换(sequence transduction)模型主要基于复杂的循环神经网络(Recurrent Neural Networks, RNNs)或卷积神经网络(Convolutional Neural Networks, CNNs),这些模型通常包含编码器(encoder)和解码器(decoder)。 性能最优的模型通常通过“ 注意…...
究竟什么是AI提示词?深入解析与实战应用
随着人工智能技术的飞速发展,AI提示词(AI Prompt)逐渐成为自然语言处理(NLP)领域的热门话题。无论是GPT-3、ChatGPT还是其他大型语言模型,提示词都扮演着至关重要的角色。那么,究竟什么是AI提示…...
deep-research开源框架 Agentic Reasoning
Agentic-Reasoning是由牛津大学团队开源的推理框架. 该框架在GPQA博士级科学题库上准确率提升35%,生物学问题得分从62%跃升至79%,显著优于DeepSeek-R1等闭源模型。 特色:Agentic-Reasoning在定义和实现code agent上做的非常出色。可以借鉴。…...
解锁智能变革密码:浙江大学2025年DeepSeek行业应用案例集深度解析
引言:AI技术驱动的时代浪潮 2025年,人工智能技术已从实验室走向千行百业,成为推动社会经济发展的核心引擎。在这一背景下,浙江大学联合DeepSeek团队推出的《2025年DeepSeek行业应用案例集》(以下简称“案例集”&#…...
C# Unity 唐老狮 No.5 模拟面试题
本文章不作任何商业用途 仅作学习与交流 安利唐老狮与其他老师合作的网站,内有大量免费资源和优质付费资源,我入门就是看唐老师的课程 打好坚实的基础非常非常重要: 全部 - 游习堂 - 唐老狮创立的游戏开发在线学习平台 - Powered By EduSoho 如果你发现了文章内特殊的字体格式,…...
《2025软件测试工程师面试》功能测试篇
什么是功能测试? 功能测试是通过验证产品功能是否满足用户需求的过程,主要关注软件的功能是否符合需求规格说明,包括软件的各种功能、特性、性能、安全性和易用性等。 功能测试的流程包括哪些步骤? 需求分析:明确软件需求,确定测试范围。测试计划:制定详细的测试计划,…...
DeepSeek如何快速开发PDF转Word软件
一、引言 如今,在线工具的普及让PDF转Word成为了一个常见需求,常见的PDF转Word工具有收费的WPS,免费的有PDFGear,以及在线工具SmallPDF、iLovePDF、24PDF等。然而,大多数免费在线转换工具存在严重隐私风险——文件需上…...
ROS环境搭建
ROS首次搭建环境 注:以下内容都是在已经安装好ros的情况下如何搭建workplace 一、创建工作空间二、创建ROS包三、注意 注:以下内容都是在已经安装好ros的情况下如何搭建workplace 如果没有安装好,建议鱼香ros一步到位:鱼香ROS 我也是装了好久…...
深入探索DeepSeek开源之旅:开源Week全程解析
摘要 在农历新年刚刚结束之际,DeepSeek以卓越的开源精神,连续六天举办了开源Week活动。这一系列活动不仅展示了DeepSeek在技术领域的活跃度和影响力,还彰显了其对开源社区的贡献。通过这次活动,DeepSeek吸引了众多开发者和技术爱好…...
Redis是什么?如何使用Redis进行缓存操作?
Redis(Remote Dictionary Server)是一款高性能的内存键值存储系统,广泛用于缓存、消息队列、会话存储和实时数据处理等场景。它基于内存存储,支持多种数据结构,如字符串、列表、集合、有序集合和哈希表等,具…...
Unity学习笔记之——ugui的性能优化
在Unity中UI优化的核心问题就是重绘和批处理之间的平衡 一、Canvas优化要点 1.优化原因: (1)Unity为了性能优化,会合并Canvas下的所有元素; (2)如果把所有面板放到一个Canvas下,会…...
【三.大模型实战应用篇】【2.智能学员辅导系统:与大模型的深度交互】
早上七点半,初三学生小林打开数学辅导APP,发现AI老师准确指出了他昨晚作业中三次跳步计算的坏习惯——这比他亲妈观察得还细致。这背后是一场发生在代码深处的"脑力风暴",让我们潜入智能辅导系统与大模型深度交互的"暗室",看看那些让教育产生化学反应的…...
【vue-echarts】——04.配置项---legend
文章目录 一、配置项-legend图例二、显示结果一、配置项-legend图例 图例组件展现了不同系列的标记,颜色和名字。可以通过点击图例控制哪些系列不显示。 代码如下 Demo4View.vue <template><div class="about">...
面试题02.02.返回倒数第k个节点
实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。 注意:本题相对原题稍作改动 示例: 输入: 1->2->3->4->5 和 k 2 输出: 4 说明: 给定的 k 保证是有效的。 题解ÿ…...
剑指 Offer II 041. 滑动窗口的平均值
comments: true edit_url: https://github.com/doocs/leetcode/edit/main/lcof2/%E5%89%91%E6%8C%87%20Offer%20II%20041.%20%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E7%9A%84%E5%B9%B3%E5%9D%87%E5%80%BC/README.md 剑指 Offer II 041. 滑动窗口的平均值 题目描述 给定一个整数…...
OCR PDF 文件是什么?它包含什么内容?
有些 PDF 文件是通过扫描纸质书页生成的,这类文件有其独特的特点。有时,原始书籍是唯一可用的版本,因此只能通过扫描的方式获取内容。 如何识别 OCR PDF 文件? 你通常可以从外观上辨别 OCR PDF 文件——页面上的文本看起来像“锯…...
什么是最终一致性,它对后端系统的意义是什么
最终一致性(Eventual Consistency)是分布式系统中的一种一致性模型。与传统的强一致性模型不同,最终一致性并不要求系统在任何时刻都保持一致,而是保证在足够的时间后,所有节点的数据最终会达到一致的状态。换句话说,系统允许短时间内出现数据的不一致性,但最终会通过某…...
CSS3中布局方式说明
CSS3 提供了多种灵活的布局方式,适用于不同的场景和需求。以下是主要的布局方式及其特点: 1. Flexbox 布局(弹性盒子) 用途:一维布局(水平或垂直方向排列元素)。特点: 通过 display…...
【开源-常用开源c/c++日志管理模块对比】
[TOC](开源-常用开源c/c日志管理模块对比) 项目名称语言优点缺点适用场景开源代码链接spdlogC高性能,支持异步日志;丰富的格式化功能;跨平台;易于集成。依赖C11或更高版本;不适合嵌入式系统。高…...
基于log4j的自定义traceid实现
思路就是spring 做切面拦截请求,切面入口时生成traceId,然后放到MDC里面(就是threadLocal,MDC是log框架提供的工具类,能方便在配置文件里面引用插入的值)。 切面结束时traceId。 import com.sing.monitor…...
如何在网页上显示3D CAD PMI
在现代制造业中,3D CAD模型已成为产品设计和制造的核心。为了更有效地传达设计意图和制造信息,产品和制造信息(PMI)被嵌入到3D模型中。然而,如何在网页上清晰、准确地显示这些3D CAD PMI,成为了一个重要的技…...
LLMR: Real-time Prompting of Interactive Worldsusing Large Language Models
LLMR-使用大型语言模型的交互式世界实时建模 ABSTRACT 我们提出了混合现实的大语言模型(LLMR),一个使用LLM实时创建和修改交互式混合现实体验的框架。LLMR利用新颖的策略来解决理想训练数据稀缺的困难情况,或者设计目标需要综合内…...
使用 OpenLIT 对 LLM 应用进行可观测
大规模语言模型(LLM)的可观测性 随着大规模语言模型(LLM)在各个领域的广泛应用,确保这些模型的稳定性和性能变得至关重要。为了实现这一目标,可观测性(Observability)成为了一个关键…...
C与C++的区别
C 深度剖析:对比 C 语言的显著差异 在编程的浩瀚宇宙中,C 和 C 堪称两颗耀眼的巨星,各自绽放出独一无二的光彩。C 语言作为经典的结构化编程语言,多年来在系统开发、嵌入式编程等领域始终占据着举足轻重的地位。而 C 作为 C 语言…...
【极客时间】浏览器工作原理与实践-2 宏观视角下的浏览器 (6讲) - 2.6 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的?
https://time.geekbang.org/column/article/118826 2.6 渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的? 2.5介绍了渲染流水线中的 DOM 生成、样式计算和布局三个阶段,2.6讲解渲染流水线后面的阶段…...
开放鸿蒙认证,OpenHarmony兼容性认证介绍
Ⅰ、OpenHarmony开放鸿蒙兼容性测试认证:使用官方测试套件,对照PCS自检表中的必测项,在本地搭建的环境中对伙伴设备进行预测,直至取得合格的兼容性测试报告。 注:2025年01月01日起,不支持新产品基于老分支…...
磁盘空间不足|如何安全清理以释放磁盘空间(开源+节流)
背景: 最近往数据库里存的东西有点多,磁盘不够用 查看磁盘使用情况 df -h /dev/sda5(根目录 /) 已使用 92% 咱们来开源节流 目录 背景: 一、开源 二、节流 1.查找 大于 500MB 的文件: 1. Snap 缓存…...
【2】好未来JAVA开发工程师部分笔试题解析
编程题 1.降序的子数组最大元素和 给你一个正整数组成的数组nums,返回nums中一个降序子数组的最大可能元素和。 子数组是数组中的一个连续数字序列。 已知子数组[nums l, nums l1, … , nums r-1, nums r],若对所有l (1<i<r),nums …...
LeetCode 21. 合并两个有序链表(Python)
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 输入:l1 [1,2,4], l2 [1,3,4] 输出:[1,1,2,3,4,4] 示例 2: 输入:l1 [], l2 [] 输出:[] 示例 3: 输…...
Y3学习打卡
网络结构图 YOLOv5配置了4种不同大小的网络模型,分别是YOLOv5s、YOLOv5m、YOLOv5l、YOLOv5x,其中 YOLOv5s 是网络深度和宽度最小但检测速度最快的模型,其他3种模型都是在YOLOv5s的基础上不断加深、加宽网络使得网络规模扩大,在增强…...
2025-3-3 二叉树的存储结构
一、二叉树的存储结构( 顺序存储,链式存储) 1.顺序存数--(用数组) (完全二叉树)常考的基本操作: i 的左孩子 -----2i 右孩子-----2i1 i的父节点-----[i/2] 向下取整 i所在的层…...
RK3588V2--ES8388声卡适配记录
RK3588V2--ES8388声卡适配记录 1. ES8388声卡简单介绍2. 适配过程2.1 设备树配置 3. 问题分析与解决3.1 现声卡平台设备, 最终注册失败3.2 系统查看 I2C 设备3.3 怀疑是没有上拉电阻? 4. 测试阶段4.1 耳机接口测试--OK4.2 MIC测试--无法使用 5. 分析总结 1. ES8388声…...
【http://noi.openjudge.cn/】4.3算法之图论——1538:Gopher II
[【http://noi.openjudge.cn/】4.3算法之图论——1538:Gopher II] 题目 查看提交统计提问 总时间限制: 2000ms 内存限制: 65536kB 描述 The gopher family, having averted the canine threat, must face a new predator. The are n gophers and m gopher holes, each at di…...
PySide(PyQT)的视图(QGraphicsView)范例(一) 基本框架
最近学习了视图(QGraphicsView)的知识,总结一下,做一个demo以备忘。在demo中使用了场景(QGraphicsScene)、矩形框(QGraphicsRectItem)等构件,以及演示了常用的设置方法和…...
opencv 模板匹配方法汇总
在OpenCV中,模板匹配是一种在较大图像中查找特定模板图像位置的技术。OpenCV提供了多种模板匹配方法,通过cv2.matchTemplate函数实现,该函数支持的匹配方式主要有以下6种,下面详细介绍每种方法的原理、特点和适用场景。 1. cv2.T…...
_mm_shuffle_epi32解析
一 概述 _mm_shuffle_epi32和_MMSHUFFLE是与SSE指令集相关的开发工具,主要用于SIMD向量操作。 二 _mm_shuffle_epi32 函数 功能:对128位整数向量(__m128i)中的四个32位整数进行重排序 原型:__m128i _mm_shuffle_epi32 (__m128i a, int imm…...
Tauri+React+Ant Design跨平台开发环境搭建指南
TauriReactAnt Design跨平台开发环境搭建指南 一、环境配置与工具链搭建 1.1 基础环境准备 必备组件: Rust工具链(v1.77): curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh Node.js LTS(v20.11.1&a…...
《基于Selenium的论坛系统自动化测试实战报告》
一、项目背景与技术选型 项目简介 目标系统:论坛系统 核心功能:用户注册/登录、会话框发送信息、好友列表、信息发送 技术栈:html Springboot MySQL数据库 为什么选择Selenium 支持多浏览器兼容性测试(Chrome/Firefox/Edge&…...
洛谷 P11830 省选联考2025 幸运数字 题解
题意 小 X 有 n n n 个正整数二元组 ( a i , b i ) ( 1 ≤ i ≤ n ) (a_i, b_i) (1 \leq i \leq n) (ai,bi)(1≤i≤n)。他将会维护初始为空的可重集 S S S,并对其进行 n n n 轮操作。第 i ( 1 ≤ i ≤ n ) i (1 \leq i \leq n) i(1≤i≤n) 轮操作中&#…...
清华北大DeepSeek六册
「清华北大-Deepseek使用手册」 链接:https://pan.quark.cn/s/98782f7d61dc 「清华大学Deepseek整理) 1-6版本链接:https://pan.quark.cn/s/72194e32428a AI学术工具公测链接:https://pan.baidu.com/s/104w_uBB2F42Da0qnk78_ew …...
ubuntu部署gitlab-ce及数据迁移
ubuntu部署gitlab-ce及数据迁移 进行前梳理: 在esxi7.0 Update 3 基础上使用 ubuntu22.04.5-server系统对 gitlab-ce 16.10进行部署,以及将gitlab-ee 16.9 数据进行迁移到gitlab-ce 16.10 进行后总结: 起初安装了极狐17.8.3-jh 版本(不支持全局中文,就没用了) …...
什么是 MGX:MetaGPT
什么是 MGX:MetaGPT MetaGPT是由思码逸(OpenDILab)团队开发的一款专注于生成式AI驱动的软件开发框架,MGX可能是其衍生或升级的相关成果,它创新性地将大语言模型引入软件开发流程,模拟人类软件团队的协作方式,能让用户通过自然语言描述需求,即可自动生成完整的软件项目,…...
C++,leecode字符串常见API
在LeetCode上刷C题目时,熟练掌握字符串相关的常见API可以大大提高代码效率和可读性。以下是C标准库(<string>)中常用的字符串操作API: 1. 初始化和赋值 std::string s1 "hello"; // 直接初始化 std::string s2…...