【Android】在平板上实现Rs485的数据通讯

前言

在工业控制领域,Android 设备通过 RS485 接口与 PLC(可编程逻辑控制器)通信是一种常见的技术方案。最近在实现一个项目需要和plc使用485进行通讯,记录下实现的方式。

我这边使用的从平的Android平板,从平里面已经对这个串口进行了优化和提供了开发工具包,这样哦我们就不需要自己实现这方面的东西了,【Uart 】就是提供的工具类。

正文

先贴代码

public class Rs485Util {

    private static final String TAG = "Rs485Util";
    private static final String UART_PATH = "/dev/ttyWK1";
    private static final int BAUD_RATE = 19200;
    private static final String QUERY_COMMAND = "01030000001AC401";
    private static final int QUERY_INTERVAL = 199; // 定时查询间隔(毫秒)
    private static final int SEND_RETRY_COUNT = 3; // 命令重试次数
    private static final int SEND_DELAY = 51; // 重试间隔(毫秒)
    private static final int COMMAND_INTERVAL = 101; // 不同命令间间隔(毫秒)

    private static volatile Rs485Util instance;
    private Uart uart485;
    private final ScheduledExecutorService queryExecutor; // 定时查询线程池
    private final ScheduledExecutorService commandExecutor; // 命令处理线程池
    private final BlockingQueue<String> commandQueue;
    private final AtomicBoolean isRunning = new AtomicBoolean(false);
    private final AtomicBoolean isQuerying = new AtomicBoolean(false);
    private final AtomicBoolean isQueryPaused = new AtomicBoolean(false); // 查询暂停标记
    private String lastCommand = null; // 上一条命令记录

    // 重要指令
    private static final String START_COMMAND_1 = "010600060001A80B";
    private static final String START_COMMAND_0 = "01060006000069CB";
    private static final String STOP_COMMAND_1 = "010600070001F9CB";
    private static final String STOP_COMMAND_0 = "010600070000380B";

