news 2026/9/16 15:00:23

IoT-For-Beginners 消费级项目实战:在 Wio Terminal 上实现文本转语音(Text to Speech)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
IoT-For-Beginners 消费级项目实战:在 Wio Terminal 上实现文本转语音(Text to Speech)

IoT-For-Beginners 消费级项目实战:在 Wio Terminal 上实现文本转语音(Text to Speech)

【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners

本文基于 Azure IoT 教育项目IoT-For-Beginners的 "消费级 IoT(Consumer IoT)" 单元第 23 课——"设置定时器并提供语音反馈"(课程目录),完整讲解如何在Wio Terminal上利用 Azure Speech 服务将文本合成为语音,并通过 ReSpeaker 扬声器播放。你将掌握:为嵌入式设备搭建"语音列表查询 + 文本转语音"两套 Azure Functions 无服务器函数、用 librosa 解决 44.1KHz 采样率重采样问题、把合成音频流式写入 SD 卡,以及用 Docker 容器将函数应用部署到云端并完成 TLS 证书迁移。读完即可在本地构建出一个能"开口说话"的智能定时器。

文本转语音(Text to Speech)概述

上一课中,你用 Azure Speech 服务把语音(Speech)转换成了文本(Text),即"语音转文本(Speech to Text)";而本课要做的是反方向的"文本转语音(Text to Speech,TTS)"——把一段文本合成为包含语音的音频数据。

整个智能定时器的交互闭环是:用户对着麦克风说出"set a 3 minute timer"→ 语音转文本 → LUIS 语言理解提取定时时长 → 设备设置定时器 →文本转语音把"timer started / Times up"等提示文本合成为语音播放出来。本文聚焦最后一环:文本转语音。

在前一课中你已经使用过 Speech 服务 SDK 完成语音转文本,而同一个 Speech 服务同样可用于把文本转回语音,无需额外引入新的云服务。

获取可用语音列表

调用文本转语音 API 时,必须显式指定使用哪个"语音(voice)"来合成,因为每种语言都支持一批不同的发音人(不同音色、性别、风格)。每种语言支持的语音列表可以通过 Speech 服务提供的 REST 接口获取。

为什么不能直接在 Wio Terminal 上获取语音列表

这里正是微控制器(MCU)的局限所在:获取文本转语音服务所支持的全部语音列表,返回的是一份超过 77KB 的 JSON 文档,对 Wio Terminal 来说体积过大、无法处理。撰写本文时,完整列表包含215 个语音,每个语音由类似下面的 JSON 文档定义:

{ "Name": "Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)", "DisplayName": "Aria", "LocalName": "Aria", "ShortName": "en-US-AriaNeural", "Gender": "Female", "Locale": "en-US", "StyleList": [ "chat", "customerservice", "narration-professional", "newscast-casual", "newscast-formal", "cheerful", "empathetic" ], "SampleRateHertz": "24000", "VoiceType": "Neural", "Status": "GA" }

这是一份Aria语音的元数据,包含多个语音风格(StyleList)。而真正用于文本转语音请求的,其实只有它的短名称(ShortName)en-US-AriaNeural

因此正确的做法不是把整份列表下载到单片机解码,而是编写无服务器(Serverless)代码:由云函数按你使用的语言把列表过滤、裁剪到极小体积,Wio Terminal 再通过 HTTP 调用这个云函数获取精简后的语音列表,并从列表中挑选合适的语音(例如第一个)。

任务一:创建获取语音列表的无服务器函数(get-voices)

仓库中已提供完整参考实现:functions/smart-timer-trigger/get-voices。

1. 打开smart-timer-trigger函数项目并激活虚拟环境

在 VS Code 中打开smart-timer-trigger项目并打开终端,确认虚拟环境已激活;如果没有,杀掉终端重新创建。

2. 在local.settings.json中配置 Speech 密钥与区域

打开local.settings.json,加入语音 API 密钥和区域设置(仓库参考文件见 local.settings.json):

"SPEECH_KEY": "<key>", "SPEECH_LOCATION": "<location>"
  • <key>替换为你的 Speech 服务资源的 API 密钥;
  • <location>替换为你创建 Speech 服务资源时使用的区域,如eastasiawesteurope

该文件中的Values是 Azure Functions 运行时注入的环境变量,Python 代码里通过os.environ['SPEECH_KEY']/os.environ['SPEECH_LOCATION']读取。

3. 新建名为get-voices的 HTTP 触发器

