目录
今天核心任务是梳理清楚
Crazyradio
的构造过程和发送
packet
的核心机理。
class Crazyradio: public ITransport, public USBDevice
{
public:
enum Datarate
{
Datarate_250KPS = 0,
Datarate_1MPS = 1,
Datarate_2MPS = 2,
};
enum Power
{
Power_M18DBM = 0,
Power_M12DBM = 1,
Power_M6DBM = 2,
Power_0DBM = 3,
};
}
从上述Crazyradio
定义中可以看出,该类有两个基类ITransport
和USBDevice
,两个枚举变量Datarate
和Power
ITransport
:- 一个抽象基类,定义了数据传输的接口,
Crazyradio
类需要实现这些接口来完成数据的收发操作。
- 一个抽象基类,定义了数据传输的接口,
USBDevice
:- 一个表示 USB 设备的基类,
Crazyradio
作为一个 USB 设备,继承该类可以复用一些 USB 设备的通用功能,如 USB 连接、数据读写等。
- 一个表示 USB 设备的基类,
接下来,对上述两个类进行深扒:
USBDevice.h
#pragma once
#include <stdint.h> // 包含了uint8_t、uint16_t、uint32_t等
struct libusb_context; // USB设备的上下文
struct libusb_device_handle; // USB设备句柄
class USBDevice
{
protected:
libusb_context* m_ctx; // USB设备的上下文
libusb_device_handle* m_hanlde; // USB设备句柄
.
float m_version; // USB设备的版本号
private:
uint16_t m_idVendor; // 存储USB设备的厂商ID
uint16_t m_idProduct; // 存储USB设备的产品ID
}
从上可知,USBDevice
的核心变量包括:
- USB设备的上下文
- USB设备的句柄
- USB设备的版本号
- USB设备的厂商ID
- USB设备的产品ID
class USBDevice
{
public:
USBDevice(uint16_t idVendor, uint16_t idProduct);
virtual ~USBDevice();
protected:
void open(uint32_t devid); // 打开指定devid的USB设备
void sendVendorSetup(
uint8_t request,
uint16_t value,
uint16_t index,
const unsigned char* data,
uint16_t length); // 向 USB 设备发送控制请求
static uint32_t numDevices(uint16_t idVendor,uint16_t idProduct);
}
[!NOTE]
- 静态特性:由于该函数是静态的,这意味着你不需要创建
USBDevice
类的对象就可以直接调用它。静态成员函数属于类本身,而不是类的某个具体对象。可以通过类名直接调用,调用方式为USBDevice::numDevices(idVendor, idProduct)
。- 优势体现:在程序的初始化阶段或者一些全局的操作中,可能还没有创建
USBDevice
对象,但又需要知道特定 USB 设备的数量,这时静态成员函数就非常有用。它提供了一种方便的方式来获取设备信息,而不需要额外创建对象,节省了资源和代码复杂度。
上述USBDevice
类设计的细节来看的话,该类的作用是在系统的libusb
基库的基础上向上封装了一些接口类。
设备的初始化、打开、配置等基础操作
USBDevice.cpp
#include "USBDevice.h"
#include <sstream>
#include <stdexcept>
#include <libusb-1.0/libusb.h>
<sstream>
库的简介:<sstream>
头文件提供了用于字符串流操作的类和函数。- 字符串流允许你像操作输入输出流(如
std::cin
和std::cout
)一样操作字符串 - 它将字符串作为流进行处理,方便进行数据的格式化输入输出
- 常用于将不同类型的数据转换为字符串,或者从字符串中提取不同类型的数据。
<sstream>
库的接口:std::istringstream
:- 用于从字符串中读取数据,类似于
std::cin
从标准输入读取数据。 - 它可以将字符串解析为不同类型的数据,例如将字符串中的数字提取出来。
- 用于从字符串中读取数据,类似于
std::ostringstream
:- 用于向字符串中写入数据,类似于
std::cout
向标准输出写入数据。 - 它可以将不同类型的数据格式化为字符串。
- 用于向字符串中写入数据,类似于
std::stringstream
:- 既可以从字符串中读取数据,也可以向字符串中写入数据,
- 结合了
std::istringstream
和std::ostringstream
的功能。
<stdexcept>
库的简介:- 定义了一系列用于异常处理的标准异常类。
<stdexcept>
库的接口:std::runtime_error
:- 表示运行时错误,通常用于表示在程序运行过程中发生的错误,例如文件打开失败、网络连接中断等。
std::logic_error
:- 表示逻辑错误,通常是由于程序逻辑上的错误导致的,例如传递给函数的参数不符合要求。
std::out_of_range
:- 表示越界错误,常用于表示访问数组、容器等时索引超出了有效范围。
std::invalid_argument
:- 表示无效参数错误,当传递给函数的参数无效时抛出该异常。
从头文件可知:
class USBDevice
{
public:
USBDevice(uint16_t idVendor, uint16_t idProduct)
{
libusb_init(&m_ctx); // 初始化usb的内容?
}
virtual ~USBDevice();
protected:
// 打开指定devid的USB设备
void open(uint32_t devid)
{
libusb_get_device_list(NULL, &list);
libusb_free_device_list(list, 1);
libusb_open(found, &m_handle); // 打开指定devid的USB设备并返回到句柄m_handle
libusb_set_configuration(m_handle, 1);
}
// 向USB设备发送厂商自定义的设置请求
void sendVendorSetup(
uint8_t request, // 控制请求的类型,不同请求有不同作用
uint16_t value, // 传递与请求相关的特定值
uint16_t index, // 指定请求的索引,比如接口号、端点号等
const unsigned char* data, // 指向无符号字符数组的常量指针
uint16_t length); // 要发送的数据的长度
{
int status = libusb_control_transfer(
m_handle, // usb设备句柄
LIBUSB_REQUEST_TYPE_VENDOR,
request,
value,
index,
(unsigned char*)data,
length,
/*timeout*/ 1000); // 请求的超时时间
if (status != LIBUSB_SUCCESS)
throw std::runtime_error(libusb_error_name(status));
}
static uint32_t numDevices(uint16_t idVendor,uint16_t idProduct);
protected:
libusb_context* m_ctx; //usb设备的上下文信息
libusb_device_handle *m_handle; // usb设备的句柄
}
从上述具体函数实现可知,USBDevice
类主要是封装了打开USB
设备的open
,核心是libusb_open
,另一个就是通过USB
设备发送数据的sendVendorSetup
,核心接口是libusb_control_transfer
。
ITransport
#include <stdint.h>
#include <fstream>
其头文件的头文件很简单,从后面可知,其似乎封装上了std::ofstream
的m_file
.
class ITransport
{
public:
struct ACK{}__attribute__((packed));
protected:
bool m_enableLogging;
std::ofstream m_file;
}
这里有个出现频率很高的私有变量m_enableLogging
,以及之前找很久的log文件:std::ofstream
类型的m_file
除此之外,还有比较感兴趣的ACK
:
class ITransport
{
public:
struct Ack
{
Ack(): ack(0), size(0){} //默认构造函数
uint8_t ack:1; // 位域,使用1bit来存储ACK的数值,0:未确认,1:已确认
uint8_t powerDet:1;
uint8_t retry:4; // 位域,使用4bit来存储retry的数值,传输失败后重传的次数
uint8_t data[32]; // 存储与确认信息相关的数据
uint8_t size; // 记录 data 数组中实际有效的数据长度
}__attribute__((packed)); // GCC 编译器的一个扩展属性,不对结构体进行字节对齐优化。
此处与上面看到的,Ack.ack
似乎有一些关系,其实从ack.retry
中可以直接读出来ACK
重传的次数。
class ITransport
{
public:
ITransport():m_enableLogging(false){}
virtual ~ITransport() {}
// 纯虚函数
virtual void sendPacket(const uint8_t* data, uint32_t length, Ack& result) = 0;
virtual void sendPacketNoAck(const uint8_t* data, uint32_t length) = 0;
void enableLogging(bool enable);
protected:
void logPacket(const uint8_t* data, uint32_t length);
void logAck(const Ack& ack);
}
从上可以看出,该ITransport
的核心作用是一个定义传输层
的抽象基类
,通常用于为派生类提供统一的接口规范
ITransport::sendPacket
和ITransport::sendPacketNoAck
,这两个虚函数都是留给顶层派生类进行实现的,此处是CPP
的一个知识点,包含纯虚函数的类为抽象基类,不能直接实例化,其派生类必须实现纯虚函数才能被实例化。
由于纯虚函数没有实现,接下来只需要关心其中三个有函数实现的内容就可以了。
void ITransport::enableLogging(bool enable)
{
m_enableLogging = enable;
if (m_enableLogging) {
m_file.open("transport.log");
} else {
m_file.close();
}
}
从上可知,如果使能了m_enableLogging
,会在本文件所在目录内打开日志文件transport.log
?但是我在项目中并没有见过该文件,此ITransport
类似乎跟我之前写Log
时遇到的文件不好处理的问题有些关系,后续可回头再看。
[!NOTE]
牢记,日志文件只有在
m_enableLogging
时才会创建。
// 用于记录发送的数据包信息到日志文件中
void ITransport::logPacket(
const uint8_t* data, // 要发送的数据包数据的指针
uint32_t length) // 数据包的长度
{
m_file << "sendPacket: ";
for (size_t i = 0; i < length; ++i)
m_file << std::hex << (int)data[i] << " ";
if (length > 0) {
uint8_t byte = data[0]; // 取出包头
int port = ((byte >> 4) & 0xF); // 提取高4位作为端口号。
int channel = ((byte >> 0) & 0x3); // 提取低2位作为通道号
m_file << " (port: " << port << " channel: " << channel << ")";
}
m_file << std::endl;
}
// 日志信息示例
sendPacket: 12 34 56 78 (port: 1 channel: 2)
void ITransport::logAck(const Ack& ack) // 将ack信息写到日志文件中
{
m_file << "received: ";
for (size_t i = 0; i < ack.size; ++i) {
m_file << std::hex << (int)ack.data[i] << " ";
}
if (ack.size > 0) {
uint8_t byte = ack.data[0];
int port = ((byte >> 4) & 0xF);
int channel = ((byte >> 0) & 0x3);
m_file << " (port: " << port << " channel: " << channel << ")";
}
m_file << std::endl;
}