    private Rs485Util() {
        // 初始化命令线程池(单线程,确保命令顺序执行)
        commandExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread thread = new Thread(r, "RS485-Command-Thread");
            thread.setDaemon(true);
            return thread;
        });

        // 初始化查询线程池(单线程,定时发送查询指令)
        queryExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread thread = new Thread(r, "RS485-Query-Thread");
            thread.setDaemon(true);
            return thread;
        });

        // 命令队列(存储待发送的命令指令)
        commandQueue = new LinkedBlockingQueue<>();
    }

    // 单例模式获取实例
    public static Rs485Util getInstance() {
        if (instance == null) {
            synchronized (Rs485Util.class) {
                if (instance == null) {
                    instance = new Rs485Util();
                }
            }
        }
        return instance;
    }

    /**
     * 打开485串口并初始化通信
     */
    public synchronized void open485Uart() {
        if (isRunning.get()) {
            Log.e(TAG, "串口已处于打开状态");
            return;
        }

        commandExecutor.execute(() -> {
            try {
                // 初始化串口
                uart485 = new Uart(UART_PATH, BAUD_RATE, true);
                uart485.setReceiveListener(bytes -> {
                    String receiveData = DigitalTransUtil.byte2hex(bytes);
                    PLCUtil.analyzePLCData(receiveData);
                });
                uart485.start();
                Log.e(TAG, "串口启动成功");
                isRunning.set(true);

                // 启动定时查询任务
                startQueryTask();

                // 启动命令队列处理
                processCommandQueue();

            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "启动485串口失败:" + e.getMessage());
                closeResources(); // 异常时释放资源
            }
        });
    }

    /**
     * 启动定时查询任务(带暂停判断)
     */
    private void startQueryTask() {
        if (isQuerying.get()) return;

        // 定时发送查询指令,每次发送前检查是否被暂停
        queryExecutor.scheduleAtFixedRate(() -> {
            // 仅在运行中且未被暂停时发送查询指令
            if (uart485 != null && isRunning.get() && !isQueryPaused.get()) {
                try {
                    byte[] sendByte = DigitalTransUtil.hex2byte(QUERY_COMMAND);
                    uart485.send(sendByte);
                    Log.d(TAG, "发送查询指令: " + QUERY_COMMAND);
                } catch (Exception e) {
                    Log.e(TAG, "查询命令发送失败: " + e.getMessage());
                }
            }
        }, 0, QUERY_INTERVAL, TimeUnit.MILLISECONDS);

        isQuerying.set(true);
    }

    /**
     * 处理命令队列(发送命令时暂停查询)
     */
    private void processCommandQueue() {
        commandExecutor.execute(() -> {
            while (isRunning.get()) {
                try {
                    // 从队列获取命令(阻塞等待新命令)
                    String command = commandQueue.take();
                    // 1. 暂停定时查询(确保命令发送时无查询干扰)
//                    pauseQuery();
                    // 2. 不同命令间等待间隔
                    if (lastCommand != null && !lastCommand.equals(command)) {
                        Thread.sleep(COMMAND_INTERVAL);
                    }
                    // 3. 发送命令(带重试)
                    sendCommandInternal(command);
                    // 4. 更新最后一条命令记录
                    lastCommand = command;
                    // 5. 恢复定时查询(命令发送完成)
//                    resumeQuery();
                } catch (InterruptedException e) {
                    Log.e(TAG, "命令处理线程被中断", e);
                    Thread.currentThread().interrupt();
                    break;
                } catch (Exception e) {
                    Log.e(TAG, "命令处理异常: " + e.getMessage());
                    // 异常时也需恢复查询
                    resumeQuery();
                }
            }
        });
    }

    /**
     * 内部发送命令(带重试机制)
     */
    private void sendCommandInternal(String strCommand) throws InterruptedException {
        if (uart485 == null || !isRunning.get()) {
            Log.e(TAG, "串口未初始化或已关闭,无法发送命令");
            return;
        }

        for (int i = 0; i < SEND_RETRY_COUNT; i++) {
            try {
                byte[] sendByte = DigitalTransUtil.hex2byte(strCommand);
                uart485.send(sendByte);
                Log.e(TAG, "命令发送成功(第" + (i + 1) + "次): " + strCommand);

                // 非最后一次重试时等待间隔
                if (i < SEND_RETRY_COUNT - 1) {
                    Thread.sleep(SEND_DELAY);
                }
            } catch (Exception e) {
                Log.e(TAG, "命令发送失败(第" + (i + 1) + "次): " + e.getMessage());

                // 最后一次重试失败时,仍继续后续流程(避免阻塞)
                if (i == SEND_RETRY_COUNT - 1) {
                    Log.e(TAG, "命令达到最大重试次数: " + strCommand);
                }
            }
        }
    }

    /**
     * 暂停定时查询
     */
    private synchronized void pauseQuery() {
        if (!isQueryPaused.get()) {
            isQueryPaused.set(true);
            Log.e(TAG, "暂停定时查询");
        }
    }

    /**
     * 恢复定时查询
     */
    private synchronized void resumeQuery() {
        if (isQueryPaused.get()) {
            isQueryPaused.set(false);
            Log.e(TAG, "恢复定时查询");
        }
    }

    /**
     * 发送命令接口(线程安全)
     */
    public void sendString(String strCommand) {
        if (!isRunning.get()) {
            Log.e(TAG, "串口未打开,无法发送命令");
            return;
        }

        try {

            boolean isSpecial = START_COMMAND_1.equals(strCommand) || STOP_COMMAND_1.equals(strCommand);

            if (isSpecial) {
                commandQueue.clear();
            }

            commandQueue.put(strCommand);
            Log.e(TAG, "命令已加入队列: " + strCommand);
        } catch (InterruptedException e) {
            Log.e(TAG, "添加命令到队列被中断", e);
            Thread.currentThread().interrupt();
        }
    }

    /**
     * 停止485串口通信
     */
    public synchronized void stop485Uart() {
        if (!isRunning.get()) {
            Log.e(TAG, "串口已处于关闭状态");
            return;
        }

        Log.e(TAG, "正在停止485串口通信...");
        isRunning.set(false);
        isQuerying.set(false);
        isQueryPaused.set(false); // 重置暂停状态

        closeResources();
    }

    /**
     * 关闭所有资源
     */
    private void closeResources() {
        // 关闭查询线程池
        if (queryExecutor != null) {
            queryExecutor.shutdownNow();
            try {
                if (!queryExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) {
                    Log.e(TAG, "查询任务未能及时关闭");
                }
            } catch (InterruptedException e) {
                queryExecutor.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }

        // 关闭命令线程池
        if (commandExecutor != null) {
            commandExecutor.shutdownNow();
            try {
                if (!commandExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) {
                    Log.e(TAG, "命令任务未能及时关闭");
                }
            } catch (InterruptedException e) {
                commandExecutor.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }

        // 清空命令队列
        commandQueue.clear();

        // 关闭串口
        if (uart485 != null) {
            try {
                uart485.stop();
            } catch (Exception e) {
                Log.e(TAG, "关闭串口异常: " + e.getMessage());
            }
            uart485 = null;
        }

        Log.e(TAG, "485串口资源已完全释放");
    }

    /**
     * 检查串口是否已打开
     */
    public boolean isUartOpen() {
        return isRunning.get();
    }
}

代码解析

使用两个单线程调度线程池实现任务分离:

queryExecutor:负责定时发送查询指令,采用scheduleAtFixedRate实现固定间隔执行

commandExecutor:处理命令队列,确保命令按顺序执行

线程池配置为守护线程(thread.setDaemon(true)),避免应用退出时线程残留。这种分离设计保证了查询任务和命令任务的独立性,防止相互干扰。

通过BlockingQueue实现命令的缓冲与有序处理:

所有命令先进入队列等待,由专门的线程按顺序取出并发送
特殊命令(启动 / 停止)具有清空队列的优先权:

boolean isSpecial = START_COMMAND_1.equals(strCommand) || STOP_COMMAND_1.equals(strCommand);
if (isSpecial) { commandQueue.clear(); }

这种设计解决了多线程发送命令的冲突问题,保证了命令执行的顺序性,同时确保关键操作(如启动 / 停止)能够立即执行。

关键方法解析
1. 串口初始化:open485Uart()

该方法是启动通信的入口,主要完成:

检查当前状态,避免重复打开
初始化 UART 设备,配置端口路径(/dev/ttyWK1)和波特率(19200)
设置接收数据的监听器,实现数据的异步处理:

uart485.setReceiveListener(bytes -> {//数据的解析处理
    String receiveData = DigitalTransUtil.byte2hex(bytes);
    PLCUtil.analyzePLCData(receiveData);
});
2. 定时查询:startQueryTask()

实现对 PLC 的定时查询功能:

采用固定间隔(199ms)发送查询指令QUERY_COMMAND
发送前检查运行状态和暂停标记,确保仅在合适状态下发送
通过scheduleAtFixedRate实现周期性执行

3. 命令处理:processCommandQueue()

命令处理的核心流程:

从队列阻塞获取命令(commandQueue.take())
不同命令间保持固定间隔(101ms),避免命令发送过于密集
调用sendCommandInternal()实际发送命令(包含重试逻辑)
更新最后一条命令记录,用于间隔判断

4. 资源释放:closeResources()

该方法负责在通信结束或异常时释放所有资源:

关闭线程池(shutdownNow() + awaitTermination)
清空命令队列,避免残留命令干扰
关闭串口设备,释放硬件资源
重置所有状态标记,确保下次启动正常

在和PLC对接的时候,哥们建议我在进行定时循环或者类似的操作的时候,最好不要把时间卡在5、10 等 5 的倍数,核心逻辑还是与 PLC 扫描周期的 “同步冲突” 有关

避免同步重叠

PLC 的扫描周期通常是动态变化的(如因程序复杂度波动在 8~12ms)。若通讯间隔固定为 10ms(5 的倍数),可能与 PLC 的扫描周期 “同步”—— 例如 PLC 在第 10ms、20ms 时正处于数据刷新阶段,此时外部设备(如 SCADA、HMI)发送通讯请求,可能导致:
数据读取不完整:PLC 尚未完成输出刷新,读取到的是 “旧数据”。
通讯响应延迟:PLC 优先处理内部程序,暂时搁置通讯请求,导致外部设备超时。

实际建议

通讯间隔应避开 PLC 的典型扫描周期范围,或采用非固定间隔(如随机增加 1~2ms 偏移量)。例如:
若 PLC 扫描周期约为 10ms,通讯间隔可设为 12ms 或 8ms,减少同步概率。
对于需要高频通讯的场景(如毫秒级控制),建议采用 PLC 支持的高速通讯协议(如 Profinet IO、EtherCAT),而非依赖定时轮询。

还有PLC的撞包概念

“撞包” 是工业通讯中的通俗说法,指数据帧冲突,即多个设备在同一时间向 PLC 的通讯总线发送数据,导致信号干扰、数据丢失。

常见场景

采用半双工通讯协议(如 RS485 总线的 Modbus RTU)时,总线上的多个从设备(如传感器、变频器)若同时向 PLC(主设备)发送响应,会导致数据帧重叠。
总线负载过高:当多个设备的通讯频率过高(如间隔过短),总线上数据帧密集,容易发生碰撞。

解决方式

采用全双工协议(如 Profinet、EtherNet/IP):通过交换机实现点对点通讯,避免总线冲突。
严格主从机制:如 Modbus RTU 中,由 PLC(主设备)轮流查询从设备,从设备仅在被询问时响应,禁止主动发送数据。
降低总线负载:控制总线上的设备数量,或延长通讯间隔,确保数据帧发送时间不重叠。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值