W3C万维物联网解析:编程API篇

608 阅读16分钟

2019 年 10 月 21 日,作者在 “W3C 万维物联网标准简介” 一文中简单介绍了 W3C Web of Things(WoT)工作组制定的 WoT 标准以及它们的最新状态:

规范当前状态
WoT ArchitectureCR
WoT Thing DescriptionCR
WoT Scripting APIWD,Working Draft
WoT Binding TemplatesWorking Group Note
WoT Security and Privacy ConsiderationsWorking Group Note

本系列将从 WoT 标准本身出发,对目前已经进入 CR 阶段(W3C 标准的阶段参见下图)的 WoT Architecture(WoT 架构)、WoT Thing Description(WoT 物描述)以及处于 WD 阶段的 WoT Scripting API(WoT 编程 API)进行一次快速解析。

如下图所示,标准进入 CR 阶段意味着内容已经相对稳定,WD 阶段则意味着较大的不确定性,而 Working Group Note(工作组备忘)则变数很大。因此处于 CR 阶段的 “架构” 和“物描述”是值得花时间了解的(成为正式推荐标准 REC 的可能性很大),而处于 WD 阶段的编程 API 在 2019 年 10 月 28 日做了一次大的内容改版,几乎完全废弃了上一版的内容,只能说接近稳定状态,但编程 API 始终是开发者所喜闻乐见的,所以本系列也会介绍。

W3C Process Document,www.w3.org/2019/Proces…

  1. 编程 API 简介

WoT Scripting API 描述如何通过脚本暴露和消费物体,同时定义了通用的物发现 API。基于 WoT 架构定义的 “消费体”(consumed thing)和 “暴露体”(consumed thing),这个规范提供了不同层次的交互操作能力。

首先,客户端通过消费 TD(Thing Description)可以创建一个本地运行时资源模型,即消费体。消费体支持访问远程设备上的服务端物体暴露的属性、动作和事件。

其次,服务端负责暴露物体,为此需要:

  • 定义 TD
  • 初始化一个实现该 TD 所定义 WoT 接口的软件栈,以服务于对暴露属性、动作和事件的请求
  • 最终发布 TD(比如发布到一个物体目录,以便消费者发现)
  1. 支持的场景

编程 API 支持以下脚本使用场景。

2.1 消费物

  • 消费物体的 TD,如基于暴露 WoT 交互的 TD 创建一个编程对象:

    • 读取一或多个属性的值
    • 设置一或多个属性的值
    • 观察属性值的变化
    • 调用动作
    • 观察物体发出的事件
    • 推断(内省)TD,包括基于 TD 链接的资源

2.2 暴露物

  • 暴露物体包括生成协议绑定以便访问底层功能

  • 基于提供的字符串序列化格式的 TD 或既有物体对象创建本地要暴露的物体

  • 给物体添加属性定义

  • 从物体删除属性定义

  • 给物体添加动作定义

  • 从物体删除动作定义

  • 给物体添加事件定义

  • 从物体删除事件定义

  • 发送事件,如通知订阅了该事件的所有监听程序

  • 为外部请求注册处理程序

    • 取得属性的值
    • 更新属性的值
    • 执行动作:接收来自请求的参数,执行定义的动作,返回结果

2.3 发现物

  • 通过发送广播请求在 WoT 网络中发现所有物体
  • 发现在本地 WoT 运行时中运行的物体
  • 发现邻近的物体,如通过 NFC 或蓝牙连接的物体
  • 通过向一个注册表服务发送发现请求发现物体
  • 通过物体描述定义的过滤器发现物体
  • 通过语义查询发现物体
  • 停止或阻止进行中的发现过程
  • 可选地给发现过程指定超时时间,超时后停止 / 阻止继续发现
  1. 接口及类型定义

3.1 ThingDescription 类型

typedef object ThingDescription;

下面是通过 URL 获取一个 ThingDescription 类型实例的示例:

例 1:获取 TD

try {
  let res = await fetch('https://tds.mythings.biz/sensor11');
  // ... 可以对res.headers进行额外检查
  let td = await res.json();
  let thing = new ConsumedThing(td);
  console.log("Thing name: " + thing.getThingDescription().title);
} catch (err) {
  console.log("Fetching TD failed", err.message);
}

此外,规范也定义了如何扩展 TD 和验证 TD。

3.2 WOT 接口

