无人值守地磅

发布时间:2024年01月12日

无人值守地磅

需求

最近接到一个无人值守的项目,主要业务就是车辆入厂时需要先在地磅上称重,称完重后才能入厂,并且我们还需要知道车辆进入了哪个库,方便后面出厂时计算价格,车辆出厂时需要再上磅称重后,称完重我们就可以打印出它的磅单,有进出厂的重量以及进入的料库等信息。

接到这个项目的时候才发现基本都是硬件对接,自己又是第一次对接硬件,所以耗时两周才对好,并且现场联调都调了一周。涉及的硬件有:摄像头(海康)、道闸(海康)、地磅、红绿灯、LED显示大屏幕、语音播报音响、打印机、红外对射。硬件看着挺多,其实只有摄像头、地磅、红绿灯、大屏幕、红外对射需要我们对接,道闸直接通过线连接海康摄像头,由海康摄像头控制,我们只需要调用海康对应的sdk接口发送命令即可,而语音播报音响则直接连接LED显示大屏幕,由大屏幕让音响播放对应的文字。打印机则是一个单独的windows系统,可以直接访问服务器写的页面。

业务流程

车辆入厂前会先进行车牌识别,识别后会抬起对应的道闸并且会将地磅两边红绿灯由默认的绿灯转为红灯,车辆就可以上磅称重了,称完重后会保存对应车牌和重量信息,然后进行播报车牌号和重量,打开进厂的道闸并且将这侧红绿灯置为绿灯,表示可通行。车辆下磅后会将另一侧的红绿灯由红转为绿,即完成了进厂流程。

库中也有对应的摄像头,当车辆倒车入库时就会识别,识别后会将车牌信息传到服务器,服务器可以根据摄像头ip来区分是哪个库,并且进的是什么车,然后绑定到对应的进厂记录中,此时就完成了入库。

出厂时也需要识别摄像头后,红绿灯由绿变红且道闸打开,当车辆在磅上称完重后就会打开出厂道闸,并且播报对应的称重记录,打印机会展示该车辆进厂和出厂记录、库信息等,司机可以在打印机面前打印磅单,然后下磅完成一套流程。

道闸的开由我们调用海康sdk接口实现,而关闭则是根据车辆检测雷达,当雷达检测到没有车辆后就会自动关闭道闸,因此不需要我们控制关闭,有时会出现砸车现象,则需要使用地感线圈和检测雷达来保证道闸不要提前落下,这就不需要开发中的流程了。

对接

海康摄像头

摄像头就是用来识别当前车牌的,直接在开放平台下载对应demo即可。

海康sdk下载地址:海康开放平台 (hikvision.com)

根据对应服务器的系统下载对应的sdk包即可。下载解压后根据自己业务使用对应的demo即可,我们这个项目使用的是对应的报警布防监听demo。

布防监听分为两种:报警布防、报警监听

报警布防:开启摄像头的布防功能,当摄像头识别到了车牌后,会向指定的服务端地址回调事件,会将图片、车牌号等信息作为参数传到服务器,服务器就可以根据接收的数据来进行对应的业务流程。

报警监听:报警监听功能需要在后台中去配置服务器的报警地址(ip和port),代码端只需要监听端口即可,这样服务器才能拿到数据。

两者区别:报警布防需要登录设备,使用登录后的用户标识来开启布防,而报警监听不需要登录设备,直接使用Alarm.startListen("10.16.36.108",(short)7200);就可以开启监听。

我采用的是第一种报警布防功能,并且将demo中对应代码都封装了,这里建议封装一下代码,不然看起来比较混乱。下面的代码是demo中的main方法,其中有两个重点,一个是设置的回调函数,一个是设备登录的信息。

