From 5bf14aed888cd0e258e325c65f14022dad02985b Mon Sep 17 00:00:00 2001 From: baoshiwei <baoshiwei@shlanbao.cn> Date: 星期四, 17 七月 2025 15:32:01 +0800 Subject: [PATCH] 更改为通过websocket获取数据 --- src/components/ForceChart.vue | 77 +--- src/pipe_client.ts | 11 src-tauri/src/main.rs | 2 src/components/ThreeDDisplay.vue | 55 -- src/components/GaugeDisplay.vue | 67 +--- README.md | 4 src/components/ThreeDDisplay copy.vue | 525 ++++++++++++++++++++++++++++++++ src/components/TableDisplay.vue | 42 -- src/utils/dataFetcher.ts | 68 ++++ src-tauri/src/ws_client.rs | 45 ++ src-tauri/src/lib.rs | 14 src/App.vue | 27 + src-tauri/Cargo.toml | 6 13 files changed, 751 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 3d9a6a4..620ab6f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ 杩欎釜妯℃澘鏃ㄥ湪甯姪鎮ㄥ紑濮嬩娇鐢� Vue 3 鍜� TypeScript 鍦� Vite 涓繘琛屽紑鍙戙�傝妯℃澘浣跨敤 Vue 3 `<script setup>` SFCs锛岃鏌ョ湅 [script setup 鏂囨。](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) 浜嗚В鏇村淇℃伅銆� +## 椤圭洰浠嬬粛 + +鏈」鐩负"鍏淮鍔涗紶鎰熷櫒娴嬭瘯杞欢"锛屽熀浜� Tauri + Vue3 + TypeScript 寮�鍙戙�傝蒋浠堕�氳繃绠¢亾閫氫俊瀹炴椂鎺ユ敹鍏淮鍔涳紙Fx, Fy, Fz, Mx, My, Mz锛夋暟鎹紝鏀寔鏇茬嚎鍥俱�佷华琛ㄧ洏銆佹暟鎹〃鏍煎拰 3D 鍧愭爣杞寸瓑澶氱鍙鍖栨柟寮忥紝鏂逛究鐢ㄦ埛瀵瑰叚缁村姏浼犳劅鍣ㄨ繘琛岃皟璇曘�佹祴璇曞拰鏁版嵁鍒嗘瀽銆傞�傜敤浜庣鐮斻�佸伐涓氱瓑澶氱搴旂敤鍦烘櫙銆� + ## 鎺ㄨ崘鐨� IDE 璁剧疆 - [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3d32d50..7cea56c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,3 +25,9 @@ tokio = { version = "1.45.0", features = ["rt", "net", "time"] } rand = "0.8.5" +once_cell = "1.19" +tokio-tungstenite = "0.21" +futures-util = "0.3" +tungstenite = "0.21" +anyhow = "1.0" + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 193a629..8e0934c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,9 @@ mod tokio_utils; mod pipe_server; +mod ws_client; +use crate::ws_client::LATEST_WS_DATA; + + use std::sync::Mutex; use tauri::async_runtime::spawn; @@ -9,6 +13,10 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ use crate::pipe_server::start_pipe_server; +#[tauri::command] +fn get_latest_ws_data() -> Option<String> { + LATEST_WS_DATA.lock().unwrap().clone() +} #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) @@ -38,6 +46,8 @@ .map_err(|e| e.to_string()) } + + // #[cfg_attr(mobile, tauri::mobile_entry_point)] // pub fn run() { // tauri::Builder::default() @@ -60,10 +70,12 @@ backend_task: false, })) // Add a command we can use to check - .invoke_handler(tauri::generate_handler![greet, set_complete, start_pipe_server_command, send_pipe_message]) + .invoke_handler(tauri::generate_handler![greet, set_complete, start_pipe_server_command, send_pipe_message, get_latest_ws_data]) // Use the setup hook to execute setup related tasks // Runs before the main loop, so no windows are yet created .setup(|app| { + // 鍚姩 WebSocket 瀹㈡埛绔紙寮傛浠诲姟锛� + ws_client::start_ws_client(); // Spawn setup as a non-blocking task so the windows can be // created and ran while it executes spawn(setup(app.handle().clone())); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9feb543..1da1ce2 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - six_axis_force_lib::run() + six_axis_force_lib::run(); } diff --git a/src-tauri/src/ws_client.rs b/src-tauri/src/ws_client.rs new file mode 100644 index 0000000..0184b8c --- /dev/null +++ b/src-tauri/src/ws_client.rs @@ -0,0 +1,45 @@ +use std::sync::{Arc, Mutex}; +use once_cell::sync::Lazy; +use tokio::runtime::Runtime; +use tokio_tungstenite::connect_async; +use futures_util::{StreamExt}; + +// 鍏ㄥ眬缂撳瓨鏈�鏂板叚缁村姏鏁版嵁 +pub static LATEST_WS_DATA: Lazy<Arc<Mutex<Option<String>>>> = Lazy::new(|| Arc::new(Mutex::new(None))); + +// WebSocket 鏈嶅姟绔湴鍧�锛堝彲鏍规嵁瀹為檯鎯呭喌淇敼锛� +const WS_SERVER_URL: &str = "ws://192.168.21.25:5000"; + +pub fn start_ws_client() { + // 鍚姩 tokio runtime + std::thread::spawn(|| { + let rt = Runtime::new().unwrap(); + rt.block_on(async move { + if let Err(e) = ws_client_task().await { + eprintln!("WebSocket 瀹㈡埛绔惎鍔ㄥけ璐�: {}", e); + } + }); + }); +} + +async fn ws_client_task() -> anyhow::Result<()> { + println!("灏濊瘯杩炴帴 WebSocket 鏈嶅姟绔�: {}", WS_SERVER_URL); + let (ws_stream, _) = connect_async(WS_SERVER_URL).await?; + println!("WebSocket 宸茶繛鎺�"); + let (_, mut read) = ws_stream.split(); + while let Some(msg) = read.next().await { + match msg { + Ok(tungstenite::Message::Text(text)) => { + // 鍋囪鏈嶅姟绔帹閫佺殑灏辨槸鍏淮鍔涙暟鎹殑 JSON 瀛楃涓� + let mut data = LATEST_WS_DATA.lock().unwrap(); + *data = Some(text.clone()); + }, + Ok(_) => {}, + Err(e) => { + eprintln!("WebSocket 璇诲彇閿欒: {}", e); + break; + } + } + } + Ok(()) +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index bb69d54..f6a644e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,6 +12,7 @@ const showChart = ref(false); const pipeName = 'tauri-pipe-server'; const currentDisplay = ref('line'); // 榛樿鏄剧ず鏇茬嚎鍥� +const dataSource = ref<'pipe' | 'ws'>('ws'); // 榛樿鏁版嵁婧愪负绠¢亾 async function startPipeServer() { try { @@ -43,6 +44,13 @@ <template> <TitleBar :currentDisplay="currentDisplay" @start-service="startPipeServer" @select-display="(mode) => currentDisplay = mode" /> <main class="container"> + <!-- <div class="data-source-switch"> + <label>鏁版嵁婧愶細</label> + <select v-model="dataSource"> + <option value="pipe">绠¢亾</option> + <option value="ws">WebSocket</option> + </select> + </div> --> <div class="display-mode-icons"> <button :class="['icon-button', { active: currentDisplay === 'line' }]" @click="currentDisplay = 'line'" title="鏇茬嚎鍥�"> @@ -54,9 +62,9 @@ <button :class="['icon-button', { active: currentDisplay === 'table' }]" @click="currentDisplay = 'table'" title="鏁版嵁琛ㄦ牸"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="7" rx="2"/><path d="M10 12H6"/><path d="M18 12h-4"/><path d="M10 16H6"/><path d="M18 16h-4"/></svg> </button> - <!-- <button :class="['icon-button', { active: currentDisplay === '3d' }]" @click="currentDisplay = '3d'" title="3D鍧愭爣杞�"> + <button :class="['icon-button', { active: currentDisplay === '3d' }]" @click="currentDisplay = '3d'" title="3D鍧愭爣杞�"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5l10-5l-10-5z"/><path d="M2 17l10 5l10-5"/><path d="M2 12l10 5l10-5"/></svg> - </button> --> + </button> </div> <div class="header-row"> <h2>Force Sensor Data Visualization</h2> @@ -66,10 +74,10 @@ <!-- 鏄剧ず鍔涗紶鎰熷櫒鏁版嵁 --> <div v-if="showChart" class="chart-wrapper"> - <ForceChart v-if="currentDisplay === 'line'" :pipeName="pipeName" /> - <GaugeDisplay v-else-if="currentDisplay === 'gauge'" :pipeName="pipeName" /> - <TableDisplay v-else-if="currentDisplay === 'table'" :pipeName="pipeName" /> - <ThreeDDisplay v-else-if="currentDisplay === '3d'" :pipeName="pipeName" /> + <ForceChart v-if="currentDisplay === 'line'" :pipeName="pipeName" :dataSource="dataSource" /> + <GaugeDisplay v-else-if="currentDisplay === 'gauge'" :pipeName="pipeName" :dataSource="dataSource" /> + <TableDisplay v-else-if="currentDisplay === 'table'" :pipeName="pipeName" :dataSource="dataSource" /> + <ThreeDDisplay v-else-if="currentDisplay === '3d'" :pipeName="pipeName" :dataSource="dataSource" /> </div> <!-- <div class="input-group"> @@ -124,6 +132,13 @@ color: #000; font-size: 0.9em; } + +.data-source-switch { + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} </style> <style> diff --git a/src/components/ForceChart.vue b/src/components/ForceChart.vue index 138a1c3..bc3c801 100644 --- a/src/components/ForceChart.vue +++ b/src/components/ForceChart.vue @@ -1,11 +1,7 @@ <script setup lang="ts"> -import { ref, onMounted, onUnmounted} from 'vue'; +import { ref, onMounted, onUnmounted } from 'vue'; import * as echarts from 'echarts'; -import { sendToPipe } from '../pipe_client'; - -const props = defineProps<{ - pipeName: string; -}>(); +import { createDataReceiver } from '../utils/dataFetcher'; const chartContainer = ref<HTMLElement | null>(null); let chart: echarts.ECharts | null = null; @@ -64,37 +60,43 @@ name: 'Fx', type: 'line', data: dataHistory.value[0], - smooth: true + smooth: true, + showSymbol: false }, { name: 'Fy', type: 'line', data: dataHistory.value[1], - smooth: true + smooth: true, + showSymbol: false }, { name: 'Fz', type: 'line', data: dataHistory.value[2], - smooth: true + smooth: true, + showSymbol: false }, { name: 'Mx', type: 'line', data: dataHistory.value[3], - smooth: true + smooth: true, + showSymbol: false }, { name: 'My', type: 'line', data: dataHistory.value[4], - smooth: true + smooth: true, + showSymbol: false }, { name: 'Mz', type: 'line', data: dataHistory.value[5], - smooth: true + smooth: true, + showSymbol: false } ] }; @@ -150,53 +152,22 @@ chart?.resize(); } -// 鐘舵�佸彉閲忥紝鐢ㄤ簬鎺у埗閿欒鏄剧ず棰戠巼 -const errorCount = ref(0); -const maxConsecutiveErrors = 5; -const showingError = ref(false); - -// 鎺ユ敹绠¢亾鏁版嵁鐨勫嚱鏁� -async function receiveForceData() { - try { - // 浠庣閬撴帴鏀舵暟鎹� - const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); - - // 鎴愬姛鎺ユ敹鏁版嵁锛岄噸缃敊璇鏁� - errorCount.value = 0; - if (showingError.value) { - showingError.value = false; - console.log('绠¢亾閫氫俊宸叉仮澶�'); - } - - // 瑙f瀽鎺ユ敹鍒扮殑鏁版嵁 - try { - const forceData = JSON.parse(response); - if (Array.isArray(forceData) && forceData.length === 6) { - updateChart(forceData); - } - } catch (e) { - console.warn('瑙f瀽鏁版嵁澶辫触:', e); - } - } catch (err) { - // 澧炲姞閿欒璁℃暟 - errorCount.value++; - - // 鍙湪杩炵画閿欒杈惧埌闃堝�兼椂鏄剧ず閿欒淇℃伅锛岄伩鍏嶆棩蹇楀埛灞� - if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { - showingError.value = true; - console.error('绠¢亾閫氫俊鎸佺画澶辫触锛岃妫�鏌ユ湇鍔$鐘舵��:', err); - } - } -} - // 瀹氭椂鑾峰彇鏁版嵁 let dataTimer: number | null = null; + onMounted(() => { initChart(); - // 姣忕鑾峰彇涓�娆℃暟鎹� - dataTimer = window.setInterval(receiveForceData, 1000); + const receiveData = createDataReceiver( (forceData) => { + updateChart(forceData); + }); + + // 姣�100ms鑾峰彇涓�娆℃暟鎹� + dataTimer = window.setInterval(receiveData, 100); + + // 鐩戝惉绐楀彛澶у皬鍙樺寲锛岃皟鏁村浘琛ㄥぇ灏� + window.addEventListener('resize', handleResize); }); onUnmounted(() => { diff --git a/src/components/GaugeDisplay.vue b/src/components/GaugeDisplay.vue index a322a3f..9630d2c 100644 --- a/src/components/GaugeDisplay.vue +++ b/src/components/GaugeDisplay.vue @@ -1,11 +1,7 @@ <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; import * as echarts from 'echarts'; -import { sendToPipe } from '../pipe_client'; - -const props = defineProps<{ - pipeName: string; -}>(); +import { createDataReceiver } from '../utils/dataFetcher'; const chartContainer = ref<HTMLElement | null>(null); let chart: echarts.ECharts | null = null; @@ -38,8 +34,8 @@ { name: 'Fx', type: 'gauge', - min: -100, - max: 100, + min: 1000, + max: 4000, splitNumber: 10, radius: '30%', center: ['16.7%', '30%'], @@ -61,8 +57,8 @@ { name: 'Fy', type: 'gauge', - min: -100, - max: 100, + min: 1000, + max: 4000, splitNumber: 10, radius: '30%', center: ['50%', '30%'], @@ -84,8 +80,8 @@ { name: 'Fz', type: 'gauge', - min: -100, - max: 100, + min: 1000, + max: 4000, splitNumber: 10, radius: '30%', center: ['83.3%', '30%'], @@ -107,8 +103,8 @@ { name: 'Mx', type: 'gauge', - min: -10, - max: 10, + min: 1000, + max: 4000, splitNumber: 10, radius: '30%', center: ['16.7%', '75%'], @@ -130,8 +126,8 @@ { name: 'My', type: 'gauge', - min: -10, - max: 10, + min: 1000, + max: 4000, splitNumber: 10, radius: '30%', center: ['50%', '75%'], @@ -153,8 +149,8 @@ { name: 'Mz', type: 'gauge', - min: -10, - max: 10, + min: 1000, + max: 4000, splitNumber: 10, radius: '30%', center: ['83.3%', '75%'], @@ -209,46 +205,17 @@ }); } -// 鐘舵�佸彉閲忥紝鐢ㄤ簬鎺у埗閿欒鏄剧ず棰戠巼 -const errorCount = ref(0); -const maxConsecutiveErrors = 5; -const showingError = ref(false); -// 鎺ユ敹绠¢亾鏁版嵁鐨勫嚱鏁� -async function receiveData() { - try { - const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); - - // 閲嶇疆閿欒璁℃暟 - errorCount.value = 0; - showingError.value = false; - - // 瑙f瀽鏁版嵁 - try { - const forceData = JSON.parse(response); - if (Array.isArray(forceData) && forceData.length === 6) { - updateChart(forceData); - } - } catch (parseErr) { - console.error('鏁版嵁瑙f瀽閿欒:', parseErr); - } - } catch (err) { - // 澧炲姞閿欒璁℃暟 - errorCount.value++; - - // 鍙湪杩炵画閿欒杈惧埌闃堝�间笖灏氭湭鏄剧ず閿欒鏃舵樉绀� - if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { - console.error('绠¢亾閫氫俊澶辫触:', err); - showingError.value = true; - } - } -} let dataInterval: number | null = null; onMounted(() => { initChart(); + const receiveData = createDataReceiver((forceData) => { + updateChart(forceData); + }); + // 姣�200ms鑾峰彇涓�娆℃暟鎹� dataInterval = window.setInterval(receiveData, 200); diff --git a/src/components/TableDisplay.vue b/src/components/TableDisplay.vue index 1a11ada..9e6eb01 100644 --- a/src/components/TableDisplay.vue +++ b/src/components/TableDisplay.vue @@ -1,10 +1,6 @@ <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue'; -import { sendToPipe } from '../pipe_client'; - -const props = defineProps<{ - pipeName: string; -}>(); +import { createDataReceiver } from '../utils/dataFetcher'; interface ForceData { timestamp: string; @@ -42,44 +38,14 @@ } } -// 鐘舵�佸彉閲忥紝鐢ㄤ簬鎺у埗閿欒鏄剧ず棰戠巼 -const errorCount = ref(0); -const maxConsecutiveErrors = 5; -const showingError = ref(false); -// 鎺ユ敹绠¢亾鏁版嵁鐨勫嚱鏁� -async function receiveData() { - try { - const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); - - // 閲嶇疆閿欒璁℃暟 - errorCount.value = 0; - showingError.value = false; - - // 瑙f瀽鏁版嵁 - try { - const forceData = JSON.parse(response); - if (Array.isArray(forceData) && forceData.length === 6) { - updateTable(forceData); - } - } catch (parseErr) { - console.error('鏁版嵁瑙f瀽閿欒:', parseErr); - } - } catch (err) { - // 澧炲姞閿欒璁℃暟 - errorCount.value++; - - // 鍙湪杩炵画閿欒杈惧埌闃堝�间笖灏氭湭鏄剧ず閿欒鏃舵樉绀� - if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { - console.error('绠¢亾閫氫俊澶辫触:', err); - showingError.value = true; - } - } -} let dataInterval: number | null = null; onMounted(() => { + const receiveData = createDataReceiver( (forceData) => { + updateTable(forceData); + }); // 姣�500ms鑾峰彇涓�娆℃暟鎹� dataInterval = window.setInterval(receiveData, 500); }); diff --git a/src/components/ThreeDDisplay copy.vue b/src/components/ThreeDDisplay copy.vue new file mode 100644 index 0000000..239e4d5 --- /dev/null +++ b/src/components/ThreeDDisplay copy.vue @@ -0,0 +1,525 @@ +<script setup lang="ts"> +import { ref, onMounted, onUnmounted } from 'vue'; +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; +import { sendToPipe } from '../pipe_client'; + +const props = defineProps<{ + pipeName: string; +}>(); + +const containerRef = ref<HTMLElement | null>(null); +let scene: THREE.Scene | null = null; +let camera: THREE.PerspectiveCamera | null = null; +let renderer: THREE.WebGLRenderer | null = null; +let controls: OrbitControls | null = null; + +// 鍔涘拰鍔涚煩鐨勭澶� +let forceArrows: THREE.ArrowHelper[] = []; +let torqueArcs: THREE.Line[] = []; + +// 褰撳墠鍔涘拰鍔涚煩鏁版嵁 +const currentData = ref<number[]>([0, 0, 0, 0, 0, 0]); + +// 棰滆壊瀹氫箟 +const colors = { + fx: 0xff0000, // 绾㈣壊 + fy: 0x00ff00, // 缁胯壊 + fz: 0x0000ff, // 钃濊壊 + mx: 0xff00ff, // 绱壊 + my: 0xffff00, // 榛勮壊 + mz: 0x00ffff // 闈掕壊 +}; + +// 鍒濆鍖�3D鍦烘櫙 +function initScene() { + if (!containerRef.value) return; + + // 鍒涘缓鍦烘櫙 + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + + // 鍒涘缓鐩告満 + const width = containerRef.value.clientWidth; + const height = containerRef.value.clientHeight; + camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); + camera.position.set(5, 5, 5); + camera.lookAt(0, 0, 0); + + // 鍒涘缓娓叉煋鍣� + renderer = new THREE.WebGLRenderer({ antialias: true }); + renderer.setSize(width, height); + containerRef.value.appendChild(renderer.domElement); + + // 娣诲姞杞ㄩ亾鎺у埗鍣� + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.25; + + // 娣诲姞鍧愭爣杞� + const axesHelper = new THREE.AxesHelper(4); + axesHelper.setColors( + new THREE.Color(0x707070), // X杞撮鑹� + new THREE.Color(0x707070), // Y杞撮鑹� + new THREE.Color(0x707070) // Z杞撮鑹� + ); + // 璁剧疆鍧愭爣杞寸殑娓叉煋椤哄簭涓鸿緝浣庡�� + axesHelper.renderOrder = -1; + if (axesHelper instanceof THREE.Object3D) { + axesHelper.traverse((child) => { + if (child instanceof THREE.Line) { + if (child.material instanceof THREE.LineBasicMaterial) { + child.material.depthTest = false; + } + } + }); + } + scene.add(axesHelper); + + // 娣诲姞鍧愭爣杞存爣绛� + const textDistance = 4.2; // 鍧愭爣杞撮暱搴� + 涓�鐐归棿璺� + + // 鍒涘缓鏍囩绮剧伒 + function createAxisLabel(text: string, position: THREE.Vector3) { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) return null; + + canvas.width = 64; + canvas.height = 64; + + context.fillStyle = '#ffffff'; + context.font = 'bold 48px Arial'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillText(text, 32, 32); + + const texture = new THREE.CanvasTexture(canvas); + const spriteMaterial = new THREE.SpriteMaterial({ map: texture }); + const sprite = new THREE.Sprite(spriteMaterial); + + sprite.position.copy(position); + sprite.scale.set(0.5, 0.5, 1); + return sprite; + } + + // 鍒涘缓骞舵坊鍔燲YZ杞存爣绛� + const labelX = createAxisLabel('X', new THREE.Vector3(textDistance, 0, 0)); + const labelY = createAxisLabel('Y', new THREE.Vector3(0, textDistance, 0)); + const labelZ = createAxisLabel('Z', new THREE.Vector3(0, 0, textDistance)); + + if (labelX) scene.add(labelX); + if (labelY) scene.add(labelY); + if (labelZ) scene.add(labelZ); + + // 娣诲姞缃戞牸 + const gridHelper = new THREE.GridHelper(20, 20, 0x404040, 0x404040); + gridHelper.material.opacity = 0.5; + gridHelper.material.transparent = true; + scene.add(gridHelper); + + // 娣诲姞鐜鍏� + const ambientLight = new THREE.AmbientLight(0xffffff, 0.7); + scene.add(ambientLight); + + // 娣诲姞鏂瑰悜鍏� + const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); + directionalLight.position.set(10, 10, 10); + scene.add(directionalLight); + + // 鍒濆鍖栧姏鍜屽姏鐭╃殑绠ご + initForceArrows(); + initTorqueArcs(); + + // 寮�濮嬪姩鐢诲惊鐜� + animate(); + + // 鍝嶅簲绐楀彛澶у皬鍙樺寲 + window.addEventListener('resize', handleResize); +} + +// 鍒濆鍖栧姏鐨勭澶� +function initForceArrows() { + if (!scene) return; + + // 娓呴櫎鐜版湁鐨勭澶� + forceArrows.forEach(arrow => scene?.remove(arrow)); + forceArrows = []; + + // 鍒涘缓涓変釜鍔涚殑绠ご (Fx, Fy, Fz) + const headLength = 0.5; + const headWidth = 0.3; + + // 鍒涘缓鍔涚殑绠ご (鍒濆闀垮害涓�0) + const fxArrow = new THREE.ArrowHelper( + new THREE.Vector3(1, 0, 0), + new THREE.Vector3(0, 0, 0), + 0.001, + colors.fx, + headLength, + headWidth + ); + fxArrow.renderOrder = 1; + + + const fyArrow = new THREE.ArrowHelper( + new THREE.Vector3(0, 1, 0), + new THREE.Vector3(0, 0, 0), + 0.001, + colors.fy, + headLength, + headWidth + ); + fyArrow.renderOrder = 1; + + + const fzArrow = new THREE.ArrowHelper( + new THREE.Vector3(0, 0, 1), + new THREE.Vector3(0, 0, 0), + 0.001, + colors.fz, + headLength, + headWidth + ); + fzArrow.renderOrder = 1; + + + // 娣诲姞鍒板満鏅� + scene.add(fxArrow); + scene.add(fyArrow); + scene.add(fzArrow); + + // 淇濆瓨寮曠敤 + forceArrows = [fxArrow, fyArrow, fzArrow]; +} + +// 鍒濆鍖栧姏鐭╃殑寮у舰琛ㄧず +function initTorqueArcs() { + if (!scene) return; + + // 娓呴櫎鐜版湁鐨勫姬褰� + torqueArcs.forEach(arc => scene?.remove(arc)); + torqueArcs = []; + + // 鍒涘缓涓変釜鍔涚煩鐨勫姬褰� (Mx, My, Mz) + const radius = 2; + const segments = 32; + + // 鍒涘缓X杞村姏鐭╁姬褰� (缁昘杞存棆杞�) + const mxGeometry = new THREE.BufferGeometry(); + const mxPoints = []; + for (let i = 0; i <= segments; i++) { + const theta = (i / segments) * Math.PI; + mxPoints.push(new THREE.Vector3(0, radius * Math.cos(theta), radius * Math.sin(theta))); + } + mxGeometry.setFromPoints(mxPoints); + const mxArc = new THREE.Line(mxGeometry, new THREE.LineBasicMaterial({ color: colors.mx, linewidth: 2 })); + + // 鍒涘缓Y杞村姏鐭╁姬褰� (缁昚杞存棆杞�) + const myGeometry = new THREE.BufferGeometry(); + const myPoints = []; + for (let i = 0; i <= segments; i++) { + const theta = (i / segments) * Math.PI; + myPoints.push(new THREE.Vector3(radius * Math.cos(theta), 0, radius * Math.sin(theta))); + } + myGeometry.setFromPoints(myPoints); + const myArc = new THREE.Line(myGeometry, new THREE.LineBasicMaterial({ color: colors.my, linewidth: 2 })); + + // 鍒涘缓Z杞村姏鐭╁姬褰� (缁昛杞存棆杞�) + const mzGeometry = new THREE.BufferGeometry(); + const mzPoints = []; + for (let i = 0; i <= segments; i++) { + const theta = (i / segments) * Math.PI; + mzPoints.push(new THREE.Vector3(radius * Math.cos(theta), radius * Math.sin(theta), 0)); + } + mzGeometry.setFromPoints(mzPoints); + const mzArc = new THREE.Line(mzGeometry, new THREE.LineBasicMaterial({ color: colors.mz, linewidth: 2 })); + + // 娣诲姞鍒板満鏅� + scene.add(mxArc); + scene.add(myArc); + scene.add(mzArc); + + // 淇濆瓨寮曠敤 + torqueArcs = [mxArc, myArc, mzArc]; + + // 鍒濆鐘舵�佷笅闅愯棌寮у舰 + torqueArcs.forEach(arc => { + arc.visible = false; + }); +} + +// 鏇存柊鍔涘拰鍔涚煩鐨勫彲瑙嗗寲 +function updateVisualization(forceData: number[]) { + if (!scene) return; + + // 鏇存柊褰撳墠鏁版嵁 + currentData.value = forceData; + + // 璁$畻鍔涚殑鏈�澶у�硷紝鐢ㄤ簬褰掍竴鍖� + const maxForce = Math.max( + Math.abs(forceData[0]), + Math.abs(forceData[1]), + Math.abs(forceData[2]) + ) || 1; // 閬垮厤闄や互闆� + + // 璁$畻鍔涚煩鐨勬渶澶у�硷紝鐢ㄤ簬褰掍竴鍖� + const maxTorque = Math.max( + Math.abs(forceData[3]), + Math.abs(forceData[4]), + Math.abs(forceData[5]) + ) || 1; // 閬垮厤闄や互闆� + + // 鏇存柊鍔涚殑绠ご + const forceScale = 3; // 鏈�澶ч暱搴︿负3涓崟浣� + for (let i = 0; i < 3; i++) { + const force = forceData[i]; + const length = Math.abs(force) / maxForce * forceScale; + + // 鏇存柊绠ご闀垮害鍜屾柟鍚� + if (forceArrows[i]) { + // 濡傛灉鍔涗负闆舵垨闈炲父灏忥紝鍒欏嚑涔庝笉鍙 + if (Math.abs(force) < 0.1) { + forceArrows[i].setLength(0.001); + } else { + forceArrows[i].setLength(length); + + // 濡傛灉鍔涗负璐燂紝鍒欏弽杞柟鍚� + if (force < 0) { + const dir = new THREE.Vector3(); + if (i === 0) dir.set(-1, 0, 0); + if (i === 1) dir.set(0, -1, 0); + if (i === 2) dir.set(0, 0, -1); + forceArrows[i].setDirection(dir); + } else { + const dir = new THREE.Vector3(); + if (i === 0) dir.set(1, 0, 0); + if (i === 1) dir.set(0, 1, 0); + if (i === 2) dir.set(0, 0, 1); + forceArrows[i].setDirection(dir); + } + } + } + } + + // 鏇存柊鍔涚煩鐨勫姬褰� + for (let i = 0; i < 3; i++) { + const torque = forceData[i + 3]; + + // 鏄剧ず鎴栭殣钘忓姏鐭╁姬褰� + if (torqueArcs[i]) { + if (Math.abs(torque) < 0.1) { + torqueArcs[i].visible = false; + } else { + torqueArcs[i].visible = true; + + // 鏍规嵁鍔涚煩鏂瑰悜鏃嬭浆寮у舰 + const arc = torqueArcs[i]; + arc.rotation.set(0, 0, 0); // 閲嶇疆鏃嬭浆 + + // 鏍规嵁鍔涚煩澶у皬鍜屾柟鍚戣缃棆杞� + const rotationAngle = (Math.abs(torque) / maxTorque) * Math.PI / 2; + + if (i === 0) { // Mx + arc.rotation.x = torque > 0 ? rotationAngle : -rotationAngle; + } else if (i === 1) { // My + arc.rotation.y = torque > 0 ? rotationAngle : -rotationAngle; + } else if (i === 2) { // Mz + arc.rotation.z = torque > 0 ? rotationAngle : -rotationAngle; + } + } + } + } +} + +// 鍔ㄧ敾寰幆 +function animate() { + if (!scene || !camera || !renderer || !controls) return; + + requestAnimationFrame(animate); + + // 鏇存柊鎺у埗鍣� + controls.update(); + + // 娓叉煋鍦烘櫙 + renderer.render(scene, camera); +} + +// 澶勭悊绐楀彛澶у皬鍙樺寲 +function handleResize() { + if (!containerRef.value || !camera || !renderer) return; + + const width = containerRef.value.clientWidth; + const height = containerRef.value.clientHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); +} + +// 鐘舵�佸彉閲忥紝鐢ㄤ簬鎺у埗閿欒鏄剧ず棰戠巼 +const errorCount = ref(0); +const maxConsecutiveErrors = 5; +const showingError = ref(false); + +// 鎺ユ敹绠¢亾鏁版嵁鐨勫嚱鏁� +async function receiveForceData() { + try { + // 浠庣閬撴帴鏀舵暟鎹� + const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); + + // 鎴愬姛鎺ユ敹鏁版嵁锛岄噸缃敊璇鏁� + errorCount.value = 0; + if (showingError.value) { + showingError.value = false; + console.log('绠¢亾閫氫俊宸叉仮澶�'); + } + + // 瑙f瀽鎺ユ敹鍒扮殑鏁版嵁 + try { + const forceData = JSON.parse(response); + if (Array.isArray(forceData) && forceData.length === 6) { + updateVisualization(forceData); + } + } catch (e) { + console.warn('瑙f瀽鏁版嵁澶辫触:', e); + } + } catch (err) { + // 澧炲姞閿欒璁℃暟 + errorCount.value++; + + // 鍙湪杩炵画閿欒杈惧埌闃堝�兼椂鏄剧ず閿欒淇℃伅锛岄伩鍏嶆棩蹇楀埛灞� + if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { + showingError.value = true; + console.error('绠¢亾閫氫俊鎸佺画澶辫触锛岃妫�鏌ユ湇鍔$鐘舵��:', err); + } + } +} + +// 瀹氭椂鑾峰彇鏁版嵁 +let dataTimer: number | null = null; + +onMounted(() => { + initScene(); + + // 姣忕鑾峰彇涓�娆℃暟鎹� + dataTimer = window.setInterval(receiveForceData, 3000); +}); + +onUnmounted(() => { + // 娓呯悊瀹氭椂鍣ㄥ拰浜嬩欢鐩戝惉 + if (dataTimer !== null) { + clearInterval(dataTimer); + } + + window.removeEventListener('resize', handleResize); + + // 娓呯悊Three.js璧勬簮 + if (renderer) { + renderer.dispose(); + containerRef.value?.removeChild(renderer.domElement); + } + + // 娓呴櫎鍦烘櫙涓殑鎵�鏈夊璞� + if (scene) { + scene.clear(); + } + + // 娓呯┖寮曠敤 + scene = null; + camera = null; + renderer = null; + controls = null; + forceArrows = []; + torqueArcs = []; +}); +</script> + +<template> + <div class="three-container"> + <div class="three-scene" ref="containerRef"></div> + <div class="data-panel"> + <h3 style="color: #ffffff;">鍏淮鍔涙暟鎹�</h3> + <div class="data-row"> + <div class="data-item" style="color: #ff0000"> + <span class="label">Fx:</span> + <span class="value">{{ currentData[0].toFixed(2) }} N</span> + </div> + <div class="data-item" style="color: #00ff00"> + <span class="label">Fy:</span> + <span class="value">{{ currentData[1].toFixed(2) }} N</span> + </div> + <div class="data-item" style="color: #0000ff"> + <span class="label">Fz:</span> + <span class="value">{{ currentData[2].toFixed(2) }} N</span> + </div> + </div> + <div class="data-row"> + <div class="data-item" style="color: #ff00ff"> + <span class="label">Mx:</span> + <span class="value">{{ currentData[3].toFixed(2) }} Nm</span> + </div> + <div class="data-item" style="color: #ffff00"> + <span class="label">My:</span> + <span class="value">{{ currentData[4].toFixed(2) }} Nm</span> + </div> + <div class="data-item" style="color: #00ffff"> + <span class="label">Mz:</span> + <span class="value">{{ currentData[5].toFixed(2) }} Nm</span> + </div> + </div> + </div> + </div> +</template> + +<style scoped> +.three-container { + width: 100%; + height: 540px; + margin: 20px 0; + display: flex; + flex-direction: column; + background-color: #ffffff; + border-radius: 8px; + overflow: hidden; +} + +.three-scene { + width: 100%; + height: 450px; + position: relative; +} + +.data-panel { + padding: 10px 20px; + background-color: #000000; + border-top: 1px solid #535353; +} + +.data-panel h3 { + margin: 0 0 10px 0; + font-size: 16px; + text-align: center; +} + +.data-row { + display: flex; + justify-content: space-around; + margin-bottom: 5px; +} + +.data-item { + display: flex; + align-items: center; + font-weight: bold; +} + +.label { + margin-right: 5px; +} + +.value { + font-family: monospace; +} +</style> \ No newline at end of file diff --git a/src/components/ThreeDDisplay.vue b/src/components/ThreeDDisplay.vue index 239e4d5..10c71da 100644 --- a/src/components/ThreeDDisplay.vue +++ b/src/components/ThreeDDisplay.vue @@ -2,11 +2,8 @@ import { ref, onMounted, onUnmounted } from 'vue'; import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { sendToPipe } from '../pipe_client'; +import { createDataReceiver } from '../utils/dataFetcher'; -const props = defineProps<{ - pipeName: string; -}>(); const containerRef = ref<HTMLElement | null>(null); let scene: THREE.Scene | null = null; @@ -358,53 +355,25 @@ renderer.setSize(width, height); } -// 鐘舵�佸彉閲忥紝鐢ㄤ簬鎺у埗閿欒鏄剧ず棰戠巼 -const errorCount = ref(0); -const maxConsecutiveErrors = 5; -const showingError = ref(false); -// 鎺ユ敹绠¢亾鏁版嵁鐨勫嚱鏁� -async function receiveForceData() { - try { - // 浠庣閬撴帴鏀舵暟鎹� - const response = await sendToPipe(props.pipeName, 'GET_FORCE_DATA'); - - // 鎴愬姛鎺ユ敹鏁版嵁锛岄噸缃敊璇鏁� - errorCount.value = 0; - if (showingError.value) { - showingError.value = false; - console.log('绠¢亾閫氫俊宸叉仮澶�'); - } - - // 瑙f瀽鎺ユ敹鍒扮殑鏁版嵁 - try { - const forceData = JSON.parse(response); - if (Array.isArray(forceData) && forceData.length === 6) { - updateVisualization(forceData); - } - } catch (e) { - console.warn('瑙f瀽鏁版嵁澶辫触:', e); - } - } catch (err) { - // 澧炲姞閿欒璁℃暟 - errorCount.value++; - - // 鍙湪杩炵画閿欒杈惧埌闃堝�兼椂鏄剧ず閿欒淇℃伅锛岄伩鍏嶆棩蹇楀埛灞� - if (errorCount.value >= maxConsecutiveErrors && !showingError.value) { - showingError.value = true; - console.error('绠¢亾閫氫俊鎸佺画澶辫触锛岃妫�鏌ユ湇鍔$鐘舵��:', err); - } - } -} + // 瀹氭椂鑾峰彇鏁版嵁 let dataTimer: number | null = null; onMounted(() => { initScene(); + animate(); - // 姣忕鑾峰彇涓�娆℃暟鎹� - dataTimer = window.setInterval(receiveForceData, 3000); + const receiveData = createDataReceiver( (forceData) => { + updateVisualization(forceData); + }); + + // 姣�100ms鑾峰彇涓�娆℃暟鎹� + dataTimer = window.setInterval(receiveData, 100); + + // 鐩戝惉绐楀彛澶у皬鍙樺寲 + window.addEventListener('resize', handleResize); }); onUnmounted(() => { diff --git a/src/pipe_client.ts b/src/pipe_client.ts index a96e3cf..37b8c10 100644 --- a/src/pipe_client.ts +++ b/src/pipe_client.ts @@ -32,4 +32,15 @@ // 鎵�鏈夐噸璇曢兘澶辫触锛屾姏鍑烘渶鍚庝竴涓敊璇� throw lastError; +} + +export async function getLatestWsData(): Promise<string | null> { + try { + const result = await invoke<string | null>('get_latest_ws_data'); + console.log("getlatestwsData", result); + return result; + } catch (err) { + console.error('鑾峰彇 WebSocket 鏁版嵁澶辫触:', err); + return null; + } } \ No newline at end of file diff --git a/src/utils/dataFetcher.ts b/src/utils/dataFetcher.ts new file mode 100644 index 0000000..05bfdfe --- /dev/null +++ b/src/utils/dataFetcher.ts @@ -0,0 +1,68 @@ +import { getLatestWsData } from '../pipe_client'; + +// const pipeName = 'tauri-pipe-server'; + +// const dataSource = 'ws'; // pipe|ws + +export async function fetchForceData() { + try { + let response: string | null = null; + + // if (dataSource === 'pipe') { + // if (!pipeName) throw new Error('Pipe name is required for pipe data source'); + // response = await sendToPipe(pipeName, 'GET_FORCE_DATA'); + // } else if (dataSource === 'ws') { + response = await getLatestWsData(); + if (!response) return null; + // } + + if (!response) return null; + + const jsonData = JSON.parse(response); + const forceData = jsonData[0]; + if (Array.isArray(forceData) && forceData.length === 6) { + // 灏嗘暟缁勪腑鐨勬瘡涓暟鍒嗗埆*4000/4194303 + for (let i = 0; i < forceData.length; i++) { + forceData[i] = forceData[i] * 4000 / 4194303; + } + return forceData; + } + + return null; + } catch (error) { + console.error('Error fetching force data:', error); + return null; + } +} + +export function createDataReceiver( callback: (data: number[]) => void) { + let errorCount = 0; + const maxConsecutiveErrors = 5; + let showingError = false; + + return async () => { + try { + const forceData = await fetchForceData(); + console.log('forceData111', forceData); + if (forceData) { + // 閲嶇疆閿欒璁℃暟 + errorCount = 0; + if (showingError) { + showingError = false; + console.log('鏁版嵁閫氫俊宸叉仮澶�'); + } + console.log('forceData222', forceData); + callback(forceData); + } + } catch (err) { + // 澧炲姞閿欒璁℃暟 + errorCount++; + + // 鍙湪杩炵画閿欒杈惧埌闃堝�兼椂鏄剧ず閿欒淇℃伅锛岄伩鍏嶆棩蹇楀埛灞� + if (errorCount >= maxConsecutiveErrors && !showingError) { + showingError = true; + console.error('鏁版嵁閫氫俊鎸佺画澶辫触锛岃妫�鏌ユ湇鍔$鐘舵��:', err); + } + } + }; +} \ No newline at end of file -- Gitblit v1.9.3