[SecureContext, Exposed=(Window,Worker)]
interface WOT {
  // methods defined in UA conformance classes
  Promise<ConsumedThing> consume(ThingDescription td);
  Promise<ExposedThing> produce(ThingDescription td);
  ThingDiscovery discover(optional ThingFilter filter = null);
};

WOT 接口的实例将以某种名称暴露在windowworker中。由上面定义可知,WOT 接口包含 3 个方法:

  • consume():以td为参数,返回 Promise,解决为ConsumedThing对象,表示操作物体的客户端接口;
  • produce():以td为参数,返回 Promise,解决为ExposedThing对象,该对象扩展包含服务器端接口的ConsumedThing对象;
  • discover():启动发现流程,并提供匹配filter参数的ThingDescription对象。

这 3 个方法可分别用于在客户端、服务端创建消费体、产生暴露体和发现 TD。

3.3 ConsumedThing 接口

ConsumedThing接口的定义如下所示,ConsumedThing的实例即消费体,拥有一系列操作物体的客户端 API。

[SecureContext, Exposed=(Window,Worker)]
interface ConsumedThing {
  constructor(ThingDescription td);
  Promise<any> readProperty(
        DOMString propertyName,
        optional InteractionOptions options = null
        );
  Promise<PropertyMap> readAllProperties(
        optional InteractionOptions options = null
        );
  Promise<PropertyMap> readMultipleProperties(
        sequence<DOMString> propertyNames,
        optional InteractionOptions options = null
        );
  Promise<void> writeProperty(
        DOMString propertyName,
        any value,
        optional InteractionOptions options = null
        );
  Promise<void> writeMultipleProperties(
        PropertyMap valueMap,
        optional InteractionOptions options = null
        );
  Promise<any> invokeAction(
        DOMString actionName,
        optional any params = null,
        optional InteractionOptions options = null
        );
  Promise<void> observeProperty(
        DOMString name,
        WotListener listener,
        optional InteractionOptions options = null
        );
  Promise<void> unobserveProperty(DOMString name);
  Promise<void> subscribeEvent(
        DOMString name,
        WotListener listener,
        optional InteractionOptions options = null
        );
  Promise<void> unsubscribeEvent(DOMString name);
  ThingDescription getThingDescription();
};
dictionary InteractionOptions {
  object uriVariables;
};
typedef object PropertyMap;
callback WotListener = void(any data);

下面我们简单看一看ConsumedThing接口定义的成员。

  • constructor() 构造函数:在取得 JSON 对象表示的 TD 后,就可以以之为参数创建ConsumedThing对象。
  • getThingDescription()方法:返回表示ConsumedThing对象的 TD。在使用物体前,应用可以先检查 TD 中的元数据,以确定其能力。
  • InteractionOptions字典:保存根据 TD 而决定需要暴露给应用脚本的交互选项。在当前版本的规范中,只支持 URI 模板变量,表现为 WoT-TD 中定义的解析之后的 JSON 对象。
  • PropertyMap类型:代表字符串类型的属性名与该属性取值的映射。用于一次性操作多个属性。
  • readProperty()方法:读取一个属性的值。接收字符串参数propertyName和可选的InteractionOptions类型的options参数。返回any类型的属性值。
  • readMultipleProperties()方法:以一或多个请求读取多个属性的值。接收一个字符串序列参数propertyNames和一个可选的InteractionOptions类型的options参数。返回一个对象,键为propertyNames中的字符串,值为相应属性的值。
  • readAllProperties()方法:以一或多个请求读取物体全部属性的值。接收一个字符串序列参数propertyNames和一个可选的InteractionOptions类型的options参数。返回一个对象,键为propertyNames中的字符串,值为相应属性的值。
  • writeProperty()方法:写入一个属性。接收字符串参数propertyName、值参数value和可选的InteractionOptions类型的options参数。返回成功或失败。
  • writeMultipleProperties()方法:一个请求写入多个属性。接收对象类型的properties参数,键为属性名,值为属性值,和可选的InteractionOptions类型的options参数。返回成功或失败。
  • WotListener回调:用户提供接收any参数的回调,用于观察属性变化和处理事件通知。
  • observeProperty()方法:请求订阅某个属性值变化的通知。接收一个字符串参数propertyName、一个WotListener回调listener和一个可选的InteractionOptions类型的options参数。返回成功或失败。
  • unobserveProperty()方法:请求取消订阅某个属性值变化的通知。接收一个字符串参数propertyName,返回成功或失败。
  • invokeAction()方法:请求调用某个动作并返回结果。接收一个字符串参数actionName、一个可选的any类型的参数params和一个可选的InteractionOptions类型的options参数。返回动作的结果或错误。
  • subscribeEvent()方法:请求订阅事件通知。接收一个字符串参数eventName、、一个WotListener回调listener和一个可选的InteractionOptions类型的options参数。返回成功或失败。
  • unsubscribeEvent()方法:请求取消订阅事件通知。接收一个字符串参数eventName,返回成功或失败。

