Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ esptool.py --chip esp32s3 -p /dev/ttyACM0 write_flash \

## 当前状态

**版本**: 0.4.6
**阶段**: Phase 39 完成 - LPMU 接入上层网络
**版本**: 0.4.7
**阶段**: Phase 40 完成 - 自动风扇温度失效降速优化

### 已完成功能

Expand Down
17 changes: 13 additions & 4 deletions components/ts_drivers/src/ts_fan.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

/*===========================================================================*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
37 changes: 27 additions & 10 deletions components/ts_webui/web/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 19 additions & 13 deletions components/ts_webui/web/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? `
<div class="fan-auto-meta ${fan.guard_active ? 'is-guard' : ''} ${fan.temp_stale ? 'is-stale' : ''}">
<span>${autoStateText}</span>
${typeof fan.guard_temperature === 'number' ? `<span>${guardLabel} ${fan.guard_temperature.toFixed(1)}°C</span>` : ''}
${typeof fan.predicted_temperature === 'number' ? `<span>${predictedLabel} ${fan.predicted_temperature.toFixed(1)}°C</span>` : ''}
${typeof fan.slope_c_per_min === 'number' ? `<span>${fan.slope_c_per_min.toFixed(2)}°C/min</span>` : ''}
${typeof fan.slope_c_per_min === 'number' ? `<span>${slopeLabel} ${fan.slope_c_per_min.toFixed(2)}${slopeUnit}</span>` : ''}
</div>
` : '';

return `
<div class="fan-card ${isOff ? 'is-off' : ''}">
<div class="fan-header">
<span class="fan-title">${_fanTitle}</span>
<span class="fan-status-badge" style="background:${currentMode.color}20;color:${currentMode.color}" title="${currentMode.label}">
<i class="${currentMode.iconRi}"></i>
</span>
<div class="fan-header-main">
<span class="fan-title">${_fanTitle}</span>
${autoMeta}
</div>
<div class="fan-header-actions">
${mode === 'auto' ? `<button type="button" class="fan-auto-info-btn" onclick="showFanAutoHelpModal()" title="${_autoHelpTitleSafe}" aria-label="${_autoHelpTitleSafe}"><i class="ri-information-line"></i></button>` : ''}
<span class="fan-status-badge" style="background:${currentMode.color}20;color:${currentMode.color}" title="${currentMode.label}">
<i class="${currentMode.iconRi}"></i>
</span>
</div>
</div>
<div class="fan-speed-display">
<span class="fan-speed-num">${displayDuty}</span>
<span class="fan-speed-percent">%</span>
${mode === 'auto' ? `<button type="button" class="fan-auto-info-btn" onclick="showFanAutoHelpModal()" title="${_autoHelpTitleSafe}" aria-label="${_autoHelpTitleSafe}"><i class="ri-information-line"></i></button>` : ''}
${rpm > 0 ? `<div class="fan-rpm-small">${rpm} RPM</div>` : ''}
</div>
${autoMeta}
<div class="fan-mode-tabs">
<button class="fan-mode-tab ${mode === 'off' ? 'active off' : ''}" onclick="setFanMode(${fan.id}, 'off')">${_off}</button>
<button class="fan-mode-tab ${mode === 'manual' ? 'active manual' : ''}" onclick="setFanMode(${fan.id}, 'manual')">${_manual}</button>
Expand Down
14 changes: 8 additions & 6 deletions components/ts_webui/web/js/lang/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -2735,13 +2735,15 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('en-US', {
editTempCurve: 'Edit Temperature Curve',
manualModeHint: 'Switch to manual mode to adjust',
autoStateIdle: 'Idle',
autoStateBaseline: 'Baseline',
autoStateActive: 'Adaptive',
autoStateGuard: 'Guard',
autoStateStale: 'Stale',
autoStateBaseline: 'Following curve',
autoStateActive: 'Adaptive speed',
autoStateGuard: 'Protection active',
autoStateStale: 'Temperature stale',
autoStateUnknown: 'Unknown',
guardTempShort: 'Guard',
predictedTempShort: 'Predicted',
guardTempShort: 'Safety reference',
predictedTempShort: '45s forecast',
slopeTempShort: 'Rise rate',
slopeTempUnit: '°C/min',
autoHelpTitle: 'What makes Auto mode different?',
autoHelpIntro: 'Auto mode adjusts fan speed based on the current cooling condition of the device, instead of only following a fixed curve.',
autoHelpCurveDiff: 'Curve mode works like a fixed rule table: when temperature reaches a point, the fan uses the matching speed. It is stable and predictable when you want to define the cooling behavior yourself.',
Expand Down
14 changes: 8 additions & 6 deletions components/ts_webui/web/js/lang/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -2720,13 +2720,15 @@ if (typeof i18n !== 'undefined') i18n.registerLanguage('zh-CN', {
editTempCurve: '编辑温度曲线',
manualModeHint: '切换到手动模式后可调节',
autoStateIdle: '待机',
autoStateBaseline: '基线',
autoStateActive: '自适应',
autoStateGuard: '保护',
autoStateStale: '失效',
autoStateBaseline: '按曲线运行',
autoStateActive: '自适应调速',
autoStateGuard: '保护介入',
autoStateStale: '温度失效',
autoStateUnknown: '未知',
guardTempShort: '保护',
predictedTempShort: '预测',
guardTempShort: '安全参考温度',
predictedTempShort: '45秒预测',
slopeTempShort: '升温速度',
slopeTempUnit: '°C/分钟',
autoHelpTitle: '自动模式有什么不同?',
autoHelpIntro: '自动模式会根据设备当前的散热状态实时调节风扇,而不是只按一条固定曲线运行。',
autoHelpCurveDiff: '曲线模式更像一张固定规则表:温度到多少,风扇就转到多少。它稳定、可预期,适合你想手动定义散热策略的场景。',
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.6
0.4.7
Loading