作为一名机械牛马,有时调试过程中,需要打皮带(测量皮带张紧力)。发现既然张力计的原理是通过采集皮带振动的声音,而手机本身不就有麦克风哩。经网上简单搜寻,App Store 和 Play 商店均没有此类应用,那么便可自己开发一款🤔。

成品在音波式皮带张力计,点击即用。

所有计算均在您的设备本地完成,加载完成后可无需网络服务。也可在这里找到
博客最下方页脚处
博客最下方页脚处

技术原理

当我们通过施加冲击使皮带轮之间的皮带振动时,它开始不规则地摆动,但逐渐地摆动会形成固有的规则运动,此时可以用如下公式描述。

$$ T = 4 \cdot M \cdot W \cdot S^2 \cdot f^2 \cdot 10^{-9} $$

符号含义单位
$T$张力N
$M$单位质量g/m/mm
$W$皮带宽度mm
$S$跨距mm
$f$振动频率Hz
  • $S$ 为跨距(外公切线长),而非中心距。 在已知大小轮直径和中心距的情况下,跨距 $S$ 可通过如下公式计算,$L$ 为中心距,$D$ 和 $d$ 分别为大小圆直径。
    $$ S = \sqrt{L^2 - (\frac{D - d}{2})^2} $$

  • $10^{-9}$ 来自单位换算(g→kgmm→m)。

代码实现

这一块主要靠的是 chatGPT,源代码如下,也可以通过 GitHub 查看。如果有更好的稳定段检测或滤波逻辑,欢迎与我展开讨论。

GitHub

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<!-- 最重要的视口设置 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">

<title id="title"></title>
<style>
:root {
--bg: #ffffff;
--fg: #000000;
--card: #f3f3f3;
--primary: #007bff;
--primary-hover: #0056b3;
}
.dark {
--bg: #121212;
--fg: #e0e0e0;
--card: #1e1e1e;
--primary: #0d6efd;
--primary-hover: #0b5ed7;
}

body {
background: var(--bg);
color: var(--fg);
font-family: system-ui, sans-serif;
max-width: 480px;
margin: 20px auto;
}
.card {
background: var(--card);
padding: 10px;
border-radius: 6px;
}
input, select, button {
font-size: 16px;
margin: 6px 0;
}

/* 主题按钮 - 小尺寸 */
#theme {
width: auto;
padding: 8px 16px;
height: auto;
min-height: 40px; /* 保持最小高度 */
font-size: 14px;
font-weight: normal;
background: var(--card);
color: var(--fg);
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}

#theme:hover {
background: var(--bg);
border-color: var(--primary);
}

.dark #theme {
border-color: #555;
}

.dark #theme:hover {
border-color: var(--primary);
}

/* 开始按钮 - 大尺寸 */
#startBtn {
width: 100%;
height: 64px;
font-size: 24px;
font-weight: bold;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 10px;
margin-bottom: 10px;
}

#startBtn:hover {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}

#startBtn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(0, 123, 255, 0.2);
}

/* 其他输入元素样式 */
input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: var(--bg);
color: var(--fg);
box-sizing: border-box;
}
select {
width: 50%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background: var(--bg);
color: var(--fg);
box-sizing: border-box;
}
.dark input, .dark select {
border-color: #555;
background: #2a2a2a;
}

canvas {
width: 100%;
height: 160px;
border: 1px solid #555;
margin-top: 10px;
justify-content: center;
}

.topbar {
display: flex;
gap: 6px;
align-items: center;
margin-bottom: 15px;
}

#lang {
flex-grow: 1;
height: 40px; /* 与主题按钮高度匹配 */
}

</style>
</head>

<body>

<h2 id="caption" style="text-align: center"></h2>

<div class="topbar">
<select id="lang">
<option value="zh-CN">简体中文</option>
<option value="zh-TW">繁體中文</option>
<option value="en">English</option>
<option value="ja">日本語</option>
</select>
<button id="theme"></button>
</div>

<div class="card">
<label id="lblM"></label>
<input id="mass" type="number" value="4.1">

<label id="lblW"></label>
<input id="width" type="number" value="10">

<label id="lblS"></label>
<input id="span" type="number" value="100">

<input type="range" min="0.3" max="5" step="0.1" value="3" id="mySlider">

