功能: 添加消耗少量流量功能
This commit is contained in:
@@ -119,6 +119,9 @@
|
|||||||
<div class="bento-control__actions">
|
<div class="bento-control__actions">
|
||||||
<button class="button button--secondary button--compact" id="mobileDataToggle" type="button"
|
<button class="button button--secondary button--compact" id="mobileDataToggle" type="button"
|
||||||
data-action="disable">强制关闭流量</button>
|
data-action="disable">强制关闭流量</button>
|
||||||
|
<button class="button button--primary button--compact is-hidden" id="mobileDataConsume"
|
||||||
|
type="button">消耗一点流量</button>
|
||||||
|
<span class="form-status mobile-data-traffic-hint" id="mobileDataTrafficHint">开启流量后才会显示消耗流量按钮。</span>
|
||||||
<p class="form-status mobile-data-status" id="mobileDataStatus" role="status"></p>
|
<p class="form-status mobile-data-status" id="mobileDataStatus" role="status"></p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
+43
-1
@@ -1,6 +1,8 @@
|
|||||||
const state = {
|
const state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
mobileDataBusy: false,
|
mobileDataBusy: false,
|
||||||
|
trafficBusy: false,
|
||||||
|
currentMobileData: null,
|
||||||
pushChannels: [],
|
pushChannels: [],
|
||||||
selectedPushChannel: -1,
|
selectedPushChannel: -1,
|
||||||
pushLoaded: false
|
pushLoaded: false
|
||||||
@@ -39,6 +41,8 @@ const elements = {
|
|||||||
mobileDataState: document.querySelector('#mobileDataState'),
|
mobileDataState: document.querySelector('#mobileDataState'),
|
||||||
mobileDataHint: document.querySelector('#mobileDataHint'),
|
mobileDataHint: document.querySelector('#mobileDataHint'),
|
||||||
mobileDataToggle: document.querySelector('#mobileDataToggle'),
|
mobileDataToggle: document.querySelector('#mobileDataToggle'),
|
||||||
|
mobileDataConsume: document.querySelector('#mobileDataConsume'),
|
||||||
|
mobileDataTrafficHint: document.querySelector('#mobileDataTrafficHint'),
|
||||||
mobileDataStatus: document.querySelector('#mobileDataStatus'),
|
mobileDataStatus: document.querySelector('#mobileDataStatus'),
|
||||||
moduleModel: document.querySelector('#moduleModel'),
|
moduleModel: document.querySelector('#moduleModel'),
|
||||||
moduleVersion: document.querySelector('#moduleVersion'),
|
moduleVersion: document.querySelector('#moduleVersion'),
|
||||||
@@ -180,6 +184,7 @@ function formatMobileDataContexts(contexts = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderMobileData(mobileData = {}) {
|
function renderMobileData(mobileData = {}) {
|
||||||
|
state.currentMobileData = mobileData;
|
||||||
const unknown = mobileData.status === 'unknown';
|
const unknown = mobileData.status === 'unknown';
|
||||||
const enabled = Boolean(mobileData.enabled || mobileData.anyActive);
|
const enabled = Boolean(mobileData.enabled || mobileData.anyActive);
|
||||||
const cid = mobileData.cid || 1;
|
const cid = mobileData.cid || 1;
|
||||||
@@ -193,21 +198,30 @@ function renderMobileData(mobileData = {}) {
|
|||||||
setText(elements.mobileDataHint, mobileData.error || (isMipcall ? '未能确认应用层拨号状态,可先强制断开。' : '未能确认移动数据状态,可先强制关闭。'));
|
setText(elements.mobileDataHint, mobileData.error || (isMipcall ? '未能确认应用层拨号状态,可先强制断开。' : '未能确认移动数据状态,可先强制关闭。'));
|
||||||
elements.mobileDataToggle.textContent = isMipcall ? '强制断开拨号' : '强制关闭流量';
|
elements.mobileDataToggle.textContent = isMipcall ? '强制断开拨号' : '强制关闭流量';
|
||||||
elements.mobileDataToggle.dataset.action = 'disable';
|
elements.mobileDataToggle.dataset.action = 'disable';
|
||||||
|
elements.mobileDataConsume.classList.add('is-hidden');
|
||||||
|
elements.mobileDataTrafficHint.classList.remove('is-hidden');
|
||||||
|
elements.mobileDataTrafficHint.textContent = '状态确认后,开启流量才会显示消耗流量按钮。';
|
||||||
} else if (enabled) {
|
} else if (enabled) {
|
||||||
setText(elements.mobileDataSummary, isMipcall ? '已连接' : '已开启');
|
setText(elements.mobileDataSummary, isMipcall ? '已连接' : '已开启');
|
||||||
setText(elements.mobileDataState, isMipcall ? '应用拨号已连接' : '流量已开启');
|
setText(elements.mobileDataState, isMipcall ? '应用拨号已连接' : '流量已开启');
|
||||||
setText(elements.mobileDataHint, activeContexts ? `${mode} 检测到 ${activeContexts} 处于活动状态。` : `CID ${cid} 处于活动状态。`);
|
setText(elements.mobileDataHint, activeContexts ? `${mode} 检测到 ${activeContexts} 处于活动状态。` : `CID ${cid} 处于活动状态。`);
|
||||||
elements.mobileDataToggle.textContent = isMipcall ? '断开拨号' : '关闭流量';
|
elements.mobileDataToggle.textContent = isMipcall ? '断开拨号' : '关闭流量';
|
||||||
elements.mobileDataToggle.dataset.action = 'disable';
|
elements.mobileDataToggle.dataset.action = 'disable';
|
||||||
|
elements.mobileDataConsume.classList.remove('is-hidden');
|
||||||
|
elements.mobileDataTrafficHint.classList.add('is-hidden');
|
||||||
} else {
|
} else {
|
||||||
setText(elements.mobileDataSummary, isMipcall ? '已断开' : '已关闭');
|
setText(elements.mobileDataSummary, isMipcall ? '已断开' : '已关闭');
|
||||||
setText(elements.mobileDataState, isMipcall ? '应用拨号已断开' : '流量已关闭');
|
setText(elements.mobileDataState, isMipcall ? '应用拨号已断开' : '流量已关闭');
|
||||||
setText(elements.mobileDataHint, isMipcall ? `${mode} CID ${cid} 未建立应用层拨号连接;已保护性尝试停用PDP。` : `${mode} CID ${cid} 未建立移动数据连接,重启后也会先保持关闭。`);
|
setText(elements.mobileDataHint, isMipcall ? `${mode} CID ${cid} 未建立应用层拨号连接;已保护性尝试停用PDP。` : `${mode} CID ${cid} 未建立移动数据连接,重启后也会先保持关闭。`);
|
||||||
elements.mobileDataToggle.textContent = isMipcall ? '开启拨号' : '开启流量';
|
elements.mobileDataToggle.textContent = isMipcall ? '开启拨号' : '开启流量';
|
||||||
elements.mobileDataToggle.dataset.action = 'enable';
|
elements.mobileDataToggle.dataset.action = 'enable';
|
||||||
|
elements.mobileDataConsume.classList.add('is-hidden');
|
||||||
|
elements.mobileDataTrafficHint.classList.remove('is-hidden');
|
||||||
|
elements.mobileDataTrafficHint.textContent = isMipcall ? '先开启拨号后,才可以执行一次MPING消耗少量流量。' : '先开启流量后,才可以执行一次MPING消耗少量流量。';
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.mobileDataToggle.disabled = state.mobileDataBusy;
|
elements.mobileDataToggle.disabled = state.mobileDataBusy;
|
||||||
|
elements.mobileDataConsume.disabled = state.mobileDataBusy || state.trafficBusy || !enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatus(status, info) {
|
function renderStatus(status, info) {
|
||||||
@@ -681,7 +695,34 @@ async function toggleMobileData() {
|
|||||||
setStatusMessage(elements.mobileDataStatus, err.message, 'error');
|
setStatusMessage(elements.mobileDataStatus, err.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
state.mobileDataBusy = false;
|
state.mobileDataBusy = false;
|
||||||
elements.mobileDataToggle.disabled = false;
|
renderMobileData(state.currentMobileData || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consumeMobileDataTraffic() {
|
||||||
|
if (state.mobileDataBusy || state.trafficBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.trafficBusy = true;
|
||||||
|
elements.mobileDataToggle.disabled = true;
|
||||||
|
elements.mobileDataConsume.disabled = true;
|
||||||
|
setStatusMessage(elements.mobileDataStatus, '正在执行MPING,会消耗少量流量...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestJSON('/api/modem/mobile-data/consume', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ target: '8.8.8.8' })
|
||||||
|
});
|
||||||
|
renderMobileData(result.data?.mobileData || state.currentMobileData || {});
|
||||||
|
const pingMessage = result.data?.ping?.message || result.message || '已完成流量消耗测试。';
|
||||||
|
setStatusMessage(elements.mobileDataStatus, pingMessage, 'success');
|
||||||
|
await refreshLogs();
|
||||||
|
} catch (err) {
|
||||||
|
setStatusMessage(elements.mobileDataStatus, err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
state.trafficBusy = false;
|
||||||
|
renderMobileData(state.currentMobileData || {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,6 +828,7 @@ elements.pushTestButton.addEventListener('click', testPushChannel);
|
|||||||
elements.pushChannelForm.elements.type.addEventListener('change', handlePushTypeChange);
|
elements.pushChannelForm.elements.type.addEventListener('change', handlePushTypeChange);
|
||||||
elements.refreshButton.addEventListener('click', refreshAll);
|
elements.refreshButton.addEventListener('click', refreshAll);
|
||||||
elements.mobileDataToggle.addEventListener('click', toggleMobileData);
|
elements.mobileDataToggle.addEventListener('click', toggleMobileData);
|
||||||
|
elements.mobileDataConsume.addEventListener('click', consumeMobileDataTraffic);
|
||||||
elements.logsButton.addEventListener('click', refreshLogs);
|
elements.logsButton.addEventListener('click', refreshLogs);
|
||||||
elements.messagesButton.addEventListener('click', refreshMessages);
|
elements.messagesButton.addEventListener('click', refreshMessages);
|
||||||
elements.smsForm.addEventListener('submit', sendSMS);
|
elements.smsForm.addEventListener('submit', sendSMS);
|
||||||
|
|||||||
+26
@@ -514,6 +514,32 @@ class APIServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 通过模组执行一次MPING,消耗少量流量验证连通性
|
||||||
|
this.app.post('/api/modem/mobile-data/consume', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ping = await this.modem.consumeMobileDataTraffic({
|
||||||
|
target: req.body?.target || '8.8.8.8'
|
||||||
|
});
|
||||||
|
const mobileData = await this.modem.getMobileDataStatus();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '已消耗少量流量完成连通性测试',
|
||||||
|
data: {
|
||||||
|
ping,
|
||||||
|
mobileData
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('流量消耗测试失败:', err);
|
||||||
|
res.status(err.statusCode || 500).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
data: err.data || null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 发送AT命令
|
// 发送AT命令
|
||||||
this.app.post('/api/modem/at', async (req, res) => {
|
this.app.post('/api/modem/at', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+144
@@ -668,6 +668,150 @@ class ModemManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async consumeMobileDataTraffic(options = {}) {
|
||||||
|
const target = this.normalizePingTarget(options.target || '8.8.8.8');
|
||||||
|
const status = await this.getMobileDataStatus();
|
||||||
|
|
||||||
|
if (status.status === 'unknown') {
|
||||||
|
const err = new Error(status.error || '未能确认移动数据状态,无法执行流量消耗测试');
|
||||||
|
err.statusCode = 503;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status.anyActive) {
|
||||||
|
const err = new Error('当前未开启流量,请先开启后再执行流量消耗测试');
|
||||||
|
err.statusCode = 409;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.runMPing(target, {
|
||||||
|
timeout: options.timeout || 35000,
|
||||||
|
pingTimeoutSeconds: options.pingTimeoutSeconds || 30,
|
||||||
|
count: options.count || 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizePingTarget(value) {
|
||||||
|
const target = String(value || '').trim();
|
||||||
|
if (!target || !/^[A-Za-z0-9.:-]{1,253}$/.test(target)) {
|
||||||
|
throw new Error('Ping目标地址无效');
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
runMPing(target, options = {}) {
|
||||||
|
if (!this.port || !this.port.isOpen) {
|
||||||
|
throw new Error('串口未打开');
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = options.timeout || 35000;
|
||||||
|
const pingTimeoutSeconds = Math.min(Math.max(Number(options.pingTimeoutSeconds) || 30, 1), 255);
|
||||||
|
const count = Math.min(Math.max(Number(options.count) || 1, 1), 10);
|
||||||
|
const command = `AT+MPING="${target}",${pingTimeoutSeconds},${count}`;
|
||||||
|
|
||||||
|
logger.info(`准备通过模组执行MPING,目标: ${target}`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const lines = [];
|
||||||
|
let settled = false;
|
||||||
|
let commandAccepted = false;
|
||||||
|
|
||||||
|
const finish = (err, result = null) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
this.parser.removeListener('data', handler);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
finish(new Error(`MPING超时,未收到结果: ${target}`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
const handler = (line) => {
|
||||||
|
const trimmed = String(line).trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(trimmed);
|
||||||
|
|
||||||
|
if (trimmed === 'OK') {
|
||||||
|
commandAccepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.includes('ERROR')) {
|
||||||
|
finish(new Error(`MPING命令失败: ${this.formatATResponse(lines.join('\n'))}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('+MPING:')) {
|
||||||
|
const result = this.parseMPingResult(trimmed, target);
|
||||||
|
result.commandAccepted = commandAccepted;
|
||||||
|
result.rawResponse = lines.join('\n');
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
logger.info(`✓ ${result.message}`);
|
||||||
|
finish(null, result);
|
||||||
|
} else {
|
||||||
|
const err = new Error(result.message);
|
||||||
|
err.statusCode = 502;
|
||||||
|
err.data = result;
|
||||||
|
finish(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.parser.on('data', handler);
|
||||||
|
|
||||||
|
logger.debug(`>> ${command}`);
|
||||||
|
this.port.write(`${command}\r\n`, (err) => {
|
||||||
|
if (err) {
|
||||||
|
finish(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parseMPingResult(line, target) {
|
||||||
|
const payload = line.slice('+MPING:'.length).trim();
|
||||||
|
const params = payload.match(/"[^"]*"|[^,]+/g)?.map(item => {
|
||||||
|
const value = item.trim();
|
||||||
|
return value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
||||||
|
}) || [];
|
||||||
|
const resultCode = Number.parseInt(params[0], 10);
|
||||||
|
const responseTarget = params[1] || target;
|
||||||
|
const packetLength = Number.parseInt(params[2], 10);
|
||||||
|
const latencyMs = Number.parseInt(params[3], 10);
|
||||||
|
const ttl = Number.parseInt(params[4], 10);
|
||||||
|
const hasReplyFields = params.length >= 4;
|
||||||
|
const success = (resultCode === 0 || resultCode === 1) && hasReplyFields;
|
||||||
|
const message = success
|
||||||
|
? `MPING成功: ${responseTarget}, 延迟 ${Number.isFinite(latencyMs) ? `${latencyMs}ms` : '未知'}, TTL ${Number.isFinite(ttl) ? ttl : '未知'}`
|
||||||
|
: `MPING失败或目标不可达,错误码: ${Number.isFinite(resultCode) ? resultCode : payload}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
target: responseTarget,
|
||||||
|
resultCode: Number.isFinite(resultCode) ? resultCode : null,
|
||||||
|
packetLength: Number.isFinite(packetLength) ? packetLength : null,
|
||||||
|
latencyMs: Number.isFinite(latencyMs) ? latencyMs : null,
|
||||||
|
ttl: Number.isFinite(ttl) ? ttl : null,
|
||||||
|
message,
|
||||||
|
rawLine: line
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async waitForMobileDataState(enabled, timeout = 10000) {
|
async waitForMobileDataState(enabled, timeout = 10000) {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
let status = await this.getMobileDataStatus();
|
let status = await this.getMobileDataStatus();
|
||||||
|
|||||||
Reference in New Issue
Block a user