在NanoPC-T6开发板上通过USB串口通信实现光源控制功能

在NanoPC-T6开发板上通过USB串口通信实现光源控制功能

  • 1、整体任务描述
  • [2、 Android Project代码实现](#2、 Android Project代码实现)
    • [2.1 'com.github.mik3y:usb-serial-for-android:3.8.0'依赖库导入](#2.1 'com.github.mik3y:usb-serial-for-android:3.8.0'依赖库导入)
    • [2.2 Java辅助类UsbLightManager实现](#2.2 Java辅助类UsbLightManager实现)
    • [2.3 布局文件activity_main.xml代码实现](#2.3 布局文件activity_main.xml代码实现)
    • [2.4 调用辅助类和布局MainActivity.java代码实现](#2.4 调用辅助类和布局MainActivity.java代码实现)
  • 3、最终实现的灯光控制效果
  • 写在最后

1、整体任务描述

最近老师安排了项目上的一项任务:在NanoPC-T6开发板上通过USB串口通信实现光源控制功能。硬件层面的接线和相应的串口指令已经由其他人完成,我只需要负责在Android项目中开发相应的光源控制逻辑即可。

任务可拆分为以下关键内容:

  • 1、在Android中识别特定USB设备并连接;
  • 2、给相应的串口地址发送特定的灯光控制指令;

灯光控制指令的示意图如下:

USB接口信息如下,无论是Mac还是Windoes下都有唯一的ID,这块只是临时把控制光源的辅助板子USB接线插到电脑上找到唯一的硬件ID用于在Android项目中连接,找到后还需要将该USB线插回板子上。

1️⃣ mac系统查看硬件ID:

2️⃣ windoes系统下查看硬件VID和PID:

2、 Android Project代码实现

2.1 'com.github.mik3y:usb-serial-for-android:3.8.0'依赖库导入

在Android项目的app/build.gradle文件的dependencies项中加入'com.github.mik3y:usb-serial-for-android:3.8.0'

java 复制代码
dependencies {
    implementation libs.appcompat
    implementation libs.material
    implementation libs.constraintlayout
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core
    // new add
    implementation 'com.github.mik3y:usb-serial-for-android:3.8.0'
}

2.2 Java辅助类UsbLightManager实现

对应的路径为:

完整代码如下:

java 复制代码
package utils;

import android.content.Context;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.util.Log;

// 导入驱动库
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver;
import com.hoho.android.usbserial.driver.ProbeTable;
import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.driver.UsbSerialProber;
import com.hoho.android.usbserial.util.SerialInputOutputManager;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executors;

public class UsbLightManager {

    private static final String TAG = "UsbLightManager";
    private UsbSerialPort serialPort;
    private SerialInputOutputManager usbIoManager;
    private final int BAUD_RATE = 115200;

    // 用于标记连接状态
    private boolean isConnected = false;

    public interface LightCallback {
        void onDataReceived(String message);
        void onError(String message);
        void onConnected();
    }

    private LightCallback callback;

    // 串口数据监听器
    private final SerialInputOutputManager.Listener mListener = new SerialInputOutputManager.Listener() {
        @Override
        public void onNewData(byte[] data) {
            if (callback != null) callback.onDataReceived(bytesToHex(data));
        }

        @Override
        public void onRunError(Exception e) {
            isConnected = false;
            if (callback != null) callback.onError("IO 异常: " + e.getMessage());
            disconnect();
        }
    };

    public UsbLightManager(Context context, LightCallback callback) {
        this.callback = callback;
    }

    /**
     * 获取当前连接状态
     */
    public boolean isConnected() {
        return isConnected;
    }

    /**
     * 连接设备
     */
    public boolean connect(UsbDevice device, UsbDeviceConnection connection, UsbManager usbManager) {
        if (device == null || connection == null || usbManager == null) return false;

        // 1. 配置设备驱动探测表
        ProbeTable customTable = new ProbeTable();
        // 添加 GD32 设备 ID
        customTable.addProduct(0x28e9, 0x018a, CdcAcmSerialDriver.class);
        // 如果需要支持 CH340,可以取消下面注释
        // customTable.addProduct(0x1A86, 0xE5E3, Ch34xSerialDriver.class);

        UsbSerialProber prober = new UsbSerialProber(customTable);
        List<UsbSerialDriver> drivers = prober.findAllDrivers(usbManager);

        // 兜底策略:如果自定义表没找到,尝试默认表
        if (drivers.isEmpty()) {
            drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager);
        }

        UsbSerialDriver driver = null;
        for (UsbSerialDriver d : drivers) {
            if (d.getDevice().equals(device)) {
                driver = d;
                break;
            }
        }

        if (driver == null || driver.getPorts().isEmpty()) {
            if (callback != null) callback.onError("未找到匹配驱动: " + String.format("0x%04X", device.getVendorId()));
            return false;
        }

        serialPort = driver.getPorts().get(0);

        try {
            serialPort.open(connection);
            // 设置波特率 115200, 8数据位, 1停止位, 无校验
            serialPort.setParameters(BAUD_RATE, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
            serialPort.setDTR(true);
            serialPort.setRTS(true);

            usbIoManager = new SerialInputOutputManager(serialPort, mListener);
            Executors.newSingleThreadExecutor().submit(usbIoManager);

            isConnected = true;
            Log.d(TAG, "已连接到设备");
            if (callback != null) callback.onConnected();
            return true;

        } catch (IOException e) {
            isConnected = false;
            if (callback != null) callback.onError("打开失败: " + e.getMessage());
            disconnect();
            return false;
        }
    }

    /**
     * 控制灯光开关
     * @param on true=开灯(默认50%), false=关灯
     */
    public void setPower(boolean on) {
        if (serialPort == null || !isConnected) return;

        byte[] command;
        if (on) {
            // 开灯指令: A5 00 01 03 32 FF FF 26 (默认50%亮度)
            command = new byte[] {
                    (byte) 0xA5, 0x00, 0x01, 0x03, 0x32, (byte) 0xFF, (byte) 0xFF, 0x26
            };
        } else {
            // 关灯指令: A5 00 01 02 00 00 57
            command = new byte[] {
                    (byte) 0xA5, 0x00, 0x01, 0x02, 0x00, 0x00, 0x57
            };
        }
        writeCommand(command);
    }

    /**
     * 调节亮度 (1-100)
     */
    public void setBrightness(int level) {
        if (serialPort == null || !isConnected) return;

        // 范围限制
        if (level < 1) level = 1;
        if (level > 100) level = 100;

        // 校验和计算公式: 0x58 - level
        // 例如 level=1 (0x01) -> 0x58 - 0x01 = 0x57
        // 例如 level=50 (0x32) -> 0x58 - 0x32 = 0x26
        // 例如 level=100 (0x64) -> 0x58 - 0x64 = -12 (0xF4)
        byte checksum = (byte) (0x58 - level);

        // 亮度指令: A5 00 01 03 [Level] FF FF [Checksum]
        byte[] command = new byte[] {
                (byte) 0xA5, (byte) 0x00, (byte) 0x01, (byte) 0x03,
                (byte) level, (byte) 0xFF, (byte) 0xFF, checksum
        };

        writeCommand(command);
    }

    /**
     * 统一发送方法
     */
    private void writeCommand(byte[] command) {
        try {
            if (serialPort != null) {
                serialPort.write(command, 100); // timeout 100ms
            }
        } catch (IOException e) {
            Log.e(TAG, "发送指令失败", e);
            // 发送失败通常意味着连接断开或不稳定
            if (callback != null) callback.onError("发送失败");
        }
    }

    /**
     * 断开连接
     */
    public void disconnect() {
        isConnected = false;
        if (usbIoManager != null) {
            usbIoManager.stop();
            usbIoManager = null;
        }
        if (serialPort != null) {
            try {
                serialPort.close();
            } catch (IOException ignored) {}
            serialPort = null;
        }
    }

    /**
     * 辅助工具:字节转Hex字符串
     */
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) sb.append(String.format("%02X ", b));
        return sb.toString();
    }
}

2.3 布局文件activity_main.xml代码实现

布局文件res/layout/activity_main.xml完整代码实现如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="20dp">

<!--    <TextView-->
<!--        android:id="@+id/sample_text"-->
<!--        android:layout_width="wrap_content"-->
<!--        android:layout_height="wrap_content"-->
<!--        android:text="Hello World!" />-->

    <Switch
        android:id="@+id/switch_light"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="灯光开关"
        android:layout_marginBottom="16dp" />

    <TextView
        android:id="@+id/tv_brightness_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="当前亮度: 0%"
        android:textSize="18sp" />

    <SeekBar
        android:id="@+id/seekbar_brightness"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:max="100"
        android:progress="0" />

</LinearLayout>

对应效果示意图如下:

2.4 调用辅助类和布局MainActivity.java代码实现

MainActivity.java完整代码如下:

java 复制代码
package com.example.lightv2;

import androidx.appcompat.app.AppCompatActivity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.CompoundButton;
import android.widget.SeekBar;
import android.widget.Toast;

// 项目utils路径导入
import utils.UsbLightManager;
import com.example.lightv2.databinding.ActivityMainBinding;

// 驱动相关导入
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver;
import com.hoho.android.usbserial.driver.ProbeTable;
import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialProber;

import java.util.HashMap;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private UsbLightManager usbManager;

    private static final String ACTION_USB_PERMISSION = "com.example.lightv2.USB_PERMISSION";

    // 限流防止串口发送过快
    private long lastSendTime = 0;

    // 【关键修复】防止重复请求权限的标志位
    private boolean isPermissionPending = false;

    // 记录灯光是否开启的状态 (逻辑锁)
    private boolean isLightOn = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // 1. 初始化 Manager
        initUsbManager();

        // 2. 初始化控件逻辑
        setupSwitch();
        setupSeekBar();

        // 初始状态:未连接时禁用开关
        binding.switchLight.setEnabled(false);
    }

    private void initUsbManager() {
        usbManager = new UsbLightManager(this, new UsbLightManager.LightCallback() {
            @Override
            public void onDataReceived(String message) {
                Log.d("GD32", "RX: " + message);
            }

            @Override
            public void onError(String message) {
                runOnUiThread(() -> {
                    Toast.makeText(MainActivity.this, "错误: " + message, Toast.LENGTH_LONG).show();
                    binding.tvBrightnessLabel.setText("状态: 出错 - " + message);

                    // 出错时重置UI状态
                    resetUIState();
                });
            }

            @Override
            public void onConnected() {
                runOnUiThread(() -> {
                    Toast.makeText(MainActivity.this, "✅ 连接成功!", Toast.LENGTH_SHORT).show();
                    binding.tvBrightnessLabel.setText("状态: 已连接 (等待开启)");

                    // 连接成功,允许操作开关
                    binding.switchLight.setEnabled(true);
                    // 默认状态为关
                    binding.switchLight.setChecked(false);
                });
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 注册广播
        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        registerReceiver(usbReceiver, filter);

        // 【关键修复】如果正在请求权限,或者已经连接了,就不要再反复执行诊断流程了
        if (!isPermissionPending && !usbManager.isConnected()) {
            diagnoseAndConnect();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(usbReceiver);

        // 页面不可见时断开连接,并重置UI
        usbManager.disconnect();
        resetUIState();
    }

    private void resetUIState() {
        isLightOn = false;
        isPermissionPending = false;
        binding.switchLight.setChecked(false);
        binding.switchLight.setEnabled(false);
        binding.seekbarBrightness.setProgress(0);
    }

    /**
     * 【诊断与连接核心】
     */
    private void diagnoseAndConnect() {
        UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);

        // 步骤 1: 检查物理设备
        HashMap<String, UsbDevice> rawDeviceList = manager.getDeviceList();
        if (rawDeviceList.isEmpty()) {
            binding.tvBrightnessLabel.setText("状态: 未检测到USB设备");
            return;
        }

        // 步骤 2: 查找驱动
        ProbeTable customTable = new ProbeTable();
        // 你的 GD32 ID
        customTable.addProduct(0x28e9, 0x018a, CdcAcmSerialDriver.class);
        UsbSerialProber prober = new UsbSerialProber(customTable);
        List<UsbSerialDriver> drivers = prober.findAllDrivers(manager);

        // 兜底默认驱动
        if (drivers.isEmpty()) {
            drivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
        }

        if (drivers.isEmpty()) {
            binding.tvBrightnessLabel.setText("状态: 驱动不匹配");
            return;
        }

        UsbSerialDriver driver = drivers.get(0);
        UsbDevice device = driver.getDevice();

        // 步骤 3: 检查权限
        if (manager.hasPermission(device)) {
            Log.d("USB_DIAG", "已有权限,直接连接");
            connectDevice(device);
        } else {
            // 【关键修复】只有在没有挂起的请求时才请求权限
            if (!isPermissionPending) {
                Log.d("USB_DIAG", "请求USB权限...");
                isPermissionPending = true; // 标记正在请求

                int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0;
                PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), flags);
                manager.requestPermission(device, permissionIntent);
            }
        }
    }

    private void connectDevice(UsbDevice device) {
        UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
        UsbDeviceConnection connection = manager.openDevice(device);
        usbManager.connect(device, connection, manager);
    }

    private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_USB_PERMISSION.equals(action)) {
                synchronized (this) {
                    // 【关键修复】收到结果,无论成功失败,都重置标志位
                    isPermissionPending = false;

                    UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        if (device != null) {
                            connectDevice(device);
                        }
                    } else {
                        Toast.makeText(MainActivity.this, "❌ 权限被拒绝", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        }
    };

    /**
     * 设置开关逻辑
     */
    private void setupSwitch() {
        binding.switchLight.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                // 如果是系统自动重置状态(如断开连接时),不发送指令
                if (!usbManager.isConnected() && buttonView.isEnabled()) {
                    return;
                }

                if (!usbManager.isConnected()) {
                    // 防止未连接时用户强行点击(虽然已禁用,加一层保险)
                    buttonView.setChecked(false);
                    return;
                }

                usbManager.setPower(isChecked);
                isLightOn = isChecked;

                if (isChecked) {
                    binding.tvBrightnessLabel.setText("状态: 灯光开启 (50%)");
                    // 协议规定:开灯默认 50% 亮度,同步 UI
                    binding.seekbarBrightness.setProgress(50);
                } else {
                    binding.tvBrightnessLabel.setText("状态: 灯光已关闭");
                    // 关灯可以归零进度条,显得更直观
                    binding.seekbarBrightness.setProgress(0);
                }
            }
        });
    }

    /**
     * 设置进度条逻辑
     */
    private void setupSeekBar() {
        binding.seekbarBrightness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (fromUser) {
                    // 【逻辑限制】如果灯没开,禁止调节
                    if (!isLightOn) {
                        return;
                    }

                    binding.tvBrightnessLabel.setText("当前亮度: " + progress + "%");

                    // 简单的限流,防止滑动太快串口阻塞
                    long currentTime = System.currentTimeMillis();
                    if (currentTime - lastSendTime > 50) {
                        usbManager.setBrightness(progress);
                        lastSendTime = currentTime;
                    }
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // 开始触摸时检查状态
                if (!isLightOn) {
                    Toast.makeText(MainActivity.this, "请先打开灯光开关!", Toast.LENGTH_SHORT).show();
                }
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // 如果没开灯,松手时把进度条弹回 0
                if (!isLightOn) {
                    seekBar.setProgress(0);
                    return;
                }
                // 确保最后一次手指离开时的值发送出去
                usbManager.setBrightness(seekBar.getProgress());
            }
        });
    }
}