下面是一个客户端脚本的例子,展示了如何通过 URL 获取 TD、创建ConsumedThing、读取元数据(title)、读取属性值、订阅属性变化、订阅 WoT 事件以及取消订阅。

例 2:客户端 API 示例

try {
    let res = await fetch("https://tds.mythings.org/sensor11");
    let td = res.json();
    let thing = new ConsumedThing(td);
    console.log("Thing " + thing.getThingDescription().title + " consumed.");
} catch(e) {
    console.log("TD fetch error: " + e.message); },
};
try {
    // 订阅属性“temperature”变化的通知
    await thing.observeProperty("temperature", value => {
         console.log("Temperature changed to: " + value);
    });
    // 订阅TD中定义的“ready”事件
    await thing.subscribeEvent("ready", eventData => {
         console.log("Ready; index: " + eventData);
         // 返回TD中定义的“startMeasurement”动作
         await thing.invokeAction("startMeasurement", { units: "Celsius" });
         console.log("Measurement started.");
    });
} catch(e) {
    console.log("Error starting measurement.");
}
setTimeout( () => {
    console.log("Temperature: " + await thing.readProperty("temperature"));
    await thing.unsubscribe("ready");
    console.log("Unsubscribed from the 'ready' event.");
},10000);

3.4 ExposedThing 接口

ExposedThing接口是用于操作物体的服务器 API,支持定义请求处理程序、属性、动作和事件接口。

[SecureContext, Exposed=(Window,Worker)]
interface ExposedThing: ConsumedThing {
  ExposedThing setPropertyReadHandler(
        DOMString name,
        PropertyReadHandler readHandler
        );
  ExposedThing setPropertyWriteHandler(
        DOMString name,
        PropertyWriteHandler writeHandler
        );
  ExposedThing setActionHandler(
        DOMString name, ActionHandler action);
        void emitEvent(DOMString name, any data);
        Promise<void> expose();
        Promise<void> destroy();
        };
  callback PropertyReadHandler = Promise<any>(
        optional InteractionOptions options = null
            );
  callback PropertyWriteHandler = Promise<void>(
        any value,
            optional InteractionOptions options = null
            );
  callback ActionHandler = Promise<any>(
        any params,
            optional InteractionOptions options = null
            );

ExposedThing接口扩展ConsumedThing接口,可以基于一个完整的或不完整的ThingDescription对象构建实例。ExposedThingConsumedThing继承了以下方法:

  • readProperty()
  • readMultipleProperties()
  • readAllProperties()
  • writeProperty()
  • writeMultipleProperties()
  • writeAllProperties()

这些方法跟ConsumedThing中的方法拥有同样的算法,不同之处在于对底层平台发送请求可能会以本地方法或库实现,不一定需要调用网络操作。

ExposedThingConsumedThing接口的实现提供默认的方法与ExposedThing交互。

构建ExposedThing之后,应用脚本可以初始化它的属性并设置可选的读、写和动作请求处理程序(即由实现提供的默认值)。应用脚本提供的处理程序可以使用默认处理程序,从而扩展默认行为,但也可以绕过它们,重写默认行为。最后,应用脚本会在ExposedThing上调用expose(),以便开始服务外部请求。