在函数应用项目根目录下的 VS Code 终端中执行:

func new --name get-voices --template "HTTP trigger"

这会在项目中创建get-voices文件夹及对应的__init__.pyfunction.json

4. 用以下代码替换get-voices/__init__.py的全部内容

import json import os import requests import azure.functions as func def main(req: func.HttpRequest) -> func.HttpResponse: location = os.environ['SPEECH_LOCATION'] speech_key = os.environ['SPEECH_KEY'] req_body = req.get_json() language = req_body['language'] url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list' headers = { 'Ocp-Apim-Subscription-Key': speech_key } response = requests.get(url, headers=headers) voices_json = json.loads(response.text) voices = filter(lambda x: x['Locale'].lower() == language.lower(), voices_json) voices = map(lambda x: x['ShortName'], voices) return func.HttpResponse(json.dumps(list(voices)), status_code=200)

这段代码向语音列表端点发起一次 HTTP GET 请求。返回的语音列表是包含所有语言语音的一大块 JSON,于是代码先按请求体中传入的languagefilter过滤出该语言的语音,再用map提取出每个语音的ShortName,最后以 JSON 数组形式返回。因为文本转语音请求只需要短名称,所以只返回这一个字段,把数据体积从 77KB(撰写时)大幅压缩——例如仅返回美国英语语音时只有408 字节

💁 提示:你可以按需调整过滤条件,只挑选你偏好的语音。

5. 本地运行并用 curl 测试

启动函数应用后,用 curl 这类工具调用(测试方式与text-to-timerHTTP 触发器相同),注意把语言作为 JSON 请求体传入:

{ "language":"<language>" }

<language>替换为你的语言代码,例如en-GBzh-CNen-US

任务二:从 Wio Terminal 获取语音

1. 打开smart-timer工程并配置函数 URL

在 VS Code 中打开smart-timer工程(仓库参考工程见 code-spoken-response/wio-terminal/smart-timer)。打开config.h头文件,添加函数应用 URL:

const char *GET_VOICES_FUNCTION_URL = "<URL>";

<URL>替换为你函数应用中get-voicesHTTP 触发器的 URL。它和TEXT_TO_TIMER_FUNCTION_URL基本一致,只是函数名从text-to-timer换成get-voices。仓库的 config.h 中三个函数 URL 并列展示如下:

const char *TEXT_TO_TIMER_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/text-to-timer"; const char *GET_VOICES_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/get-voices"; const char *TEXT_TO_SPEECH_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/text-to-speech";

2. 新建src/text_to_speech.h头文件

src文件夹下新建text_to_speech.h,用于定义"文本转语音"类。文件顶部加入以下 include 指令:

#pragma once #include <Arduino.h> #include <ArduinoJson.h> #include <HTTPClient.h> #include <Seeed_FS.h> #include <SD/Seeed_SD.h> #include <WiFiClient.h> #include <WiFiClientSecure.h> #include "config.h" #include "speech_to_text.h"

其中ArduinoJson用于构造/解析 JSON,HTTPClient用于调用无服务器函数,Seeed_FS.hSD/Seeed_SD.h用于把音频写入 SD 卡,WiFiClient.h/WiFiClientSecure.h用于本地 HTTP 与云端 HTTPS 两种场景(仓库参考实现见 text_to_speech.h,其中仅保留了config.h的包含)。

3. 声明TextToSpeech类与全局实例

class TextToSpeech { public: private: }; TextToSpeech textToSpeech;

textToSpeech作为全局实例供应用其余部分使用。

4. 声明 WiFi 客户端

调用函数应用需要 WiFi 客户端,在类的private区添加:

WiFiClient _client;

5. 添加所选语音字段

private区添加保存选中语音的字段:

String _voice;

6. 添加init函数获取第一个语音

public区添加一个init函数,用于获取第一个语音:

void init() { }

7. 构造携带语言的 JSON 文档

语音列表接口需要语言参数,在init中构造 JSON 并序列化:

DynamicJsonDocument doc(1024); doc["language"] = LANGUAGE; String body; serializeJson(doc, body);

LANGUAGEconfig.h中定义的宏(如"en-US"),DynamicJsonDocument(1024)预留了 1KB 动态内存——考虑到 Wio Terminal 的 SRAM 有限,文档按需分配内存是这里的关键设计。

8. 创建 HTTPClient 并 POST JSON

