* Add docs about time_estimate * Fix: Problems with graceful program exit caused by Flutter refactoring * Add: sw_OpenBrowser() & sw_OpenOrcaWebview * Fix: NetworkTestDialog Crash & Add: Lan Device test \ cloud test
25 KiB
M109/M190温度等待时间计算修复方案(方案B:智能预热)
文档版本: v1.0 创建日期: 2025-12-06 目标机型: U1多喷头打印机(也适用于其他多喷头机型) 优先级: 高 预计影响: 多喷头打印时间估算精度从±30-50%提升到±5-15%
一、问题背景
1.1 核心问题描述
问题1: M109(喷头加热等待)没有计算等待时间
- 位置:
src/libslic3r/GCode/GCodeProcessor.cpp:3640-3655 - 现象: 只更新温度值,不调用
simulate_st_synchronize()添加等待时间 - 影响: 实际等待30-80秒,时间估算为0秒
问题2: M190(热床加热等待)没有计算等待时间
- 位置:
src/libslic3r/GCode/GCodeProcessor.cpp:3696-3701 - 现象: 只更新温度值,不调用
simulate_st_synchronize()添加等待时间 - 影响: 实际等待60-180秒,时间估算为0秒
问题3: 预热逻辑未与M109等待时间关联
- 位置:
src/libslic3r/GCode/GCodeProcessor.cpp:4620-4648 - 现象: M104预热是异步的,M109等待时间没有考虑预热效果
- 影响: 对U1多头机型,时间估算会严重高估50-100%
1.2 U1机型的特殊性
U1是多喷头打印机,具有以下特点:
- 工具切换频繁: 多色打印时可能有50+次工具切换
- 依赖预热逻辑: 每次切换前20-30秒就开始预热新工具
- 预热效果显著: 到达M109时,温度通常已接近目标值
- 不考虑预热的后果: 每次切换错误地添加30-40秒等待时间,50次切换会多估算25-33分钟
1.3 为什么必须实现方案B
简单方案(方案A)的问题:
- 不考虑预热,直接计算温差÷加热速率
- 对单喷头打印机可能可以接受(略微高估)
- 对U1多头机型完全不可接受:
- 每次工具切换都有预热
- 预热成功率接近100%(因为preheat_time设置合理)
- 错误地为每次切换添加30-40秒等待
- 累积误差巨大
方案B的优势:
- 追踪M104预热状态和时间
- M109时计算已预热时间,扣除已加热的温度
- 准确反映实际等待时间
- 适配U1的预热逻辑
二、方案B详细设计
2.1 核心思路
时间轴示例(工具切换场景):
t=100s: 打印中(使用T0)
t=110s: 预热逻辑插入 M104 T1 S220(开始预热T1)⬅️ 记录预热开始时间
t=110s~130s: 继续使用T0打印,T1在后台加热
t=130s: 工具切换,执行 M109 T1 S220 ⬅️ 计算等待时间
- 当前时间:130s
- 预热开始时间:110s
- 已预热时间:20s
- 加热速率:2.5°C/s
- 已加热温度:20s × 2.5°C/s = 50°C
- 温度差:220°C - 170°C = 50°C
- 剩余需加热:50°C - 50°C = 0°C
- 等待时间:0s ✅ 准确!
对比方案A(不考虑预热):
- 温度差:220°C - 170°C = 50°C
- 等待时间:50°C ÷ 2.5°C/s = 20s ❌ 错误!
2.2 数据结构设计
预热状态结构体
// 在 GCodeProcessor.hpp 中定义
struct PreheatingState {
bool is_preheating = false; // 是否正在预热
float preheat_start_time = 0.0f; // 预热开始时的估算时间戳
float preheat_target_temp = 0.0f; // 预热目标温度
float preheat_start_temp = 0.0f; // 预热开始时的温度(备用)
};
struct BedPreheatingState {
bool is_preheating = false;
float preheat_start_time = 0.0f;
float preheat_target_temp = 0.0f;
float preheat_start_temp = 0.0f;
};
GCodeProcessor新增成员变量
class GCodeProcessor
{
private:
// ... 现有成员 ...
// 加热速率配置(从PrintConfig加载)
float m_extruder_heating_rate; // 喷头加热速率(°C/s),默认2.5
float m_bed_heating_rate; // 热床加热速率(°C/s),默认0.75
// 预热状态追踪(关键数据结构)
std::vector<PreheatingState> m_extruder_preheat_states; // 每个喷头的预热状态
BedPreheatingState m_bed_preheat_state; // 热床预热状态
};
2.3 配置参数设计
PrintConfig新增参数
文件: src/libslic3r/PrintConfig.cpp
def = this->add("extruder_heating_rate", coFloat);
def->label = L("喷头加热速率");
def->tooltip = L("喷头的加热速率,单位°C/秒。用于打印时间预估。\n"
"不同打印机的加热速率差异较大,建议根据实际测试调整。\n"
"测试方法:从室温加热到目标温度,记录时间,计算速率。\n"
"典型值:2-4°C/s");
def->sidetext = L("°C/s");
def->min = 0.5;
def->max = 10.0;
def->mode = comAdvanced; // 高级选项
def->set_default_value(new ConfigOptionFloat(2.5));
def = this->add("bed_heating_rate", coFloat);
def->label = L("热床加热速率");
def->tooltip = L("热床的加热速率,单位°C/秒。用于打印时间预估。\n"
"热床加热速度通常比喷头慢。\n"
"测试方法:从室温加热到目标温度,记录时间,计算速率。\n"
"典型值:0.5-1.5°C/s");
def->sidetext = L("°C/s");
def->min = 0.1;
def->max = 3.0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.75));
文件: src/libslic3r/PrintConfig.hpp
// 在 MachineEnvelopeConfig 或 PrintConfig 类中添加
((ConfigOptionFloat, extruder_heating_rate))
((ConfigOptionFloat, bed_heating_rate))
三、核心函数实现
3.1 配置加载 - apply_config()
文件: src/libslic3r/GCode/GCodeProcessor.cpp
void GCodeProcessor::apply_config(const PrintConfig& config)
{
// ... 现有代码 ...
// 加载加热速率配置
m_extruder_heating_rate = config.extruder_heating_rate;
m_bed_heating_rate = config.bed_heating_rate;
// 确保速率在合理范围内(防御性编程)
m_extruder_heating_rate = std::clamp(m_extruder_heating_rate, 0.5f, 10.0f);
m_bed_heating_rate = std::clamp(m_bed_heating_rate, 0.1f, 3.0f);
}
3.2 初始化 - reset()
文件: src/libslic3r/GCode/GCodeProcessor.cpp
void GCodeProcessor::reset()
{
// ... 现有代码 ...
// 初始化预热状态数组(根据喷头数量)
m_extruder_preheat_states.clear();
m_extruder_preheat_states.resize(m_extruder_temps.size());
for (auto& state : m_extruder_preheat_states) {
state.is_preheating = false;
state.preheat_start_time = 0.0f;
state.preheat_target_temp = 0.0f;
state.preheat_start_temp = 0.0f;
}
// 初始化热床预热状态
m_bed_preheat_state.is_preheating = false;
m_bed_preheat_state.preheat_start_time = 0.0f;
m_bed_preheat_state.preheat_target_temp = 0.0f;
m_bed_preheat_state.preheat_start_temp = 0.0f;
}
3.3 M104处理 - process_M104()(记录预热开始)
文件: src/libslic3r/GCode/GCodeProcessor.cpp
原始代码:
void GCodeProcessor::process_M104(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp)) {
m_extruder_temps[m_extruder_id] = new_temp;
}
}
修改后代码:
void GCodeProcessor::process_M104(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp)) {
size_t target_extruder = m_extruder_id;
// 处理T参数(指定喷头)
float val;
if (line.has_value('T', val)) {
target_extruder = static_cast<size_t>(val);
}
// 更新温度
if (target_extruder < m_extruder_temps.size()) {
m_extruder_temps[target_extruder] = new_temp;
}
// 🔥 关键:记录预热状态
if (target_extruder < m_extruder_preheat_states.size()) {
m_extruder_preheat_states[target_extruder].is_preheating = true;
m_extruder_preheat_states[target_extruder].preheat_start_time =
get_time(PrintEstimatedStatistics::ETimeMode::Normal);
m_extruder_preheat_states[target_extruder].preheat_target_temp = new_temp;
m_extruder_preheat_states[target_extruder].preheat_start_temp =
m_extruder_temps[target_extruder];
}
}
}
关键点说明:
get_time()返回当前的估算时间(不是实际时钟时间)- 记录目标温度和当前温度(当前温度可用于更精确的计算)
- 支持T参数指定喷头
3.4 M109处理 - process_M109()(考虑预热效果)
文件: src/libslic3r/GCode/GCodeProcessor.cpp
原始代码:
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('R', new_temp)) {
float val;
if (line.has_value('T', val)) {
size_t eid = static_cast<size_t>(val);
if (eid < m_extruder_temps.size())
m_extruder_temps[eid] = new_temp;
}
else
m_extruder_temps[m_extruder_id] = new_temp;
}
else if (line.has_value('S', new_temp))
m_extruder_temps[m_extruder_id] = new_temp;
// ❌ 没有添加等待时间
}
修改后代码:
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
float new_temp;
float target_temp = 0;
size_t target_extruder = m_extruder_id;
bool use_r_param = false; // R参数可以等待降温
// 解析参数
if (line.has_value('R', new_temp)) {
use_r_param = true;
target_temp = new_temp;
float val;
if (line.has_value('T', val)) {
target_extruder = static_cast<size_t>(val);
if (target_extruder < m_extruder_temps.size())
m_extruder_temps[target_extruder] = new_temp;
}
else
m_extruder_temps[m_extruder_id] = new_temp;
}
else if (line.has_value('S', new_temp)) {
target_temp = new_temp;
m_extruder_temps[m_extruder_id] = new_temp;
}
// 🔥 计算等待时间(考虑预热)
if (target_extruder >= m_extruder_temps.size())
return; // 无效喷头ID
float current_temp = m_extruder_temps[target_extruder];
float temp_diff = target_temp - current_temp;
bool is_heating = temp_diff > 0;
// 只有温差超过阈值才计算等待时间
if (std::abs(temp_diff) > 5.0f) {
float wait_time = 0.0f;
float heating_rate = m_extruder_heating_rate;
// 降温处理(R参数)
if (!is_heating) {
// 降温速度比加热慢
// 被动降温:约0.5-1°C/s
// 主动风扇降温:约1-2°C/s
heating_rate = 0.8f; // 保守估计
temp_diff = -temp_diff; // 转为正值
}
// 检查是否有预热(只对加热有效)
if (is_heating &&
target_extruder < m_extruder_preheat_states.size() &&
m_extruder_preheat_states[target_extruder].is_preheating) {
// 计算从预热开始到现在已经过去的时间
float current_time = get_time(PrintEstimatedStatistics::ETimeMode::Normal);
float elapsed_preheat_time = current_time -
m_extruder_preheat_states[target_extruder].preheat_start_time;
// 计算预热期间已经加热了多少度
float preheated_temp_diff = elapsed_preheat_time * heating_rate;
// 计算剩余需要加热的温度
float remaining_temp_diff = std::max(0.0f, temp_diff - preheated_temp_diff);
// 计算剩余等待时间
if (remaining_temp_diff > 0.0f) {
wait_time = remaining_temp_diff / heating_rate;
}
// else: 预热已经完成,等待时间为0
// 清除预热状态
m_extruder_preheat_states[target_extruder].is_preheating = false;
} else {
// 没有预热或降温,完整计算等待时间
wait_time = temp_diff / heating_rate;
}
// 限制在合理范围
// 喷头加热最长120秒(从室温到300°C)
wait_time = std::clamp(wait_time, 0.0f, 120.0f);
// 只有等待时间>1秒才添加(避免噪音)
if (wait_time > 1.0f) {
simulate_st_synchronize(wait_time);
}
}
}
关键点说明:
- 支持R参数: 可以等待降温,使用较慢的降温速率
- 预热效果计算: 已预热时间 × 加热速率 = 已加热温度
- 剩余等待时间: 温度差 - 已加热温度 / 加热速率
- 边界情况处理:
- 温度已达标(预热成功)→ 等待时间0秒
- 预热失败或部分成功 → 计算剩余等待时间
- 降温 → 使用降温速率
3.5 M140处理 - process_M140()(记录热床预热)
文件: src/libslic3r/GCode/GCodeProcessor.cpp
原始代码:
void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp))
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
}
修改后代码:
void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp)) {
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ?
(int)new_temp : m_highest_bed_temp;
// 🔥 记录热床预热状态
m_bed_preheat_state.is_preheating = true;
m_bed_preheat_state.preheat_start_time =
get_time(PrintEstimatedStatistics::ETimeMode::Normal);
m_bed_preheat_state.preheat_target_temp = new_temp;
m_bed_preheat_state.preheat_start_temp = m_highest_bed_temp;
}
}
3.6 M190处理 - process_M190()(考虑预热效果)
文件: src/libslic3r/GCode/GCodeProcessor.cpp
原始代码:
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp))
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
// ❌ 没有添加等待时间
}
修改后代码:
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{
float new_temp;
if (line.has_value('S', new_temp)) {
float current_bed_temp = m_highest_bed_temp;
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ?
(int)new_temp : m_highest_bed_temp;
float temp_diff = new_temp - current_bed_temp;
// 只有加热且温差>5°C才计算
if (temp_diff > 5.0f) {
float wait_time = 0.0f;
// 🔥 检查是否有预热
if (m_bed_preheat_state.is_preheating) {
float current_time = get_time(PrintEstimatedStatistics::ETimeMode::Normal);
float elapsed_preheat_time = current_time -
m_bed_preheat_state.preheat_start_time;
float preheated_temp_diff = elapsed_preheat_time * m_bed_heating_rate;
float remaining_temp_diff = std::max(0.0f, temp_diff - preheated_temp_diff);
if (remaining_temp_diff > 0.0f) {
wait_time = remaining_temp_diff / m_bed_heating_rate;
}
m_bed_preheat_state.is_preheating = false;
} else {
wait_time = temp_diff / m_bed_heating_rate;
}
// 热床加热较慢,最长300秒(5分钟)
wait_time = std::clamp(wait_time, 0.0f, 300.0f);
if (wait_time > 1.0f) {
simulate_st_synchronize(wait_time);
}
}
}
}
四、实施步骤
步骤1:添加配置参数(15分钟)
-
修改
src/libslic3r/PrintConfig.cpp- 添加
extruder_heating_rate定义 - 添加
bed_heating_rate定义
- 添加
-
修改
src/libslic3r/PrintConfig.hpp- 在
MachineEnvelopeConfig或相应类中声明配置
- 在
-
编译测试
build_release_vs2022.bat slicer
步骤2:修改GCodeProcessor头文件(10分钟)
- 修改
src/libslic3r/GCode/GCodeProcessor.hpp- 添加
PreheatingState结构体定义 - 添加
BedPreheatingState结构体定义 - 添加成员变量:
m_extruder_heating_ratem_bed_heating_ratem_extruder_preheat_statesm_bed_preheat_state
- 添加
步骤3:修改GCodeProcessor实现(45分钟)
-
修改
src/libslic3r/GCode/GCodeProcessor.cppapply_config()- 加载配置reset()- 初始化预热状态process_M104()- 记录喷头预热process_M109()- 计算等待时间(考虑预热)process_M140()- 记录热床预热process_M190()- 计算等待时间(考虑预热)
-
编译测试
build_release_vs2022.bat slicer
步骤4:验证测试(30分钟)
-
简单测试: 单次工具切换的双色模型
- 检查G-code中的M104和M109位置
- 对比估算时间 vs 手动计算时间
- 验证预热逻辑是否生效
-
复杂测试: 50+次工具切换的多色模型
- 对比估算时间改善
- 验证累积误差是否可接受
-
边缘测试:
- 首层打印前的M109(无预热)
- 温度跨度大的切换(PLA→PETG)
- 降温等待(M109 R参数)
步骤5:参数调优(可选)
-
实测U1的加热速率
- 从室温到220°C的加热时间
- 从150°C到220°C的加热时间
- 计算实际加热速率
-
调整U1配置文件中的默认值
{ "extruder_heating_rate": 3.0, // 根据实测调整 "bed_heating_rate": 0.8 }
五、测试计划
5.1 单元测试
测试场景1: 无预热的M109
输入:
- 当前温度:25°C
- 目标温度:220°C
- 无预热状态
预期:
- 温度差:195°C
- 等待时间:195 / 2.5 = 78秒
测试场景2: 预热成功的M109
输入:
- 当前温度:170°C
- 目标温度:220°C
- 预热开始时间:t=100s
- 当前时间:t=120s
- 已预热时间:20s
预期:
- 温度差:50°C
- 已加热温度:20s × 2.5 = 50°C
- 剩余需加热:0°C
- 等待时间:0秒 ✅
测试场景3: 预热部分成功的M109
输入:
- 当前温度:170°C
- 目标温度:220°C
- 预热开始时间:t=100s
- 当前时间:t=110s
- 已预热时间:10s
预期:
- 温度差:50°C
- 已加热温度:10s × 2.5 = 25°C
- 剩余需加热:25°C
- 等待时间:25 / 2.5 = 10秒 ✅
测试场景4: 降温等待(M109 R参数)
输入:
- 当前温度:220°C
- 目标温度:150°C(R参数)
- 无预热(降温不适用预热)
预期:
- 温度差:70°C
- 降温速率:0.8°C/s
- 等待时间:70 / 0.8 = 87.5秒
5.2 集成测试
测试用例1: 双色打印(2-3次工具切换)
模型:简单双色立方体
工具切换次数:3次
预期:
- 每次切换估算时间接近实际时间
- 总时间估算误差 < 10%
测试用例2: 多色打印(50+次工具切换)
模型:复杂多色模型
工具切换次数:50+
预期:
- 累积误差 < 15%
- 不会出现时间估算爆炸
测试用例3: 首层打印(无预热)
场景:首次加热,start_gcode中的M109
预期:
- 正确计算加热等待时间
- 估算时间与实际接近
5.3 性能测试
测试项目:
- 预热状态追踪的内存开销(应该可忽略)
- G-code处理速度(应该无明显影响)
- 大模型处理(100k+行G-code)
六、风险评估与缓解
6.1 风险点
| 风险 | 严重性 | 概率 | 缓解措施 |
|---|---|---|---|
| 预热时间追踪不准确 | 高 | 中 | 详细测试,边界情况处理 |
| 加热速率配置不准确 | 中 | 高 | 提供实测指南,保守默认值 |
| get_time()函数语义错误 | 高 | 低 | 代码审查,验证时间戳 |
| 边缘情况未处理 | 中 | 中 | 全面测试,防御性编程 |
| 性能影响 | 低 | 低 | 性能测试 |
6.2 回滚方案
如果方案B出现严重问题,可以快速回滚到保守方案(方案A):
// 临时禁用预热逻辑的快速修复
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
// ... 解析参数 ...
// 简化版:不考虑预热
float temp_diff = target_temp - current_temp;
if (temp_diff > 5.0f) {
float wait_time = temp_diff / m_extruder_heating_rate;
wait_time = std::clamp(wait_time, 0.0f, 120.0f);
if (wait_time > 1.0f) {
simulate_st_synchronize(wait_time);
}
}
}
七、预期效果
7.1 时间估算改善
场景1: 首层打印前加热
- 现状: 实际60秒,估算0秒
- 修复后: 实际60秒,估算58秒 ✅
场景2: 工具切换(有预热)
- 现状: 实际5秒,估算0秒
- 简单方案: 实际5秒,估算30秒 ❌
- 方案B: 实际5秒,估算3秒 ✅
场景3: 50次工具切换的打印
- 现状: 实际250秒,估算0秒
- 简单方案: 实际250秒,估算1500秒 ❌(多估算20分钟)
- 方案B: 实际250秒,估算150秒 ✅(误差±2.5分钟)
7.2 整体精度提升
| 场景 | 现状误差 | 方案A误差 | 方案B误差 |
|---|---|---|---|
| 单色打印 | ±20% | ±10% | ±5% |
| 双色打印(少量切换) | ±30% | ±25% | ±8% |
| 多色打印(频繁切换) | ±50% | ±70% ⚠️ | ±12% ✅ |
八、后续优化方向
8.1 短期优化(1-2周)
-
GUI配置界面
- 在"打印机设置 → 高级选项"中添加加热速率配置
- 提供"测试加热速率"功能按钮
-
实测数据收集
- 收集U1实际打印的加热时间数据
- 调整默认值以更贴近实际
-
文档完善
- 用户手册:如何测试和配置加热速率
- 开发文档:预热逻辑的实现原理
8.2 中期优化(1-2个月)
-
动态加热速率
- 根据温度区间调整加热速率
- 如:50-150°C较快,150-250°C较慢
-
降温速率配置
- 添加
extruder_cooling_rate配置 - 区分被动降温和主动风扇降温
- 添加
-
遥测数据收集
- 收集估算时间 vs 实际时间的对比数据
- 持续优化算法
8.3 长期优化(3-6个月)
-
机器学习优化
- 基于历史打印数据训练模型
- 预测更精确的加热时间
-
环境因素考虑
- 考虑环境温度对加热速率的影响
- 根据打印材料调整加热速率
九、FAQ
Q1: 为什么不能使用简单方案(方案A)?
A: 简单方案不考虑预热效果,对U1多头机型会导致时间估算严重高估50-100%。U1的预热逻辑是核心功能,每次工具切换都会预热,如果不考虑预热,会错误地为每次切换添加30-40秒等待时间。
Q2: 预热状态追踪会不会很复杂?
A: 实现并不复杂:
- M104时记录预热开始时间和目标温度
- M109时计算已预热时间,扣除已加热的温度
- 清除预热状态
核心逻辑只需要20-30行代码。
Q3: 如果预热时间设置不合理怎么办?
A: 预热时间由preheat_time配置决定,是用户可调的。即使预热时间设置不合理:
- 预热时间过短 → M109会计算剩余等待时间,不会低估
- 预热时间过长 → M109会识别温度已达标,等待时间为0
算法具有鲁棒性。
Q4: 加热速率如何测试?
A: 测试方法:
1. 从室温启动打印机
2. 执行 M104 S220(开始加热)
3. 记录开始时间
4. 观察温度曲线,记录达到220°C的时间
5. 计算速率:(220 - 室温) / 时间
示例:
- 室温:25°C
- 目标:220°C
- 时间:78秒
- 速率:(220-25)/78 = 2.5°C/s
Q5: 方案B会影响性能吗?
A: 几乎不会:
- 预热状态结构体很小(每个喷头16字节)
- 只在处理M104/M109时计算,次数很少
- 计算量微不足道(几次浮点运算)
性能影响可忽略不计。
十、总结
核心要点
✅ 必须实现方案B 对U1多头机型,不考虑预热会导致时间估算严重高估
✅ 实现并不复杂 核心逻辑只需修改6个函数,添加约100行代码
✅ 效果显著 时间估算精度从±30-50%提升到±5-15%
✅ 风险可控 有清晰的测试计划和回滚方案
实施建议
- 一次性完整实现:不要分阶段,必须同时实现预热追踪
- 充分测试:特别是多工具切换场景
- 实测校准:根据U1实测数据调整默认参数
- 文档完善:提供用户配置指南
成功标准
- ✅ 多色打印时间估算误差 < 15%
- ✅ 工具切换等待时间估算接近实际(误差 < 5秒)
- ✅ 无性能影响
- ✅ 无回归bug
文档完成日期: 2025-12-06 待实施状态: 方案已完成,待开发实施