MechDancer Team 1329

欢迎访问 MechDancer 网站!

这里会发布一些我们的技术及开发进程。

我们的软件文档位于:https://docs.mechdancer.org/

同时,我们还在进行 VEX V5 PROS 文档的翻译:http://pros.mechdancer.org/

扫描二维码,关注 交大附中智能社

PROS 文档翻译贡献指南

随着 V5 的发布,Purdue ACM SIGBots 更新了对应的软件文档。我们目前着手于 V5 文档的译制,v5-chinese 这个文件夹为 v5 的拷贝,所有中文翻译在此进行。

搭建文档编译环境

为了良好的翻译体验及更高的翻译效率,在本地构建出文档工程并预览是很有必要的。下面几步介绍了如何搭建起该文档工程的编译环境。

克隆工程

找一个地方,执行命令将仓库克隆到本地:

1
> git clone https://github.com/MechDancer/pros-docs

安装 PROS CLI

由于文档中使用 sphix-click 扩展将源码中的部分文档渲染进网页,因此需要先安装 PROS CLI。你可以在这里找到安装指南。

初始化子模块

sphinx_rtd_themesphinx-tabs 目录下运行:

1
2
> git submodule init
> git submodule update

初始化两个依赖的源码仓库。之后分别安装这两个子模块中的依赖:

1
2
3
4
5
> cd sphinx_rtd_theme
> pip install ./

> cd ../sphinx-tabs
> pip install ./

安装依赖

进入工程文件夹,使用 pip 安装其他依赖:

1
2
> cd pros-docs
> pip install -r requirements.txt

开始构建

运行命令:

1
> make chinese

若构建成功便会在工程文件夹中得到 ./build/v5-chinese/html 目录,其中包含网页文件。

注意:Windows 系统中可能不存在 make 命令,即无法执行 Makefile,此时可替换为 sphinx-build -M html v5-chinese/ build/v5-chinese/

进行预览

我们推荐使用 Visual Studio Code 作为文本编辑工具,它具有支持 rst 的插件,可以进行实时预览。

安装扩展

在扩展商店中安装 reStructuredText 扩展。之后进行配置:

  • Restructuredtext: Conf Path 填写 ${workspaceFolder}/conf.py
  • Restructuredtext › Linter: Executable Path 填写 doc8 安装位置
  • Restructuredtext: Sphinx Build Path 填写 sphinx-build 安装位置

启用自动保存

我们推荐打开 Visual Studio Code 的失焦点自动保存,这样可以点击右侧预览窗口随时得到最新渲染的网页。

打开项目

在顶部栏中点击 文件 > 打开文件夹,选择 v5-chinese 目录即可。享受翻译带来的快乐:)

确定性有穷自动机实现单词、数字和注释扫描

介绍

通过将整段的字符串结成单词,扫描器 可以显著简化语法分析的难度。而构造分词扫描器的通用方法就是使用 正则表达式有穷自动机。有时设计一个新语言的第一步就是实现正则引擎,以便方便地测试词法分析,但是直接编码来产生自动机也是可行的,并且在词类较少时更方便调试和修改。

  • 下面我们所说的 单词(token) 指的是一类具有特殊含义的字符串,为了与通常的单词(word)区分开,这种 单词 都使用粗体。

在代码中,扫描器是这样的东西:

1
2
3
4
5
6
interface Scanner<T> {           // 输入单元类型为 T 的有状态的扫描器
val length: Int // 匹配状态:当前匹配的长度
val complete: Boolean // 匹配状态:是否在可接受的结束状态
operator fun invoke(char: T) // 输入一个 T 类型的单元 char
fun reset() // 重置内部状态
} //

基于自动机的扫描器包含一系列 状态,其中有且仅有一个是起始状态,并有一部分状态是可接受的结束状态。每次扫描开始时,状态机从起始状态出发,对每一个输入单元发生一次状态转移,直到所有单元都输入到扫描器或扫描器因不合法的字符落入到不存在/错误的的状态中。