3、最终实现的灯光控制效果

由于csdn上传图片限制大小为5MB,因此这里画质较糊,但可以看出主要功能已经完成。

下面附几张高清图,1️⃣ 打开光源:

2️⃣ 关闭光源:

3️⃣ 亮度97%:

写在最后

  • 由于笔者🖊️精力有限且本文更多的目的是通过📒博客记录学习过程并分享更多知识,因此文中部分描述不太具体,如有不太理解💫的地方可在评论区👀留言。非特殊赶deadline⏰或假期⛱️期间,笔者会经常上线回复💬。如有不便之处,请海涵~
  • 另外,创造不易,转载请注明出处💗💗💗~
相关推荐
用户693717500138442 分钟前
21.Kotlin 接口:接口 (Interface):抽象方法、属性与默认实现
android·后端·kotlin
溪饱鱼42 分钟前
主动与被动AI交互范式
前端·后端·aigc
写代码的皮筏艇44 分钟前
Sequelize 详细指南
前端·后端
敲代码的嘎仔1 小时前
LeetCode面试HOT100——160. 相交链表
java·学习·算法·leetcode·链表·面试·职场和发展
用户294655509191 小时前
游戏开发中的向量魔法
后端
敲代码的嘎仔1 小时前
LeetCode面试HOT100—— 206. 反转链表
java·数据结构·学习·算法·leetcode·链表·面试
雨中飘荡的记忆1 小时前
设计模式之适配器模式详解
java·设计模式·适配器模式
兔子零10241 小时前
nginx 配置长跑(上):从一份 server 到看懂整套路由规则
后端·nginx
客梦1 小时前
数据结构-图结构
java·数据结构·笔记