fix: 修复单 SIM 卡逻辑
This commit is contained in:
+14
-1
@@ -70,6 +70,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 80px;
|
||||
@@ -726,6 +730,10 @@ h3 {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.sim-slot-grid.is-single {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sim-slot {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -737,6 +745,11 @@ h3 {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.sim-slot.is-single {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.sim-slot.is-active {
|
||||
border-color: rgba(93, 184, 114, 0.55);
|
||||
box-shadow: inset 3px 0 0 var(--success);
|
||||
@@ -1592,4 +1605,4 @@ body.modal-open {
|
||||
.field-row--wide {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+70
-6
@@ -50,6 +50,7 @@ const elements = {
|
||||
simStatus: document.querySelector('#simStatus'),
|
||||
simSlot0: document.querySelector('#simSlot0'),
|
||||
simSlot1: document.querySelector('#simSlot1'),
|
||||
simSlotGrid: document.querySelector('.sim-slot-grid'),
|
||||
simSlot0State: document.querySelector('#simSlot0State'),
|
||||
simSlot1State: document.querySelector('#simSlot1State'),
|
||||
simSlot0Phone: document.querySelector('#simSlot0Phone'),
|
||||
@@ -281,7 +282,18 @@ function renderSimSlot(slot, card, stateNode, phoneNode) {
|
||||
}
|
||||
|
||||
card.classList.toggle('is-active', Boolean(slot.active));
|
||||
setText(stateNode, slot.active ? '当前使用' : '未选中');
|
||||
if (slot.active && slot.present === false) {
|
||||
setText(stateNode, '当前未就绪');
|
||||
} else if (slot.active) {
|
||||
setText(stateNode, '当前使用');
|
||||
} else if (slot.present === true) {
|
||||
setText(stateNode, '可用');
|
||||
} else if (slot.present === false) {
|
||||
setText(stateNode, '未检测到SIM');
|
||||
} else {
|
||||
setText(stateNode, '未确认');
|
||||
}
|
||||
|
||||
const phoneText = formatSimSlotPhone(slot);
|
||||
if (phoneNode) {
|
||||
phoneNode.textContent = phoneText;
|
||||
@@ -289,29 +301,81 @@ function renderSimSlot(slot, card, stateNode, phoneNode) {
|
||||
}
|
||||
}
|
||||
|
||||
function getVisibleSimSlots(sim = {}, activeSlot = null) {
|
||||
const slots = sim.slots || [];
|
||||
const presentSlots = slots.filter(slot => slot.present === true);
|
||||
|
||||
if (presentSlots.length >= 2) {
|
||||
return presentSlots;
|
||||
}
|
||||
|
||||
if (activeSlot?.present === true) {
|
||||
return [activeSlot];
|
||||
}
|
||||
|
||||
return slots.filter(slot => slot.active || slot.present !== false);
|
||||
}
|
||||
|
||||
function getSimModeLabel(sim = {}, activeSlot = null) {
|
||||
const hardwareSwitchable = Boolean(sim.supported && sim.canSwitch);
|
||||
if (!hardwareSwitchable) {
|
||||
return '单卡/未检测到双卡';
|
||||
}
|
||||
|
||||
const presentSlots = (sim.slots || []).filter(slot => slot.present === true);
|
||||
if (presentSlots.length >= 2) {
|
||||
return sim.modeLabel || '双卡';
|
||||
}
|
||||
|
||||
if (activeSlot?.present === true) {
|
||||
return `仅检测到 ${getSimDisplayLabel(activeSlot)}`;
|
||||
}
|
||||
|
||||
return '未检测到可用SIM';
|
||||
}
|
||||
|
||||
function renderSimStatus(sim = {}) {
|
||||
state.currentSim = sim;
|
||||
|
||||
const supported = Boolean(sim.supported && sim.canSwitch);
|
||||
const activeSlot = getActiveSimSlot(sim);
|
||||
const modeLabel = supported ? (sim.modeLabel || '双卡') : '单卡/未检测到双卡';
|
||||
const modeLabel = getSimModeLabel(sim, activeSlot);
|
||||
|
||||
setText(elements.simMode, modeLabel);
|
||||
|
||||
renderSimSlot(getSimSlot(sim, 0), elements.simSlot0, elements.simSlot0State, elements.simSlot0Phone);
|
||||
renderSimSlot(getSimSlot(sim, 1), elements.simSlot1, elements.simSlot1State, elements.simSlot1Phone);
|
||||
const visibleSlots = getVisibleSimSlots(sim, activeSlot);
|
||||
const singleSlot = visibleSlots.length === 1;
|
||||
const visibleSlotNumbers = new Set(visibleSlots.map(slot => slot.slot));
|
||||
|
||||
elements.simSlotGrid?.classList.toggle('is-single', singleSlot);
|
||||
|
||||
[
|
||||
[getSimSlot(sim, 0), elements.simSlot0, elements.simSlot0State, elements.simSlot0Phone],
|
||||
[getSimSlot(sim, 1), elements.simSlot1, elements.simSlot1State, elements.simSlot1Phone]
|
||||
].forEach(([slot, card, stateNode, phoneNode]) => {
|
||||
if (card) {
|
||||
card.hidden = !visibleSlotNumbers.has(slot.slot);
|
||||
card.classList.toggle('is-single', singleSlot && visibleSlotNumbers.has(slot.slot));
|
||||
}
|
||||
renderSimSlot(slot, card, stateNode, phoneNode);
|
||||
});
|
||||
|
||||
elements.simSwitchButtons.forEach((button) => {
|
||||
const slot = Number.parseInt(button.dataset.simSwitch, 10);
|
||||
const slotInfo = getSimSlot(sim, slot);
|
||||
const isActive = activeSlot?.slot === slot;
|
||||
button.disabled = state.simBusy || Boolean(sim.switching) || !supported || isActive;
|
||||
const unavailable = slotInfo.present === false;
|
||||
button.hidden = singleSlot;
|
||||
button.disabled = state.simBusy || Boolean(sim.switching) || !supported || isActive || unavailable;
|
||||
button.textContent = isActive ? '正在使用' : `切到SIM${slot + 1}`;
|
||||
});
|
||||
|
||||
if (state.simBusy || sim.switching) {
|
||||
setStatusMessage(elements.simStatus, '正在切换SIM...');
|
||||
} else if (supported && activeSlot) {
|
||||
} else if (supported && activeSlot?.present === true) {
|
||||
setStatusMessage(elements.simStatus, `当前 ${getSimDisplayLabel(activeSlot)}`, 'success');
|
||||
} else if (supported && activeSlot?.present === false) {
|
||||
setStatusMessage(elements.simStatus, `${getSimDisplayLabel(activeSlot)} 未就绪,请检查是否插卡`, 'error');
|
||||
} else if (!supported) {
|
||||
setStatusMessage(elements.simStatus, '双卡不可用');
|
||||
} else {
|
||||
|
||||
+229
-17
@@ -33,6 +33,7 @@ class ModemManager extends EventEmitter {
|
||||
switchSlot: null,
|
||||
bindSlot: null,
|
||||
switching: false,
|
||||
probing: false,
|
||||
slots: this.createSimSlots(),
|
||||
lastCheckedAt: null,
|
||||
error: '',
|
||||
@@ -63,6 +64,7 @@ class ModemManager extends EventEmitter {
|
||||
slot,
|
||||
name: `SIM${slot + 1}`,
|
||||
active: false,
|
||||
present: null,
|
||||
phoneNumber,
|
||||
phoneLabel: phoneNumber || `SIM${slot + 1}`,
|
||||
lastCheckedAt: null
|
||||
@@ -538,6 +540,25 @@ class ModemManager extends EventEmitter {
|
||||
}));
|
||||
}
|
||||
|
||||
updateSimSlotPresence(status, slotNumber, present) {
|
||||
const slot = this.normalizeSimSlot(slotNumber);
|
||||
if (slot === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.slots = status.slots.map(item => {
|
||||
if (item.slot !== slot) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
present: Boolean(present),
|
||||
lastCheckedAt: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getBusinessSimSlot(status = this.sim) {
|
||||
return this.normalizeSimSlot(status.bindSlot) ?? this.normalizeSimSlot(status.switchSlot);
|
||||
}
|
||||
@@ -637,14 +658,27 @@ class ModemManager extends EventEmitter {
|
||||
retryDelay = 2000,
|
||||
settleDelay = 3000,
|
||||
confirmTimeout = 15000,
|
||||
confirmInterval = 1000
|
||||
confirmInterval = 1000,
|
||||
simReadyAttempts = 10,
|
||||
simReadyDelay = 1000
|
||||
} = options;
|
||||
|
||||
const currentSlot = await this.querySwitchSimSlot().catch(() => this.sim.switchSlot);
|
||||
this.sim.switchSlot = currentSlot;
|
||||
|
||||
if (currentSlot === targetSlot) {
|
||||
await this.waitSIMReady();
|
||||
try {
|
||||
await this.waitSIMReady({
|
||||
attempts: simReadyAttempts,
|
||||
delay: simReadyDelay,
|
||||
label: `SIM${targetSlot + 1}`
|
||||
});
|
||||
this.updateSimSlotPresence(this.sim, targetSlot, true);
|
||||
} catch (err) {
|
||||
this.updateSimSlotPresence(this.sim, targetSlot, false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return targetSlot;
|
||||
}
|
||||
|
||||
@@ -674,11 +708,155 @@ class ModemManager extends EventEmitter {
|
||||
this.sim.switchSlot = targetSlot;
|
||||
this.markActiveSimSlot(this.sim, this.getBusinessSimSlot(this.sim) ?? targetSlot);
|
||||
await this.sleep(settleDelay);
|
||||
await this.waitSIMReady();
|
||||
|
||||
try {
|
||||
await this.waitSIMReady({
|
||||
attempts: simReadyAttempts,
|
||||
delay: simReadyDelay,
|
||||
label: `SIM${targetSlot + 1}`
|
||||
});
|
||||
this.updateSimSlotPresence(this.sim, targetSlot, true);
|
||||
} catch (err) {
|
||||
this.updateSimSlotPresence(this.sim, targetSlot, false);
|
||||
await this.restoreSwitchSimSlot(currentSlot, `${context}失败后回退`).catch((restoreErr) => {
|
||||
logger.warn(`${context}失败后回退SIM失败: ${restoreErr.message}`);
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
return targetSlot;
|
||||
}
|
||||
|
||||
async restoreSwitchSimSlot(slotNumber, context = 'SIM回退', timeout = 20000) {
|
||||
const slot = this.normalizeSimSlot(slotNumber);
|
||||
if (slot === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.warn(`${context}: 恢复到SIM${slot + 1}`);
|
||||
await this.sendATCommand(`AT+SWITCHSIM=${slot}`, 10000).catch((err) => {
|
||||
logger.warn(`${context}命令失败: ${err.message}`);
|
||||
});
|
||||
|
||||
const restored = await this.waitForSwitchSimSlot(slot, timeout, 1000);
|
||||
if (restored) {
|
||||
this.sim.switchSlot = slot;
|
||||
this.markActiveSimSlot(this.sim, this.getBusinessSimSlot(this.sim) ?? slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.warn(`${context}: 未能确认恢复到SIM${slot + 1}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
async restoreBindSimSlot(slotNumber, context = '业务SIM回退') {
|
||||
const slot = this.normalizeSimSlot(slotNumber);
|
||||
if (slot === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.sendATWithRetry(`AT+BINDSIM=${slot}`, {
|
||||
timeout: 5000,
|
||||
retries: 1,
|
||||
retryDelay: 500,
|
||||
label: `${context}到SIM${slot + 1}`
|
||||
}).catch((err) => {
|
||||
logger.warn(`${context}到SIM${slot + 1}失败: ${err.message}`);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async restoreSimAfterProbe(switchSlot, bindSlot, context = 'SIM探测后恢复') {
|
||||
const targetSwitchSlot = this.normalizeSimSlot(switchSlot);
|
||||
const targetBindSlot = this.normalizeSimSlot(bindSlot) ?? targetSwitchSlot;
|
||||
|
||||
if (targetSwitchSlot !== null) {
|
||||
await this.restoreSwitchSimSlot(targetSwitchSlot, context).catch((err) => {
|
||||
logger.warn(`${context}切回SIM${targetSwitchSlot + 1}失败: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (targetBindSlot !== null) {
|
||||
await this.restoreBindSimSlot(targetBindSlot, context);
|
||||
}
|
||||
}
|
||||
|
||||
async probeInactiveSimSlot(status, slotNumber, originalSlot, originalBindSlot = originalSlot) {
|
||||
const targetSlot = this.normalizeSimSlot(slotNumber);
|
||||
const restoreSlot = this.normalizeSimSlot(originalSlot);
|
||||
const restoreBindSlot = this.normalizeSimSlot(originalBindSlot) ?? restoreSlot;
|
||||
if (targetSlot === null || restoreSlot === null || targetSlot === restoreSlot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info(`探测SIM${targetSlot + 1}是否可用`);
|
||||
let switched = false;
|
||||
|
||||
try {
|
||||
const resp = await this.sendATCommand(`AT+SWITCHSIM=${targetSlot}`, 10000);
|
||||
if (!resp.includes('OK')) {
|
||||
logger.warn(`探测SIM${targetSlot + 1}切换返回非OK: ${this.formatATResponse(resp)}`);
|
||||
}
|
||||
|
||||
switched = await this.waitForSwitchSimSlot(targetSlot, 15000, 1000);
|
||||
if (!switched) {
|
||||
this.updateSimSlotPresence(status, targetSlot, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.sleep(1000);
|
||||
await this.waitSIMReady({
|
||||
attempts: 5,
|
||||
delay: 1000,
|
||||
label: `SIM${targetSlot + 1}`
|
||||
});
|
||||
this.updateSimSlotPresence(status, targetSlot, true);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.warn(`探测SIM${targetSlot + 1}失败: ${err.message}`);
|
||||
this.updateSimSlotPresence(status, targetSlot, false);
|
||||
return false;
|
||||
} finally {
|
||||
if (switched) {
|
||||
await this.restoreSimAfterProbe(restoreSlot, restoreBindSlot, `探测SIM${targetSlot + 1}后恢复`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async probeSimSlotPresence(status, currentSlot) {
|
||||
const originalSlot = this.normalizeSimSlot(currentSlot);
|
||||
if (!status.supported || !status.canSwitch || originalSlot === null) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (this.sim.switching || this.sim.probing) {
|
||||
return status;
|
||||
}
|
||||
|
||||
const originalBindSlot = this.normalizeSimSlot(status.bindSlot) ?? originalSlot;
|
||||
|
||||
this.sim.probing = true;
|
||||
try {
|
||||
for (const slot of status.slots) {
|
||||
if (slot.slot !== originalSlot) {
|
||||
await this.probeInactiveSimSlot(status, slot.slot, originalSlot, originalBindSlot);
|
||||
}
|
||||
}
|
||||
|
||||
await this.restoreSimAfterProbe(originalSlot, originalBindSlot, 'SIM探测结束恢复');
|
||||
|
||||
const restoredSlot = await this.querySwitchSimSlot().catch(() => originalSlot);
|
||||
status.switchSlot = this.normalizeSimSlot(restoredSlot) ?? originalSlot;
|
||||
status.bindSlot = await this.queryBindSimSlot();
|
||||
this.markActiveSimSlot(status, this.getBusinessSimSlot(status));
|
||||
return status;
|
||||
} finally {
|
||||
this.sim.probing = false;
|
||||
status.probing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForSwitchSimSlot(targetSlot, timeout = 15000, interval = 1000) {
|
||||
const startedAt = Date.now();
|
||||
|
||||
@@ -806,12 +984,27 @@ class ModemManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
isSimReadyResponse(resp) {
|
||||
return String(resp || '').includes('+CPIN: READY');
|
||||
}
|
||||
|
||||
async isCurrentSimReady(timeout = 2000) {
|
||||
try {
|
||||
const resp = await this.sendATCommand('AT+CPIN?', timeout);
|
||||
return this.isSimReadyResponse(resp);
|
||||
} catch (err) {
|
||||
logger.debug(`查询当前SIM就绪状态失败: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getSimStatus(options = {}) {
|
||||
return await this.refreshSimStatus(options);
|
||||
}
|
||||
|
||||
async refreshSimStatus(options = {}) {
|
||||
const refreshIdentity = options.refreshIdentity === true;
|
||||
const probeSlots = options.probeSlots === true;
|
||||
const status = {
|
||||
...this.sim,
|
||||
slots: this.sim.slots.map(slot => ({ ...slot })),
|
||||
@@ -839,6 +1032,8 @@ class ModemManager extends EventEmitter {
|
||||
if (switchSlot !== null) {
|
||||
status.canSwitch = true;
|
||||
status.switchSlot = switchSlot;
|
||||
const currentSimReady = await this.isCurrentSimReady();
|
||||
this.updateSimSlotPresence(status, switchSlot, currentSimReady);
|
||||
} else {
|
||||
status.switchSlot = null;
|
||||
status.error = '未能读取当前AT SIM卡槽';
|
||||
@@ -852,6 +1047,10 @@ class ModemManager extends EventEmitter {
|
||||
this.updateSimSlotPhoneNumber(status, switchSlot, phoneNumber);
|
||||
}
|
||||
|
||||
if (probeSlots && status.canSwitch && switchSlot !== null) {
|
||||
await this.probeSimSlotPresence(status, switchSlot);
|
||||
}
|
||||
|
||||
status.lastCheckedAt = new Date().toISOString();
|
||||
this.sim = status;
|
||||
return this.getCachedSimStatus();
|
||||
@@ -896,12 +1095,20 @@ class ModemManager extends EventEmitter {
|
||||
await this.switchSimForIdentityRead(targetSlot, {
|
||||
context: `切换SIM${targetSlot + 1}`,
|
||||
purpose: '用于业务绑定',
|
||||
retries: 4,
|
||||
retryDelay: 3000,
|
||||
settleDelay: 3000
|
||||
retries: 2,
|
||||
retryDelay: 1000,
|
||||
settleDelay: 1000,
|
||||
confirmTimeout: 5000,
|
||||
confirmInterval: 1000,
|
||||
simReadyAttempts: 3,
|
||||
simReadyDelay: 1000
|
||||
});
|
||||
} else {
|
||||
await this.waitSIMReady();
|
||||
await this.waitSIMReady({
|
||||
attempts: 3,
|
||||
delay: 1000,
|
||||
label: `SIM${targetSlot + 1}`
|
||||
});
|
||||
}
|
||||
|
||||
if (this.normalizeSimSlot(before.bindSlot) !== targetSlot) {
|
||||
@@ -1753,7 +1960,7 @@ class ModemManager extends EventEmitter {
|
||||
|
||||
// 7. 按文档配置短信功能,并启用本项目需要的PDU模式
|
||||
await this.configureSMS();
|
||||
await this.refreshSimStatus({ refreshIdentity: true });
|
||||
await this.refreshSimStatus({ refreshIdentity: true, probeSlots: true });
|
||||
|
||||
logger.info('模组初始化完成');
|
||||
this.emit('ready');
|
||||
@@ -1762,24 +1969,29 @@ class ModemManager extends EventEmitter {
|
||||
/**
|
||||
* 等待SIM卡完成初始化
|
||||
*/
|
||||
async waitSIMReady() {
|
||||
for (let attempt = 1; attempt <= 10; attempt++) {
|
||||
async waitSIMReady(options = {}) {
|
||||
const attempts = options.attempts ?? 10;
|
||||
const delay = options.delay ?? 1000;
|
||||
const timeout = options.timeout ?? 2000;
|
||||
const label = options.label || 'SIM卡';
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt++) {
|
||||
try {
|
||||
const resp = await this.sendATCommand('AT+CPIN?', 2000);
|
||||
if (resp.includes('+CPIN: READY')) {
|
||||
logger.info('✓ SIM卡已就绪');
|
||||
const resp = await this.sendATCommand('AT+CPIN?', timeout);
|
||||
if (this.isSimReadyResponse(resp)) {
|
||||
logger.info(`✓ ${label}已就绪`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`SIM卡未就绪(${attempt}/10): ${this.formatATResponse(resp)}`);
|
||||
logger.warn(`${label}未就绪(${attempt}/${attempts}): ${this.formatATResponse(resp)}`);
|
||||
} catch (err) {
|
||||
logger.warn(`查询SIM卡状态失败(${attempt}/10): ${err.message}`);
|
||||
logger.warn(`查询${label}状态失败(${attempt}/${attempts}): ${err.message}`);
|
||||
}
|
||||
|
||||
await this.sleep(1000);
|
||||
await this.sleep(delay);
|
||||
}
|
||||
|
||||
throw new Error('SIM卡未就绪');
|
||||
throw new Error(`${label}未就绪,可能未插卡或SIM初始化失败`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user