java调用周立功USBCAN SDK读取汽车总线数据
java调用周立功USBCAN SDK读取汽车总线数据
- 准备
-
- 新的改变
- 后端
- 前端
准备
我这测试用的两个usbcan-2e-u盒子,一个发送端一个接收端,两个盒子要通过can口进行串联起来,测试工具用的canTest或者zcanpro,这两个工具在官网上都可以下载。
项目是要做一个汽车端的工控屏(win10)的数据大屏,效果如下:
新的改变
由于周立功sdk只有c和python的,没有Java的,项目其他业务逻辑都已经用Java写好,所以这里直接参照官网提供的python demo进行改造。
后端
Java项目技术栈是springboot+thymeleaf的,把dll文件放到Java项目的resources目录下。其中zlgcan.dll文件和kerneldlls文件夹是从周立功官网上的pythonDemo里拷贝过来的,目前在 PYTHON型号合集\\USBCAN-xE-U系列_。
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>