对象目录与 Provider
头文件:<dlt698/service/object.hpp>,命名空间 dlt698::service。
值类型
using ObjectValue = std::variant<std::uint8_t, model::Data>; ///< DAR 或精确类型 Data
struct ActionValue {
std::uint8_t dar = 0;
std::optional<model::Data> data;
};
ObjectValue 是 variant:拿到 uint8_t 就是 DAR,拿到 model::Data 才是值。判断方式:
if (auto* dar = std::get_if<std::uint8_t>(&value))
handle_error(unsigned(*dar));
else
handle_value(std::get<model::Data>(value));
Schema
struct AttributeSchema {
std::uint8_t number = 2; ///< 1 至 31
model::DataType type = model::DataType::null;
bool readable = true;
bool writable = false; ///< 写入须显式授权
bool record = false; ///< 记录型属性须显式声明
};
struct MethodSchema {
std::uint8_t number = 1; ///< 1 至 255
std::optional<model::DataType> parameter_type;
std::optional<model::DataType> return_type;
bool executable = true;
};
struct ObjectSchema {
std::uint16_t oi = 0;
std::string name;
std::vector<AttributeSchema> attributes;
std::vector<MethodSchema> methods;
};
writable 默认关闭
AttributeSchema::writable 默认是 false。已经注册为只读的 schema 不会因为需要写入而自动获得权限,必须显式打开。
属性编号在 1 至 31 之间互不重复,方法编号在 1 至 255 之间互不重复。属性零的整个对象读取未实现。
ObjectRegistry
class ObjectRegistry {
public:
Result<void> register_object(ObjectSchema schema, std::shared_ptr<IObjectProvider> provider);
ObjectValue read(const model::Oad& attribute) const;
std::uint8_t write(const model::Oad& attribute, const model::Data& value) const;
ActionValue invoke(const model::Omd& method, const model::Data& parameter) const;
protocol::apdu::RecordResult read_record(const protocol::apdu::GetRecord& query) const;
};
register_object 要求至少定义一个属性或方法,OI 不得重复。成功返回 Result<void>,schema 非法或 OI 重复返回错误。
目录做什么、不做什么
目录负责校验,provider 负责取值:
锁约定
目录先检查权限,再释放目录锁后调用 provider,最后校验完整属性的 Data 类型。这样 provider 可以重入本对象,不会死锁。
provider 抛出的异常会被目录转成 DAR=255。
DAR 取值
IObjectProvider
class IObjectProvider {
public:
virtual ~IObjectProvider() = default;
virtual ObjectValue read(const model::Oad& attribute) = 0;
virtual std::uint8_t write(const model::Oad& attribute, const model::Data& value);
virtual ActionValue invoke(const model::Omd& method, const model::Data& parameter);
virtual protocol::apdu::RecordResult read_record(const protocol::apdu::GetRecord& query);
};
只有 read 是纯虚函数。write、invoke、read_record 都有默认实现并返回拒绝:
write 默认拒绝,保持既有只读 provider 的合约不变。
invoke 默认拒绝。
read_record 默认返回 DAR=3。
实现约定:
attribute 是完整 OAD,目录不会隐式丢弃属性特征位。
value/parameter 在调用期间有效,不得保留借用引用。
- 不得阻塞等待调用它的会话执行器。
- 调用期间不得保留
ByteView 之类的借用引用;需要长期持有就复制数据。
- 并发读取的线程安全由你自己保证。
- 返回值必须拥有全部内存——不能返回指向内部临时缓冲区的视图。
MemoryObject
MemoryObject 是线程安全的本地模拟 provider,用于测试和联调。
class MemoryObject final : public IObjectProvider {
public:
void set(std::uint8_t attribute, model::Data value); ///< 本地配置,不是协议 SET
ObjectValue read(const model::Oad& attribute) override;
std::uint8_t write(const model::Oad& attribute, const model::Data& value) override;
void bind_method(std::uint8_t number,
std::function<ActionValue(const model::Omd&, const model::Data&)> handler);
void bind_record(std::uint8_t attribute,
std::function<protocol::apdu::RecordResult(const protocol::apdu::GetRecord&)> handler);
ActionValue invoke(const model::Omd& method, const model::Data& parameter) override;
protocol::apdu::RecordResult read_record(const protocol::apdu::GetRecord& query) override;
};
set 不是 SET
Warning
set 是本地配置方法
MemoryObject::set(attribute, Data) 设置属性的完整值,它是初始化模拟数据用的本地方法,与协议 SET 服务无关。协议 SET 走的是 write。
set 只支持属性特征零,属性编号非法抛 std::invalid_argument。
索引规则
- 索引零读取完整值。
- 非零索引从 1 开始,读取 array/structure 的一级元素。
- 非法索引返回
DAR=8。
write 只替换已有的完整值或一级元素,不会创建未知属性,并且保持原类型:类型不匹配返回 DAR=7,非法索引返回 DAR=8,属性不存在返回 DAR=4。
方法与重入
bind_method 注册的模式只能是模式零,方法编号不能为零或回调为空(抛 std::invalid_argument)。回调复制一份后释放对象锁,因此允许在回调内重入本对象的读写。
未定义的方法返回 DAR=4,非零模式返回 DAR=3。
bind_record 的处理器在对象锁外调用,必须返回拥有内存且列宽一致的快照,并解释全部所需选择器。属性编号非法或回调为空抛 std::invalid_argument。
完整示例
auto objects = std::make_shared<dlt698::service::ObjectRegistry>();
auto object = std::make_shared<dlt698::service::MemoryObject>();
object->set(2, dlt698::model::UInt16{10});
// 用 weak_ptr 打破引用环:目录持有 provider,provider 的回调不能再捕获 shared_ptr。
object->bind_method(1,
[weak = std::weak_ptr<dlt698::service::MemoryObject>(object)](
const dlt698::model::Omd&, const dlt698::model::Data& parameter)
-> dlt698::service::ActionValue {
weak.lock()->set(2, parameter);
return {0, parameter};
});
if (!objects->register_object(
{0x2000,
"可写值及模拟方法",
{{2, dlt698::model::DataType::uint16, true, true}},
{{1, dlt698::model::DataType::uint16, dlt698::model::DataType::uint16, true}}},
object))
return 1;
注意引用环
register_object 的目录持有 provider 的 shared_ptr。如果 provider 的方法回调又捕获这个 shared_ptr,就形成引用环,对象永远不会被释放。用 std::weak_ptr 并在回调内 lock()。
局限
- 不隐含单位、倍率或真实计量能力——
ScalerUnit 只保存倍率和单位,换算由调用方做。
- 已提供 118 个常用标准 OI 的只读绑定辅助,涵盖电能、最大需量、状态字、谐波与参数;应用按设备点表选择属性并提供实际数据,完整目录继续扩充。
- 记录查询的选择器业务语义由 provider 解释,库只校验声明和权限。
第三阶段提供日/月冻结和常用事件后端,标准适配器会转发 read_record 并校验表头、列宽和单元类型;通用 provider 仍可实现其他选择器业务。
注册就是声明测点
register_object 不只是"挂一个回调"。在 698 里,点表是应用侧注册出来的,不是从协议读出来的——注册 (oi, attribute) 的 schema 和 provider,等于宣告"我提供这些测点";GET 时目录按完整 OAD 查 schema,查不到返回 DAR=4。
这套结构为什么要这样设计、点表里的 OI 从哪里查、记录型对象为什么只能二维定位,见对象模型与测点寻址。