当前位置: 首页 > news >正文

HAMqttDevice:嵌入式设备Home Assistant MQTT自动发现配置生成库

1. HAMqttDevice 库深度解析:面向 Home Assistant 的嵌入式 MQTT 发现配置生成器

1.1 设计定位与工程价值

HAMqttDevice 是一个轻量级、零依赖的 C++ 库,专为资源受限的嵌入式平台(如 ESP32、ESP8266)设计,其核心使命并非实现 MQTT 协议栈或网络通信,而是精准解决 Home Assistant(HA)MQTT 自动发现机制中 JSON 配置载荷(discovery payload)的动态生成难题。在实际 IoT 设备开发中,工程师常面临如下痛点:

  • 手动拼接 JSON 字符串极易出错:引号转义、字段顺序、大小写敏感、必填项遗漏;
  • HA 配置字段随版本演进频繁变化(如device_class新增值、availability_mode引入),硬编码难以维护;
  • 多设备类型(binary_sensorlightclimate等)需重复编写相似逻辑,违反 DRY 原则;
  • 嵌入式平台内存紧张,需避免动态内存分配(malloc/String过度使用)和冗余字符串拷贝。

HAMqttDevice 以“配置即代码”(Configuration-as-Code)思想切入,将 HA 的 YAML 配置规范映射为 C++ 类接口,通过编译期约束(enum DeviceType)和链式调用(enableStateTopic())强制开发者显式声明设备语义,从根本上规避运行时配置错误。其本质是嵌入式领域的领域特定语言(DSL)封装——将 HA 的 MQTT Discovery 协议抽象为可组合、可验证的 C++ 对象模型。


2. 核心架构与数据流分析

2.1 类结构与生命周期

HAMqttDevice类采用单例式构造与不可变配置模式,其对象生命周期严格绑定于设备初始化阶段:

class HAMqttDevice { private: const String _name; // 设备显示名称(用户可见) const DeviceType _type; // 设备类型枚举(决定基础字段) const String _haPrefix; // HA 发现前缀(默认 "ha") String _stateTopic; // 状态主题(可选,由 enableStateTopic() 触发生成) String _commandTopic; // 命令主题(可选,由 enableCommandTopic() 触发生成) String _attributesTopic; // 属性主题(可选,由 enableAttributesTopic() 触发生成) std::map<String, String> _configVars; // 用户自定义配置键值对 public: HAMqttDevice(const String& name, DeviceType type, const String& haMQTTPrefix = "ha"); // 链式调用接口(返回 *this 实现 Fluent Interface) HAMqttDevice& enableStateTopic(); HAMqttDevice& enableCommandTopic(); HAMqttDevice& enableAttributesTopic(); HAMqttDevice& addConfigVar(const String& key, const String& value); // 生成函数(纯计算,无副作用) String getConfigTopic() const; String getConfigPayload() const; };

关键设计决策解析

