diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 574cc09..9b3ab7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,11 +138,25 @@ jobs: pattern: tianshanos-firmware-* path: firmware + - name: Resolve release tag + id: release_tag + run: | + if [ "${{ github.event_name }}" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + TAG="${GITHUB_REF_NAME}" + else + VERSION="${{ needs.build.outputs.version }}" + TAG="v${VERSION%%+*}" + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "Release tag: $TAG" + - name: Check if release already exists (main branch) if: github.ref == 'refs/heads/main' id: check run: | - TAG="v${{ needs.build.outputs.version }}" + TAG="${{ steps.release_tag.outputs.tag }}" if gh release view "$TAG" 2>/dev/null; then echo "skip=true" >> $GITHUB_OUTPUT echo "⏭️ Release $TAG already exists, skipping" @@ -154,8 +168,8 @@ jobs: if: github.event_name == 'release' || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && steps.check.outputs.skip != 'true') uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.event_name == 'release' && github.event.release.tag_name || startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.build.outputs.version) }} - name: ${{ github.event_name == 'release' && github.event.release.tag_name || startsWith(github.ref, 'refs/tags/') && github.ref_name || format('v{0}', needs.build.outputs.version) }} + tag_name: ${{ steps.release_tag.outputs.tag }} + name: ${{ steps.release_tag.outputs.tag }} files: | firmware/**/*.bin firmware/**/flasher_args.json diff --git a/README.md b/README.md index 7d57ded..14a4c6b 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,8 @@ esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \ ## 当前状态 -**版本**: 0.4.6 -**阶段**: Phase 39 完成 - LPMU 接入上层网络 +**版本**: 0.4.7 +**阶段**: Phase 40 完成 - 自动风扇温度失效降速优化 ### 已完成功能 diff --git a/components/ts_drivers/src/ts_fan.c b/components/ts_drivers/src/ts_fan.c index e127e98..8c3cf39 100644 --- a/components/ts_drivers/src/ts_fan.c +++ b/components/ts_drivers/src/ts_fan.c @@ -39,6 +39,7 @@ #define FAN_AUTO_GAIN_MAX 2.00f #define FAN_AUTO_RISE_RATE_PER_SEC 12.0f #define FAN_AUTO_FALL_RATE_PER_SEC 0.30f +#define FAN_AUTO_STALE_DUTY 25 #define FAN_AUTO_HISTORY_SIZE 32 /*===========================================================================*/ @@ -405,16 +406,19 @@ static esp_err_t apply_adaptive_auto(fan_instance_t *fan, const ts_temp_data_t * int16_t control_temp = TS_TEMP_DEFAULT_VALUE; int16_t guard_temp = TS_TEMP_DEFAULT_VALUE; uint8_t old_duty = fan->current_duty; + bool recovered_from_stale = fan->temp_stale; if (!get_adaptive_temperatures(temp_data, &control_temp, &guard_temp)) { fan->temp_stale = true; fan->auto_state = TS_FAN_AUTO_STATE_STALE; fan->response_observing = false; fan->auto_fall_credit = 0.0f; - fan->target_duty = 100; + fan->guard_active = false; + fan->guard_release_since_ms = 0; + fan->target_duty = FAN_AUTO_STALE_DUTY; fan->predicted_temperature = fan->control_temperature; fan->last_auto_update_ms = now_ms; - return update_pwm(fan, 100); + return update_pwm(fan, FAN_AUTO_STALE_DUTY); } fan->temp_stale = false; @@ -494,11 +498,16 @@ static esp_err_t apply_adaptive_auto(fan_instance_t *fan, const ts_temp_data_t * bool allow_down = (guard_temp <= FAN_AUTO_GUARD_RELEASE_TEMP) && (!slope_valid || fan->slope_c_per_min <= 0.0f); - if (target < fan->current_duty && !allow_down) { + if (target < fan->current_duty && !allow_down && !recovered_from_stale) { target = fan->current_duty; } - target = apply_auto_rate_limit(fan, target, allow_down, now_ms); + if (recovered_from_stale && target < fan->current_duty) { + fan->auto_fall_credit = 0.0f; + fan->last_auto_update_ms = now_ms; + } else { + target = apply_auto_rate_limit(fan, target, allow_down, now_ms); + } fan->target_duty = target; esp_err_t ret = update_pwm(fan, target); diff --git a/components/ts_webui/web/css/style.css b/components/ts_webui/web/css/style.css index 1f2150f..863c703 100644 --- a/components/ts_webui/web/css/style.css +++ b/components/ts_webui/web/css/style.css @@ -3001,13 +3001,32 @@ button.btn-gray:hover, .fan-header { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; + gap: 12px; margin-bottom: 16px; } +.fan-header-main { + display: flex; + align-items: center; + flex: 1; + flex-wrap: wrap; + gap: 8px 10px; + min-width: 0; +} + +.fan-header-actions { + display: flex; + align-items: center; + flex: 0 0 auto; + gap: 8px; +} + .fan-title { font-size: 1.1rem; font-weight: 600; + line-height: 1.5; + white-space: nowrap; } .fan-status-badge { @@ -3038,26 +3057,23 @@ button.btn-gray:hover, } .fan-auto-info-btn { - width: 24px; - height: 24px; + width: 22px; + height: 22px; padding: 0; - margin-left: 6px; - border: 1px solid var(--border-color); + border: none; border-radius: 50%; - background: var(--bg-elevated); + background: transparent; color: var(--text-muted); display: inline-flex; align-items: center; justify-content: center; - vertical-align: 16px; cursor: pointer; transition: all var(--t-fast); } .fan-auto-info-btn:hover { color: var(--text-primary); - background: var(--bg-card); - border-color: var(--text-muted); + background: transparent; } .fan-auto-info-btn i { @@ -3094,9 +3110,10 @@ button.btn-gray:hover, flex-wrap: wrap; gap: 6px; align-items: center; - margin: -4px 0 14px; + margin: 0; font-size: 0.75rem; color: var(--text-light); + min-width: 0; } .fan-auto-meta span { diff --git a/components/ts_webui/web/js/app.js b/components/ts_webui/web/js/app.js index 82e4560..5bf7863 100644 --- a/components/ts_webui/web/js/app.js +++ b/components/ts_webui/web/js/app.js @@ -1484,40 +1484,46 @@ function updateFanInfo(data) { const _autoHelpTitleSafe = typeof escapeHtml === 'function' ? escapeHtml(_autoHelpTitle) : _autoHelpTitle; const autoStateLabels = { idle: typeof t === 'function' ? t('fanPage.autoStateIdle') : '待机', - baseline: typeof t === 'function' ? t('fanPage.autoStateBaseline') : '基线', - active: typeof t === 'function' ? t('fanPage.autoStateActive') : '自适应', - guard: typeof t === 'function' ? t('fanPage.autoStateGuard') : '保护', - stale: typeof t === 'function' ? t('fanPage.autoStateStale') : '失效', + baseline: typeof t === 'function' ? t('fanPage.autoStateBaseline') : '按曲线运行', + active: typeof t === 'function' ? t('fanPage.autoStateActive') : '自适应调速', + guard: typeof t === 'function' ? t('fanPage.autoStateGuard') : '保护介入', + stale: typeof t === 'function' ? t('fanPage.autoStateStale') : '温度失效', unknown: typeof t === 'function' ? t('fanPage.autoStateUnknown') : '未知' }; const autoState = fan.auto_state || 'unknown'; const autoStateText = autoStateLabels[autoState] || autoState; - const guardLabel = typeof t === 'function' ? t('fanPage.guardTempShort') : '保护'; - const predictedLabel = typeof t === 'function' ? t('fanPage.predictedTempShort') : '预测'; + const guardLabel = typeof t === 'function' ? t('fanPage.guardTempShort') : '安全参考温度'; + const predictedLabel = typeof t === 'function' ? t('fanPage.predictedTempShort') : '45秒预测'; + const slopeLabel = typeof t === 'function' ? t('fanPage.slopeTempShort') : '升温速度'; + const slopeUnit = typeof t === 'function' ? t('fanPage.slopeTempUnit') : '°C/分钟'; const autoMeta = hasAutoTelemetry ? `
` : ''; return `