在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⏰或假期⛱️期间,笔者会经常上线回复💬。如有不便之处,请海涵~
  • 另外,创造不易,转载请注明出处💗💗💗~
相关推荐
崎岖Qiu几秒前
【设计模式笔记19】:建造者模式
java·笔记·设计模式·建造者模式
西城微科方案开发2 分钟前
体重电子秤MCU芯片方案
单片机·嵌入式硬件
⑩-3 小时前
SpringCloud-Sleuth链路追踪实战
后端·spring·spring cloud
SUPER52663 小时前
本地开发环境_spring-ai项目启动异常
java·人工智能·spring
冷崖3 小时前
原子锁操作
c++·后端
moxiaoran57533 小时前
Spring AOP开发的使用场景
java·后端·spring
一线大码7 小时前
Gradle 基础篇之基础知识的介绍和使用
后端·gradle
Java猿_7 小时前
Spring Boot 集成 Sa-Token 实现登录认证与 RBAC 权限控制(实战)
android·spring boot·后端
小王师傅668 小时前
【轻松入门SpringBoot】actuator健康检查(上)
java·spring boot·后端
醒过来摸鱼8 小时前
Java classloader
java·开发语言·python