HTTPClient httpClient; httpClient.begin(_client, GET_VOICES_FUNCTION_URL); int httpResponseCode = httpClient.POST(body);

9. 检查响应码并取出第一个语音

if (httpResponseCode == 200) { String result = httpClient.getString(); Serial.println(result); DynamicJsonDocument doc(1024); deserializeJson(doc, result.c_str()); JsonArray obj = doc.as<JsonArray>(); _voice = obj[0].as<String>(); Serial.print("Using voice "); Serial.println(_voice); } else { Serial.print("Failed to get voices - error "); Serial.println(httpResponseCode); }

响应为 200(成功)时,把返回的语音短名称 JSON 数组解析出来,取第 0 项存入_voice;失败则打印错误码便于排查。

10. 关闭 HTTP 连接

httpClient.end();

11. 在main.cpp中集成

打开main.cpp,在文件顶部添加:

#include "text_to_speech.h"

并在setup函数中、speechToText.init();调用之下初始化:

textToSpeech.init();

仓库 main.cpp 的setup顺序是:连接 WiFi → 初始化 Flash(sfud_init)→ 初始化麦克风mic.init()speechToText.init()textToSpeech.init(),最后打印Ready.

12. 构建、上传并观察串口输出

构建代码、上传到 Wio Terminal,并通过串口监视器测试(确保函数应用正在运行)。你将看到函数应用返回的可用语音列表以及选中的语音:

--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time --- More details at http://bit.ly/pio-monitor-filters --- Miniterm on /dev/cu.usbmodem1101 9600,8,N,1 --- --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- Connecting to WiFi.. Connected! Got access token. ["en-US-JennyNeural", "en-US-JennyMultilingualNeural", "en-US-GuyNeural", "en-US-AriaNeural", "en-US-AmberNeural", "en-US-AnaNeural", "en-US-AshleyNeural", "en-US-BrandonNeural", "en-US-ChristopherNeural", "en-US-CoraNeural", "en-US-ElizabethNeural", "en-US-EricNeural", "en-US-JacobNeural", "en-US-MichelleNeural", "en-US-MonicaNeural", "en-US-AriaRUS", "en-US-BenjaminRUS", "en-US-GuyRUS", "en-US-ZiraRUS"] Using voice en-US-JennyNeural Ready.

可以看到,串口输出了 19 个 en-US 语音的短名称列表,设备自动选中第一个en-US-JennyNeural作为合成语音。

将文本转换为语音

拿到语音后,就可以用它把文本合成为音频了。语音列表遇到的内存限制同样存在于语音合成环节——合成返回的音频不能直接缓存在内存中,因此需要把音频写入 SD 卡,再通过 ReSpeaker 播放。

💁 本课程项目前面的课程里,你用 Flash 存储器保存从麦克风采集到的语音;本课改用 SD 卡,是因为用 Seeed 音频库从 SD 卡播放音频更简单。

采样率不匹配:44.1KHz 限制

还有一个必须考虑的限制:Speech 服务返回的音频格式与 Wio Terminal 支持的格式不一致。与完整计算机不同,微控制器的音频库对音频格式的支持非常有限。例如,能在 ReSpeaker 上播放声音的Seeed Arduino Audio 库只支持 44.1KHz 采样率的音频;而 Azure Speech 服务虽然能以多种格式提供音频,但只用8KHz、16KHz、24KHz 和 48KHz这些采样率。这意味着音频必须被重采样到 44.1KHz——这项工作对 Wio Terminal 来说资源开销过大,尤其是内存。

处理这类数据变换时,更好的做法是交给无服务器代码,特别是数据本身就来自 Web 调用时:Wio Terminal 调用一个无服务器函数,把要转换的文本传过去,函数内既调用 Speech 服务完成文本转语音,又顺便把音频重采样到所需采样率,最后把 Wio Terminal 需要的音频形式返回,由设备存入 SD 卡、经 ReSpeaker 播放。这样把重采样的计算压力从单片机转移到了云端。

任务三:创建文本转语音的无服务器函数(text-to-speech)

仓库参考实现见 functions/smart-timer-trigger/text-to-speech。

1. 新建text-to-speechHTTP 触发器

smart-timer-trigger项目根目录的 VS Code 终端中执行:

func new --name text-to-speech --template "HTTP trigger"

2. 在requirements.txt中加入 librosa

librosa):

librosa

随后安装依赖:

pip install -r requirements.txt

⚠️ 如果你使用 Linux(包括 Raspberry Pi OS),可能需要先安装libsndfile

