import config from '../../dxmodules/dxConfig.js' import std from '../../dxmodules/dxStd.js' import ntp from '../../dxmodules/dxNtp.js' import dxos from '../../dxmodules/dxOs.js' import driver from '../driver.js' import bus from '../../dxmodules/dxEventBus.js' import utils from '../common/utils/utils.js' import tz from "../../dxmodules/dxTimeZones.js"; // 版本文件路径 const VERSION_FILE = '/etc/app/region.conf' /** * 判断是否是国际版本 * @returns {boolean} true表示国际版本,false表示国内版本 */ function isInternationalVersion() { try { let savedVersion = std.loadFile(VERSION_FILE) if (savedVersion) { savedVersion = savedVersion.trim() } // 文件有内容代表国际版本,无内容代表国内版本 return savedVersion && savedVersion=="INTL" } catch (e) { // 如果文件不存在或读取失败,默认返回false(国内版本) return false } } // 通用校验规则 const validators = { ip: v => /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(v), ipOrDomainWithPort: v => /^(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(:\d{1,5})?$/.test(v), positiveInt: v => /^[1-9]\d*$/.test(v), nonNegativeInt: v => /^([1-9]\d*|0{1})$/.test(v), string: v => typeof v === 'string', number: v => typeof v === 'number', switch: v => [0, 1].includes(v), range: (min, max) => v => typeof v === 'number' && v >= min && v <= max, isIntInRange: (min, max) => v => Number.isInteger(v) && v >= min && v <= max, includes: (...values) => v => values.includes(v), mqttProtocol: v => { if (typeof v !== 'string') return false; const supportedProtocols = ['tcp://', 'ssl://', 'mqtt://', 'mqtts://']; let hostPort = v; let hasSupportedProtocol = false; // 1. 检查是否以支持的协议开头 for (const protocol of supportedProtocols) { if (v.startsWith(protocol)) { hasSupportedProtocol = true; hostPort = v.slice(protocol.length); // 去掉协议前缀 break; } } // 2. 如果包含其他 "://" 协议,拒绝 if (!hasSupportedProtocol && v.includes('://')) return false; if (!hostPort) return false; // 3. 校验端口 if (!validators.ipOrDomainWithPort(hostPort)) return false; // 4. 补充端口 const portMatch = hostPort.match(/:(\d{1,5})$/); if (portMatch) { const port = parseInt(portMatch[1], 10); if (port < 1 || port > 65535) return false; } return true; }, password: v => typeof v === 'string' && v.length >= 8, language: v => { // 所有支持的语言列表 const allLanguages = ['CN', 'EN', 'ES', 'FR', 'DE', 'RU', 'AR', 'PT', 'KO'] // 首先检查是否是支持的语言 if (!allLanguages.includes(v)) { return false } // 根据版本判断是否允许该语言 const isInternational = isInternationalVersion() if (isInternational) { // 国际版本:不能接受 CN return v !== 'CN' } else { // 国内版本:只能接受 CN return v === 'CN' } }, percentage: v => validators.nonNegativeInt(v) && v >= 0 && v <= 100, time: v => /^([1-9]\d{0,9}|0)$/.test(v), weComMqttAddr: v => typeof v === 'string' && v.startsWith('__WECOM_MQTT_ADDR__') } // 配置回调处理器 const callbacks = { face: { similarity: v => driver.face.setConfig({ com_threshold: v }), livenessOff: v => driver.face.setConfig({ liv_enable: v }), livenessVal: v => driver.face.setConfig({ liv_threshold: v }), recheck: v => driver.face.setConfig({ det_timeout_ms: v === 0 ? 100000 : 8000 }), showNir: v => driver.face.capPreviewEnable(v), }, ntp: { gmt: v => ntp.updateGmt(v), timeZone: v => { tz.updateTimeZone(v) tz.reboot() }, }, net: { ip: v => driver.screen.reload() }, sys: { mode: v => setMode(v), heart_en: () => bus.fire(driver.mqtt.RESTART_HEARTBEAT), heart_time: () => bus.fire(driver.mqtt.RESTART_HEARTBEAT), pwd: v => driver.screen.reload(), time: v => driver.ntp.setTime(v) }, access: { fire: v => driver.gpiokey.statusConfig(v), fireStatus: v => driver.gpiokey.controlFire(v), offlineAccessNum: v => bus.fire("verifyPassRecords", v) }, base: { backlight: v => driver.display.setBacklight(v), brightness: v => driver.pwm.setWhitePower(v), nirBrightness: v => driver.pwm.setNirPower(v), showIp: v => driver.screen.hideIp(v), showSn: v => driver.screen.hideSn(v), appMode: v => driver.screen.appMode(v), screenOff: () => bus.fire("screenManagerRefresh"), screensaver: () => bus.fire("screenManagerRefresh"), volume: v => driver.audio.volume(v), language: () => driver.screen.changeLanguage() } } // 配置项定义 const configSchema = { face: { similarity: validators.range(0, 1), livenessOff: validators.switch, livenessVal: validators.range(0, 100), showNir: validators.switch, stranger: validators.includes(0, 1, 2), voiceMode: validators.includes(0, 1, 2), voiceModeDate: validators.string, recheck: validators.includes(0, 1), }, mqtt: { addr: validators.mqttProtocol, username: validators.string, password: validators.string, qos: validators.includes(0, 1, 2), prefix: validators.string, willtopic: validators.string, onlinecheck: validators.includes(0, 1, 2), timeout: validators.positiveInt, clientIdSuffix: validators.includes(0, 1), }, net: { type: validators.includes(0, 1, 2, 4), dhcp: validators.includes(1, 2, 3), ip: validators.ip, gateway: validators.ip, dns: v => { return validators.ip(v) || utils.isEmpty(v) }, mask: validators.ip, mac: validators.string, ssid: validators.string, psk: validators.string }, ntp: { ntp: validators.switch, server: validators.ip, hour: validators.range(0, 23), gmt: validators.range(0, 24), timeZone: v => { if (typeof v !== 'string') return false const path = `${tz.root}${v}` if (!std.exist(path)) { return false } return true } }, sys: { mode: validators.string, nfc: validators.switch, nfcIdentityCardEnable: validators.includes(1, 3), pwd: validators.switch, strangerImage: validators.switch, accessImageType: validators.switch, devType: validators.includes(0, 1, 2), restartCount: validators.nonNegativeInt, heart_en: validators.switch, heart_time: v => validators.positiveInt(v) && v >= 30, weComStatus: validators.switch, bleKey: validators.string, scanInterval: validators.positiveInt, time: validators.time, weComMqttAddr: validators.weComMqttAddr }, access: { relayTime: validators.positiveInt, offlineAccessNum: v => validators.positiveInt(v) && v <= 2000, fire: validators.switch, fireStatus: validators.switch, tamper: validators.switch, uploadToCloud: validators.switch }, base: { firstLogin: validators.switch, backlight: validators.percentage, brightness: validators.percentage, nirBrightness: validators.percentage, showIp: validators.switch, showSn: validators.switch, appMode: validators.switch, screenOff: validators.nonNegativeInt, screensaver: validators.nonNegativeInt, volume: validators.isIntInRange(0, 10), password: v => v.length >= 1, language: validators.language, showProgramCode: validators.switch }, passwordAccess: { passwordAccess: validators.switch } } // 需要重启的配置项 const rebootRequiredConfigs = new Set([ 'ntp.ntp', 'ntp.server', 'ntp.gmt', 'base.language' ]) // 工具函数 function setMode(params) { dxos.systemWithRes(`echo 'app' > /etc/.app_v1`, 2) dxos.setMode(params) } function validateConfig(section, key, value) { const sectionSchema = configSchema[section] if (!sectionSchema) { throw new Error(`不支持的配置分组: ${section}`) } const validator = sectionSchema[key] if (!validator) { throw new Error(`不支持的配置项: ${section}.${key}`) } if (!validator(value)) { throw new Error(`配置项 ${section}.${key} 的值无效: ${value}`) } } function shouldReboot(configData) { return Object.entries(configData).some(([section, sectionData]) => { return rebootRequiredConfigs.has(section) || Object.keys(sectionData).some(key => rebootRequiredConfigs.has(`${section}.${key}`)) }) } function handlePostConfigActions(configData) { const hasNetConfig = 'net' in configData const hasMqttConfig = 'mqtt' in configData const needsReboot = shouldReboot(configData) // 重启处理 if (needsReboot) { driver.screen.upgrade({ title: "confirm.restartDevice", content: "confirm.restartDeviceDis" }) dxos.asyncReboot(3) } // 网络重连处理 if (hasNetConfig) { std.setTimeout(() => driver.net.reconnect(), 1000) } // MQTT重新初始化处理 if (hasMqttConfig) { driver.mqtt.reinit() } } // 主要服务对象 const configService = { needReboot: Array.from(rebootRequiredConfigs), /** * 配置验证并保存 * @param {Object} data - 配置数据对象 * @returns {boolean|string} - 成功返回true,失败返回错误信息 */ configVerifyAndSave(data) { try { // 基础数据格式验证 if (!data || typeof data !== 'object') { return '配置数据格式无效' } const configsToSave = [] const callbacksToExecute = [] // 验证并收集配置项 for (const [section, sectionData] of Object.entries(data)) { for (let [key, value] of Object.entries(sectionData)) { // 验证配置项 validateConfig(section, key, value) if (section == 'sys' && key == 'weComMqttAddr') { const PREFIX = '__WECOM_MQTT_ADDR__' value = value.substring(PREFIX.length) } if (section == 'mqtt' && key == 'addr' && !value.includes('://')) { value = "mqtt://" + value } // 收集要保存的配置 configsToSave.push({ section, key, value }) // 收集要执行的回调 const sectionCallbacks = callbacks[section] if (sectionCallbacks && sectionCallbacks[key]) { callbacksToExecute.push({ callback: sectionCallbacks[key], value }) } } } // 批量保存配置 configsToSave.forEach(({ section, key, value }) => { config.set(`${section}.${key}`, value) }) config.save() // 执行回调 callbacksToExecute.forEach(({ callback, value }) => { callback(value) }) // 处理后置动作(重启、网络重连、MQTT重初始化) handlePostConfigActions(data) return true } catch (error) { return error.message } } } export default configService