通过非常简单的代码就可以实现一个自动机引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 确定性自动机扫描器
* @param table 状态转移表(*行序号从 1 开始*,*0 表示错误状态*)
* @param ending 合法结束状态
* @param map 字符到转移表列序号的映射关系(*-1 表示无效字符*)
*/
class DFA<T>(
private val table: List<List<Int>>,
private val ending: Set<Int>,
private val map: (T) -> Int
) : Scanner<T> {
// 当前状态(序号)
// 正数表示正在匹配
// 负数表示匹配失败前的最后状态,即能匹配的部分的结束状态
private var state = 1

override var length: Int = 0; private set
override val complete get() = abs(state) in ending

override operator fun invoke(char: T) {
if (state > 0) {
state = map(char)
// 是一个有意义的字符
.takeIf { it >= 0 }
// 查找转移表
?.let { table[state - 1][it] }
// 不导致错误状态
?.takeIf { it != 0 }
// 匹配长度增加
?.also { length }
// 否则标记匹配结束
?: -state
}
}

override fun reset() {
state = 1
length = 0
}
}

上面的代码展示了我实现的自动机引擎。其中类的构造参数描述了某一个状态机实例的结构。

  • table 是用数字表示的状态转移表,表的每一行表示一个状态,每一列表示在某状态下针对一类输入字符将转移到哪个状态;
  • ending 是合法结束状态的集合,状态机根据当前状态是否集合中的元素判断匹配是否完成;
  • map 是实际输入字符到状态转移表列的映射关系。用 -1 列表示转到错误状态;

这是数字扫描器实际使用的状态机参数,后面会讲到如何决定这些参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
table = //     0   1   d   b   h   x   .    //
listOf(listOf( 2, 11, 11, 0, 0, 0, 12), // 1 -> ε
listOf(11, 11, 11, 3, 0, 7, 12), // 2 -> 0
listOf( 4, 4, 0, 0, 0, 0, 5), // 3 -> 0b
listOf( 4, 4, 0, 0, 0, 0, 5), // 4 -> 0b...
listOf( 6, 6, 0, 0, 0, 0, 0), // 5 -> 0b... .
listOf( 6, 6, 0, 0, 0, 0, 0), // 6 -> 0b... . ...
listOf( 8, 8, 8, 8, 8, 0, 9), // 7 -> 0x
listOf( 8, 8, 8, 8, 8, 0, 9), // 8 -> 0x...
listOf(10, 10, 10, 10, 10, 0, 0), // 9 -> 0x... .
listOf(10, 10, 10, 10, 10, 0, 0), // 10 -> 0x... . ...
listOf(11, 11, 11, 0, 0, 0, 12), // 11 -> ...
listOf(13, 13, 13, 0, 0, 0, 0), // 12 -> ... .
listOf(13, 13, 13, 0, 0, 0, 0)), // 13 -> ... . ...
ending =
setOf(2, 4, 6, 8, 10, 11, 13),
map =
{ c: Char ->
when (c.toLowerCase()) {
'0' -> 0 // 0
'1' -> 1 // 1
in '0'..'9' -> 2 // d
'b' -> 3 // b
in 'a'..'f' -> 4 // h
'x' -> 5 // x
'.' -> 6 // .
else -> -1 // e
}
}

实现一个基于自动机的扫描器有三个步骤:

  1. 根据要扫描的 单词 的特征设计识别 单词 的正则表达式;
  2. 直接将正则表达式(及其连接、选择和 Kleene 星号等元操作)转化为包含空转移的非确定性自动机 NFA;
  3. 对NFA进行约化和化简得到确定性有穷自动机 DFA;

示例

虽然编译原理是每个计算机专业学生的必修课,但网上其实连能正确扫描 C 语言风格注释的 DFA 实现也很难找到。此处就以这个为例子介绍如何构造自动机扫描器。

  1. 正则

    C 语言风格的注释有两种形式:

    • 嵌入型 /* … */
    • 行尾型 // …

    第一步先写出辨别这两种注释的正则表达式:

    • 嵌入型 /*(?*)*/
    • 行尾型 //(?*)

    其中小括号内的 * 是 Kleene 星号,表示匹配任意字符任意次。

  2. NFA

    正则对应的 NFA 如图所示。现在先假设我们直接使用换行符分句,因此两种形式都不接受换行符,行尾型也不需要换行符来结束,因此可以直接简化到自环,不必再写出 Kleene 星号。

    NFA

  3. DFA

    由上图已经可以清晰地看到对于注释扫描来说,输入字符只有 3 类:

    | 字符 | 代号 | 列号 |
    | :–: | :–: | :–: |
    | / | / | 0 |
    | | | 1 |
    | 其他 | e | 2 |

    因此可以简化 DFA,主要操作就是:

    • 消除 ε 转换的歧义性;
    • 消除相同符号的歧义性;

    转化后如图:

    DFA

  1. 编写代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // ------ //  /  *  e    //
    listOf(listOf(2, 0, 0), // 1 -> ε
    listOf(3, 4, 0), // 2 -> /
    listOf(3, 3, 3), // 3 -> // ...
    listOf(6, 5, 6), // 4 -> /*
    listOf(7, 5, 6), // 5 -> /* ... *
    listOf(6, 5, 6), // 6 -> /* ...
    listOf(0, 0, 0)), // 7 -> /* ... */
    setOf(5, 7),
    { c: Char ->
    when (c) {
    '/' -> 0
    '*' -> 1
    else -> 2
    }
    }

