From 8cda026ac29027fcc1704d714b6339c8e3e4d00c Mon Sep 17 00:00:00 2001 From: vee Date: Thu, 30 Jul 2026 18:55:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + apps/web-console/app.js | 233 +++++++++++++++--- apps/web-console/index.html | 20 +- apps/web-console/styles.css | 5 +- config/modes/vehicles/denza_n7/baby_nap.yaml | 4 +- .../denza_n7/calf_decompression_float.yaml | 4 +- .../modes/vehicles/denza_n7/cruise_sofa.yaml | 4 +- .../vehicles/denza_n7/easy_entry_exit.yaml | 4 +- .../denza_n7/electric_beach_chair.yaml | 4 +- .../vehicles/denza_n7/leisure_lounger.yaml | 4 +- .../vehicles/denza_n7/meditation_space.yaml | 4 +- config/modes/vehicles/denza_n7/nap.yaml | 4 +- config/modes/vehicles/denza_n7/office.yaml | 4 +- .../denza_n7/queen_passenger_driving.yaml | 4 +- .../modes/vehicles/denza_n7/zero_gravity.yaml | 4 +- config/vehicles/denza_n7.yaml | 8 +- domains/seat/config/capabilities.yaml | 12 +- .../tests/integration/test_seat_service.py | 14 +- tests/e2e/test_seat_web_api.py | 8 + tests/integration/test_multidomain_service.py | 2 +- tests/unit/test_mode_profile.py | 43 +++- tests/unit/test_vehicle_profile.py | 5 +- 22 files changed, 315 insertions(+), 86 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c906a5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.venv/ +.env +.env.auth +__pycache__/ +*.py[cod] +artifacts/runs/ +tmp/*.log diff --git a/apps/web-console/app.js b/apps/web-console/app.js index 5853616..2f243f7 100755 --- a/apps/web-console/app.js +++ b/apps/web-console/app.js @@ -19,6 +19,8 @@ const state = { result: null, progressTimer: null, seatTwin: null, + seatActuatorState: {}, + availableSeatAxes: null, }; const $ = (selector, root = document) => root.querySelector(selector); @@ -1629,36 +1631,83 @@ function classifyReferenceMaterial(material) { } const TWIN_OUTLINE_STYLE = { - body: { color: 0x315d69, lineOpacity: 0.52, fillOpacity: 0.014, renderOrder: 30 }, - glass: { color: 0x78a7b2, lineOpacity: 0.18, fillOpacity: 0.006, renderOrder: 31 }, - detail: { color: 0x536f76, lineOpacity: 0.28, fillOpacity: 0.009, renderOrder: 32 }, - wheel: { color: 0x203d45, lineOpacity: 0.68, fillOpacity: 0.02, renderOrder: 33 }, - interior: { color: 0x607e83, lineOpacity: 0.24, fillOpacity: 0.008, renderOrder: 36 }, - light: { color: 0x536f76, lineOpacity: 0.28, fillOpacity: 0.009, renderOrder: 32 }, + body: { + color: 0x315d69, + lineOpacity: 0.36, + occludedColor: 0xa7b8bb, + occludedLineOpacity: 0.12, + fillOpacity: 0.11, + thresholdAngle: 58, + renderOrder: 30, + }, + glass: { color: 0x78a7b2, lineOpacity: 0, fillOpacity: 0.045, renderOrder: 31 }, + detail: { color: 0x536f76, lineOpacity: 0, fillOpacity: 0.028, renderOrder: 32 }, + wheel: { + color: 0x203d45, + lineOpacity: 0.52, + occludedColor: 0x9eafb2, + occludedLineOpacity: 0.14, + fillOpacity: 0.18, + thresholdAngle: 50, + renderOrder: 33, + }, + interior: { color: 0x607e83, lineOpacity: 0, fillOpacity: 0.012, renderOrder: 36 }, + light: { color: 0x536f76, lineOpacity: 0, fillOpacity: 0.07, renderOrder: 32 }, }; function addTwinMeshOutline(THREE, mesh, style, thresholdAngle = 30) { - if (!mesh.geometry?.attributes?.position || mesh.userData.twinOutline) return null; + if ( + !mesh.geometry?.attributes?.position + || mesh.userData.twinOutline + || style.lineOpacity <= 0 + ) return null; const geometry = new THREE.EdgesGeometry(mesh.geometry, thresholdAngle); if (!geometry.getAttribute("position")?.count) { geometry.dispose(); return null; } - const material = new THREE.LineBasicMaterial({ + const visibleMaterial = new THREE.LineBasicMaterial({ color: style.color, transparent: true, opacity: style.lineOpacity, depthWrite: false, depthTest: true, + depthFunc: THREE.LessEqualDepth, toneMapped: false, }); - const outline = new THREE.LineSegments(geometry, material); - outline.name = `${mesh.name || "mesh"}_outline`; - outline.renderOrder = style.renderOrder; - outline.frustumCulled = mesh.frustumCulled; - mesh.add(outline); - mesh.userData.twinOutline = outline; - return outline; + visibleMaterial.onBeforeCompile = (shader) => { + shader.vertexShader = shader.vertexShader.replace( + "#include ", + "#include \ngl_Position.z -= 0.0006 * gl_Position.w;", + ); + }; + visibleMaterial.customProgramCacheKey = () => "twin-visible-line-depth-bias-v1"; + const visibleOutline = new THREE.LineSegments(geometry, visibleMaterial); + visibleOutline.name = `${mesh.name || "mesh"}_outline_visible`; + visibleOutline.renderOrder = style.renderOrder + 1; + visibleOutline.frustumCulled = mesh.frustumCulled; + mesh.add(visibleOutline); + mesh.userData.twinOutline = visibleOutline; + + const occludedOpacity = style.occludedLineOpacity ?? 0; + if (occludedOpacity > 0) { + const occludedMaterial = new THREE.LineBasicMaterial({ + color: style.occludedColor ?? style.color, + transparent: true, + opacity: occludedOpacity, + depthWrite: false, + depthTest: true, + depthFunc: THREE.GreaterDepth, + toneMapped: false, + }); + const occludedOutline = new THREE.LineSegments(geometry, occludedMaterial); + occludedOutline.name = `${mesh.name || "mesh"}_outline_occluded`; + occludedOutline.renderOrder = style.renderOrder; + occludedOutline.frustumCulled = mesh.frustumCulled; + mesh.add(occludedOutline); + mesh.userData.twinOccludedOutline = occludedOutline; + } + return visibleOutline; } function applyTwinOutlineStyle(THREE, root, options = {}) { @@ -1694,6 +1743,9 @@ function configureTwinSolidEntity(THREE, roots, options = {}) { root.traverse((object) => { if (!object.isMesh || !object.visible) return; if (object.userData.twinOutline) object.userData.twinOutline.visible = false; + if (object.userData.twinOccludedOutline) { + object.userData.twinOccludedOutline.visible = false; + } const materials = Array.isArray(object.material) ? object.material : [object.material]; materials.filter(Boolean).forEach((material) => { material.transparent = Boolean(options.transparent); @@ -1792,12 +1844,20 @@ function configureReferenceVehicleModel(THREE, vehicle) { const material = sourceMaterial.clone(); const descriptor = referenceMaterialDescriptor(material); const category = classifyReferenceMaterial(material); + const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail; const key = `${category}:${material.name || "unnamed"}`; report.materials.set(key, (report.materials.get(key) || 0) + 1); - if (material.map) { - material.map.colorSpace = THREE.SRGBColorSpace; - material.map.anisotropy = 8; - } + [ + "map", + "aoMap", + "bumpMap", + "normalMap", + "roughnessMap", + "metalnessMap", + "emissiveMap", + ].forEach((textureKey) => { + if (textureKey in material) material[textureKey] = null; + }); if (material.emissive) { material.emissive.setHex(0x000000); if ("emissiveIntensity" in material) material.emissiveIntensity = 0; @@ -1832,11 +1892,15 @@ function configureReferenceVehicleModel(THREE, vehicle) { } else if (/chrome|metal|alloy|rim/.test(descriptor) && material.color) { material.color.setHex(0xaeb9bb); } - const outlineStyle = TWIN_OUTLINE_STYLE[category] || TWIN_OUTLINE_STYLE.detail; material.transparent = true; material.opacity = outlineStyle.fillOpacity; - material.depthWrite = false; - material.side = THREE.DoubleSide; + material.depthWrite = ["body", "detail", "wheel"].includes(category); + material.depthTest = true; + material.side = category === "glass" ? THREE.DoubleSide : THREE.FrontSide; + material.wireframe = false; + if ("flatShading" in material) material.flatShading = false; + if ("roughness" in material) material.roughness = category === "body" ? 0.72 : 0.82; + if ("metalness" in material) material.metalness = category === "wheel" ? 0.48 : 0.08; if (material.emissive) material.emissive.setHex(0x000000); if ("emissiveIntensity" in material) material.emissiveIntensity = 0; material.needsUpdate = true; @@ -1851,7 +1915,12 @@ function configureReferenceVehicleModel(THREE, vehicle) { const outlineCategory = categoryPriority.find((category) => categories.includes(category)) || "detail"; const outlineStyle = TWIN_OUTLINE_STYLE[outlineCategory]; object.renderOrder = outlineStyle.renderOrder - 1; - addTwinMeshOutline(THREE, object, outlineStyle, 30); + addTwinMeshOutline( + THREE, + object, + outlineStyle, + outlineStyle.thresholdAngle ?? 60, + ); }); return report; } @@ -2048,6 +2117,7 @@ function initSeatTwin() { const THREE = window.THREE; const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" }); + renderer.autoClear = false; renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); if ("outputColorSpace" in renderer) renderer.outputColorSpace = THREE.SRGBColorSpace; else renderer.outputEncoding = THREE.sRGBEncoding; @@ -2058,8 +2128,11 @@ function initSeatTwin() { scene.background = new THREE.Color(0xe8eeec); scene.fog = new THREE.Fog(0xe8eeec, 12, 19); const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 40); - scene.add(new THREE.HemisphereLight(0xffffff, 0x7d8b90, 2.2)); + const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x7d8b90, 2.2); + hemisphereLight.layers.enable(1); + scene.add(hemisphereLight); const keyLight = new THREE.DirectionalLight(0xffffff, 3.2); + keyLight.layers.enable(1); keyLight.position.set(-4, 7, 6); keyLight.castShadow = true; keyLight.shadow.mapSize.set(1024, 1024); @@ -2069,6 +2142,7 @@ function initSeatTwin() { keyLight.shadow.camera.bottom = -4; scene.add(keyLight); const fillLight = new THREE.DirectionalLight(0x9fc7d7, 1.15); + fillLight.layers.enable(1); fillLight.position.set(5, 3, -5); scene.add(fillLight); @@ -2148,6 +2222,14 @@ function initSeatTwin() { }); scene.add(fixedModels.driver.root); vehicleSystems.root.add(fixedModels.rear); + [ + vehicleSystems.root, + models.passenger.root, + fixedModels.driver.root, + fixedModels.rear, + ].forEach((root) => { + root.traverse((object) => object.layers.set(1)); + }); const initialDevice = () => ({ ventilation: { state: "off", level: 0 }, @@ -2300,6 +2382,7 @@ function initSeatTwin() { const anchorX = (projectedPosition.x * 0.5 + 0.5) * width; const anchorY = (-projectedPosition.y * 0.5 + 0.5) * height; const offset = { + longitudinal: { x: -104, y: 50 }, backrest_recline: { x: -108, y: -28 }, cushion_tilt: { x: 106, y: -4 }, legrest_height: { x: 112, y: 52 }, @@ -2388,7 +2471,16 @@ function initSeatTwin() { ); camera.lookAt(cameraTargetX, 1.14, -0.08); positionTwinCallouts(); + renderer.clear(); + camera.layers.set(0); renderer.render(scene, camera); + renderer.clearDepth(); + const sceneBackground = scene.background; + scene.background = null; + camera.layers.set(1); + renderer.render(scene, camera); + scene.background = sceneBackground; + camera.layers.set(0); if (!canvas.dataset.rendered) canvas.dataset.rendered = "true"; window.requestAnimationFrame(animate); } @@ -2461,6 +2553,58 @@ function applyTwinAngleRanges(vehicle) { }); } +function applySeatMotorAvailability(vehicle) { + const motors = vehicle?.seat?.motors || {}; + const availableAxes = new Set(Object.keys(motors)); + state.availableSeatAxes = availableAxes; + Object.keys(TWIN_DEFAULT_POSITIONS).forEach((axis) => { + const available = availableAxes.has(axis); + $$(`[data-axis="${axis}"], [data-twin-axis="${axis}"], [data-callout="${axis}"]`) + .forEach((item) => { + item.classList.toggle("seat-axis-unavailable", !available); + item.setAttribute("aria-hidden", String(!available)); + }); + const motorName = motors[axis]?.name || axis; + const twinLabel = axis === "longitudinal" + ? `${motorName}位置` + : ["cushion_tilt", "backrest_recline", "legrest_height"].includes(axis) + ? `${motorName}夹角` + : motorName; + $$(`[data-twin-axis="${axis}"] > span, [data-callout="${axis}"] > span`) + .forEach((label) => { label.textContent = twinLabel; }); + }); + const cushionName = motors.cushion_tilt?.name; + const backrestName = motors.backrest_recline?.name; + const legrestName = motors.legrest_height?.name; + const backrestIncludedLabel = $('[data-callout="backrest_included_angle"] > span'); + const legrestIncludedLabel = $('[data-callout="legrest_included_angle"] > span'); + if (backrestIncludedLabel && backrestName && cushionName) { + backrestIncludedLabel.textContent = `${backrestName}-${cushionName}夹角`; + } + if (legrestIncludedLabel && cushionName && legrestName) { + legrestIncludedLabel.textContent = `${cushionName}-${legrestName}夹角`; + } +} + +function renderSeatMotorRows(vehicle) { + const motors = Object.entries(vehicle?.seat?.motors || {}); + $$(".seat-card .motion-state-grid").forEach((grid) => { + const fragment = document.createDocumentFragment(); + motors.forEach(([axis, motor]) => { + const item = document.createElement("div"); + item.className = "motion-axis"; + item.dataset.axis = axis; + const label = document.createElement("span"); + label.textContent = motor?.name || axis; + const value = document.createElement("b"); + value.textContent = "--"; + item.append(label, value, document.createElement("i")); + fragment.append(item); + }); + grid.replaceChildren(fragment); + }); +} + function selectVehicle(vehicleId, { silent = false } = {}) { const vehicle = findVehicleProfile(vehicleId); if (!vehicle) return; @@ -2468,6 +2612,8 @@ function selectVehicle(vehicleId, { silent = false } = {}) { const select = $("#vehicleSelect"); if (select) select.value = vehicle.id; applyTwinAngleRanges(vehicle); + renderSeatMotorRows(vehicle); + applySeatMotorAvailability(vehicle); const longitudinal = vehicle.seat?.motors?.longitudinal; const backrest = vehicle.seat?.motors?.backrest_recline; $("#vehicleNote").textContent = longitudinal && backrest @@ -2476,7 +2622,7 @@ function selectVehicle(vehicleId, { silent = false } = {}) { if (state.result?.actuator_state) { renderSeatState(state.result.actuator_state); } else if (state.seatTwin?.actuatorState) { - updateSeatTwin(state.seatTwin.actuatorState); + renderSeatState(state.seatTwin.actuatorState); } if (!silent) showToast(`已切换车型:${vehicle.name}`); } @@ -2767,10 +2913,41 @@ function resetSeatState() { }); } +function mergeSeatActuatorState(previousState, patchState) { + const mergedState = { ...(previousState || {}) }; + Object.entries(patchState || {}).forEach(([seat, functions]) => { + const previousSeat = mergedState[seat] || {}; + const mergedSeat = { ...previousSeat }; + Object.entries(functions || {}).forEach(([feature, value]) => { + if (feature === "position") { + const previousPosition = previousSeat.position || {}; + const mergedPosition = { ...previousPosition }; + Object.entries(value || {}).forEach(([axis, motion]) => { + mergedPosition[axis] = motion && typeof motion === "object" + ? { ...(previousPosition[axis] || {}), ...motion } + : motion; + }); + mergedSeat.position = mergedPosition; + return; + } + mergedSeat[feature] = value && typeof value === "object" && !Array.isArray(value) + ? { ...(previousSeat[feature] || {}), ...value } + : value; + }); + mergedState[seat] = mergedSeat; + }); + return mergedState; +} + function renderSeatState(actuatorState) { - resetSeatState(); if (!actuatorState) return; - Object.entries(actuatorState).forEach(([seat, functions]) => { + const mergedActuatorState = mergeSeatActuatorState( + state.seatActuatorState, + actuatorState, + ); + state.seatActuatorState = mergedActuatorState; + resetSeatState(); + Object.entries(mergedActuatorState).forEach(([seat, functions]) => { Object.entries(functions).forEach(([feature, value]) => { if (feature === "position") { Object.entries(value).forEach(([axis, motion]) => { @@ -2793,7 +2970,7 @@ function renderSeatState(actuatorState) { $("b", item).textContent = value.state === "on" ? `ON · L${value.level}` : "OFF"; }); }); - updateSeatTwin(actuatorState); + updateSeatTwin(mergedActuatorState); } function renderDomainState(domainState) { diff --git a/apps/web-console/index.html b/apps/web-console/index.html index f1bca97..5c6dca9 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -10,7 +10,7 @@ - + @@ -289,7 +289,7 @@
- +
全景天幕透光 100% 仪表台 HVAC关闭 @@ -348,13 +348,7 @@ 通风 -- 加热 --
-
-
水平--
-
坐垫俯仰--
-
靠背--
-
腿托升降--
-
腿托伸缩--
-
+
副驾front_right
@@ -362,13 +356,7 @@ 通风 -- 加热 --
-
-
水平--
-
坐垫俯仰--
-
靠背--
-
腿托升降--
-
腿托伸缩--
-
+
diff --git a/apps/web-console/styles.css b/apps/web-console/styles.css index 444f1f2..16348b0 100755 --- a/apps/web-console/styles.css +++ b/apps/web-console/styles.css @@ -3015,11 +3015,14 @@ pre { bottom: 18px; } -.joint-twin-shell [data-callout="longitudinal"], .joint-twin-shell [data-callout="legrest_extension"] { display: none !important; } +.seat-axis-unavailable { + display: none !important; +} + .joint-twin-shell .twin-callout { min-width: 67px; padding: 5px 6px; diff --git a/config/modes/vehicles/denza_n7/baby_nap.yaml b/config/modes/vehicles/denza_n7/baby_nap.yaml index bc7922f..aec1748 100644 --- a/config/modes/vehicles/denza_n7/baby_nap.yaml +++ b/config/modes/vehicles/denza_n7/baby_nap.yaml @@ -13,8 +13,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 19 - reason: 宝宝午睡模式水平电机目标 40mm,按腾势 N7 215mm 行程换算。 + position_pct: 81 + reason: 宝宝午睡模式水平电机目标 175mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/calf_decompression_float.yaml b/config/modes/vehicles/denza_n7/calf_decompression_float.yaml index 3bc7c2a..bc3489d 100644 --- a/config/modes/vehicles/denza_n7/calf_decompression_float.yaml +++ b/config/modes/vehicles/denza_n7/calf_decompression_float.yaml @@ -14,8 +14,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 49 - reason: 小腿解压悬浮模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。 + position_pct: 51 + reason: 小腿解压悬浮模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/cruise_sofa.yaml b/config/modes/vehicles/denza_n7/cruise_sofa.yaml index f203278..6940af5 100644 --- a/config/modes/vehicles/denza_n7/cruise_sofa.yaml +++ b/config/modes/vehicles/denza_n7/cruise_sofa.yaml @@ -13,8 +13,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 49 - reason: 巡航沙发模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。 + position_pct: 51 + reason: 巡航沙发模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/easy_entry_exit.yaml b/config/modes/vehicles/denza_n7/easy_entry_exit.yaml index f72e80e..ae9a8b5 100644 --- a/config/modes/vehicles/denza_n7/easy_entry_exit.yaml +++ b/config/modes/vehicles/denza_n7/easy_entry_exit.yaml @@ -13,8 +13,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 0 - reason: 轻松进出模式上下车水平电机目标 0mm。 + position_pct: 100 + reason: 轻松进出模式上下车水平电机目标 215mm。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/electric_beach_chair.yaml b/config/modes/vehicles/denza_n7/electric_beach_chair.yaml index 24d9c2b..a00839b 100644 --- a/config/modes/vehicles/denza_n7/electric_beach_chair.yaml +++ b/config/modes/vehicles/denza_n7/electric_beach_chair.yaml @@ -14,8 +14,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 0 - reason: 电感沙滩椅水平电机目标 0mm。 + position_pct: 100 + reason: 电感沙滩椅水平电机目标 215mm。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/leisure_lounger.yaml b/config/modes/vehicles/denza_n7/leisure_lounger.yaml index 163e364..040d5e7 100644 --- a/config/modes/vehicles/denza_n7/leisure_lounger.yaml +++ b/config/modes/vehicles/denza_n7/leisure_lounger.yaml @@ -13,8 +13,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 49 - reason: 休闲躺椅模式水平电机目标 105mm,按腾势 N7 215mm 行程换算。 + position_pct: 51 + reason: 休闲躺椅模式水平电机目标 110mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/meditation_space.yaml b/config/modes/vehicles/denza_n7/meditation_space.yaml index ad688bb..9a146dc 100644 --- a/config/modes/vehicles/denza_n7/meditation_space.yaml +++ b/config/modes/vehicles/denza_n7/meditation_space.yaml @@ -14,8 +14,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 0 - reason: 冥想空间模式水平电机目标 0mm。 + position_pct: 100 + reason: 冥想空间模式水平电机目标 215mm。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/nap.yaml b/config/modes/vehicles/denza_n7/nap.yaml index fa958d9..5643f1b 100644 --- a/config/modes/vehicles/denza_n7/nap.yaml +++ b/config/modes/vehicles/denza_n7/nap.yaml @@ -13,8 +13,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 100 - reason: 小憩模式水平电机目标 215mm,按腾势 N7 215mm 行程换算。 + position_pct: 0 + reason: 小憩模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/office.yaml b/config/modes/vehicles/denza_n7/office.yaml index 6178c5f..2ebe45b 100644 --- a/config/modes/vehicles/denza_n7/office.yaml +++ b/config/modes/vehicles/denza_n7/office.yaml @@ -13,8 +13,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 28 - reason: 办公模式水平电机目标 60mm,按腾势 N7 215mm 行程换算。 + position_pct: 72 + reason: 办公模式水平电机目标 155mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/queen_passenger_driving.yaml b/config/modes/vehicles/denza_n7/queen_passenger_driving.yaml index 72536e4..65489ac 100644 --- a/config/modes/vehicles/denza_n7/queen_passenger_driving.yaml +++ b/config/modes/vehicles/denza_n7/queen_passenger_driving.yaml @@ -14,8 +14,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 28 - reason: 女王副驾水平电机目标 60mm,按腾势 N7 215mm 行程换算。 + position_pct: 72 + reason: 女王副驾水平电机目标 155mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/modes/vehicles/denza_n7/zero_gravity.yaml b/config/modes/vehicles/denza_n7/zero_gravity.yaml index 0064998..6f69f66 100644 --- a/config/modes/vehicles/denza_n7/zero_gravity.yaml +++ b/config/modes/vehicles/denza_n7/zero_gravity.yaml @@ -16,8 +16,8 @@ operations: target_scope: passenger operation: set parameters: - position_pct: 0 - reason: 零重力模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。 + position_pct: 100 + reason: 零重力模式水平电机目标 215mm,按腾势 N7 215mm 行程换算。 - domain: seat capability: seat.set_cushion_tilt target_scope: passenger diff --git a/config/vehicles/denza_n7.yaml b/config/vehicles/denza_n7.yaml index e0407f5..950e3ea 100644 --- a/config/vehicles/denza_n7.yaml +++ b/config/vehicles/denza_n7.yaml @@ -6,15 +6,15 @@ seat: name: 水平 unit: mm range: 215 - reference_zero: rearmost - positive_direction: forward + reference_zero: forwardmost + positive_direction: backward hall_total_pulses: 480 cushion_tilt: name: 坐盆 unit: percent range: 100 - reference_zero: lowest - positive_direction: up + reference_zero: highest + positive_direction: down hall_total_pulses: 374 backrest_recline: name: 座椅靠背 diff --git a/domains/seat/config/capabilities.yaml b/domains/seat/config/capabilities.yaml index 6665223..f168542 100755 --- a/domains/seat/config/capabilities.yaml +++ b/domains/seat/config/capabilities.yaml @@ -63,9 +63,10 @@ spec: confirmation_policy: on_ambiguity - name: seat.set_longitudinal_position description: >- - Set one front seat's fore-aft rail position. position_pct is an absolute target: 0 is the - rearmost mechanical limit and 100 is the forwardmost limit. Resolve relative requests such - as "move back a little" against the current position supplied in runtime state. For a + Set one front seat's fore-aft rail position. For the selected Denza N7 profile, position_pct + is an absolute target: 0 is the forwardmost mechanical limit and 100 is the rearmost limit. + Resolve relative requests such as "move back a little" against the current position supplied + in runtime state: forward decreases the value and backward increases it. For a vehicle-specific physical relative request, distance_mm may be supplied and the runtime converts it to an absolute position_pct before CAN compilation. target_devices: @@ -92,8 +93,9 @@ spec: confirmation_policy: on_ambiguity - name: seat.set_cushion_tilt description: >- - Set the seat-cushion pitch for one front seat. position_pct is absolute: 0 places the front - edge at its lowest limit and 100 raises the front edge to its highest tilt limit. + Set the seat-cushion pitch for one front seat. For the selected Denza N7 profile, + position_pct is absolute: 0 places the front edge at its highest limit and 100 lowers the + front edge to its lowest tilt limit. target_devices: - seat_cushion_tilt_motor target_scopes: diff --git a/domains/seat/tests/integration/test_seat_service.py b/domains/seat/tests/integration/test_seat_service.py index 32b06ee..4e88449 100755 --- a/domains/seat/tests/integration/test_seat_service.py +++ b/domains/seat/tests/integration/test_seat_service.py @@ -335,7 +335,7 @@ class SeatServiceIntegrationTests(unittest.TestCase): "domain": "seat", "capability": "seat.set_longitudinal_position", "target_scope": "driver", - "operation": "decrease", + "operation": "increase", "parameters": [{"name": "position_pct", "value": position}], "reason": "Model resolved the relative request.", } @@ -349,7 +349,7 @@ class SeatServiceIntegrationTests(unittest.TestCase): with patch.object( self.service.openai, "interpret", - side_effect=[output(40), output(30)], + side_effect=[output(60), output(70)], ) as interpret: first = self.service.process( "主驾往后一点", "openai", "conv-motion-follow-up" @@ -362,18 +362,18 @@ class SeatServiceIntegrationTests(unittest.TestCase): first["actuator_state"]["driver"]["position"]["longitudinal"][ "position_pct" ], - 40, + 60, ) self.assertEqual( second["actuator_state"]["driver"]["position"]["longitudinal"][ "position_pct" ], - 30, + 70, ) second_context = interpret.call_args_list[1].args[1] self.assertEqual( second_context["devices"]["driver"]["position"]["longitudinal"]["position_pct"], - 40, + 60, ) def test_model_relative_physical_motion_uses_vehicle_profile(self) -> None: @@ -384,7 +384,7 @@ class SeatServiceIntegrationTests(unittest.TestCase): "domain": "seat", "capability": "seat.set_longitudinal_position", "target_scope": "driver", - "operation": "increase", + "operation": "decrease", "parameters": [{"name": "distance_mm", "value": 50}], "reason": "Move forward by a physical distance.", }, @@ -414,7 +414,7 @@ class SeatServiceIntegrationTests(unittest.TestCase): self.assertEqual(result["status"], "succeeded") self.assertEqual(result["vehicle"]["id"], "denza_n7") operations = result["function_config"]["spec"]["operations"] - self.assertEqual(operations[0]["parameters"], {"position_pct": 73}) + self.assertEqual(operations[0]["parameters"], {"position_pct": 27}) self.assertEqual( operations[1]["parameters"], {"position_pct": 38, "angle_deg": 116}, diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py index f345bfd..b510d38 100755 --- a/tests/e2e/test_seat_web_api.py +++ b/tests/e2e/test_seat_web_api.py @@ -134,6 +134,14 @@ class SeatWebApiTests(unittest.TestCase): runtime["vehicle"]["seat"]["motors"]["longitudinal"]["range"], 215, ) + self.assertEqual( + runtime["vehicle"]["seat"]["motors"]["longitudinal"]["reference_zero"], + "forwardmost", + ) + self.assertEqual( + runtime["vehicle"]["seat"]["motors"]["cushion_tilt"]["reference_zero"], + "highest", + ) capability_names = {item["name"] for item in runtime["capabilities"]} self.assertIn("seat.save_posture_preset", capability_names) self.assertIn("seat.apply_posture_preset", capability_names) diff --git a/tests/integration/test_multidomain_service.py b/tests/integration/test_multidomain_service.py index 157cb92..42fa859 100755 --- a/tests/integration/test_multidomain_service.py +++ b/tests/integration/test_multidomain_service.py @@ -242,7 +242,7 @@ class MultiDomainServiceTests(unittest.TestCase): by_capability = {item["capability"]: item for item in operations} self.assertEqual( by_capability["seat.set_longitudinal_position"]["parameters"], - {"position_pct": 0}, + {"position_pct": 100}, ) self.assertEqual( by_capability["seat.set_cushion_tilt"]["parameters"], diff --git a/tests/unit/test_mode_profile.py b/tests/unit/test_mode_profile.py index b3ffac0..68b077b 100644 --- a/tests/unit/test_mode_profile.py +++ b/tests/unit/test_mode_profile.py @@ -24,7 +24,7 @@ class ModeProfileRegistryTests(unittest.TestCase): self.assertEqual(requests[0]["capability"], "seat.set_longitudinal_position") self.assertEqual(requests[0]["target_scope"], "passenger") self.assertIn( - {"name": "position_pct", "value": 0}, + {"name": "position_pct", "value": 100}, requests[0]["parameters"], ) by_capability = {request["capability"]: request for request in requests} @@ -88,6 +88,47 @@ class ModeProfileRegistryTests(unittest.TestCase): "calf_decompression_float", ) + def test_denza_mode_positions_use_corrected_n7_zero_points(self) -> None: + registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes") + expected_positions = { + "baby_nap": (81, 30), + "calf_decompression_float": (51, 75), + "cruise_sofa": (51, 30), + "easy_entry_exit": (100, 0), + "electric_beach_chair": (100, 0), + "leisure_lounger": (51, 30), + "meditation_space": (100, 44), + "nap": (0, 0), + "office": (72, 22), + "queen_passenger_driving": (72, 38), + "zero_gravity": (100, 100), + } + + profiles = { + profile.id: profile + for profile in registry.profiles() + if profile.id in expected_positions and profile.vehicle_ids == ("denza_n7",) + } + actual_positions = {} + for profile_id, profile in profiles.items(): + requests = { + request["capability"]: request + for request in registry.requests_for_profiles([profile]) + } + actual_positions[profile_id] = tuple( + next( + parameter["value"] + for parameter in requests[capability]["parameters"] + if parameter["name"] == "position_pct" + ) + for capability in ( + "seat.set_longitudinal_position", + "seat.set_cushion_tilt", + ) + ) + + self.assertEqual(actual_positions, expected_positions) + def test_common_modes_are_visible_with_vehicle_specific_overrides(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) diff --git a/tests/unit/test_vehicle_profile.py b/tests/unit/test_vehicle_profile.py index e867b13..95870f4 100644 --- a/tests/unit/test_vehicle_profile.py +++ b/tests/unit/test_vehicle_profile.py @@ -26,7 +26,10 @@ class VehicleProfileTests(unittest.TestCase): }) self.assertEqual(profile.seat_motors["longitudinal"].range, 215) self.assertEqual(profile.seat_motors["longitudinal"].hall_total_pulses, 480) - self.assertEqual(profile.seat_motors["longitudinal"].reference_zero, "rearmost") + self.assertEqual(profile.seat_motors["longitudinal"].reference_zero, "forwardmost") + self.assertEqual(profile.seat_motors["longitudinal"].positive_direction, "backward") + self.assertEqual(profile.seat_motors["cushion_tilt"].reference_zero, "highest") + self.assertEqual(profile.seat_motors["cushion_tilt"].positive_direction, "down") self.assertEqual(profile.seat_motors["backrest_recline"].range, 68) self.assertEqual(profile.seat_motors["backrest_recline"].hall_total_pulses, 917) self.assertEqual(profile.seat_motors["legrest_height"].range, 78)