public static void main(String[] args) throws InterruptedException {

        if (hCNetSDK == null) {
            if (!createSDKInstance()) {
                System.out.println("Load SDK fail");
                return;
            }
        }
        //linux系统建议调用以下接口加载组件库
        if (osSelect.isLinux()) {
            HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
            HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
            //这里是库的绝对路径,请根据实际情况修改,注意改路径必须有访问权限
            String strPath1 = System.getProperty("user.dir") + "/lib/libcrypto.so.1.1";
            String strPath2 = System.getProperty("user.dir") + "/lib/libssl.so.1.1";

            System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
            ptrByteArray1.write();
            hCNetSDK.NET_DVR_SetSDKInitCfg(3, ptrByteArray1.getPointer());

            System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
            ptrByteArray2.write();
            hCNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());

            String strPathCom = System.getProperty("user.dir") + "/lib/";
            HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
            System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());
            struComPath.write();
            hCNetSDK.NET_DVR_SetSDKInitCfg(2, struComPath.getPointer());
        }

        /**初始化*/
        hCNetSDK.NET_DVR_Init();
        /**加载日志*/
        hCNetSDK.NET_DVR_SetLogToFile(3, "./sdklog", false);
        //设置报警回调函数
        if (fMSFCallBack_V31 == null) {
            fMSFCallBack_V31 = new FMSGCallBack_V31();
            Pointer pUser = null;
            
            //todo:这里是一个重点,设置的回调地址就是当摄像头识别到车牌后会向这个回调函数传数据,,在回调函数中去处理车牌信息即可
            if (!hCNetSDK.NET_DVR_SetDVRMessageCallBack_V31(fMSFCallBack_V31, pUser)) {
                System.out.println("设置回调函数失败!");
                return;
            } else {
                System.out.println("设置回调函数成功!");
            }
        }
        /** 设备上传的报警信息是COMM_VCA_ALARM(0x4993)类型,
         在SDK初始化之后增加调用NET_DVR_SetSDKLocalCfg(enumType为NET_DVR_LOCAL_CFG_TYPE_GENERAL)设置通用参数NET_DVR_LOCAL_GENERAL_CFG的byAlarmJsonPictureSeparate为1,
         将Json数据和图片数据分离上传,这样设置之后,报警布防回调函数里面接收到的报警信息类型为COMM_ISAPI_ALARM(0x6009),
         报警信息结构体为NET_DVR_ALARM_ISAPI_INFO(与设备无关,SDK封装的数据结构),更便于解析。*/

        HCNetSDK.NET_DVR_LOCAL_GENERAL_CFG struNET_DVR_LOCAL_GENERAL_CFG = new HCNetSDK.NET_DVR_LOCAL_GENERAL_CFG();
        struNET_DVR_LOCAL_GENERAL_CFG.byAlarmJsonPictureSeparate = 1;   //设置JSON透传报警数据和图片分离
        struNET_DVR_LOCAL_GENERAL_CFG.write();
        Pointer pStrNET_DVR_LOCAL_GENERAL_CFG = struNET_DVR_LOCAL_GENERAL_CFG.getPointer();
        hCNetSDK.NET_DVR_SetSDKLocalCfg(17, pStrNET_DVR_LOCAL_GENERAL_CFG);

    	//此处是摄像头的ip地址以及端口号、账号、密码,端口号默认的8000,账号和密码在摄像头说明书上都有,注意摄像头和服务器需要能ping通,不然到时候无法得到回调信息。
        Alarm.login_V40( "10.16.36.103", (short) 8000, "admin", "hik12345");  //登录设备

        Alarm.setAlarm();//报警布防,和报警监听二选一即可

//        Alarm.startListen("10.16.36.108",(short)7200);//报警监听,不需要登陆设备
        while (true) {
            //这里加入控制台输入控制,是为了保持连接状态,当输入Y表示布防结束
            System.out.print("请选择是否撤出布防(Y/N):\n");
            Scanner input = new Scanner(System.in);
            String str = input.next();
            if (str.equals("Y")) {
                break;
            }
        }
        Alarm.logout();
        //释放SDK
        hCNetSDK.NET_DVR_Cleanup();
        return;
    }

以上只是demo中的案例,将对应的业务写在回调函数中即可。这里简单的讲一下我写时业务摄像头的业务,代码太多,贴起来比较麻烦。

摄像头业务:当车辆进入到摄像头识别区域后,摄像头会将对应的车牌信息传输到回调函数中,根据回调的对象取出图片和车牌号,将车牌号和保存后的图片地址存入redis,key可以自己设置,该key的值用于后面车辆称完重后绑定重量的。因此我的这个项目车辆识别车牌后必须上磅,不然会导致重量和车牌号不对应。当业务处理完毕后

注意点:

? 在使用海康demo对接时,将保存图片的方法开启异步,提高打开道闸等操作,最后导致项目自动down掉。最后根据hs_err_pid.log日志文件分析到报错点在保存图片,并且提示没找到该内存区域,后来我就异步操作替换为同步就解决了问题。可能是因为开启异步后,回调方法执行完毕了删除了图片数据,导致异步中拿不到图片数据了。

地磅

? 地磅就是用来获取当前车辆称重数据的。地磅对接是直接问厂商要的对接文档,并且地磅的数据传输用的不是网口,购买了串口转网口的有人设备,在设备的后台可以配置上传数据的ip和端口号,则服务器只需要监听该端口后,即可拿到数据。

