/** * base-url.js - 设备 IP / 基础 URL 公共助手 * * 各页面共用 localStorage key dcu_esp_ip(OTA / CAN / 电机页一致)。 * 本模块只提供纯 localStorage + 默认值的通用实现;页面特有的输入框回退 * (如 CAN 页 el.toolIp、OTA 页 #espIp)由各自入口在此基础上包装。 * * 写入一律经 saveDeviceIp 的 IPv4 校验:这是"哪个端点算当前设备"的唯一落盘点, * 脏值在这里被拒掉,比在每个调用方各判一次可靠(固件 AP/掉租约时会上报 * ip:"0.0.0.0",见 ESP32_COMM/main/server/http_handlers.cpp:1776)。 */ export var LS_ESP_IP = 'dcu_esp_ip'; export var DEFAULT_ESP_IP = '192.168.31.127'; /** @description 最近一次"选中"的设备快照(ip + 版本指纹),供端点一致性核对。 */ export var LS_ESP_DEVICE = 'dcu_esp_device'; /** @description 局域网发现结果缓存(带 observedAt),只给 UI 当候选种子用。 */ export var LS_LAN_DEVICES = 'dcu_lan_devices'; /** * @description 严格点分十进制 IPv4,并拒掉不能当 API 目标的保留段。 * 0/8(含固件脏值 0.0.0.0)与 255.255.255.255 直接拒;169.254/16 是链路本地, * 板子不会在那儿服务 HTTP,也拒——宁可让人重填,也不要网页打到一个 * 永远不通的地址上还显示"已连接"。 * @param {string} s 待校验字符串 * @returns {boolean} */ export function isValidIpv4(s) { var parts = String(s == null ? '' : s).trim().split('.'); if (parts.length !== 4) return false; var nums = []; for (var i = 0; i < 4; i++) { var p = parts[i]; if (!/^\d{1,3}$/.test(p)) return false; var n = parseInt(p, 10); if (n > 255) return false; nums.push(n); } if (nums[0] === 0 || nums[0] === 255) return false; if (nums[0] === 169 && nums[1] === 254) return false; return true; } /** @description 读取 localStorage 中保存的设备 IP,无有效值返回空串。 */ export function getSavedDeviceIp() { try { var saved = localStorage.getItem(LS_ESP_IP); if (saved && saved.trim()) return saved.trim(); } catch (_) {} return ''; } /** * @description 保存设备 IP。非法值**拒绝且不改写现值**,返回 false—— * 不能用 DEFAULT_ESP_IP 去"兜住"脏输入,那会把配置自证成连接成功。 * @param {string} ip * @returns {boolean} 是否真的写进去了 */ export function saveDeviceIp(ip) { var v = String(ip == null ? '' : ip).trim(); if (!isValidIpv4(v)) return false; try { localStorage.setItem(LS_ESP_IP, v); } catch (_) { return false; } return true; } /** @description 设备 IP:localStorage 优先,否则默认值。 */ export function getDeviceIp() { return getSavedDeviceIp() || DEFAULT_ESP_IP; } /** @description 拼接设备基础 URL(http://)。 */ export function getBaseUrl() { return 'http://' + getDeviceIp(); } /** * @description 读最近一次选中的设备指纹;结构不对一律当没有。 * @returns {{ip:string, version:string, git_hash:string, observedAt:number, source:string}|null} */ export function getDeviceMeta() { try { var raw = localStorage.getItem(LS_ESP_DEVICE); if (!raw) return null; var o = JSON.parse(raw); if (!o || typeof o !== 'object' || !isValidIpv4(o.ip)) return null; return o; } catch (_) {} return null; } /** @description 写设备指纹(与 IP 同时机,由 deviceLink.selectDevice 统一调用)。 */ export function saveDeviceMeta(meta) { try { localStorage.setItem(LS_ESP_DEVICE, JSON.stringify(meta || {})); } catch (_) {} } /** * @description 读发现结果缓存;返回数组(可能为空),坏数据直接丢弃不猜。 * @returns {Array} */ export function getLanDevices() { try { var raw = localStorage.getItem(LS_LAN_DEVICES); if (!raw) return []; var arr = JSON.parse(raw); if (!Array.isArray(arr)) return []; return arr.filter(function (d) { return d && isValidIpv4(d.ip); }); } catch (_) {} return []; } /** @description 存发现结果缓存(上限 16 条,超了截断——它只是候选种子,不是台账)。 */ export function saveLanDevices(list) { var arr = Array.isArray(list) ? list.slice(0, 16) : []; try { localStorage.setItem(LS_LAN_DEVICES, JSON.stringify(arr)); } catch (_) {} }