以下是ExposedThing接口定义的成员。

  • PropertyReadHandler回调:在收到外部读取某个属性的请求时调用的函数,定义如何处理该请求。返回承诺 Promise 并在name参数与属性匹配时解决为属性的值,或者未找到属性或者无法获取属性的值时以一个错误拒绝。

  • setPropertyReadHandler()方法:接收字符串参数namePropertyReadHandler类型的参数readHandler。设置在读取匹配name的属性时执行的处理程序。出错则抛出。返回this对象以支持连缀调用。

    readHandler回调函数应该实现读取属性的逻辑,且应该在底层平台收到读取属性的请求时由实现调用。

    对任意属性,必须最多有一个处理程序,因此新加的处理程序必须替换之前的处理程序。如果给定属性不存在任何 初始的处理程序,实现应该基于 TD 实现一个默认属性读取处理程序。

  • PropertyWriteHandler回调:在收到外部写入某个属性的请求时调用的函数,定义如何处理该请求。参数中需要包含新的value并返回承诺 Promise,在匹配name的属性更新为value时解决,如果没找到属性或者值无法更新则拒绝。

  • setPropertyWriteHandler()方法:接收字符串参数namePropertyWriteHandler类型的参数writeHandler。设置在写入匹配name的属性时执行的处理程序。出错则抛出。返回this对象以支持连缀调用。

    对任意属性,必须最多有一个处理程序,因此新加的处理程序必须替换之前的处理程序。如果给定属性不存在任何 初始的处理程序,实现应该基于 TD 实现一个默认属性更新处理程序。

  • ActionHandler回调:在收到外部执行某个动作的请求时调用的函数,定义如何处理该请求。参数中需要包含params字典。返回承诺 Promise,出错时拒绝,成功时解决。

  • setActionHandler()方法:接收字符串参数nameActionHandler类型的参数action。设置在匹配name的动作执行的处理程序。出错则抛出。返回this对象以支持连缀调用。

    action回调函数应该实现动作的逻辑,且应该在底层平台收到执行动作的请求时由实现调用。

    对任意动作,必须最多有一个处理程序,因此新加的处理程序必须替换之前的处理程序。

  • emitEvent()方法:接收字符串参数name表示事件名称和any类型的参数data

  • expose()方法:开始服务外部对物体的请求,从而支持属性、动作和事件的 WoT 交互。

  • destroy()方法:停止服务外部对物体的请求并销毁对象。注意,最终的取消注册应该在调用这个方法之前完成。

以下示例展示了如何基于先构造的不完全的 TD 对象创建ExposedThing

例 3:创建包含简单属性的 ExposedThing

try {
let temperaturePropertyDefinition = {
 type: "number",
 minimum: -50,
 maximum: 10000
};
let tdFragment = {
 properties: {
   temperature: temperaturePropertyDefinition
 },
 actions: {
   reset: {
     description: "Reset the temperature sensor",
     input: {
       temperature: temperatureValueDefinition
     },
     output: null,
     forms: []
   },
 },
 events: {
   onchange: temperatureValueDefinition
 }
};
let thing1 = await WOT.produce(tdFragment);
// 初始化属性
await thing1.writeProperty("temperature", 0);
// 添加服务处理程序
thing1.setPropertyReadHandler("temperature", () => {
  return readLocalTemperatureSensor();  // Promise
});
// 启动服务
await thing1.expose();
} catch (err) {
console.log("Error creating ExposedThing: " + err);
}

下面的例子展示了如何在已有的ExposedThing上添加或修改属性定义:取得其td属性,增加或修改,然后再用它创建另一个ExposedThing

例 4:添加对象属性

try {
// 创建thing1 TD的深拷贝
let instance = JSON.parse(JSON.stringify(thing1.td));
const statusValueDefinition = {
 type: "object",
 properties: {
   brightness: {
     type: "number",
     minimum: 0.0,
     maximum: 100.0,
     required: true
   },
   rgb: {
     type: "array",
     "minItems": 3,
     "maxItems": 3,
     items : {
         "type" : "number",
         "minimum": 0,
         "maximum": 255
     }
   }
};
instance["name"] = "mySensor";
instance.properties["brightness"] = {
 type: "number",
 minimum: 0.0,
 maximum: 100.0,
 required: true,
};
instance.properties["status"] = statusValueDefinition;
instance.actions["getStatus"] = {
 description: "Get status object",
 input: null,
 output: {
   status : statusValueDefinition;
 },
 forms: [...]
};
instance.events["onstatuschange"] = statusValueDefinition;
instance.forms = [...];  // update
var thing2 = new ExposedThing(instance);
// TODO: add service handlers
await thing2.expose();
});
} catch (err) {
console.log("Error creating ExposedThing: " + err);
}

3.5 ThingDiscovery 接口

发现是分布式应用,需要网络节点(客户端、服务器、目录服务)的供应与支持。这个 API 对多种 IoT 部署场景支持的典型发现模式的客户端进行建模。

ThingDiscovery对象接收一个过滤器参数,并提供了控制发现过程的属性和方法。

[SecureContext, Exposed=(Window,Worker)]
interface ThingDiscovery {
  constructor(optional ThingFilter filter = null);
  readonly attribute ThingFilter? filter;
  readonly attribute boolean active;
  readonly attribute boolean done;
  readonly attribute Error? error;
  void start();
  Promise<ThingDescription> next();
  void stop();
};