? 数据拿到后就剩下解析,解析直接根据文档中的含义来将十六进制数据转为需要的字符串,最后根据自己的业务去判断该数据是否为地磅稳定值,如果不是稳定值那就不记录。如果是稳定值说明可以保存到数据库,将对应某个部分的数据拿出来解析成重量即可。

? 遇见的问题:

? 1.地磅默认为重量为稳定值0,所以在判断是否稳定的时候还需要给个最小阈值,因为我们的地磅给大车使用,因此我设置最小为5吨,当大于5吨并且稳定的时候表示车辆称重完毕,可以走后面业务;

? 2.车辆在地磅上移动的时候,地磅可能会存在误判重量,导致给我的稳定值是一个错误重量,因此我将稳定值进行了计数操作,当地磅给我的稳定值次数大于20次时才进行业务操作,否则只是保存到redis中,并记录该稳定值的次数,如果redis中已存在并且稳定值一样的话,就将稳定值+1,如果不一样就将稳定值修改为新的稳定值,将次数置为1重新计数。

误判重量:因为地磅是根据滤波来判断是否稳定的,车辆在地磅上运行的时候可能会出现滤波异常的情况,导致重量偏小。(举例:比如一个车辆他其实是20吨,但是在地磅上称重的时候,当一半车身上了地磅并且车辆行驶很慢,此时才10吨,因为车辆行驶较慢让地磅误认为重量稳定,因此他会告诉我10吨已经稳定,就导致我们系统重量错误)

地磅业务:当车辆上磅后,地磅会给我称重数据并且告诉我是否稳定,当我发现稳定的时候,会获取redis中最近识别的车牌号和图片,将车牌号和重量绑定起来保存到数据库中,并且我会标记一下哪个地磅已经称完重保存到redis,打开一侧道闸,将红绿灯变成绿灯表示可以通行,当车辆下磅后,地磅重量又变成0并且稳定了,我会判断redis中这个地磅是否标记了称完重,如果标记了,我需要将另一侧红绿灯置为绿灯,则地磅业务结束。但是在出厂的时候地磅业务多了一个将车牌号存入redis中,供打印机根据车牌号获取称重记录使用,因为只有在出厂的时候需要打印磅单,进厂的时候不需要打印机打印。

红绿灯

? 红绿灯就是来规定车辆当前是否能上下磅的一个显示。红绿灯对接时设备是没有连接好线的,并且也没有网口,因此我们购买了一个网口控制器,将红绿灯的红灯通电线连接到一个口,变绿灯通电线连到另一个口,并且这个控制器是网口的,也有软件可以控制他调ip地址。

? 根据控制器厂商提供的文档,系统中采用udp通信,当需要变红灯的时候就会向控制器发送一个指令,当控制器接收到该指令的时候,就会向某个口通电,从而形成变灯操作。

遇见的问题

? 当我去向控制器发送指令的时候,控制器也会向我们返回变灯成功的标识,而发送指令可能存在丢失情况,因此我们需要根据这个标识来判断变灯是否成功,成功就不做操作了,没成功就重试在变灯。

? 他响应的标识会存在一些问题,比如标识不全、标识会响应一些不是标识的字母、不响应标识。因为存在这些问题,所以需要避免这些问题。系统中用的hutool中的工具类NioClient,这里给出相关代码,改代码用来获取当前红绿灯是什么颜色。


    /**
     * 获取红绿灯颜色
     */
    public static String getColor(String ip){
        //存放红绿灯响应编码
        String[] a = new String[1];

        NioClient client = new NioClient(ip, port);
        client.setChannelHandler((sc)->{
            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
            //从channel读数据到缓冲区
            int readBytes = sc.read(readBuffer);
            if (readBytes > 0) {
                //Flips this buffer.  The limit is set to the current position and then
                // the position is set to zero,就是表示要从起始位置开始读取数据
                readBuffer.flip();
                //returns the number of elements between the current position and the  limit.
                // 要读取的字节长度
                byte[] bytes = new byte[readBuffer.remaining()];
                //将缓冲区的数据读到bytes数组
                readBuffer.get(bytes);
                String body = byte2Hex(bytes);

                //"AB1234"是正常响应
                if (!"AB1234".equals(body)){
                    client.close();
                    sc.close();
                    a[0] = body;
                }
            } else if (readBytes < 0) {
                sc.close();
            }
        });
        client.listen();

        //发送获取灯颜色的指令
        client.write(ByteBuffer.wrap(DataConver.parseByte(RedAndGreenConstant.Code.GET_COLOR)));

        //todo:这里不能使用死循环判断是否接收到数据,因为可能出现设备不响应标识的情况
        //睡眠等待红绿灯设备响应值,只有前面连接上了就会有响应
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info("ip:{}, 接收到红绿灯响应消息为{}", ip, a[0]);
        //有可能返回的指令会直接跟AB1234连接到一起,导致无法识别是红还是绿,所以这里可以把AB1234替换成空
        if (a[0] != null){
            //因为可能会出现返回的红绿灯标识和数据通信成功标识连接到一起的情况
            a[0] = a[0].replace("AB1234", "");
            if (a[0].equals(RedAndGreenConstant.Code.RED)){
                log.info("ip:{} 红绿灯是红灯", ip);
                return "red";
            }else if (a[0].equals(RedAndGreenConstant.Code.GREEN)){
                log.info("ip:{} 红绿灯是绿灯", ip);
                return "green";
            }
        }
        getColor(ip);
    }