sudo apt update sudo apt install libsndfile1-dev

3. 用访问令牌替代 API 密钥

与"获取语音列表"不同,调用文本转语音 REST API 时不能直接使用 API 密钥,必须先向令牌端点换取一个访问令牌(Access Token),再用令牌认证 TTS 请求。打开text-to-speech文件夹中的__init__.py,用以下代码替换全部内容:

import io import os import requests import librosa import soundfile as sf import azure.functions as func location = os.environ['SPEECH_LOCATION'] speech_key = os.environ['SPEECH_KEY'] def get_access_token(): headers = { 'Ocp-Apim-Subscription-Key': speech_key } token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken' response = requests.post(token_endpoint, headers=headers) return str(response.text)

这段代码先从设置中读取区域和语音密钥作为模块级常量,然后定义get_access_token函数:向区域专属的 STS 令牌端点发起 POST,把 API 密钥放在Ocp-Apim-Subscription-Key头中,返回响应体即访问令牌。

4. 添加主处理函数:SSML 合成 + librosa 重采样

在这段代码下方继续添加:

playback_format = 'riff-48khz-16bit-mono-pcm' def main(req: func.HttpRequest) -> func.HttpResponse: req_body = req.get_json() language = req_body['language'] voice = req_body['voice'] text = req_body['text'] url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/v1' headers = { 'Authorization': 'Bearer ' + get_access_token(), 'Content-Type': 'application/ssml+xml', 'X-Microsoft-OutputFormat': playback_format } ssml = f'<speak version=\'1.0\' xml:lang=\'{language}\'>' ssml += f'<voice xml:lang=\'{language}\' name=\'{voice}\'>' ssml += text ssml += '</voice>' ssml += '</speak>' response = requests.post(url, headers=headers, data=ssml.encode('utf-8')) raw_audio, sample_rate = librosa.load(io.BytesIO(response.content), sr=48000) resampled = librosa.resample(raw_audio, sample_rate, 44100) output_buffer = io.BytesIO() sf.write(output_buffer, resampled, 44100, 'PCM_16', format='wav') output_buffer.seek(0) return func.HttpResponse(output_buffer.read(), status_code=200)

关键点拆解:

  • playback_format = 'riff-48khz-16bit-mono-pcm'声明了期望的输出格式:16 位、48KHz、单声道、RIFF(WAV)封装,通过X-Microsoft-OutputFormat头传给 REST API;
  • HTTP 触发器从 JSON 请求体中提取languagevoicetext三个字段;
  • 构造 SSML(语音合成标记语言):<speak>根节点声明语言,<voice name='...'>指定发音人,文本放在其中;
  • 携带Authorization: Bearer <token>访问令牌调用 TTS REST API;
  • librosa.load(..., sr=48000)以 48KHz 载入返回的音频,再通过librosa.resample重采样到44.1KHz
  • 最后用soundfilesf.write)把重采样后的音频以 16 位 PCM WAV 格式写入二进制缓冲区返回。

5. 本地运行并用 curl 测试

本地运行函数应用(或部署到云端),用 curl 调用。请求体必须包含语言、语音和文本三个字段:

{ "language": "<language>", "voice": "<voice>", "text": "<text>" }
  • <language>填语言代码,如en-GBzh-CN
  • <voice>填你想用的语音短名称;
  • <text>填要转换为语音的文本。

你可以把输出保存成文件,用任意能播放 WAV 的音频播放器试听。例如,用 Jenny Neural 语音把 "Hello" 合成为美式英语语音(函数应用本地运行时):

curl -X GET 'http://localhost:7071/api/text-to-speech' \ -H 'Content-Type: application/json' \ -o hello.wav \ -d '{ "language":"en-US", "voice": "en-US-JennyNeural", "text": "Hello" }'

这会把音频保存为当前目录下的hello.wav

任务四:从 Wio Terminal 获取语音

1. 配置TEXT_TO_SPEECH_FUNCTION_URL

打开config.h,添加函数应用 URL:

const char *TEXT_TO_SPEECH_FUNCTION_URL = "<URL>";

<URL>替换为函数应用中text-to-speechHTTP 触发器的 URL,它与TEXT_TO_TIMER_FUNCTION_URL一致,仅函数名不同。

2. 添加convertTextToSpeech方法

打开text_to_speech.h,在TextToSpeech类的public区添加:

void convertTextToSpeech(String text) { }

3. 构造请求 JSON

