diff --git a/pom.xml b/pom.xml index 7affc08..fea44ba 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,17 @@ 3.5.5 3.5.5 + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + @@ -106,11 +117,11 @@ 1.18.32 - + org.springdoc - springdoc-openapi-ui - 1.6.15 + springdoc-openapi-starter-webmvc-ui + 2.4.0 diff --git a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java index 312446c..bcac21f 100644 --- a/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java +++ b/src/main/java/com/example/lxcameraapi/service/IndustrialCamera/camera/hik/ImageCaptureService.java @@ -28,6 +28,9 @@ public class ImageCaptureService { // 设备句柄映射表 (IP -> Handle) private final Map handleMap = new ConcurrentHashMap<>(); + // 设备列表(全局变量) + private ArrayList stDeviceList = null; + // SDK初始化状态 private boolean isSdkInitialized = false; @@ -39,7 +42,10 @@ public class ImageCaptureService { @PostConstruct public void init() { try { - log.info("初始化海康相机SDK..."); + log.info("========== 开始初始化海康相机 =========="); + + // 1. 初始化SDK + log.info("步骤1: 初始化SDK..."); int nRet = MvCameraControl.MV_CC_Initialize(); if (MV_OK != nRet) { log.error("初始化SDK失败,errcode: [0x{}]", Integer.toHexString(nRet)); @@ -48,14 +54,151 @@ public class ImageCaptureService { log.info("SDK Version: {}", MV_CC_GetSDKVersion()); log.info("SDK初始化成功"); } + + // 2. 枚举设备 + log.info("步骤2: 枚举设备..."); + try { + stDeviceList = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE); + if (stDeviceList == null || stDeviceList.isEmpty()) { + log.warn("未找到任何设备"); + } else { + log.info("枚举到 {} 个设备", stDeviceList.size()); + // 打印所有设备信息 + for (int i = 0; i < stDeviceList.size(); i++) { + MV_CC_DEVICE_INFO device = stDeviceList.get(i); + if (device.transportLayerType == MV_GIGE_DEVICE) { + log.info("设备[{}] - IP: {}, 型号: {}", + i, device.gigEInfo.currentIp, device.gigEInfo.modelName); + } else if (device.transportLayerType == MV_USB_DEVICE) { + log.info("设备[{}] - USB序列号: {}", + i, device.usb3VInfo.serialNumber); + } + } + } + } catch (CameraControlException e) { + log.error("枚举设备失败", e); + } + + // 3. 初始化配置的相机 if (appConfig.getHikCamera() != null && !appConfig.getHikCamera().isEmpty()){ - initCamerasFromConfig(appConfig.getHikCamera()); + log.info("步骤3: 从配置初始化 {} 个相机...", appConfig.getHikCamera().size()); + initCamerasFromConfig(appConfig.getHikCamera()); + } else { + log.warn("配置中没有海康相机"); } + + log.info("========== 海康相机初始化完成 =========="); + log.info("已初始化相机: {}", handleMap.keySet()); + } catch (Exception e) { log.error("初始化SDK异常", e); } } + /** + * 确保相机已初始化(如果未初始化则立即初始化) + * @param ip 相机IP + * @return 是否初始化成功 + */ + public boolean ensureCameraInitialized(String ip) { + // 检查SDK是否初始化 + if (!isSdkInitialized) { + log.warn("SDK未初始化,正在重新初始化..."); + try { + int nRet = MvCameraControl.MV_CC_Initialize(); + if (MV_OK != nRet) { + log.error("SDK重新初始化失败,errcode: [0x{}]", Integer.toHexString(nRet)); + return false; + } + isSdkInitialized = true; + log.info("SDK重新初始化成功"); + } catch (Exception e) { + log.error("SDK重新初始化异常", e); + return false; + } + } + + // 检查相机句柄是否存在 + if (!handleMap.containsKey(ip)) { + log.warn("相机 {} 未初始化,正在重新初始化...", ip); + // 枚举设备(如果有全局设备列表则直接使用) + if (stDeviceList == null || stDeviceList.isEmpty()) { + try { + stDeviceList = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE); + if (stDeviceList == null || stDeviceList.isEmpty()) { + log.error("未找到任何设备"); + return false; + } + log.info("重新枚举到 {} 个设备", stDeviceList.size()); + } catch (CameraControlException e) { + log.error("枚举设备失败", e); + return false; + } + } + // 初始化单个相机 + Handle handle = initSingleCamera(ip, stDeviceList); + if (handle == null) { + log.error("相机 {} 重新初始化失败", ip); + return false; + } + log.info("相机 {} 重新初始化成功", ip); + } + return true; + } + + /** + * 主动初始化所有配置的相机 + * 可在需要时主动调用 + */ + public void initAllCameras() { + log.info("========== 主动初始化所有相机 =========="); + + // 确保SDK已初始化 + if (!isSdkInitialized) { + log.warn("SDK未初始化,正在初始化..."); + try { + int nRet = MvCameraControl.MV_CC_Initialize(); + if (MV_OK != nRet) { + log.error("SDK初始化失败,errcode: [0x{}]", Integer.toHexString(nRet)); + return; + } + isSdkInitialized = true; + log.info("SDK初始化成功"); + } catch (Exception e) { + log.error("SDK初始化异常", e); + return; + } + } + + // 清空已初始化的相机,重新初始化 + handleMap.clear(); + + // 从配置初始化 + if (appConfig.getHikCamera() != null && !appConfig.getHikCamera().isEmpty()){ + for (AppConfig.Camera camera : appConfig.getHikCamera()) { + String ip = camera.getIp(); + log.info("初始化相机: {}", ip); + if (stDeviceList == null || stDeviceList.isEmpty()) { + try { + stDeviceList = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE); + } catch (CameraControlException e) { + log.error("枚举设备失败", e); + continue; + } + } + Handle handle = initSingleCamera(ip, stDeviceList); + if (handle != null) { + log.info("相机 {} 初始化成功", ip); + } else { + log.error("相机 {} 初始化失败", ip); + } + } + } + + log.info("========== 相机初始化完成 =========="); + log.info("已初始化相机: {}", handleMap.keySet()); + } + /** * 销毁资源 */ @@ -118,8 +261,7 @@ public class ImageCaptureService { log.info("开始批量初始化 {} 个相机...", ipList.size()); - // 枚举所有设备 - ArrayList stDeviceList = null; + // 枚举所有设备(使用全局变量) try { stDeviceList = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE); if (stDeviceList == null || stDeviceList.isEmpty()) { @@ -168,6 +310,14 @@ public class ImageCaptureService { return initCameras(ipList); } + /** + * 初始化单个相机 + * @param ip 相机IP + * @return 相机句柄 + */ + private Handle initSingleCamera(String ip) { + return initSingleCamera(ip, stDeviceList); + } /** * 初始化单个相机 * @param ip 相机IP @@ -236,55 +386,110 @@ public class ImageCaptureService { } /** - * 根据IP拍照并保存 + * 根据IP拍照并保存(带重试机制) * @param ip 相机IP * @param savePath 保存路径 * @return 是否成功 */ public boolean captureImage(String ip, String savePath) { - if (!isSdkInitialized) { - log.error("SDK未初始化"); + return captureImageWithRetry(ip, savePath, 3); + } + + /** + * 根据IP拍照并保存(可配置重试次数) + * @param ip 相机IP + * @param savePath 保存路径 + * @param maxRetries 最大重试次数 + * @return 是否成功 + */ + public boolean captureImageWithRetry(String ip, String savePath, int maxRetries) { + // 确保相机已初始化(如果未初始化则立即初始化) + if (!ensureCameraInitialized(ip)) { + log.error("相机 {} 初始化失败,无法拍照", ip); return false; } // 获取相机句柄 Handle hCamera = handleMap.get(ip); if (hCamera == null) { - log.error("相机 {} 未初始化,请先调用init方法", ip); - return false; + log.warn("相机 {} 句柄为空,尝试自动初始化...", ip); + if (!ensureCameraInitialized(ip)) { + log.error("相机 {} 自动初始化失败,无法拍照", ip); + return false; + } + hCamera = handleMap.get(ip); + if (hCamera == null) { + log.error("相机 {} 自动初始化后仍无法获取句柄", ip); + return false; + } + log.info("相机 {} 自动初始化成功", ip); } - log.info("开始拍照,相机: {}, 保存路径: {}", ip, savePath); + log.info("开始拍照,相机: {}, 保存路径: {}, 最大重试: {} 次", ip, savePath, maxRetries); - try { - // 获取PayloadSize - MVCC_INTVALUE stParam = new MVCC_INTVALUE(); - int nRet = MvCameraControl.MV_CC_GetIntValue(hCamera, "PayloadSize", stParam); - if (MV_OK != nRet) { - log.error("获取PayloadSize失败,errcode: [0x{}]", Integer.toHexString(nRet)); - return false; - } + int attempt = 0; + int lastErrorCode = 0; - // 获取一帧图像 - MV_FRAME_OUT_INFO stImageInfo = new MV_FRAME_OUT_INFO(); - byte[] pData = new byte[(int) stParam.curValue]; - nRet = MvCameraControl.MV_CC_GetOneFrameTimeout(hCamera, pData, stImageInfo, 3000); + while (attempt < maxRetries) { + attempt++; + try { + // 获取PayloadSize + MVCC_INTVALUE stParam = new MVCC_INTVALUE(); + int nRet = MvCameraControl.MV_CC_GetIntValue(hCamera, "PayloadSize", stParam); + if (MV_OK != nRet) { + log.error("获取PayloadSize失败,errcode: [0x{}]", Integer.toHexString(nRet)); + lastErrorCode = nRet; + continue; + } - if (MV_OK != nRet) { - log.error("获取图像失败,errcode: [0x{}]", Integer.toHexString(nRet)); - return false; - } + // 获取一帧图像 + MV_FRAME_OUT_INFO stImageInfo = new MV_FRAME_OUT_INFO(); + byte[] pData = new byte[(int) stParam.curValue]; + nRet = MvCameraControl.MV_CC_GetOneFrameTimeout(hCamera, pData, stImageInfo, 3000); - log.info("获取图像成功: 帧[{}] 宽[{}] 高[{}]", - stImageInfo.frameNum, stImageInfo.width, stImageInfo.height); + if (MV_OK != nRet) { + log.warn("获取图像失败 (第{}次),errcode: [0x{}]", attempt, Integer.toHexString(nRet)); + lastErrorCode = nRet; + if (attempt < maxRetries) { + log.info("等待200ms后重试..."); + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + continue; + } - // 保存为JPEG格式 - return saveImageAsJpeg(hCamera, pData, stImageInfo, savePath); + log.info("获取图像成功: 帧[{}] 宽[{}] 高[{}]", + stImageInfo.frameNum, stImageInfo.width, stImageInfo.height); - } catch (Exception e) { - log.error("拍照异常", e); - return false; + // 保存为JPEG格式 + boolean saveResult = saveImageAsJpeg(hCamera, pData, stImageInfo, savePath); + if (saveResult) { + log.info("拍照成功!共尝试 {} 次", attempt); + return true; + } else { + lastErrorCode = -1; + } + + } catch (Exception e) { + log.error("拍照异常 (第{}次)", attempt, e); + lastErrorCode = -1; + if (attempt < maxRetries) { + try { + Thread.sleep(200); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } } + + log.error("拍照失败,已重试 {} 次,最后错误码: [0x{}]", attempt, Integer.toHexString(lastErrorCode)); + return false; } /** @@ -415,8 +620,9 @@ public class ImageCaptureService { public List captureBatch(String ip, String basePath, int count) { List successPaths = new ArrayList<>(); - if (!isSdkInitialized) { - log.error("SDK未初始化"); + // 确保相机已初始化 + if (!ensureCameraInitialized(ip)) { + log.error("相机 {} 初始化失败,无法连拍", ip); return successPaths; } diff --git a/src/main/java/com/example/lxcameraapi/service/s7/S7MultiPlcService.java b/src/main/java/com/example/lxcameraapi/service/s7/S7MultiPlcService.java index 016c3ae..4d31a10 100644 --- a/src/main/java/com/example/lxcameraapi/service/s7/S7MultiPlcService.java +++ b/src/main/java/com/example/lxcameraapi/service/s7/S7MultiPlcService.java @@ -48,6 +48,8 @@ import java.util.*; import java.util.List; import java.util.concurrent.*; +import static java.lang.Thread.sleep; + /** * S7 PLC 服务 * 支持连接PLC,读取数据,托盘号变化时触发上下相机拍照 @@ -259,7 +261,7 @@ public class S7MultiPlcService { // 检查是否是 S7 String 格式(有长度前缀) if (length > 2 && offset + 2 < buffer.length) { int maxLen = buffer[offset] & 0xFF; - int actualLen = buffer[offset + 1] & 0xFF; + int actualLen = buffer[offset] & 0xFF; if (maxLen > 0 && actualLen > 0) { int readLen = Math.min(actualLen, length - 2); return new String(buffer, offset + 2, readLen, StandardCharsets.UTF_8).trim(); @@ -302,10 +304,11 @@ public class S7MultiPlcService { Math.max(config.getPalletOffset(), config.getBatchOffset()), Math.max(config.getDateOffset(), config.getSnapOffset()) ); - int maxDataTypeSize = getDataTypeSize(config.getPalletDataType()); + int maxDataTypeSize = getDataTypeSize(config.getSnapDataType()); int readLength = maxOffset + maxDataTypeSize; byte[] buffer = new byte[readLength]; + int result = client.ReadArea(S7.S7AreaDB, config.getReadDataBlock(), 0, readLength, buffer); if (result != 0) { @@ -328,29 +331,41 @@ public class S7MultiPlcService { // 检查托盘号是否需要处理 String lastPallet = lastPalletCache.get(plcNumber); boolean needProcess = false; + if (data.getSnapFlag() == 0){ - if (lastPallet == null) { - // 首次启动,查数据库比对 - CameraUrl lastRecord = cameraUrlService.getOne(new QueryWrapper() - .orderByDesc("create_time").last("limit 1")); - if (lastRecord != null && !data.getPalletNo().equals(lastRecord.getPallet())) { + // 标记拍照修改 + writePhotoResult(plcNumber, (byte) 0); + }else { + if (data.getSnapFlag() == 1 && (lastPallet == null || !lastPallet.equals(data.getPalletNo()))) { + // 托盘号变化或首次启动 needProcess = true; - }else{ - lastPalletCache.put(plcNumber, data.getPalletNo()); } - } else if (!lastPallet.equals(data.getPalletNo())) { - // 托盘号变化 - needProcess = true; } +// if (lastPallet == null) { +// // 首次启动,查数据库比对 +// CameraUrl lastRecord = cameraUrlService.getOne(new QueryWrapper() +// .orderByDesc("create_time").last("limit 1")); +// +// if (data.getPalletNo()!= null && !data.getPalletNo().isEmpty()&& !data.getPalletNo().equals(lastRecord.getPallet()) && data.getSnapFlag() == 1) { +// needProcess = true; +// }else{ +// lastPalletCache.put(plcNumber, data.getPalletNo()); +// } +// } else +// } + if (needProcess) { + lastPalletCache.put(plcNumber, data.getPalletNo()); // 触发灯 if (config.getLightChannel() != null) { lightService.pulse(config.getLightChannel(), lightConfig.getDuration(), lightConfig.getRes(), lightConfig.getIp(), lightConfig.getPort()); } log.info("PLC {} 托盘号变化: {} -> {}", plcNumber, lastPallet, data.getPalletNo()); - lastPalletCache.put(plcNumber, data.getPalletNo()); + sleep(500); + + // 拍照 Map photos = capturePhotos(config, data); @@ -361,7 +376,16 @@ public class S7MultiPlcService { cameraUrl.setPallet(data.getPalletNo()); cameraUrl.setBatch(data.getBatchNo()); if (data.getDate() != null && !data.getDate().isEmpty()) { - cameraUrl.setDate(LocalDateTime.parse(data.getDate(), FULL_DATE_FORMATTER)); + try { + // 截取前14位,兼容异常格式 + String dateStr = data.getDate().trim(); + if (dateStr.length() >= 15) { + cameraUrl.setDate(LocalDateTime.parse(dateStr, FULL_DATE_FORMATTER)); + } + } catch (Exception e) { + log.warn("日期解析失败: '{}', 使用当前时间", data.getDate()); + cameraUrl.setDate(LocalDateTime.now()); + } } cameraUrl.setId(UUID.randomUUID().toString()); cameraUrl.setCreateTime(LocalDateTime.now()); @@ -422,6 +446,7 @@ public class S7MultiPlcService { log.info("PLC {} 上相机拍照: {}, 结果: {}", config.getPlcNumber(), upPath, success); } } + sleep(1000); // 下相机拍照 if (config.getCameraDown() != null && !config.getCameraDown().isEmpty()) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 683c3b7..42f02a5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -94,7 +94,6 @@ springdoc: path: /v3/api-docs # API文档路径 swagger-ui: enabled: true # 是否启用Swagger UI - path: /swagger-ui.html # Swagger UI路径 deleteFile: # cron: "0/5 * * * * ?" @@ -120,4 +119,3 @@ yoloModelConfig: # 置信度 confThreshold: 0.95 names: ['code'] - diff --git a/src/main/resources/static/camera-url-query.html b/src/main/resources/static/camera-url-query.html index 51cac23..b7e11c7 100644 --- a/src/main/resources/static/camera-url-query.html +++ b/src/main/resources/static/camera-url-query.html @@ -338,7 +338,6 @@ const tbody = document.getElementById('tableBody'); tbody.innerHTML = data.list.map(item => ` - ${item.id || '-'} ${item.pallet || '-'} ${item.batch || '-'} ${formatDateTime(item.date)}