数字扫描器

如图:

  • NFA

    NFA digit

  • DFA

    DFA digit

师大二附交流活动简报

前言

2018年9月16日,交大附中参加了在北师大二附中学术报告厅举行 2018-2019 赛季北京校际联盟第二次交流活动。本次活动参与的学校有:师大二附、交大附中、北京三帆中学、北京市广渠门中学、北京十一学校、北师大二附中(Horizons)、西城区科技馆、北京第四中学、师大附中和宣武科技馆。此此活动分为以下三个板块。

规则解读

本次活动个个学校表示了对 2018-2019 赛季规则的解读。具体分为任务难点的分析,误区的讲解,得分场地起落架的结构分析。此外众学校重新温故规则 part2 的部分,引起了大家的诸多思考。

方案交流

在这个部分里大家表达了在赛季初期对任务的理解,并相互交流了能想到的方案。同时列出了初期的方案侧重,例如主抓自动得分,高举的侧重。进入矿坑抓取得分物的结构。把得分物排除矿坑再得到得分物的方案。大家的思路百花齐放,并且都有对规则的侧重,利用,避让等。

裁判视频见面会

由于语言问题在赛季中有许多模糊不清的界定,所以大家与裁判长李博进行了在线见面讨论了一些关于规则的问题。例如 in 到底是得分物正投影在里面还是与地面接触。释放的定义是什么。大家相继提出来了很多问题。裁判长显然并不能依依解答,但他保证会在近期讲解答打包放在网上,供大家参考,尽情期待吧。

总结

这是这个赛季北京地区的第二次交流讨论会,规则下发后的第一次交流会。从交流频率来看,大家对交流的热情有了显著的提高。这是一个好兆头,希望各各学校都能在 2018-2019 赛季收获知识,当然能收获几个奖杯那是更好不过。

规范导航器接口

前言

通过前一段时间实现的动态窗口法导航,我对导航模块也有了更深一层的认识。为了使软硬件异构的机器人都能使用导航模块,我抽象出了导航模块,以及可导航运动平台的共性,其实现即规范导航器接口。本文主要介绍导航器的模型和概念,并辅以少量演示代码。

下文中“导航”与“局部路径规划”是等价的,可互换。

概念

以动态窗口法和定时弹带法为代表的一系列导航算法具有相同的运行过程,即预测→优化→控制。其中预测过程确定有效控制量的范围,并根据控制量预测机器人可能轨迹;优化过程从所有可能轨迹里找到最优的一条;控制过程执行最优轨迹对应的控制量。

局部路径规划中有2个重要的概念。

1. 轨迹 trajectory

轨迹正式局部路径规划算法应该给出的结果。那轨迹究竟是什么呢?请参阅 较为详细的阐述,但此处给出结论:所谓轨迹,即是时刻到机器人位姿的映射关系

1
trajectory := time -> pose
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 轨迹
* 时间到位姿的映射
*/
interface ITrajectory {
...

/** @return 某时刻的位姿 */
operator fun get(time: Double): Pose?

...
}

有了轨迹,就能对机器人的运动进行评判,从而选出最优的控制量。

2. 运动模型

从控制量获取轨迹的过程就是运动模型。用信号与系统的话说,控制是对定位平台的激励,而平台对控制的响应则是运动(轨迹),而激励到响应的传递函数就是运动模型。

1
model := current pose * command -> trajectory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 可导航的
* @param T 控制量类型
*/
interface INavigatable<T> {
/**
* 轨迹预测
* 基于当前位姿、速度等状态量和控制量,计算未来一段时间的运动轨迹
* 各种运动学状态量表现为位姿的 n 阶导数
* @param current 当前位姿及位姿的 n 阶导数
* @param cmd 控制量
*/
fun predict(current: Map<Int, Pose>, cmd: T): ITrajectory
}

运动模型,尤其是有速度甚至加速度参与的精确的运动模型与机械结构的设计是密不可分的。所以运动模型的数学表达应该是由机械工程师提供的。近期我将另撰一文讨论机械控制和软件的界限问题。