<button id="startBtn"></button>
<p><label id="timeleft"></label><label id="countdown">--</label> / <span id="sliderValue">3.0</span></p>
<p><label id="l_now"></label><label id="nowT">--</label>&nbsp;&nbsp;
<label id="l_frc"></label><label id="nowf">--</label></p>

<p style="text-align: center">
<canvas id="curve"></canvas>
<span id="l_avg"></span><span id="avg">--</span>&nbsp;&nbsp;
<span id="l_max"></span><span id="max">--</span>&nbsp;&nbsp;
<span id="l_min"></span><span id="min">--</span>
</p>

</div>

<div class="card">
<span id="readme" style="white-space: pre-wrap"></span>
<p style="font-family: 'Times New Roman', serif; text-align: center"><big><em>T = 4 · M · W · f ² · S ²</em></big></p>
<p><span id="friend"></span>
<a href = "https://20508888.xyz/" target= "_blank">Malvern's Blog</a>&nbsp;
<a href = "https://github.com/MalvernLove/Acoustic-Belt-Tension-Meter" target= "_blank">GitHub</a>&nbsp;
<a href = "http://chfs.20508888.xyz" target= "_blank" >File Server</a>&nbsp;
<a href = "https://uf.20508888.xyz/" target= "_blank">Malvernの状态监测</a></p>
</div>

<script src="app.js"></script>
</body>
</html>

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/* ================= 工具函数 ================= */
const $ = id => document.getElementById(id);

/* ========= 多语言 ========= */
const I18N = {
"zh-CN": {
caption: "音波式皮带张力计",
title: "音波式皮带张力计",
M: "单位质量 M(g / m / mm)",
W: "皮带宽度 W(mm)",
S: "跨距 S(mm)",
start: "开始测量",
timeleft: "剩余时间:",
l_now: "实时张力 T:",
l_frc: "实时频率 f:",
l_avg: "平均值:",
l_max: "最大值:",
l_min: "最小值:",
friend: "友情链接:",
theme: "深色/浅色界面🌗",
readme: `1. 左右移动滑块可调整测量时间,与皮带振动周期相等为最佳。
2. 如果波形图中未出现稳定波形(绿色段),建议重新测量。
3. 本工具仅供参考,无法保证得出的皮带张力值完全可靠。
4. 上述平均值、最大值和最小值均为稳定波形(绿色段)内的。
5. 计算原理如下,其中T为张力,M为单位质量,W为皮带宽度,f为频率,S为跨距。`
},
"zh-TW": {
caption: "音波式皮帶張力計",
title: "音波式皮帶張力計",
M: "單位質量 M(g / m / mm)",
W: "皮帶寬度 W(mm)",
S: "跨距 S(mm)",
start: "開始測量",
timeleft: "剩餘時間:",
l_now: "即時張力 T:",
l_frc: "實時頻率 f:",
l_avg: "平均值:",
l_max: "最大值:",
l_min: "最小值:",
friend: "友情連結:",
theme: "深色/淺色介面🌗",
readme: `1. 左右移動滑塊可調整測量時間,與皮帶振動週期相等為最佳。
2. 如果波形圖中未出現穩定波形(綠色段),建議重新測量。
3. 本工具僅供參考,無法保證所得的皮帶張力值完全可靠。
4. 上述平均值、最大值和最小值均為穩定波形(綠色段)內的。
5. 計算原理如下,其中T為張力,M為單位質量,W為皮帶寬度,f為頻率,S為跨距。`
},
"en": {
caption: "Acoustic Belt Tension Meter",
title: "Acoustic Belt Tension Meter",
M: "Mass Per Unit (g / m / mm)",
W: "Belt Width (mm)",
S: "Span Length (mm)",
start: "Start",
timeleft: "Time Left: ",
l_now: "Current Tension T: ",
l_frc: "Current Frequency f: ",
l_avg: "Average: ",
l_max: "Maximum: ",
l_min: "Minimum: ",
friend: "Friendly Links: ",
theme: "Light / Dark Mode🌗",
readme: `1. The measurement time can be adjusted by moving the slider left and right, and it is best to match the belt vibration period.
2. If a stable waveform (green segment) does not appear in the waveform graph, I recommend you to measure again.
3. This tool is for reference only, and I cannot guarantee that the belt tension is completely reliable.
4. The average, maximum, and minimum values are all within the stable waveform (green segment).
5. The calculation principle is as follows: T: Tension, M: Mass Per Unit, W: Belt Width, f: Frequency, S: Span Length.`
},
"ja": {
caption: "音波式ベルト張力計",
title: "音波式ベルト張力計",
M: "単位質量 M(g / m / mm)",
W: "ベルト幅 W(mm)",
S: "スパン長 S(mm)",
start: "測定開始",
timeleft: "残り時間: ",
l_now: "現在張力 T: ",
l_frc: "現在周波数 f: ",
l_avg: "平均: ",
l_max: "最大: ",
l_min: "最小: ",
friend: "フレンドリーリンク: ",
theme: "ライト/ダークモード🌗",
readme: `1. スライダーを左右に動かして測定時間を調整します。理想的には、ベルトの振動周期に合わせます。
2. 波形グラフに安定した波形(緑色のセグメント)が表示されない場合は、再測定をお勧めします。
3. このツールは参考用であり、得られたベルト張力値の信頼性は保証されません。
4. 上記の平均値、最大値、最小値はすべて、安定した波形(緑色のセグメント)の範囲内です。
5. 計算原理は次のとおりです。T:張力、M:単位質量、W:ベルト幅、f:周波数、S:スパン長です。`
}
};

