> 技术文档 > java调用周立功USBCAN SDK读取汽车总线数据

java调用周立功USBCAN SDK读取汽车总线数据


java调用周立功USBCAN SDK读取汽车总线数据

  • 准备
    • 新的改变
    • 后端
    • 前端

准备

我这测试用的两个usbcan-2e-u盒子,一个发送端一个接收端,两个盒子要通过can口进行串联起来,测试工具用的canTest或者zcanpro,这两个工具在官网上都可以下载。

项目是要做一个汽车端的工控屏(win10)的数据大屏,效果如下:
java调用周立功USBCAN SDK读取汽车总线数据

新的改变

由于周立功sdk只有c和python的,没有Java的,项目其他业务逻辑都已经用Java写好,所以这里直接参照官网提供的python demo进行改造。

后端

Java项目技术栈是springboot+thymeleaf的,把dll文件放到Java项目的resources目录下。其中zlgcan.dll文件和kerneldlls文件夹是从周立功官网上的pythonDemo里拷贝过来的,目前在 PYTHON型号合集\\USBCAN-xE-U系列_。

java调用周立功USBCAN SDK读取汽车总线数据
maven要引进jni包进行调用dll文件

 <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.12.1</version> </dependency>

后端改造代码

package com.example.car;import com.fasterxml.jackson.databind.ObjectMapper;import com.sun.jna.*;import jakarta.annotation.PostConstruct;import org.springframework.stereotype.Component;import java.io.IOException;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.concurrent.atomic.AtomicBoolean;@Componentpublic class ZlgCan { // 设备类型常量 public static final int ZCAN_USBCAN_2E_U = 21; public static final int INVALID_DEVICE_HANDLE = 0; // 状态常量 public static final int ZCAN_STATUS_OK = 1; public static final int ZCAN_TYPE_CAN = 0; // 设备信息结构体 public static class ZCAN_DEVICE_INFO extends Structure { public short hw_Version; public short fw_Version; public short dr_Version; public short in_Version; public short irq_Num; public byte can_Num; public byte[] str_Serial_Num = new byte[20]; public byte[] str_hw_Type = new byte[40]; public short[] reserved = new short[4]; @Override protected List<String> getFieldOrder() { return Arrays.asList(\"hw_Version\", \"fw_Version\", \"dr_Version\", \"in_Version\",  \"irq_Num\", \"can_Num\", \"str_Serial_Num\", \"str_hw_Type\", \"reserved\"); } public String getSerial() { return new String(str_Serial_Num).trim(); } public String getHwType() { return new String(str_hw_Type).trim(); } @Override public String toString() { return String.format(\"Hardware Version: 0x%04X%nFirmware Version: 0x%04X%n\" + \"Driver Version: 0x%04X%nInterface Version: 0x%04X%n\" + \"IRQ Num: %d%nCAN Num: %d%nSerial: %s%nHardware Type: %s%n\",  hw_Version, fw_Version, dr_Version, in_Version,  irq_Num, can_Num, getSerial(), getHwType()); } } // CAN帧结构体 public static class ZCAN_CAN_FRAME extends Structure { public int flags; // 包含can_id, err, rtr, eff位域 public byte can_dlc; public byte __pad; public byte __res0; public byte __res1; public byte[] data = new byte[8]; @Override protected List<String> getFieldOrder() { return Arrays.asList(\"flags\", \"can_dlc\", \"__pad\", \"__res0\", \"__res1\", \"data\"); } public int getCanId() { return flags & 0x1FFFFFFF; // 29位ID } public void setCanId(int id) { flags = (flags & ~0x1FFFFFFF) | (id & 0x1FFFFFFF); } public int getEff() { return (flags >> 31) & 0x1; // 扩展帧标志 } public int getRtr() { return (flags >> 30) & 0x1; // 远程帧标志 } public int getErr() { return (flags >> 29) & 0x1; // 错误帧标志 } } // 传输数据结构体 public static class ZCAN_Transmit_Data extends Structure { public ZCAN_CAN_FRAME frame; public int transmit_type; @Override protected List<String> getFieldOrder() { return Arrays.asList(\"frame\", \"transmit_type\"); } } // 接收数据结构体 public static class ZCAN_Receive_Data extends Structure { public ZCAN_CAN_FRAME frame; public long timestamp; @Override protected List<String> getFieldOrder() { return Arrays.asList(\"frame\", \"timestamp\"); } } // 通道初始化配置 public static class ZCAN_CHANNEL_INIT_CONFIG extends Structure { public int can_type; public CAN_INIT_CONFIG config; @Override protected List<String> getFieldOrder() { return Arrays.asList(\"can_type\", \"config\"); } public static class CAN_INIT_CONFIG extends Union { public CAN_CONFIG can; public CANFD_CONFIG canfd; @Override protected List<String> getFieldOrder() { return Arrays.asList(\"can\", \"canfd\"); } public static class CAN_CONFIG extends Structure { public int acc_code; public int acc_mask; public int reserved; public byte filter; public byte timing0; public byte timing1; public byte mode; @Override protected List<String> getFieldOrder() {  return Arrays.asList(\"acc_code\", \"acc_mask\", \"reserved\", \"filter\", \"timing0\", \"timing1\", \"mode\"); } } public static class CANFD_CONFIG extends Structure { public int acc_code; public int acc_mask; public int abit_timing; public int dbit_timing; public int brp; public byte filter; public byte mode; public short pad; public int reserved; @Override protected List<String> getFieldOrder() {  return Arrays.asList(\"acc_code\", \"acc_mask\", \"abit_timing\", \"dbit_timing\", \"brp\", \"filter\", \"mode\", \"pad\", \"reserved\"); } } } } // JNA接口映射 public interface ZlgCanLibrary extends Library { ZlgCanLibrary INSTANCE = Native.load(\"zlgcan\", ZlgCanLibrary.class); // 设备操作 Pointer ZCAN_OpenDevice(int device_type, int device_index, int reserved); int ZCAN_CloseDevice(Pointer device_handle); int ZCAN_GetDeviceInf(Pointer device_handle, ZCAN_DEVICE_INFO info); int ZCAN_IsDeviceOnLine(Pointer device_handle); // 通道操作 Pointer ZCAN_InitCAN(Pointer device_handle, int can_index, ZCAN_CHANNEL_INIT_CONFIG init_config); int ZCAN_StartCAN(Pointer chn_handle); int ZCAN_ResetCAN(Pointer chn_handle); int ZCAN_ClearBuffer(Pointer chn_handle); int ZCAN_GetReceiveNum(Pointer chn_handle, int can_type); // 数据传输 int ZCAN_Transmit(Pointer chn_handle, ZCAN_Transmit_Data[] msgs, int len); int ZCAN_Receive(Pointer chn_handle, ZCAN_Receive_Data[] rcv_msgs, int rcv_num, int wait_time); // 配置操作 int ZCAN_SetValue(Pointer device_handle, String path, String value); } @PostConstruct private void getDeviceInfo() { ZlgCanLibrary lib = ZlgCanLibrary.INSTANCE; // 打开设备 Pointer deviceHandle = lib.ZCAN_OpenDevice(ZCAN_USBCAN_2E_U, 1, 0); if (Pointer.nativeValue(deviceHandle) == INVALID_DEVICE_HANDLE) { System.err.println(\"Open USBCAN-XE-U device failed!\"); System.exit(1); } System.out.println(\"Open USBCAN-XE-U device success!\"); System.out.println(\"Device handle: \" + Pointer.nativeValue(deviceHandle)); // 获取设备信息 ZCAN_DEVICE_INFO deviceInfo = new ZCAN_DEVICE_INFO(); if (lib.ZCAN_GetDeviceInf(deviceHandle, deviceInfo) != ZCAN_STATUS_OK) { System.err.println(\"Get device information failed!\"); lib.ZCAN_CloseDevice(deviceHandle); System.exit(1); } System.out.println(\"Device Information:\\n\" + deviceInfo); // 配置通道0 Pointer channelHandle = initChannel(lib, deviceHandle, 0); System.out.println(\"Channel handle: \" + Pointer.nativeValue(channelHandle)); // 发送消息// int transmitNum = 10;// ZCAN_Transmit_Data[] msgs = new ZCAN_Transmit_Data[transmitNum];// for (int i = 0; i < transmitNum; i++) {// msgs[i] = new ZCAN_Transmit_Data();// msgs[i].transmit_type = 2; // 自发自收// msgs[i].frame = new ZCAN_CAN_FRAME();// msgs[i].frame.setCanId(i); // 标准帧ID// msgs[i].frame.can_dlc = 8; // 数据长度// for (int j = 0; j < msgs[i].frame.can_dlc; j++) {// msgs[i].frame.data[j] = (byte) j;// }// }//// int ret = lib.ZCAN_Transmit(channelHandle, msgs, transmitNum);// System.out.println(\"Transmit Num: \" + ret); // 接收消息线程 AtomicBoolean running = new AtomicBoolean(true); Thread receiveThread = new Thread(() -> { while (running.get()) { int rcvNum = lib.ZCAN_GetReceiveNum(channelHandle, ZCAN_TYPE_CAN); if (rcvNum > 0) {  ZCAN_Receive_Data[] rcvMsgs = new ZCAN_Receive_Data[rcvNum];  int actualRcvNum = lib.ZCAN_Receive(channelHandle, rcvMsgs, rcvNum, -1);  Map<String, Object> dataMap = new HashMap<>();  for (int i = 0; i < actualRcvNum; i++) { ZCAN_CAN_FRAME frame = rcvMsgs[i].frame; System.out.printf(\"[%d]: timestamps: %d, type: CAN, id: %s, dlc: %d, eff: %d, rtr: %d, data: \", i, rcvMsgs[i].timestamp, Integer.toHexString(frame.getCanId()), frame.can_dlc, frame.getEff(), frame.getRtr()); for (int j = 0; j < frame.can_dlc; j++) {// System.out.print(frame.data[j]+\" \"); System.out.print(String.format(\"%02X \", frame.data[j])); } System.out.println(); dataMap.put(\"speed\", frame.data[0]); dataMap.put(\"rpm\", frame.data[1]); dataMap.put(\"coolantTemp\", frame.data[2]); dataMap.put(\"plp\", frame.data[3]); dataMap.put(\"voltage\", frame.data[4]); dataMap.put(\"timestamp\", frame.data[5]);  }  // 通过WebSocket发送  try { String json = new ObjectMapper().writeValueAsString(dataMap); CarDataWebSocketHandler.broadcast(json);  } catch (IOException e) { e.printStackTrace();  } } try {  Thread.sleep(10); } catch (InterruptedException e) {  Thread.currentThread().interrupt(); } } }); receiveThread.start();//// // 等待用户输入停止// System.out.println(\"Press Enter to exit...\");// try {// System.in.read();// } catch (Exception e) {// e.printStackTrace();// }//// running.set(false);// try {// receiveThread.join();// } catch (InterruptedException e) {// Thread.currentThread().interrupt();// }//// // 重置通道// if (lib.ZCAN_ResetCAN(channelHandle) == ZCAN_STATUS_OK) {// System.out.println(\"ResetCAN success!\");// }//// // 关闭设备// if (lib.ZCAN_CloseDevice(deviceHandle) == ZCAN_STATUS_OK) {// System.out.println(\"CloseDevice success!\");// } } private static Pointer initChannel(ZlgCanLibrary lib, Pointer deviceHandle, int channel) { // 设置波特率 String path = channel + \"/baud_rate\"; if (lib.ZCAN_SetValue(deviceHandle, path, \"500000\") != ZCAN_STATUS_OK) { System.err.printf(\"Set CH%d CAN_E_U baud_rate failed!%n\", channel); System.exit(1); } // 初始化通道配置 ZCAN_CHANNEL_INIT_CONFIG initConfig = new ZCAN_CHANNEL_INIT_CONFIG(); initConfig.can_type = ZCAN_TYPE_CAN; initConfig.config = new ZCAN_CHANNEL_INIT_CONFIG.CAN_INIT_CONFIG(); initConfig.config.setType(ZCAN_CHANNEL_INIT_CONFIG.CAN_INIT_CONFIG.CAN_CONFIG.class); initConfig.config.can = new ZCAN_CHANNEL_INIT_CONFIG.CAN_INIT_CONFIG.CAN_CONFIG(); initConfig.config.can.acc_code = 0; initConfig.config.can.acc_mask = 0xFFFFFFFF; initConfig.config.can.mode = 0; // 设置滤波器 setFilter(lib, deviceHandle, channel); // 初始化通道 Pointer channelHandle = lib.ZCAN_InitCAN(deviceHandle, channel, initConfig); if (channelHandle == null) { System.err.printf(\"InitCAN channel %d failed!%n\", channel); System.exit(1); } // 启动通道 if (lib.ZCAN_StartCAN(channelHandle) != ZCAN_STATUS_OK) { System.err.printf(\"StartCAN channel %d failed!%n\", channel); System.exit(1); } return channelHandle; } private static void setFilter(ZlgCanLibrary lib, Pointer deviceHandle, int channel) { String basePath = channel + \"/\"; // 清除现有滤波器 if (lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_clear\", \"0\") != ZCAN_STATUS_OK) { System.err.printf(\"Set CH%d filter_clear failed!%n\", channel); System.exit(1); } // 设置标准帧滤波器 if (lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_mode\", \"0\") != ZCAN_STATUS_OK || lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_start\", \"0\") != ZCAN_STATUS_OK || lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_end\", \"0x7FF\") != ZCAN_STATUS_OK) { System.err.printf(\"Set CH%d standard frame filter failed!%n\", channel); System.exit(1); } // 设置扩展帧滤波器 if (lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_mode\", \"1\") != ZCAN_STATUS_OK || lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_start\", \"0\") != ZCAN_STATUS_OK || lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_end\", \"0x1FFFFFFF\") != ZCAN_STATUS_OK) { System.err.printf(\"Set CH%d extended frame filter failed!%n\", channel); System.exit(1); } // 确认滤波器设置 if (lib.ZCAN_SetValue(deviceHandle, basePath + \"filter_ack\", \"0\") != ZCAN_STATUS_OK) { System.err.printf(\"Set CH%d filter_ack failed!%n\", channel); System.exit(1); } }}

前端

前端是通过websocket通信获取数据的,后端相关代码如下

package com.example.car;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.WebSocketHandler;import org.springframework.web.socket.config.annotation.EnableWebSocket;import org.springframework.web.socket.config.annotation.WebSocketConfigurer;import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;@Configuration@EnableWebSocketpublic class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(carDataWebSocketHandler(), \"/car-data\") .setAllowedOrigins(\"*\"); } @Bean public WebSocketHandler carDataWebSocketHandler() { return new CarDataWebSocketHandler(); }}
package com.example.car;import org.springframework.web.socket.TextMessage;import org.springframework.web.socket.WebSocketSession;import org.springframework.web.socket.handler.TextWebSocketHandler;import java.io.IOException;import java.util.List;import java.util.concurrent.CopyOnWriteArrayList;public class CarDataWebSocketHandler extends TextWebSocketHandler { private static final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { sessions.add(session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 可处理前端发来的指令(如请求特定数据) System.out.println(message.getPayload()); } // 向所有客户端广播消息 public static void broadcast(String message) throws IOException { for (WebSocketSession s : sessions) { if (s.isOpen()) { s.sendMessage(new TextMessage(message)); } } }}

前端代码如下

<!DOCTYPE html><html lang=\"zh-CN\"><head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <title>仪表盘大屏</title> <script th:src=\"@{/js/echarts.min.js}\"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: \'Arial\', sans-serif; background: linear-gradient(135deg, #0f172a, #1e293b); color: #e2e8f0; min-height: 100vh; padding: 10px; overflow: hidden; } .dashboard { display: flex; flex-direction: column; height: calc(100vh - 40px); gap: 20px; } .dashboard-header { text-align: center; padding: 15px; background: rgba(30, 41, 59, 0.7); border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); margin-bottom: 10px; border: 1px solid rgba(100, 116, 139, 0.3); } .dashboard-header h1 { font-size: 2.2rem; background: linear-gradient(to right, #60a5fa, #38bdf8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 5px; } .dashboard-header p { color: #94a3b8; font-size: 1.1rem; } .dashboard-row { display: flex; flex: 1; gap: 20px; } .dashboard-col { flex: 1; background: rgba(30, 41, 59, 0.7); border-radius: 12px; padding: 15px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; border: 1px solid rgba(100, 116, 139, 0.3); transition: all 0.3s ease; } .dashboard-col:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4); border-color: rgba(56, 189, 248, 0.5); } .chart-title { font-size: 1.2rem; font-weight: 600; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid rgba(100, 116, 139, 0.3); color: #38bdf8; display: flex; align-items: center; gap: 10px; } .chart-title i { font-size: 1.4rem; } .chart-container { flex: 1; min-height: 200px; } /* 响应式设计 */ @media (max-width: 1200px) { .dashboard-row { flex-direction: column; } .dashboard-col { min-height: 300px; } } @media (max-width: 768px) { .dashboard-header h1 { font-size: 1.8rem; } .dashboard { height: auto; } .chart-title { font-size: 1.1rem; } } .chart-info { margin-top: 15px; padding-top: 12px; border-top: 1px solid rgba(100, 116, 139, 0.2); font-size: 0.9rem; color: #94a3b8; display: flex; justify-content: space-between; } .chart-info .value { color: #38bdf8; font-weight: bold; font-size: 1.1rem; } </style></head><body><div class=\"dashboard\"> <!-- <div class=\"dashboard-header\">--> <!-- <h1>数据可视化监控平台</h1>--> <!-- <p>实时监控系统运行状态与关键指标</p>--> <!-- </div>--> <!-- 第一行 --> <div class=\"dashboard-row\"> <div class=\"dashboard-col\"> <div class=\"chart-title\"> <i>📊</i> 速度 </div> <div id=\"chart1\" class=\"chart-container\"></div> </div> <div class=\"dashboard-col\"> <div class=\"chart-title\"> <i>🌡️</i> 水温 </div> <div id=\"chart2\" class=\"chart-container\"></div> </div> <div class=\"dashboard-col\"> <div class=\"chart-title\"> <i>💾</i> 气压 </div> <div id=\"chart3\" class=\"chart-container\"></div> </div> </div> <!-- 第二行 --> <div class=\"dashboard-row\"> <div class=\"dashboard-col\"> <div class=\"chart-title\"> <i>📶</i> 网络流量 </div> <div id=\"chart4\" class=\"chart-container\"></div> <div class=\"chart-info\"> <span>实时流量:</span> <span class=\"value\">124.5 Mbps</span> </div> </div> <div class=\"dashboard-col\"> <div class=\"chart-title\"> <i>🔋</i> 存储空间 </div> <div id=\"chart5\" class=\"chart-container\"></div> <div class=\"chart-info\"> <span>剩余空间:</span> <span class=\"value\">1.2 TB</span> </div> </div> <div class=\"dashboard-col\"> <div class=\"chart-title\"> <i>👥</i> 用户活跃度 </div> <div id=\"chart6\" class=\"chart-container\"></div> <div class=\"chart-info\"> <span>在线用户:</span> <span class=\"value\">1,245</span> </div> </div> </div></div><script th:inline=\"javascript\"> let chart1 = echarts.init(document.getElementById(\'chart1\')); let chart2 = echarts.init(document.getElementById(\'chart2\')); let chart3 = echarts.init(document.getElementById(\'chart3\')); let chart4 = echarts.init(document.getElementById(\'chart4\')); let chart5 = echarts.init(document.getElementById(\'chart5\')); let chart6 = echarts.init(document.getElementById(\'chart6\')); let color = []; let colorItem = []; colorItem.push(1); colorItem.push(\'#f00\'); color.push(colorItem); // 初始化所有图表 document.addEventListener(\'DOMContentLoaded\', function () { chart1.setOption({ series: [{ name: \'速度\', type: \'gauge\', radius: \'100%\', center: [\'50%\', \'50%\'], axisLine: {  lineStyle: { width: 30, color: [ [0.3, \'#67e0e3\'], [0.7, \'#37a2da\'], [1, \'#fd666d\'] ]  } }, pointer: {  itemStyle: { color: \'auto\'  } }, axisTick: {  distance: -30,  length: 8,  lineStyle: { color: \'#fff\', width: 2  } }, splitLine: {  distance: -30,  length: 30,  lineStyle: { color: \'#fff\', width: 4  } }, axisLabel: {  color: \'inherit\',  distance: 40,  fontSize: 20 }, detail: {  valueAnimation: true,  formatter: \'{value} km/h\',  color: \'inherit\' }, data: [{  value: 75,  name: \'速度\' }] }] }); chart2.setOption({ series: [ {  type: \'gauge\',  radius: \'100%\',  center: [\'50%\', \'66%\'],  startAngle: 200,  endAngle: -20,  min: 0,  max: 60,  splitNumber: 12,  itemStyle: { color: \'#FFAB91\'  },  progress: { show: true, width: 30  },  pointer: { show: false  },  axisLine: { lineStyle: { width: 30 }  },  axisTick: { distance: -45, splitNumber: 5, lineStyle: { width: 2, color: \'#999\' }  },  splitLine: { distance: -52, length: 14, lineStyle: { width: 3, color: \'#999\' }  },  axisLabel: { distance: -20, color: \'#999\', fontSize: 20  },  anchor: { show: false  },  title: { show: false  },  detail: { valueAnimation: true, width: \'60%\', lineHeight: 40, borderRadius: 8, offsetCenter: [0, \'-15%\'], fontSize: 40, fontWeight: \'bolder\', formatter: \'{value} °C\', color: \'inherit\'  },  data: [ { value: 20, }  ] }, {  type: \'gauge\',  radius: \'100%\',  center: [\'50%\', \'70%\'],  startAngle: 200,  endAngle: -20,  min: 0,  max: 60,  itemStyle: { color: \'#FD7347\'  },  progress: { show: true, width: 8  },  pointer: { show: false  },  axisLine: { show: false  },  axisTick: { show: false  },  splitLine: { show: false  },  axisLabel: { show: false  },  detail: { show: false  },  data: [ { value: 20, }  ] } ] }); chart3.setOption({ series: [ {  type: \'gauge\',  radius: \'80%\',  center: [\'50%\', \'55%\'],  min: 0,  max: 100,  splitNumber: 10,  axisLine: { lineStyle: { color: color, width: 3 }  },  splitLine: { distance: -18, length: 18, lineStyle: { color: \'#f00\' }  },  axisTick: { distance: -12, length: 10, lineStyle: { color: \'#f00\' }  },  axisLabel: { distance: -50, color: \'#f00\', fontSize: 25  },  anchor: { show: true, size: 20, itemStyle: { borderColor: \'#000\', borderWidth: 2 }  },  pointer: { offsetCenter: [0, \'10%\'], icon: \'path://M2090.36389,615.30999 L2090.36389,615.30999 C2091.48372,615.30999 2092.40383,616.194028 2092.44859,617.312956 L2096.90698,728.755929 C2097.05155,732.369577 2094.2393,735.416212 2090.62566,735.56078 C2090.53845,735.564269 2090.45117,735.566014 2090.36389,735.566014 L2090.36389,735.566014 C2086.74736,735.566014 2083.81557,732.63423 2083.81557,729.017692 C2083.81557,728.930412 2083.81732,728.84314 2083.82081,728.755929 L2088.2792,617.312956 C2088.32396,616.194028 2089.24407,615.30999 2090.36389,615.30999 Z\', length: \'115%\', itemStyle: { color: \'#000\' }  },  detail: { valueAnimation: true, precision: 1  },  title: { offsetCenter: [0, \'-50%\']  },  data: [ { value: 58.46, name: \'PLP\' }  ] }, {  type: \'gauge\',  center: [\'50%\', \'55%\'],  min: 0,  max: 60,  splitNumber: 6,  axisLine: { lineStyle: { color: color, width: 3 }  },  splitLine: { distance: -3, length: 18, lineStyle: { color: \'#000\' }  },  axisTick: { distance: 0, length: 10, lineStyle: { color: \'#000\' }  },  axisLabel: { distance: 10, fontSize: 25, color: \'#000\'  },  pointer: { show: false  },  title: { show: false  },  anchor: { show: true, size: 14, itemStyle: { color: \'#000\' }  } } ] }); // 图表4: 折线图 chart4.setOption({ tooltip: { trigger: \'axis\' }, legend: { data: [\'上传流量\', \'下载流量\'], textStyle: {  color: \'#e2e8f0\' }, bottom: 0 }, grid: { left: \'3%\', right: \'4%\', bottom: \'15%\', containLabel: true }, xAxis: { type: \'category\', boundaryGap: false, data: [\'00:00\', \'04:00\', \'08:00\', \'12:00\', \'16:00\', \'20:00\', \'24:00\'], axisLine: {  lineStyle: { color: \'#94a3b8\'  } }, axisLabel: {  color: \'#94a3b8\' } }, yAxis: { type: \'value\', name: \'Mbps\', nameTextStyle: {  color: \'#94a3b8\' }, axisLine: {  lineStyle: { color: \'#94a3b8\'  } }, axisLabel: {  color: \'#94a3b8\' }, splitLine: {  lineStyle: { color: \'rgba(148, 163, 184, 0.1)\'  } } }, series: [ {  name: \'上传流量\',  type: \'line\',  smooth: true,  lineStyle: { width: 3, color: \'#37a2da\'  },  symbol: \'circle\',  symbolSize: 8,  data: [20, 32, 18, 34, 50, 60, 55] }, {  name: \'下载流量\',  type: \'line\',  smooth: true,  lineStyle: { width: 3, color: \'#67e0a3\'  },  symbol: \'circle\',  symbolSize: 8,  data: [45, 60, 40, 70, 80, 110, 95] } ] }); // 图表5: 仪表盘 chart5.setOption({ series: [{ type: \'gauge\', startAngle: 180, endAngle: 0, min: 0, max: 5, splitNumber: 5, axisLine: {  lineStyle: { width: 6, color: [ [0.25, \'#FF6E76\'], [0.5, \'#FDDD60\'], [0.75, \'#58D9F9\'], [1, \'#7CFFB2\'] ]  } }, pointer: {  icon: \'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z\',  length: \'12%\',  width: 10,  offsetCenter: [0, \'-60%\'],  itemStyle: { color: \'auto\'  } }, axisTick: {  length: 12,  lineStyle: { color: \'auto\', width: 1  } }, splitLine: {  length: 15,  lineStyle: { color: \'auto\', width: 2  } }, axisLabel: {  color: \'#fff\',  fontSize: 12,  distance: -30,  formatter: function (value) { if (value === 0) { return \'0\'; } else if (value === 1) { return \'1TB\'; } else if (value === 2) { return \'2TB\'; } else if (value === 3) { return \'3TB\'; } else if (value === 4) { return \'4TB\'; } else if (value === 5) { return \'5TB\'; } return \'\';  } }, title: {  offsetCenter: [0, \'-20%\'],  fontSize: 14,  color: \'#94a3b8\' }, detail: {  fontSize: 30,  offsetCenter: [0, \'0%\'],  valueAnimation: true,  formatter: function (value) { return (5 - value).toFixed(1) + \' TB\';  },  color: \'auto\' }, data: [{  value: 3.8,  name: \'剩余空间\' }] }] }); // 图表6: 柱状图 chart6.setOption({ tooltip: { trigger: \'axis\', axisPointer: {  type: \'shadow\' } }, grid: { left: \'3%\', right: \'4%\', bottom: \'3%\', containLabel: true }, xAxis: { type: \'category\', data: [\'周一\', \'周二\', \'周三\', \'周四\', \'周五\', \'周六\', \'周日\'], axisLine: {  lineStyle: { color: \'#94a3b8\'  } }, axisLabel: {  color: \'#94a3b8\' } }, yAxis: { type: \'value\', name: \'用户数\', nameTextStyle: {  color: \'#94a3b8\' }, axisLine: {  lineStyle: { color: \'#94a3b8\'  } }, axisLabel: {  color: \'#94a3b8\' }, splitLine: {  lineStyle: { color: \'rgba(148, 163, 184, 0.1)\'  } } }, series: [ {  name: \'活跃用户\',  type: \'bar\',  barWidth: \'60%\',  itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ {offset: 0, color: \'#83bff6\'}, {offset: 0.5, color: \'#188df0\'}, {offset: 1, color: \'#188df0\'} ])  },  data: [1200, 1320, 1100, 1450, 1280, 980, 1245] } ] }); // 响应窗口大小变化 window.addEventListener(\'resize\', function () { chart1.resize(); chart2.resize(); chart3.resize(); chart4.resize(); chart5.resize(); chart6.resize(); }); }); // 初始化WebSocket连接 const socket = new WebSocket(\'ws://\' + window.location.host + \'/car-data\'); // 监听连接建立事件 socket.onopen = function() { console.log(\'WebSocket连接已建立\'); // 向服务器发送消息 socket.send(\'Hello Server!\'); }; // 处理WebSocket消息 socket.onmessage = (event) => { const data = JSON.parse(event.data); console.log(\"ws数据\",data) if(data.speed){ chart1.setOption({ series: [  { data: [ { value: data.speed.toFixed(2), name: \'速度\' } ]  } ] }); } if(data.coolantTemp) { chart2.setOption({ series: [  { data: [ { value: data.coolantTemp.toFixed(2), name: \'水温\' } ]  },  { data: [ { value: data.coolantTemp.toFixed(2) } ]  } ] }); } if(data.plp) { chart3.setOption({ series: [  { type: \'gauge\', data: [ { value: data.plp.toFixed(2), name: \'PLP\' } ]  } ] }); } };</script></body></html>