发现结果对应的内部槽位是一个内部队列,用于临时存储发现的ThingDescription对象,直到应用通过next()方法消费掉。

  • filter属性表示针对此次发现指定的ThingFilter类型的发现过滤器。
  • action属性在发现流程使用协议查找的过程中(如正在接收新 TD)为true,否则为false
  • done属性在发现已经完成、没有更多结果且发现结果为空时为true
  • error属性表示发现过程中发生的最后一个错误。通常是导致发现停止的关键错误。

构建ThingDiscovery的过程如下。

  • start()方法将active属性设置为truestop()方法将active属性设置为false,但如果发现结果中的ThingDescription对象还没有被next()消费,done属性仍然可能是false
  • 在成功调用next()方法时,active属性可能是truefalse,但done属性只有在activefalse且发现结果为空时才会被设置为true

下面总结一下start()next()stop()方法。

  • start()方法:启动发现流程。
  • next()方法:提供下一个发现的ThingDescription对象。
  • stop()方法:停止或阻止发现过程。可能无法被所有发现方法或端点支持,不过任何后续发现的结果或错误都会被抛弃,而且发现对象也会被标记为不活跃。

3.5.1 DiscoveryMethod 枚举

typedef DOMString DiscoveryMethod;

表示要发现的类型:

  • any:不限制
  • local:只发现当前设备或通过有线 / 无线连接到当前设备的设备上定义的物体。
  • directory:通过物体目录提供的服务发现
  • multicast:使用设备所在网络支持的多播协议发现

3.5.2 ThingFilter 字典

包含发现物体限制类型名值对的对象。

dictionary ThingFilter {
  (DiscoveryMethod or DOMString) method = "any";
  USVString? url;
  USVString? query;
  object? fragment;
};

  • method属性表示发现过程应该使用的发现类型。可能的值由DiscoveryMethod枚举定义,可以由解决方案以字符串值扩展(但不保证互操作性)。
  • url属性表示当前发现方法的额外信息,如服务当前发现请求的目标实体的 URL,比如一个物体目录(method值为"directory"时)或物体(method为其他值时)的 URL。
  • query属性表示实现接收的查询字符串,比如 SPARQL 或 JSON 查询。
  • fragment属性表示用于逐个属性匹配发现物体的模板对象。

3.5.3 ThingDiscovery 示例

以下例子展示了发现本地硬件暴露的物体的ThingDescription的过程,不考虑本地硬件上运行着多少 WoT 运行时。注意,发现对象可能在内部发现结果队列变空之前告终(变不活跃),因此需要持续读取ThingDescription对象,直到done属性为true。这是典型的本地和目录类型的发现过程。

例 5:发现本地硬件上暴露的物体

let discovery = new ThingDiscovery({ method: "local" });
do {
  let td = await discovery.next();
  console.log("Found Thing Description for " + td.title);
  let thing = new ConsumedThing(td);
  console.log("Thing name: " + thing.getThingDescription().title);
} while (!discovery.done);

下面的例子展示如何发现一个物体目录服务上列出的物体的ThingDescription。为安全起见,设置了超时。

例 6:通过目录发现物体

let discoveryFilter = {
method: "directory",
url: "http://directory.wotservice.org"
};
let discovery = new ThingDiscovery(discoveryFilter);
setTimeout( () => {
 discovery.stop();
 console.log("Discovery stopped after timeout.");
},
3000);
do {
let td = await discovery.next();
console.log("Found Thing Description for " + td.title);
let thing = new ConsumedThing(td);
console.log("Thing name: " + thing.getThingDescription().title);
} while (!discovery.done);
if (discovery.error) {
console.log("Discovery stopped because of an error: " + error.message);
}

接下来的例子展示了一个开放式的多播发现过程,可能不会很快结束(取决于底层协议),因此最好通过超时来结束。这种情况也倾向于一个一个地交付结果。

例 7:在网络上发现物体

let discovery = new ThingDiscovery({ method: "multicast" });
setTimeout( () => {
 discovery.stop();
 console.log("Stopped open-ended discovery");
},
10000);
do {
let td = await discovery.next();
let thing = new ConsumedThing(td);
console.log("Thing name: " + thing.getThingDescription().title);
} while (!discovery.done);

  1. 扩展阅读

关于 W3C 成立 WoT 工作组制定 WoT 标准的初衷,可以参考该文章。

  1. 相关链接