在方法内添加代码,生成要发给函数应用的 JSON:

DynamicJsonDocument doc(1024); doc["language"] = LANGUAGE; doc["voice"] = _voice; doc["text"] = text; String body; serializeJson(doc, body);

这里把语言、语音(来自_voice字段)和文本写入 JSON 文档并序列化为字符串。

4. 调用函数应用

HTTPClient httpClient; httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL); int httpResponseCode = httpClient.POST(body);

创建 HTTPClient 并用 JSON 文档向text-to-speechHTTP 触发器发起 POST。

5. 把返回的二进制音频流式写入 SD 卡

调用成功时,把函数应用返回的原始二进制数据流式写入 SD 卡文件:

if (httpResponseCode == 200) { File wav_file = SD.open("SPEECH.WAV", FILE_WRITE); httpClient.writeToStream(&wav_file); wav_file.close(); } else { Serial.print("Failed to get speech - error "); Serial.println(httpResponseCode); }

代码检查响应码,200(成功)时把二进制数据流式写入 SD 卡根目录下的SPEECH.WAV文件。writeToStreamHTTPClient提供的流式写接口,避免把整段音频读进内存——这正是应对内存限制的关键手法。

6. 关闭 HTTP 连接

在方法末尾:

httpClient.end();

7. 在say函数中触发合成

main.cppsay函数末尾添加一行,把要说的文本转换为音频(仓库 main.cpp 中完整实现如下):

void say(String text) { Serial.println(text); textToSpeech.convertTextToSpeech(text); }

say是整个语音反馈的入口:定时器启动时调用say(begin_message)播报"timer started",定时器到期时通过timer.in(total_seconds * 1000, timerExpired, ...)注册的回调timerExpired调用say(end_message)播报"Times up on your ... timer"。

播放音频

在本文对应的课程节点中,通过 ReSpeaker 播放SPEECH.WAV的内容被标注为"Coming soon"(即将推出),仓库代码中尚未包含完整的播放实现。可以参考课程说明中"用 Seeed 音频库从 SD 卡播放音频更容易"的提示,在后续课程内容中补充播放环节。

将函数应用部署到云端

为什么需要 Docker 容器

本地运行时,librosa这个 Pip 包在 Linux 上有一个默认不会安装的依赖库(即libsndfile),必须在函数应用运行前先安装。而函数应用是无服务器的——没有你可以自己管理的服务器,自然也就无法预先安装这个库。

解决方案是用 Docker 容器部署函数应用:这个容器由云平台按需部署,每当需要为函数应用启动一个新实例时(例如需求超过可用资源时,或函数应用长时间未用被关闭后再被唤起时),都会基于你的自定义镜像拉起,从而保证libsndfile等依赖始终就绪。创建函数应用并通过 Docker 部署的完整步骤可参考 Microsoft Docs 中"使用 Linux 自定义容器创建函数"的文档。

部署后把 Wio Terminal 代码切换到 HTTPS

部署完成后,函数应用会提供 HTTPS 地址,Wio Terminal 代码需要做以下迁移:

1. 把 Azure Functions 证书加入config.h

const char *FUNCTIONS_CERTIFICATE = "-----BEGIN CERTIFICATE-----\r\n" "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\r\n" "-----END CERTIFICATE-----\r\n";

这是用于验证 Azure Functions 服务端证书的 CA 证书链,是 TLS 握手验证所必需的公钥材料。Wio Terminal 的 mbedTLS 栈需要把根证书以 PEM 格式内嵌到固件中才能建立可信的 HTTPS 连接。

2. 将<WiFiClient.h>包含全部改为<WiFiClientSecure.h>

3. 把所有WiFiClient字段改为WiFiClientSecure

4. 在每个含WiFiClientSecure字段的类中添加构造函数并设置证书

_client.setCACert(FUNCTIONS_CERTIFICATE);

在构造函数里调用setCACert把内嵌的 CA 证书绑定到安全客户端上,此后HTTPClient便可通过该安全客户端访问 HTTPS 的云端函数应用。这组证书迁移操作适用于TextToSpeechSpeechToText等所有需要调用云端函数的类。

💁 本地开发时函数应用走http://明文地址,用普通WiFiClient即可;部署到云端后必须切换为WiFiClientSecure并携带证书。仓库 config.h 中还保留了本地调用 Speech 服务令牌端点所需的TOKEN_CERTIFICATESPEECH_CERTIFICATE,供对比参考。