  • const成员变量_name_type_haPrefix在构造后不可变,确保配置一致性,避免运行时误修改;
  • 延迟生成策略_stateTopic等字段仅在显式调用enable*Topic()后才构建,避免为不支持命令的传感器(如SENSOR)生成无效字段;
  • std::map存储自定义变量:提供 O(log n) 查找效率,且天然支持按键排序(HA 要求 JSON 字段顺序无关,但有序输出利于调试);
  • String类型的谨慎使用:虽依赖 ArduinoString,但库内未进行字符串拼接(如+=),所有主题生成均通过String构造函数一次性完成,减少内存碎片。

2.2 主题生成逻辑(Topic Generation)

HA 的 MQTT Discovery 要求每个设备拥有唯一且结构化的主题路径。HAMqttDevice采用两级命名空间设计:

组件生成规则示例(name="My Binary Sensor"工程意义
Base Topicha/<device_type>/<sanitized_name>ha/binary_sensor/my_binary_sensorsanitized_name将空格、特殊字符转为下划线,确保 MQTT 主题合法性(RFC 3629);<device_type>小写化,符合 HA 规范
Config Topic<base_topic>/configha/binary_sensor/my_binary_sensor/configHA 订阅此主题接收设备描述,触发自动注册
State Topic<base_topic>/stateha/binary_sensor/my_binary_sensor/state设备上报状态的通道,~占位符在 payload 中复用 base topic
Attributes Topic<base_topic>/attrha/binary_sensor/my_binary_sensor/attr上报设备元数据(如电池电量、固件版本)

getConfigTopic()直接返回base_topic + "/config",而getConfigPayload()则构建 JSON 字符串,其中~作为 base topic 的占位符,极大压缩 payload 体积(避免重复长字符串)。

2.3 JSON Payload 构建机制

getConfigPayload()的输出是标准 JSON 对象,其字段构成遵循 HA 官方文档( MQTT Discovery )。核心字段生成逻辑如下表:

JSON 字段生成条件值来源说明
~永远存在base_topicHA 解析器识别的 base topic 占位符,后续字段如stat_t使用~/state引用
name永远存在_name设备在 HA UI 中的显示名称
stat_tenableStateTopic()被调用"~/state"状态主题,HA 从此订阅设备当前状态
cmd_tenableCommandTopic()被调用"~/set"命令主题,HA 向此发布控制指令(如ON/OFF
json_attr_tenableAttributesTopic()被调用"~/attr"属性主题,用于结构化上报设备元数据
device_classaddConfigVar("device_class", ...)被调用用户传入值关键语义字段,决定 HA UI 图标与行为(如"door"显示门图标)
retainaddConfigVar("retain", ...)被调用用户传入值("true"/"false"控制 MQTT broker 是否保留该消息,对初始状态同步至关重要

JSON 构建伪代码

String payload = "{"; payload += "'~':'" + baseTopic + "',"; payload += "'name':'" + _name + "',"; if (_stateTopic.length()) payload += "'stat_t':'~/state',"; if (_commandTopic.length()) payload += "'cmd_t':'~/set',"; if (_attributesTopic.length()) payload += "'json_attr_t':'~/attr',"; for (auto& kv : _configVars) { payload += "'" + kv.first + "':'" + kv.second + "',"; } // 移除末尾逗号,闭合大括号 payload.remove(payload.length()-1); payload += "}";

注意:实际库中使用单引号包裹 JSON 字符串('key':'value'),这是 ArduinoString的常见简化写法,HA 解析器完全兼容。严格 JSON 应使用双引号,但此处为嵌入式内存优化的合理妥协。


3. 设备类型(DeviceType)详解与工程选型指南

DeviceType枚举定义了 HA 支持的 11 类设备,每类对应不同的 MQTT 主题结构、JSON 字段集及 HA 行为。理解其差异是正确使用库的前提。

3.1 设备类型功能矩阵

DeviceType典型硬件必需主题关键配置字段HA 行为特征工程注意事项
BINARY_SENSOR门窗磁、水浸、烟雾报警器stat_tdevice_class,off_delay仅 ON/OFF 状态,无中间值device_class决定图标与告警逻辑("motion"触发运动告警)
SENSOR温湿度、光照、电量stat_tunit_of_measurement,device_class,state_class连续数值,支持历史图表state_class: "measurement"启用统计聚合
SWITCH继电器、智能插座stat_t,cmd_tpayload_on,payload_off,optimistic双向控制,状态反馈optimistic: true时,HA 不等待stat_t回报即更新 UI
LIGHTRGB LED、可调光灯stat_t,cmd_tschema,rgb,brightness,color_temp支持亮度、色温、RGB 调节schema: "json"启用 JSON 格式命令({"state":"ON","brightness":128}
COVER电动窗帘、百叶窗stat_t,cmd_tset_position_topic,position_topic,tilt_command_topic支持位置控制(0-100%)、倾斜角需额外配置位置反馈主题,避免开环控制
CLIMATE空调、地暖控制器stat_t,cmd_ttemperature_state_topic,mode_state_topic,fan_mode_state_topic复杂状态机(模式、温度、风速、摆风)需精确匹配 HA 的hvac_modespreset_modes字段
ALARM_CONTROL_PANEL安防主机stat_t,cmd_tcode_arm_required,code_disarm_required,code_trigger_required多级布防/撤防/触发code_*_required: true时,HA 强制输入密码
LOCK智能门锁stat_t,cmd_tpin_code_required,assumed_state状态反馈(锁定/解锁/故障)assumed_state: true适用于无状态反馈的机械锁
FAN智能风扇stat_t,cmd_tspeed_count,oscillation_state_topic,preset_mode_state_topic支持多档风速、摇头、预设模式speed_count: 3定义三档风速
CAMERAIP 摄像头stat_ttopic,access_token,still_image_url流媒体接入(需 RTSP/HTTP)still_image_url提供快照 URL,topic为状态主题
VACUUM扫地机器人stat_t,cmd_tbattery_level_topic,fan_speed_list,send_command_topic复杂状态(清扫/暂停/返航/充电)fan_speed_list: ["low","medium","high"]定义吸力档位

3.2 设备类型选择的工程实践

  • BINARY_SENSORvsSENSOR
    若检测的是“是否漏水”(是/否),选BINARY_SENSOR并设device_class: "moisture";若检测的是“当前湿度值”(45.2%),则必须用SENSOR并配unit_of_measurement: "%"。混用将导致 HA 无法正确解析。

  • SWITCHvsLIGHT
    单路继电器控制普通灯泡,用SWITCH即可;若需调光或 RGB,则必须用LIGHT并启用schema: "json",否则 HA 无法发送亮度命令。

  • COVER的位置反馈
    仅调用enableStateTopic()会生成stat_t: "~/state",但 HA 默认期望此主题上报"open"/"closed"字符串。若硬件支持位置传感器,应额外配置position_topic: "~/position"并在 payload 中添加"pos_t": "~/position",使 HA 显示精确百分比。


4. API 接口详解与实战代码示例

4.1 构造函数与基础配置

// 构造函数签名 HAMqttDevice::HAMqttDevice( const String& name, // 设备名称(建议 ASCII,避免国际化问题) const DeviceType type, // 设备类型(编译期检查,杜绝非法值) const String& haMQTTPrefix // HA 配置中的 discovery_prefix,默认 "ha" ); // 实例:创建一个门磁传感器 HAMqttDevice doorSensor("Front Door", HAMqttDevice::BINARY_SENSOR, "homeassistant"); // 实例:创建一个支持调光的 LED 灯(注意:haMQTTPrefix 与 HA 配置一致) HAMqttDevice ledLight("Living Room LED", HAMqttDevice::LIGHT, "homeassistant");

参数选择依据

  • name:应简洁、唯一、无空格("Front_Door"优于"Front Door"),便于生成合法 topic;
  • type:严格匹配硬件功能,BINARY_SENSOR不支持brightness字段;
  • haMQTTPrefix:必须与 HA 的configuration.yamlmqtt:下的discovery_prefix:值完全一致(默认"homeassistant",非"ha")。若 HA 配置为discovery_prefix: "iot",则此处必须传"iot"

4.2 主题启用接口(Enable Methods)

// 接口签名(全部返回 *this,支持链式调用) HAMqttDevice& HAMqttDevice::enableStateTopic(); // 启用状态主题(stat_t) HAMqttDevice& HAMqttDevice::enableCommandTopic(); // 启用命令主题(cmd_t) HAMqttDevice& HAMqttDevice::enableAttributesTopic(); // 启用属性主题(json_attr_t) // 实战:为开关设备启用双向通信 HAMqttDevice relaySwitch("Garage Light", HAMqttDevice::SWITCH); relaySwitch.enableStateTopic() // 生成 stat_t: "~/state" .enableCommandTopic(); // 生成 cmd_t: "~/set" // 实战:为温湿度传感器启用状态与属性上报 HAMqttDevice dhtSensor("Kitchen DHT22", HAMqttDevice::SENSOR); dhtSensor.enableStateTopic() // stat_t: "~/state" .enableAttributesTopic(); // json_attr_t: "~/attr"

工程要点

  • enableCommandTopic()仅对SWITCHLIGHTCOVER等可执行设备有意义;对SENSOR调用无效果(库内部忽略);
  • enableAttributesTopic()是上报设备元数据(如{"battery":95,"firmware":"v1.2"})的唯一途径,强烈推荐为所有设备启用,提升可维护性。

4.3 自定义配置字段(addConfigVar)

// 接口签名 HAMqttDevice& HAMqttDevice::addConfigVar(const String& key, const String& value); // 实战:为门磁配置 device_class 和 retain doorSensor.addConfigVar("device_class", "door") // 触发门图标与开门告警 .addConfigVar("retain", "true"); // 确保 HA 重启后仍知悉门状态 // 实战:为温湿度传感器配置单位与状态类 dhtSensor.addConfigVar("unit_of_measurement", "°C") .addConfigVar("device_class", "temperature") .addConfigVar("state_class", "measurement") .addConfigVar("expire_after", "300"); // 5分钟无更新,HA 自动设为 Unknown // 实战:为灯配置 JSON Schema 与亮度范围 ledLight.addConfigVar("schema", "json") .addConfigVar("brightness", "true") .addConfigVar("rgb", "true") .addConfigVar("color_temp", "true");

关键配置字段详解

字段名适用设备值示例作用
device_classBINARY_SENSOR,SENSOR,COVER"door","temperature","garage"最核心字段,决定 HA UI 元素、告警逻辑、历史图表类型
retain所有"true","false"true时,broker 保留最后一条消息,新订阅者立即获取状态(必备)
expire_afterSENSOR,BINARY_SENSOR"300"设备离线后,HA 在 N 秒后自动将状态设为Unknown,避免陈旧数据误导
payload_on/payload_offSWITCH,LIGHT"ON","OFF"定义 HA 发布的命令字符串,需与设备固件解析逻辑严格匹配
optimisticSWITCH,LIGHT"true"true时,HA 在发送命令后立即更新 UI,不等待stat_t回报(适用于无反馈的简单设备)

4.4 配置生成与发布流程(完整工作流)

#include <Arduino.h> #include <HAMqttDevice.h> #include <ESPAsyncMQTTClient.h> // 假设使用 AsyncMQTTClient 库 // 1. 创建设备对象(全局或 static) static HAMqttDevice garageDoor("Garage Door", HAMqttDevice::COVER); // 2. 配置设备(在 setup() 中执行一次) void configureHAMqttDevice() { // 启用必需主题 garageDoor.enableStateTopic() // ~/state .enableCommandTopic() // ~/set .enableAttributesTopic(); // ~/attr // 添加关键配置 garageDoor.addConfigVar("device_class", "garage") .addConfigVar("retain", "true") .addConfigVar("optimistic", "false") // 等待硬件反馈 .addConfigVar("position_topic", "~/position") // 位置反馈主题 .addConfigVar("set_position_topic", "~/position/set"); // 位置设置主题 } // 3. 生成并发布配置(设备首次启动时执行一次) void publishHAConfig() { String configTopic = garageDoor.getConfigTopic(); // "homeassistant/cover/garage_door/config" String configPayload = garageDoor.getConfigPayload(); // '{"~":"homeassistant/cover/garage_door","name":"Garage Door","stat_t":"~/state", // "cmd_t":"~/set","json_attr_t":"~/attr","device_class":"garage","retain":"true", // "optimistic":"false","pos_t":"~/position","set_pos_t":"~/position/set"}' // 使用 MQTT 客户端发布(以 AsyncMQTTClient 为例) mqttClient.publish(configTopic.c_str(), 0, true, configPayload.c_str()); Serial.printf("Published HA config to %s\n", configTopic.c_str()); } // 4. 在主循环中上报状态(伪代码) void loop() { static unsigned long lastReport = 0; if (millis() - lastReport > 5000) { // 每5秒上报 String statePayload = getDoorState(); // "open" / "closed" / "opening" / "closing" String positionPayload = String(getDoorPosition()); // 0-100 mqttClient.publish(garageDoor.getStateTopic().c_str(), 0, false, statePayload.c_str()); mqttClient.publish(garageDoor.getPositionTopic().c_str(), 0, false, positionPayload.c_str()); // 上报属性 String attrPayload = "{\"battery\":98,\"rssi\":" + String(WiFi.RSSI()) + "}"; mqttClient.publish(garageDoor.getAttributesTopic().c_str(), 0, false, attrPayload.c_str()); lastReport = millis(); } }

关键工程实践

  • 发布时机publishHAConfig()应在设备联网成功、MQTT 连接建立后仅执行一次。重复发布可能导致 HA 重复创建实体;
  • QoS 与 RetainconfigTopic必须使用 QoS 0 +retain: true,确保 HA 重启后能重新发现设备;
  • 状态上报主题getStateTopic()返回~/state,需用baseTopic替换~后再发布(库未提供此方法,需手动处理);
  • 错误处理:生产环境需检查mqttClient.publish()返回值,失败时重试。

5. 与主流嵌入式生态的集成方案

5.1 与 ESP-IDF / Arduino Core 的兼容性

HAMqttDevice 仅依赖Stringstd::map(Arduino STL),在 ESP32/ESP8266 平台开箱即用。针对不同框架的适配要点:

  • Arduino IDE:直接添加库文件,#include <HAMqttDevice.h>即可;
  • PlatformIO:在platformio.ini中添加lib_deps = https://github.com/plapointe6/HAMqttDevice.git
  • ESP-IDF (CMake):将库目录放入components/,在CMakeLists.txt中添加set(COMPONENT_REQUIRES HAMqttDevice)

内存优化提示
menuconfig中启用CONFIG_AWS_IOT_SDK_ENABLE_MBEDTLS时,std::map可能增加 RAM 占用。若内存极度紧张,可将_configVars替换为固定大小的struct数组(如ConfigVar vars[8]),牺牲灵活性换取确定性内存占用。

5.2 与 FreeRTOS 的协同设计

在 FreeRTOS 环境中,HA 配置发布应置于独立任务,避免阻塞高优先级控制任务:

// FreeRTOS 任务:HA 配置发布 void haConfigTask(void *pvParameters) { configureHAMqttDevice(); // 配置设备对象 vTaskDelay(5000 / portTICK_PERIOD_MS); // 等待 WiFi/MQTT 连接稳定 while (1) { if (mqttConnected && wifiConnected) { publishHAConfig(); break; // 仅发布一次 } vTaskDelay(1000 / portTICK_PERIOD_MS); } vTaskDelete(NULL); } // 创建任务 xTaskCreate(haConfigTask, "HA_Config", 4096, NULL, 3, NULL);

5.3 与传感器驱动的典型集成模式

以 DHT22 温湿度传感器为例,展示从硬件读取到 HA 上报的全链路:

#include <DHT.h> #include <HAMqttDevice.h> #define DHTPIN 4 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); HAMqttDevice dhtSensor("Living_Room_DHT22", HAMqttDevice::SENSOR); void setup() { dht.begin(); dhtSensor.enableStateTopic() .enableAttributesTopic() .addConfigVar("device_class", "temperature") .addConfigVar("unit_of_measurement", "°C") .addConfigVar("state_class", "measurement") .addConfigVar("expire_after", "300"); } void loop() { float h = dht.readHumidity(); float t = dht.readTemperature(); if (!isnan(h) && !isnan(t)) { // 构建状态 payload(单值) String statePayload = String(t, 1); // "23.5" // 构建属性 payload(JSON 对象) String attrPayload = "{\"humidity\":" + String(h, 1) + ",\"battery\":100,\"rssi\":" + String(WiFi.RSSI()) + "}"; // 发布 mqttClient.publish(dhtSensor.getStateTopic().c_str(), 0, false, statePayload.c_str()); mqttClient.publish(dhtSensor.getAttributesTopic().c_str(), 0, false, attrPayload.c_str()); } delay(2000); }

6. 故障排查与最佳实践

6.1 常见问题诊断表

现象可能原因解决方案
HA 未发现设备configTopic前缀与 HAdiscovery_prefix不匹配检查 HAconfiguration.yamlmqtt:下的discovery_prefix:值,确保与HAMqttDevice构造参数一致
设备显示为UnknownstateTopic未启用或未发布状态消息调用enableStateTopic(),并在主循环中定期publish(stateTopic, stateValue)
HA UI 无图标/错误图标device_class值不合法或未设置查阅 HA device_class 文档 ,确认拼写与设备类型匹配
属性不显示在 HA UIjson_attr_t未启用或attrPayload非合法 JSON调用enableAttributesTopic(),使用在线 JSON 校验器验证attrPayload
设备反复注册(HA 出现多个同名实体)configTopic被重复发布确保publishHAConfig()在设备生命周期内仅执行一次,添加标志位防止重入

6.2 生产环境最佳实践

  • 配置校验:在setup()中添加断言,验证name是否为空或含非法字符:
    if (_name.length() == 0 || _name.indexOf(' ') != -1) { Serial.println("ERROR: Device name must not be empty or contain spaces!"); while(1); // 硬件看门狗复位 }
  • 主题缓存getConfigTopic()getStateTopic()在设备配置后不再变化,可缓存结果避免重复字符串运算;
  • 日志分级:使用Serial.printf()输出详细调试信息(如生成的 topic/payload),发布时通过宏控制开关;
  • OTA 安全:设备固件 OTA 升级后,HA 配置可能失效。建议在 OTA 完成回调中重新执行publishHAConfig()

7. 总结:HAMqttDevice 在嵌入式 IoT 开发中的定位

HAMqttDevice 并非一个“全能型” MQTT 库,而是一把精准的手术刀——它剥离了网络协议、连接管理、重连逻辑等通用层,将工程师的注意力聚焦于Home Assistant 生态中最易出错的配置环节。其价值体现在三个维度:

  • 可靠性维度:通过enum强制类型安全、const成员保证配置不可变、enable*Topic()显式声明依赖,将 90% 的配置错误拦截在编译期或初始化阶段;
  • 可维护性维度:当 HA 升级引入新字段(如availability_mode),只需在addConfigVar()中添加一行,无需重构整个 JSON 拼接逻辑;
  • 资源效率维度:零动态内存分配(std::map在 Arduino 上为静态分配)、最小化字符串操作、主题复用~占位符,完美适配 ESP32/ESP8266 的有限 RAM。

对于正在构建 Home Assistant 兼容设备的嵌入式团队,HAMqttDevice 应被视为与ArduinoJsonAsyncMQTTClient并列的基础工具库。它不替代工程师对 MQTT 协议和 HA 架构的理解,而是将这种理解固化为可复用、可验证、可审计的 C++ 接口。当你的设备在 HA 中稳定运行一年后,你不会记得publish()的具体参数,但一定会感激当初选择了这个让配置“一次写对”的库。

http://www.cnnetsun.cn/news/1468699.html

相关文章:

  • AI驱动元宇宙社交的性能测试:架构师必须掌握的4个方法
  • efficiency-nodes-comfyui:ComfyUI效率革命的革新性解决方案
  • Jimeng AI Studio快速上手:Streamlit界面中英文提示词输入最佳实践
  • 3个维度掌握MiroFish部署:从入门到精通
  • Kook Zimage真实幻想Turbo效果实测:中英文混合Prompt真的智能吗?
  • Flux Sea Studio 海景摄影生成工具:Git版本控制管理生成脚本与模型参数
  • AI辅助开发实战:利用CL值和AIDA64 Latency优化系统性能
  • Java初级项目如何实现简单的订单管理
  • LFM2.5-1.2B-Thinking-GGUF在Proteus仿真中的创意应用:生成硬件描述与测试用例
  • 像素幻梦部署案例:中小企业低成本搭建像素艺术AI内容生产平台
  • 大学生毕业设计实战指南:从选题到部署的全链路技术实践
  • Windows 10系统优化与性能加速指南:基于Debloat-Windows-10开源工具的系统健康解决方案
  • 影刀RPA操作飞书表格时,那个烦人的‘记录ID数组’问题,我是这样绕过去的
  • 梯度下降算法家族:BGD, SGD, MBGD
  • 互联网产品需求分析助手:SmallThinker-3B-Preview评审PRD与生成用户故事
  • OpenBMC传感器监控实战:从hwmon到D-Bus的完整数据流解析
  • 通过aibiye爱毕业等8款智能应用,论文撰写与代码实现过程更加流畅,AI技术为毕业设计提供先进助力
  • 【Python3教程】Python3高级篇之StringIO模块
  • Vue3 知识点总结 · 2026-03-24
  • 【MCP集成终极指南】:VS Code插件下载、安装、配置与故障排除一站式实战手册
  • Windows计算器开源版:5个颠覆性功能重塑你的数字计算体验
  • AI智能客服助手技术栈实战:从架构设计到生产环境优化
  • 伏羲天气预报伦理治理:气象AI公平性评估、区域覆盖偏差检测与修正
  • ComfyUI报错‘prompt outputs failed validation: checkpointloadersimple‘的深度解析与AI辅助修复方案
  • 嵌入式驱动工程师职业发展指南
  • SEO_ 手把手教你进行网站站内SEO优化
  • 如何用Python脚本轻松突破谷歌网盘下载限制?GDriveDL终极指南
  • 大模型推理加速实战:KV Cache原理与StreamingLLM优化技巧
  • 暖黄温柔色调 Lightroom 预设 人像建筑街拍城市 ins 风滤镜
  • 腾讯高防域名