变灯代码


    /**
     * 红转绿
     * @param ip    红绿灯ip地址
     * @return true表示切换成功,false表示连接失败
     */
    public static void toGreen(String ip){
        executor.execute(() -> {
            boolean result = send(ip, "1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //有时候变灯会有问题,这里重试机制,十次机会,变成功就退出
            int count = 0;
            while (count < 10){
                if (result){
                    log.info("ip:{} 红绿灯转绿灯成功", ip);
                    break;
                }
                result = send(ip, "1");
                count++;
                log.info("ip:{}红绿灯重试变绿灯{}次", ip, count);
            }
        });
    }


    /**
     * 使用nioClient
     * @param ip
     * @param status
     * @return
     */
    private static boolean send(String ip, String status) {
        //存放红绿灯响应编码
        String[] a = new String[1];

        NioClient client = new NioClient(ip, port);
        client.setChannelHandler((sc)->{
            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
            //从channel读数据到缓冲区
            int readBytes = sc.read(readBuffer);
            if (readBytes > 0) {
                //Flips this buffer.  The limit is set to the current position and then
                // the position is set to zero,就是表示要从起始位置开始读取数据
                readBuffer.flip();
                //returns the number of elements between the current position and the  limit.
                // 要读取的字节长度
                byte[] bytes = new byte[readBuffer.remaining()];
                //将缓冲区的数据读到bytes数组
                readBuffer.get(bytes);
                String body = byte2Hex(bytes);

                Console.log("[{}]: {}", sc.getRemoteAddress(), body);
                client.close();
                sc.close();
                a[0] = body;
            } else if (readBytes < 0) {
                sc.close();
            }
        });
        client.listen();

        String message = "";
        if ("1".equals(status)){
            // 要发送的数据   绿灯数据
            message = RedAndGreenConstant.Code.TO_GREEN;
        }else if ("2".equals(status)){
            // 要发送的数据   红灯数据
            message = RedAndGreenConstant.Code.TO_RED;
        }
        //发送获取灯颜色的指令
        client.write(ByteBuffer.wrap(DataConver.parseByte(message)));

        //睡眠等待红绿灯设备响应值,只有前面连接上了就会有响应
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.info("ip:{}, 接收到红绿灯响应消息为{}", ip, a[0]);
        if (a[0] != null){
            //避免返回的时候连接到一起
            a[0] = a[0].replace("AB1234", "");
            if ("4F4B21".equals(a[0])){
                return true;
            }
        }
        return false;
    }

红绿灯业务:当车辆识别车牌后,需要判断地磅红绿灯是否都为绿,如果不是绿则表示磅上有车,如果为绿则可以变红灯后开闸上磅。

红外对射

红外对射设备就是来规定车辆是否停在地磅的规定区域内,如果不在规定区域内,则需要语音播报,让其停到规定区域内后在称重。

对接红外对射时直接找厂商要对接文档,文档上告诉用什么方式连接红外对射,怎么获取到数据,数据如何解析。当时我这个红外对射设备需要在它的后台去配置中心设备,这个中心设备就相当于服务器上报地址,当红外对射被触发时就会报警,并且告诉中心设备我在报警。还需要在后台去配置它的报警时长,当没有物体遮挡时多久可以恢复到正常状态。

系统中则需要监听中心设备设置的端口号,并且需要根据传输的数据来分析该设备是在报警还是报警恢复了,还是正常了,参考示例代码即可。

 //当监听到报警信息为正常和报警结束时,将redis中设备的状态标识为0,当发送的信息为报警时,将redis中设备的状态标识为1
        //后期车辆上磅后,可以通过查询redis中设备的状态标识去区分有没有报警,可以根据设备的ip地址来区分是哪台设备的消息
        @Override
        public void run() {
            while (true) {
                byte[] buffer = new byte[MAX_UDP_DATA_SIZE];
                byte[] sendData = new byte[MAX_UDP_DATA_SIZE];
                packet = new DatagramPacket(buffer, buffer.length);
                try {
                    log.info("waiting udp data!");
                    socket.receive(packet);
                    String s = DataConver.byte2Hex(packet.getData());
                    if ("01".equals(s.substring(12,14))){
                        //正常接收硬件数据和响应给硬件的数据
                        sendData = DataConver.parseByte(IrConstant.Code.NORMAL_RESPONSE);
                        log.info("ip:{},端口:{},正常", packet.getAddress(), packet.getPort());

                        //如果redis中不存在该客户端的报警状态,那么就设置状态为默认0
                        String hostAddress = packet.getAddress().getHostAddress();
                        String value = redisTemplate.opsForValue().get(RedisConstant.IrRedis.IR_STATUS_KEY + hostAddress);
                        if (StrUtil.isBlank(value)){
                            redisTemplate.opsForValue().set( RedisConstant.IrRedis.IR_STATUS_KEY + hostAddress, String.valueOf(RedisConstant.IrRedis.IR_STATUS_VALUE_0));
                        }
                    }else if ("02".equals(s.substring(12,14))){
                        //接收报警后的正常响应指令
                        sendData = DataConver.parseByte(IrConstant.Code.ALARM_RESPONSE);

                        //获取是报警还是报警结束
                        String substring = s.substring(14, 16);

                        //往redis中设置硬件状态
                        String hostAddress = packet.getAddress().getHostAddress();

                        String key = RedisConstant.IrRedis.IR_STATUS_KEY + hostAddress;
                        if ("01".equals(substring)){
                            log.info("ip:{},端口:{},已经报警", packet.getAddress(), packet.getPort());
                            redisTemplate.opsForValue().set( key, String.valueOf(RedisConstant.IrRedis.IR_STATUS_VALUE_1));
                        }else{
                            log.info("ip:{},端口:{},报警结束", packet.getAddress(), packet.getPort());

                            redisTemplate.opsForValue().set( key, String.valueOf(RedisConstant.IrRedis.IR_STATUS_VALUE_0));

                        }
                    }else if ("85".equals(s.substring(12,14))){
                        //监控中心发送撤布防后硬件响应数据
                        log.info("ip:{},端口:{},硬件撤防 或 布防", packet.getAddress(), packet.getPort());
                    }

                    //响应给红外对射的心跳指令
                    DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, packet.getAddress(), packet.getPort());
                    socket.send(sendPacket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

遇见的问题:当时不知道怎么去保存每个红外对射当前的状态,后得高人指点,直接将设备状态存储到redis中,当需要设备状态的时候直接判断redis中的标识接口。因此无人值守项目百分之99都是redis的功劳。

红外对射业务:红外对射就是来限制车辆称重位置,因此只有在磅上称重结束后会判断是否报警了,如果报警了就语音播报,如果未报警就往后执行业务。

Led屏幕

? Led屏幕就是用来显示当前重量、显示当前料库名称的,并且连接音响后,语音播报当前显示的内容,告知司机重量信息和摄像头识别是否成功。

? 对接Led屏幕我们只需要向Led发送指令接口,不需要知道LED现在是什么状态。因此对接LED屏幕比较简单,找厂商要文档后,调用对应的接口即可,我的这个设备它是有自己的jar包,直接引入项目,调用jar包即可。

遇见的问题:因为这个LED屏幕他无法设置默认页面,当你让他切换到一个页面后,他就一直是这个页面,无法自动跳转到默认页面。但是我们需求中是必须要有默认页面的,比如默认页面是“欢迎你”,然后车辆称完重后,需要播报和显示当前称重信息,播报完后还需要在跳转到默认页面,因此我每次在显示别的页面时,会额外开启一个线程用来等待30秒后在发送默认页面的指令。

led业务:led屏幕就是用来显示和播报的,因此没有什么业务操作,只有向他发送指令变显示的文字即可。

第一次对接硬件,最后自己一个人完成了这一个项目,收获还是很多,为以后对接硬件增加了不少的经验,不太会写博客,有问题可以评论区问。

文章来源:https://blog.csdn.net/qq_46062118/article/details/135555991
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。