let lang = navigator.language || navigator.userLanguage;

function applyLang() {
const t = I18N[lang];
$("title").textContent = t.title;
$("caption").textContent = t.caption;
$("lblM").textContent = t.M;
$("lblW").textContent = t.W;
$("lblS").textContent = t.S;
$("startBtn").textContent = t.start;
$("friend").textContent = t.friend;
$("theme").textContent = t.theme;
$("timeleft").textContent = t.timeleft;
$("l_now").textContent = t.l_now;
$("l_frc").textContent = t.l_frc;
$("l_avg").textContent = t.l_avg;
$("l_max").textContent = t.l_max;
$("l_min").textContent = t.l_min;
$("readme").textContent = t.readme;
}
$("lang").onchange = e => { lang = e.target.value; applyLang(); };

applyLang();

/* ================= 黑暗模式(系统级) ================= */
const mediaDark = window.matchMedia("(prefers-color-scheme: dark)");
let userOverrideTheme = null; // null = 跟随系统

function applyTheme(isDark) {
document.body.classList.toggle("dark", isDark);
}

// 初始应用系统主题
applyTheme(mediaDark.matches);

// 监听系统主题变化
mediaDark.addEventListener("change", e => {
if (userOverrideTheme === null) {
applyTheme(e.matches);
}
});

// 用户手动切换(覆盖系统)
$("theme").onclick = () => {
const isDark = !document.body.classList.contains("dark");
userOverrideTheme = isDark;
applyTheme(isDark);
};


/* ================= 参数 ================= */
const slider = document.getElementById("mySlider");
const sliderValue = document.getElementById("sliderValue");
slider.addEventListener("input", function () {
sliderValue.textContent = parseFloat(this.value).toFixed(1);
});
let RECORD_MS = parseFloat(slider.value) * 1000;
const STABLE_WINDOW = 200; // ms
const STABLE_RATIO = 0.05; // 5%
const STABLE_MIN_MS = 500; // ms

/* ================= 画布 ================= */
const canvas = $("curve");
const ctx = canvas.getContext("2d");
canvas.width = 460;
canvas.height = 160;

/* ================= 音频 ================= */
let audioCtx, analyser, freqData;

/* ================= 状态 ================= */
let recording = false;
let startTime = 0;

/*
series: [{t, T}]
stableSeries: 稳定段数据
*/
let series = [];
let stableSeries = [];

/* ================= 核心计算 ================= */
function calcTension(f) {
const M = Number(mass.value);
const W = Number(width.value);
const S = Number(span.value);
return 4 * M * W * S * S * f * f * 1e-9;
}