小结与参考实现

本文完整复现了 Wio Terminal 文本转语音的整条链路:

  1. 语音列表裁剪:用get-voices无服务器函数把 77KB 的全量语音列表按语言过滤成几百字节的短名称数组(get-voices/init.py);
  2. 云端合成与重采样:用text-to-speech无服务器函数申请访问令牌、构造 SSML、调用 TTS REST API,再用 librosa 把 48KHz 音频重采样为 44.1KHz WAV(text-to-speech/init.py);
  3. 设备端播放准备:Wio Terminal 通过TextToSpeech类获取语音、合成并把音频流式写入 SD 卡SPEECH.WAV(text_to_speech.h);
  4. 云端部署:用 Docker 容器解决librosalibsndfile依赖,并把 Wio Terminal 切换到 HTTPS +WiFiClientSecure+ CA 证书。

工程依赖(platformio.ini)主要包括Seeed Arduino FSSeeed Arduino rpcWiFiSeeed_Arduino_mbedtlsArduinoJson 6.17.3arduino-timer 2.3.0等库,整体在 PlatformIO + Arduino 框架下构建(板型seeed_wio_terminal)。想动手复现的读者,可以直接对照 code-spoken-response 目录中的函数端与设备端代码,结合本文步骤逐步搭建自己的"会说话的智能定时器"。

【免费下载链接】IoT-For-Beginners12 Weeks, 24 Lessons, IoT for All!项目地址: https://gitcode.com/GitHub_Trending/io/IoT-For-Beginners

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

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

akshare 0.6.61源码包下载安装与版本锁定实践

简介&#xff1a;akshare-0.6.61.tar.gz 是 PyPI 官方发布的一个 Python 库源码包&#xff0c;专为需要批量获取中国金融行情数据的 Python 开发者、量化交易爱好者和数据分析人员准备。包内包含 236 个文件&#xff0c;压缩后仅 353KB&#xff0c;其中以 216 个 py 源码文件为…

作者头像 李华
网站建设 2026/9/16 14:58:38

微信小程序期末项目:天使童装商城实战指南

简介&#xff1a;这是一份面向高校计算机专业学生及微信小程序初学者的期末大作业实战项目&#xff0c;聚焦电商类应用开发能力培养&#xff0c;帮助学习者系统掌握小程序核心开发流程与典型业务实现。资源为ZIP压缩包&#xff0c;大小2.18MB&#xff0c;包含完整可运行的小程序…

作者头像 李华
网站建设 2026/9/16 14:58:35

企业数据标准管理:构建统一的数据治理框架

1. 数据标准管理概述数据标准管理是企业数据治理体系中的核心环节&#xff0c;它如同城市交通规则一般&#xff0c;为各类数据资产制定了统一的"通行规范"。在实际工作中&#xff0c;我们经常遇到这样的场景&#xff1a;销售部门定义的"客户编号"是8位数字…

作者头像 李华
网站建设 2026/9/16 14:55:45

PCTG-1013 PROFINET转Modbus485工业协议转换网关

北京磐创科技的PCTG-1013型网关是一种将Profinet和串口连接的工业协议转换设备。让ModbusRTU或485自由协议的设备轻松接入Profinet总线设备&#xff08;如&#xff1a;西门子S7-1200/ 1500等&#xff09;进行数据的双向转换。PCTG-1013在Profinet总线做从站&#xff0c;在Modbu…

作者头像 李华
网站建设 2026/9/16 14:55:09

Windows虚拟内存配置与OOM排查:从页面文件到常驻服务内存优化

前阵子同事抱着笔记本来找我&#xff0c;说是运行一个数据分析脚本的时候&#xff0c;系统直接弹窗提示“内存不足”&#xff0c;紧接着整个桌面卡死&#xff0c;最后只能强制重启。这种事情在Windows上太常见了&#xff0c;打开几十个Chrome标签、后台挂着Docker、IDE再开两三…

作者头像 李华
网站建设 2026/9/16 14:55:05

STM32F407与MPU6050自平衡车串级PID控制实现

简介&#xff1a;基于 STM32F407ZGT6 与 MPU6050 的二轮自平衡车项目&#xff0c;是一份面向课程设计、毕业设计及嵌入式进阶学习的完整技术资料。项目围绕速度环与位置环串级 PID 控制展开&#xff0c;能够帮助读者理解如何利用惯性传感器获取车身姿态&#xff0c;并通过闭环控…

作者头像 李华