轨迹的表示

轨迹通常来源于预测,而预测总是基于某种不变性的假设(如速度是常数或加速度是常数),因此轨迹具有时效性。定义域说明了轨迹有效的范围。

1
2
3
4
5
6
7
interface ITrajectory {
/** 定义域 */
val timeRange: DoubleRange

/** @return 某时刻的位姿 */
operator fun get(time: Double): Pose?
}

轨迹定义为时刻到机器人位姿的映射,但这种映射关系并不总是可以用函数表达的,它在不同的系统中可能是方程或其他不方便求解的特殊形式。对于这种情况,枚举通常是行之有效的表达方式。对连续轨迹进行采样,可以得到离散轨迹。采样点足够密集的离散轨迹也能正确描述一个运动。

1
2
3
4
5
6
7
8
/** 离散位姿点组成的轨迹 */
interface IDiscreteTrajectory : ITrajectory {
/** 可获取精确位姿的周期 */
val period: Double

/** 丢弃时间信息,转为有序的位姿列表 */
val path: List<Pose>
}

工程已经提供了对连续轨迹、等间隔采样离散轨迹和任意采样离散轨迹的实现,开发者可自行查阅。

状态机——(1)

前言

本周 状态机实现库 有两大变化:

  • 状态机各部分功能抽象为接口
  • 添加或修改以下功能:
    • 状态机允许初始状态悬空和修改初始状态
    • 添加线性状态机构建工具
    • 添加看门狗工具

本文主要介绍新功能的使用。

新功能使用说明

构建线性状态机

所谓线性状态机指的是状态有顺序并基于执行次数或外部条件按顺序调度的一类状态机。其本质与其他可执行后可自动跳转的状态机并无不同,因此库只需提供一个方便的构建器即可。

当前提供的 DSL 构建器:

1
2
3
4
5
6
7
val `for` = linearStateMachine {
var i = 0
once { i = 0 }
call(20) { println(++i) }
delay { time = 1; unit = SECONDS }
once { println("hello world!") }
}

once 指定的代码块将执行 1 次。可以通过 call 设置代码块的执行次数。还可以通过 forever 设置代码块无限循环(可以通过看门狗退出)。delay 用于在两个状态之间制造间隔。

状态指定的顺序就是状态执行的顺序。reset 可要求状态机回到初始状态,初始状态是无法修改的。

看门狗

有时状态执行较慢,或者状态的离开条件因为某些原因迟迟无法满足,我们希望状态机在最终无望转移时仍能继续执行下去,此时可以使用看门狗。构造看门狗时需要指定看门狗作用的状态机、其起始状态、目标状态和等待的时间。当调用看门狗的 start 方法后,看门狗将启动。当等待时间消耗殆尽,状态机会启动一次检查,如果当前状态是指定的起始状态,环境有满足目标状态的进入条件,就不再检查起始状态的离开条件,直接驱动一次跳转。

由于其强制跳转操作依赖于状态机的支持,看门狗作用的状态机必须实现 IExternalTransferable<T>

示例代码:

1
2
3
4
5
val init = state {
val dog = Watchdog(this@stateMachine, null, null, 5, SECONDS)
before = { dog.start(); ACCEPT }
doing = { i = 0 }
}

此状态机将在 init 状态初次执行 5s 后直接结束。

状态机接口

这一部分不详述,有兴趣可看类图:

class diagram

速度窗口法实现局部路径规划

前言

速度窗口法实现库 (dynamic window approach) 已完成实现和封装,并依赖了线性代数工具 的二维和三维向量工具。原理在此,现在尚未实现避障功能。

本文主要介绍使用方法和注意事项。

前提条件和注意事项

要将 DWA (dynamic window approach) 适配到自己的机器人,请保证机器人具备全场定位功能,已为可行驶区域建立笛卡尔坐标系,并能正确获取自身位姿。位姿即机器人坐标系相对场地坐标系的偏移,定义为三维向量[x, y, w],默认初始化时机器人坐标系与场地坐标系重合,机器人面朝 x 轴方向,符合右手定则。

导航模块假定机器人是非全向的,因此无法规划不与机器人坐标系 x 轴相切的路径,但原地转是受支持的。添加对全向的支持、或对个性化的机器人控制方式进行适配并不难,开发者可自行修改。

每次获取控制量需要将机器人当前的控制量和位姿提交到导航器 navigator 。导航器将对每个提交的位姿数据计算一个控制量。不会自动插值,也不会进行任务调度,因此请保证外部以合适的频率提交位姿数据,以免发生振荡或延迟。

