const fs = require("node:fs"); const path = require("node:path"); const { parse } = require("csv-parse/sync"); const { callServer } = require("./server-client"); const IDENTIFICATION_FIELDS = [ "q_in_val", "dt", "n_order", "t_c", "levels", "dead_area", "xa_full", "V_val", "repeat" ]; function getDeviceId(options = {}) { const deviceId = String(options.deviceId || process.env.REINLOOP_DEVICE_ID || "").trim(); if (!deviceId) throw new Error("发布或读取辨识配置时必须提供设备 ID"); return deviceId; } async function readParameters(filePath) { if (!filePath) throw new Error("发布参数时必须提供 JSON 文件路径"); const content = await fs.promises.readFile(path.resolve(filePath), "utf8"); const config = JSON.parse(content); return config.parameters || config; } async function uploadVolumeConfig(parameters, options = {}) { const deviceId = getDeviceId(options); const request = await callServer({ type: "getPendingVolumeConfigRequest", deviceId }, options); if (!request.pending) { throw new Error("ReinLoop 尚未发起容积参数请求,请先在客户端开始容积测试"); } const result = await callServer({ type: "uploadDataFile", fileName: "volume_measurement.json", folder: `${deviceId}/volume_config_requests/${request.requestId}` }, options); const metadata = result.uploadMetadata; if (!metadata || !metadata.url) { throw new Error("server 未返回有效的配置上传地址"); } const orderedConfig = { q_in_val: parameters.q_in_val, dt: parameters.dt, p_max: parameters.p_max, fit_low: parameters.fit_low, fit_high: parameters.fit_high, T_delta: parameters.T_delta, xa_full: parameters.xa_full, num_runs: parameters.num_runs }; const form = new FormData(); form.append( "file", new Blob([`${JSON.stringify(orderedConfig, null, 2)}\n`], { type: "application/json" }), "volume_measurement.json" ); const uploadResponse = await fetch(metadata.url, { method: "POST", body: form }); if (uploadResponse.status !== 200 && uploadResponse.status !== 204) { throw new Error(`配置文件直传失败: HTTP ${uploadResponse.status}`); } try { return await callServer({ type: "submitVolumeConfigFile", deviceId, requestId: request.requestId, fileID: result.fileID, fileName: "volume_measurement.json" }, options); } catch (error) { await callServer({ type: "deleteFile", fileID: result.fileID }, options).catch(() => {}); throw error; } } function serializeIdentificationConfig(parameters) { const rows = IDENTIFICATION_FIELDS.map((field) => { const value = field === "levels" ? parameters[field].join(",") : parameters[field]; return `${field},${field === "levels" ? `"${value}"` : value}`; }); return `parameter,value\n${rows.join("\n")}\n`; } async function uploadIdentificationConfig(parameters, options = {}) { const result = await callServer({ type: "publishIdentificationConfig", deviceId: getDeviceId(options), parameters }, options); const metadata = result.uploadMetadata; if (!metadata || !metadata.url) { throw new Error("server 未返回有效的配置上传地址"); } const form = new FormData(); form.append( "file", new Blob([serializeIdentificationConfig(parameters)], { type: "text/csv" }), "identification_config.csv" ); const uploadResponse = await fetch(metadata.url, { method: "POST", body: form }); if (uploadResponse.status !== 200 && uploadResponse.status !== 204) { throw new Error(`配置文件直传失败: HTTP ${uploadResponse.status}`); } return result; } async function publishConfig(configType, parameters, options = {}) { if (configType === "volume") { return uploadVolumeConfig(parameters, options); } if (configType !== "identification") { throw new Error(`不支持的配置类型: ${configType}`); } return uploadIdentificationConfig(parameters, options); } async function getConfig(configType, options = {}) { if (configType === "identification") { const result = await callServer({ type: "getIdentificationConfig", deviceId: getDeviceId(options) }, options); const response = await fetch(result.url); if (!response.ok) throw new Error(`配置文件下载失败: HTTP ${response.status}`); const records = parse(await response.text(), { columns: true, skip_empty_lines: true }); const values = Object.fromEntries(records.map((record) => [record.parameter, record.value])); return { q_in_val: Number(values.q_in_val), dt: Number(values.dt), n_order: Number(values.n_order), t_c: Number(values.t_c), levels: String(values.levels).split(",").map(Number), dead_area: Number(values.dead_area), xa_full: Number(values.xa_full), V_val: Number(values.V_val), repeat: Number(values.repeat) }; } return callServer({ type: "getFunctionConfig", configType }, options); } async function main() { const [command, filePath] = process.argv.slice(2); const commands = { "publish-volume": { action: "upload-volume", configType: "volume" }, "publish-identification": { action: "publish", configType: "identification" }, "get-volume": { action: "get", configType: "volume" }, "get-identification": { action: "get", configType: "identification" } }; const selected = commands[command]; if (!selected) { throw new Error( "用法: node b-admin.js [config.json]" ); } if (selected.action === "upload-volume") { const result = await publishConfig(selected.configType, await readParameters(filePath)); console.log(`配置已上传: ${result.fileID}`); return; } const result = selected.action === "publish" ? await publishConfig(selected.configType, await readParameters(filePath)) : await getConfig(selected.configType); if (selected.action === "publish") { console.log(`配置已上传: ${result.fileID}`); return; } console.dir(result, { depth: null, colors: true }); } if (require.main === module) { main().catch((error) => { console.error(error.message); process.exitCode = 1; }); } module.exports = { getConfig, publishConfig, readParameters, serializeIdentificationConfig, uploadIdentificationConfig, uploadVolumeConfig };