/* ================= 稳定段检测 ================= */
function detectStableSegment() {
stableSeries = [];

let stableStart = null;

for (let i = 0; i < series.length; i++) {
const t0 = series[i].t - STABLE_WINDOW;
const win = series.filter(p => p.t >= t0 && p.t <= series[i].t);

if (win.length < 5) continue;

const Ts = win.map(p => p.T);
const mean = Ts.reduce((a,b)=>a+b,0) / Ts.length;
const std = Math.sqrt(
Ts.reduce((a,b)=>a+(b-mean)**2,0) / Ts.length
);

if (std / mean < STABLE_RATIO) {
if (stableStart === null) stableStart = series[i].t;
} else {
if (stableStart !== null &&
series[i].t - stableStart >= STABLE_MIN_MS) {
stableSeries = series.filter(
p => p.t >= stableStart && p.t <= series[i].t
);
return;
}
stableStart = null;
}
}

if (stableStart !== null) {
stableSeries = series.filter(p => p.t >= stableStart);
}
}

/* ================= 曲线绘制 ================= */
function drawCurve() {
ctx.clearRect(0,0,canvas.width,canvas.height);
if (series.length < 2) return;

const Ts = series.map(p=>p.T);
const minT = Math.min(...Ts);
const maxT = Math.max(...Ts);
const span = Math.max(1, maxT - minT);

ctx.beginPath();
series.forEach((p,i)=>{
const x = p.t / RECORD_MS * canvas.width ;
const y = canvas.height - (p.T - minT) / span * canvas.height;
i ? ctx.lineTo(x,y) : ctx.moveTo(x,y);
});
ctx.strokeStyle = "#0070f3";
ctx.lineWidth = 2;
ctx.stroke();

// 稳定段高亮
if (stableSeries.length > 1) {
ctx.beginPath();
stableSeries.forEach((p,i)=>{
const x = p.t / RECORD_MS * canvas.width;
const y = canvas.height - (p.T - minT) / span * canvas.height;
i ? ctx.lineTo(x,y) : ctx.moveTo(x,y);
});
ctx.strokeStyle = "#00c853";
ctx.lineWidth = 3;
ctx.stroke();
}
}

/* ================= 结束处理 ================= */
function finishMeasurement() {
recording = false;

detectStableSegment();
const data = stableSeries.length ? stableSeries : series;
if (data.length === 0) return;

const Ts = data.map(p=>p.T);
const avg = Ts.reduce((a,b)=>a+b,0) / Ts.length;

$("avg").textContent = `${avg.toFixed(1)} N`;
$("max").textContent = `${Math.max(...Ts).toFixed(1)} N`;
$("min").textContent = `${Math.min(...Ts).toFixed(1)} N`;

drawCurve();
}

/* ================= 主循环 ================= */
function loop() {
if (!recording) return;

analyser.getByteFrequencyData(freqData);

const sr = audioCtx.sampleRate;
const fft = analyser.fftSize;

let idx = 0;
for (let i = 1; i < freqData.length; i++)
if (freqData[i] > freqData[idx]) idx = i;

if (freqData[idx] > 20) {
const f = idx * sr / fft;
const T = calcTension(f);

const t = performance.now() - startTime;
series.push({ t, T });

$("nowT").textContent = `${T.toFixed(1)} N`;
$("nowf").textContent = `${f.toFixed(1)} Hz`;
drawCurve();
}

const remain = Math.max(0, (RECORD_MS - (performance.now()-startTime))/1000);
$("countdown").textContent = remain.toFixed(1);

if (remain > 0) {
requestAnimationFrame(loop);
} else {
finishMeasurement();
}
}

/* ================= 启动 ================= */
$("startBtn").onclick = async () => {

RECORD_MS = Number(slider.value) * 1000;

series = [];
stableSeries = [];

if (!audioCtx) {
audioCtx = new AudioContext();
const stream = await navigator.mediaDevices.getUserMedia({audio:true});
analyser = audioCtx.createAnalyser();
analyser.fftSize = 4096;
audioCtx.createMediaStreamSource(stream).connect(analyser);
freqData = new Uint8Array(analyser.frequencyBinCount);
}

recording = true;
startTime = performance.now();
loop();
};

成品预览

音波式皮带张力计(Web版)
音波式皮带张力计(Web 版)

网站地图 | 状态监测 | 皮带张力测试 | File Server | 博友圈 | 博客说
Copyright 2022-2026 | Powered by Hexo 7.3.0 & Stellar 1.33.1
总访问量次 |