使用

1. 构造导航器

使用构造器来构造导航器:

1
val navigator = Navigator(Path(),Configuration( ... ))

或使用 DSL 构造导航器(推荐):

1
val navigator = navigator { ... }

这样构造出来的导航器没有工作路径。

2. 确定全局路径

本模块的定位是局部路径规划和导航,要实现完整的机器人控制,必须提供合理的全局路径。较为方便的测试方法是“录点”,即通过遥控示教来确定机器人全局路径。本模块内置的路径管理支持录点功能。

使用语句 navigator.path += pose来向工作路径末尾添加一个位姿点。

调用 navigator.path.clear() 将会清空工作路径。

你也可以访问 navigator.path 查看与路径相关的更多功能。

3. 进行控制

录点完毕后,应首先确定机器人位于工作路径起始点附近。加下来就可以开始向导航器提交控制量和位姿数据,并接收规划控制量。已通过并超出感兴趣区域的位姿点将自动从工作路径中移除。

提交数据,接收指令:

1
val cmd = navigator(cmd, pose)

4. 调整参数

当前本模块包含两类可调整的参数:

  • 工作参数

    工作参数是路径规划过程中使用的参数,包括感兴趣区域、速度和加速度约束以及控制量采样点数量等,现已收纳到 Configuration 数据结构,在导航器构造时通过构造器或 DSL 指定。

  • 优化参数

    优化参数是选择最优路径的标准,现包含终端位置、终端方向、全程贴合性和速度四项,已收纳到 Conditions.kt 中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private val conditions = setOf(
    //终端位置条件
    Condition(1.0) { local, _, trajectory ->
    local.nodes.last() euclid trajectory.nodes.last()
    },
    //终端方向条件
    Condition(1.0) { local, _, trajectory ->
    (local.nodes.last() deflectionTo trajectory.nodes.last()).absoluteValue
    },
    //全程贴合性条件
    Condition(1.5) { local, _, trajectory ->
    trajectory
    .nodes
    .map { pathNode -> pathNode.position }
    .sumByDouble { point ->
    local.segments.map {
    segment -> segment.distanceTo(point)
    }.min()!!
    }
    },
    //线速度条件
    Condition(1.0) { _, speed, _ ->
    log2(speed.first.absoluteValue + speed.second.absoluteValue)
    }
    )

    条件 Condition 类定义为:

    1
    class Condition(val k: Double, val f: (Trajectory, Sample, Trajectory) -> Double)

    即优化函数和系数。

    开发者可以修改各项参数,也可以按照这个格式添加自己的优化条件。各优化条件将在最优化函数计算时自动归一化。

FTC 2019赛季任务分析及建议 高挂部分

任务分析

今天凌晨发布了 FTC 2019赛季的新主题,大家从规则中不难发现高挂部分在本赛季中的分数比重相当大,作为一个退役老咸鱼,我在这里就为大家进行一下新赛季中高挂部分是分析及我对这部分机构的设计建议。根据规则,在比赛伊始机器人可以选择挂在着陆器上(至于为什么要挂在上面启动,那当然是为了获得加分2333),考虑到在一开始要进行30秒的自动阶段,这就要求机器在降下之后要有一个稳定精确的下降落点,若机器在悬挂过程中就晃晃悠悠,下降落点散布又比较大,那对自动程序必然会造成一定的障碍。在比赛的最后30秒,如何快速将“钩子”固定在着陆器上则是需要关注的重点,这就要求机械上在展开后整体有一定的刚度,不会随意晃动。

设计建议

  1. 将高挂部分设计为一个模块,以一个可上下滑动的框架作为伸缩部分为主体(类似17年抱大球双滑轨的那种),只不过这次不需要那么多级滑轨。当然也可使用铝方管代替滑轨,可实现一定的减重,但安装较为麻烦。

  2. 关于“钩子”,从规则中可知机器人需要固定在一竖直放置的U型支架上。同样的为了让机器人挂起后不易晃动,我认为设计机器的挂钩最好不要使用普通的弧形挂钩,而通过一在U型框架上的可动插销代替“钩子”,并在升降框架上安装两支点,实现框架较为稳定的固定。这样设计的优点主要就是升降过程中可让机器保持稳定,而且在“固定”与“放开”两个状态间迅速切换,便于机器在比赛开始时的落地,且能有一个较为精确的落点,方便自动程序的运行。如果担心这样在对位时仍有困难,可在卡扣两侧加装导向机构作为辅助机械定位装置,可进一步降低对位难度。

  3. 关于将机器抬升的传动机构,我认为线轴拉线、蜗轮蜗杆+小正齿轮+齿条、丝杠+丝杠法兰 的几种方式都还是比较靠谱的,其中齿条结构和丝杠结构的反馈更直接,升降也更精确,但是质量比较大。具体结构的选择还需要根据你们的想法及设计进行选择,这里就不进行过多说明。

