2. 适配层抽象接口设计:枚举相机操作、通用参数结构、工厂模式与插件化架构
适配层的核心价值在于为上层业务屏蔽不同厂商相机 SDK 的差异,通过统一的抽象接口管理所有相机操作。本章将详细讲解如何设计一套可扩展的抽象接口,包括操作枚举、通用参数结构,以及基于工厂模式与插件化架构的实现方案。
2.1 设计原则与目标
适配层抽象接口的设计需遵循以下原则:
- 最小化约定:接口只暴露必要的相机操作,避免过度抽象导致灵活性降低。
- 单向依赖:上层业务依赖抽象层,抽象层不依赖具体厂商 SDK 实现。
- 可演进:新增相机厂商时,无需修改已有接口定义,仅需追加新插件。
- 参数标准化:将各厂商差异化的参数(如曝光时间、白平衡模式)映射为统一的枚举或结构体。
2.2 枚举相机操作(Camera Operation Enum)
将所有相机操作抽象为枚举类型,便于统一调度。操作涵盖相机的生命周期管理、参数设置、图像采集与控制指令。
典型操作枚举定义(C++ 示例):
enum class CameraOperation {
// 生命周期
OPEN = 0,
CLOSE,
START_PREVIEW,
STOP_PREVIEW,
// 参数设置
SET_EXPOSURE_TIME,
SET_GAIN,
SET_WHITE_BALANCE,
SET_FRAME_RATE,
SET_RESOLUTION,
SET_TRIGGER_MODE,
// 数据获取
CAPTURE_IMAGE,
START_RECORDING,
STOP_RECORDING,
GET_FRAME,
// 控制命令
SOFTWARE_TRIGGER,
HARDWARE_TRIGGER_ENABLE,
HARDWARE_TRIGGER_DISABLE,
RESET_DEVICE,
GET_DEVICE_INFO
};
2.3 通用参数结构(Generic Parameter Structure)
不同厂商的参数类型和取值范围各异,必须设计一种可自描述的参数结构。通过参数 ID(对应枚举)、数据类型标记、值联合体或变体来实现。
| 字段名称 | 类型 | 说明 |
|---|---|---|
| paramId | CameraOperation | 对应操作枚举 |
| dataType | ParamDataType | INT32、FLOAT64、STRING、BOOLEAN 等 |
| value | ParamValue(union/variant) | 实际参数值 |
| min / max | double / int | 取值范围(可选) |
| description | std::string | 参数描述,用于日志与调试 |
// 参数数据类型枚举
enum class ParamDataType {
INT32,
FLOAT64,
BOOLEAN,
STRING,
INT32_RANGE,
FLOAT64_RANGE
};
// 参数值变体示例(C++17 std::variant)
using ParamValue = std::variant<int32_t, double, bool, std::string,
std::pair<int32_t, int32_t>,
std::pair<double, double>>;
// 通用参数结构
struct CameraParam {
CameraOperation paramId;
ParamDataType dataType;
ParamValue value;
std::string description;
};
设计考量:使用
std::variant 或 boost::variant 比裸 union 更安全,能自动处理类型匹配与异常。在 C 语言环境中可使用带类型标记的 union,并配以严格的校验函数。2.4 工厂模式:创建具体适配器实例
工厂模式用于根据厂商标识动态创建对应的适配器对象,上层只依赖抽象基类 ICameraAdapter。
- 抽象基类:
ICameraAdapter声明open()、close()、setParameter()、capture()等纯虚函数。 - 具体适配器:
HikCameraAdapter、BaslerCameraAdapter、DahengCameraAdapter等继承并实现基类接口。 - 工厂函数:根据 vendor 字符串(如
"HikVision")或相机设备信息返回适配器指针。
class ICameraAdapter {
public:
virtual ~ICameraAdapter() = default;
virtual bool open(const std::string& deviceId) = 0;
virtual bool close() = 0;
virtual bool setParameter(const CameraParam& param) = 0;
virtual bool getParameter(CameraParam& param) = 0;
virtual std::shared_ptr<Frame> capture() = 0;
// ...
};
// 工厂类
class CameraAdapterFactory {
public:
static std::unique_ptr<ICameraAdapter> create(const std::string& vendor) {
if (vendor == "HikVision") {
return std::make_unique<HikCameraAdapter>();
} else if (vendor == "Basler") {
return std::make_unique<BaslerCameraAdapter>();
} else if (vendor == "Daheng") {
return std::make_unique<DahengCameraAdapter>();
}
return nullptr;
}
};
2.5 插件化架构:动态加载适配器
在大型项目中,各厂商适配器应以独立动态库(.so / .dll)形式存在,通过插件接口注册到统一的管理器中。这样新增厂商时无需重新编译主程序。
- 插件接口:定义一个
ICameraPlugin抽象类,包含getVendorName()和createAdapter()方法。 - 注册机制:动态库加载后调用
registerPlugin()将自身注册到全局插件列表。 - 加载策略:启动时遍历指定目录下的插件库,依次加载并注册。
// 插件接口定义
class ICameraPlugin {
public:
virtual ~ICameraPlugin() = default;
virtual std::string getVendorName() const = 0;
virtual std::unique_ptr<ICameraAdapter> createAdapter() = 0;
};
// 插件注册管理器
class PluginManager {
public:
static PluginManager& instance() {
static PluginManager mgr;
return mgr;
}
void registerPlugin(std::shared_ptr<ICameraPlugin> plugin) {
plugins_[plugin->getVendorName()] = plugin;
}
std::unique_ptr<ICameraAdapter> createAdapter(const std::string& vendor) {
auto it = plugins_.find(vendor);
if (it != plugins_.end()) {
return it->second->createAdapter();
}
return nullptr;
}
private:
std::unordered_map<std::string, std::shared_ptr<ICameraPlugin>> plugins_;
};
// 动态库导出宏示例(Windows)
#define EXPORT_PLUGIN extern "C" __declspec(dllexport)
EXPORT_PLUGIN void registerHikPlugin() {
auto plugin = std::make_shared<HikCameraPlugin>();
PluginManager::instance().registerPlugin(plugin);
}
2.6 适配层架构总览(内联 SVG 结构图)
下图展示了适配层的整体架构:上层业务通过统一的抽象接口与适配层交互,适配层内部由工厂模式和插件化机制管理多个厂商适配器,最终调用各厂商 SDK 完成具体操作。
2.7 实现流程与关键步骤
- 定义枚举与参数结构:在公共头文件中声明
CameraOperation和CameraParam,确保所有厂商适配器都引用同一份定义。 - 实现抽象基类:编写
ICameraAdapter,包含纯虚函数;将参数传递统一为CameraParam类型。 - 编写第一个适配器(如 HikCameraAdapter):继承
ICameraAdapter,内部调用 Hik SDK 函数;注意将 SDK 特有的错误码映射为统一的错误枚举。 - 集成工厂模式:在
CameraAdapterFactory::create()中根据厂商字符串返回对应的适配器实例。 - 实现插件化加载:定义
ICameraPlugin接口,编写动态库装载函数,通过PluginManager管理所有已注册插件。 - 端到端验证:编写测试程序,通过
createAdapter("Basler")获取适配器,调用统一接口完成打开、设置参数、采集一帧并关闭操作。
2.8 调试与常见错误
- 参数转换错误:不同厂商的曝光时间单位不同(微秒 vs 毫秒),务必在适配器内部进行单位转换,并用日志记录原始值与转换后的值。
- 枚举值溢出:新增操作枚举时,若在已有枚举后追加,需检查所有适配器中已实现的
switch/case是否遗漏处理新枚举。 - 插件加载失败:动态库依赖缺失(如缺少厂商 SDK 运行时 Runtime)将导致
dlopen/LoadLibrary返回空;应打印详细错误信息并增加预检步骤。 - 工厂返回空指针:传入未注册的厂商名时工厂应返回 nullptr,上层调用方需做空指针检查,不可直接使用。
// 调试阶段建议在工厂函数中添加厂商列表获取方法:
std::vector<std::string> CameraAdapterFactory::listAvailableVendors() {
std::vector<std::string> vendors;
for (const auto& pair : plugins_) {
vendors.push_back(pair.first);
}
return vendors;
}
2.9 性能与稳定性优化
- 缓存工厂实例:每次调用
createAdapter()都创建新对象开销大;可考虑对象池或复用相同设备 ID 的适配器实例。 - 参数校验前置:在抽象层实现参数取值范围校验,避免无效值传递给底层 SDK 导致崩溃。
- 超时与重试机制:枚举操作中
START_PREVIEW、CAPTURE_IMAGE等可能阻塞,应在适配器内设置超时,超时后返回错误码并清理资源。 - 插件加载隔离:为每个插件库启用独立的错误处理域(如 C++ 的
try-catch跨 DLL 边界需注意异常传播问题),防止一个插件的崩溃影响整个进程。
稳定性警告:不同厂商 SDK 内部可能存在线程模型冲突(如主线程回调 vs 专用回调线程)。适配层必须明确约定回调线程归属,必要时在适配器内部进行线程切换,避免死锁或数据竞争。