建议原因

  1. 我建议使用可升降的框架主要有两个原因,一是相比于使用机械臂降钩子挂上去,升降框架的整体刚度更大,在机器运动时不宜晃动,从而可以使机器是升降点不会偏差过大,精度较高,便于自动程序的运行。二是整体框架的稳定降低了在最后30s操作手对位时的难度,从而提高了高挂的成功率。
  2. 你们可能会怀疑滑轨的强度,担心使用滑轨将机器拉起来会把滑轨拉坏,但实际上,一切在FTC中造成损坏的原因都是由于使用方式不正确而导致的。在使用滑轨时,最需要注意的一点就是滑轨的受力方向是与滑轨的短面(没有安装孔的那面)垂直的,而不是很多人所使用的长面(有安装孔的那面)。一根正常抽屉轨一般在使用正确的情况下可承受25-30kg的重量,尺寸较大的工业滑轨大多数都可承受50-70kg,所以在使用正确的情况下,完全不需要为滑轨的强度而担忧。

  3. 由于蜗轮蜗杆有自锁性,这就使得它十分适合用作高挂结构的传动系统。今年在自动阶段开始时机器人是挂在支架上的,若单纯的用程序实现闭位置环,从而将电机锁死的话不但对电机的负载较大,而且会对电机的寿命有一些不利影响。相比之下设计一个机械结构实现机械锁死又较为复杂,这种情况下使用蜗轮蜗杆不但可以简化设计,更可以利用其自身性质实现机器的锁定。

状态机——新的开端

前言

经过一个多月的反复思考和艰难抉择,最新版状态机终于完成!此版状态机吸取 SYZKLibrary 第一版、第二版的丰富实践经验,兼顾状态机原理,并融合 Stateless 之精华,用词简练,功能齐全,极具观赏和实用价值。

使用方法

1. 定义状态

每个状态机具有一系列状态,这些状态不一定都要比状态机更早定义,但构造状态机时必须至少有一个已知的有效状态,即状态机初始状态。

状态的定义包含4个可选项:

  • 1)动作(函数),状态机在当前状态所要执行的具体动作;

  • 2)前判断条件,满足状态的前判断条件才可进入状态,可用于约束初始状态,也可用于初始化资源;

  • 3)后判断条件,满足状态的后判断条件才可退出状态,也可用于释放资源;

  • 4)是否允许自动自循环,当状态动作执行完毕且没有合适的目标状态时,状态再次执行动作还是跳转到null。

  • 同一个状态机必须具有相同类型的状态,且必须实现提供这些可选项的 IState 接口。

我们推荐以下两种方法定义状态:

使用枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum class StateTest : IState {
Init {
override val loop = false
override fun doing() = run { i = 0 }
},
Add {
override val loop = false
override fun doing() = run { ++i; Unit }
override fun before() = i < 20
},
Print {
override val loop = false
override fun doing() = run { println(i) }
}
}

使用 DSL

1
2
3
4
5
6
7
8
9
10
11
12
val init = state {
doing { i = 0 }
}

val add = state {
doing { i++ }
before { i < 20 }
}

val print = state {
doing { println(i) }
}

2. 定义状态机

1
val `for` = StateMachine(Init)

提供初始状态即可。

3. 构造或注册事件

所谓事件,就是使状态机发生状态转移的函数。状态机提供了方法来构造事件。

1
val event = `for`.event(Print to Init)

通过状态机的引用可以构造事件,只需传递事件的源状态和目的状态即可。事件发生(调用)时,状态机会检查当前状态是否与状态的源匹配,以及两端节点是否接受状态转移。有时,用户可能并不急切地要求状态转移,而仅仅希望事件在状态的动作之间发生,这时可以将事件注册到状态机,状态机将在合适的时机自动调用。

使用 register 方法:

1
`for` register (Init to Print)

基于定义,此模型也支持无限状态状态机,甚至在运行间动态添加新的状态和事件。

示例

完整示例代码可以在 这里 找到。