本文将介绍在剧院自动控制系统的一个有趣项目中使用actor方法的经验。 这正是使用的印象,仅此而已。
最近,我参加了一项非常有趣的任务-现代化,但实际上-开发了一种新的自动控制系统的开发,该系统用于为其中一个剧院升降机架。
现代化的剧院(如果是大型剧院)是一个相当复杂的组织。 其中涉及许多人员,设备和各种系统。 这样的系统之一是用于“升高和降低”舞台上的风景的控制系统。 每年,现代表演,越来越多的歌剧和芭蕾舞都越来越趋于技术化。 它在动作过程中使用了许多复杂的场景及其动作。 在董事计划中积极使用了该风景,扩大了正在发生的事情的含义,甚至“发挥了自己的辅助作用”)。 总的来说,结识剧院的后台生活并了解表演期间发生的事情非常有趣。 毕竟,普通观众只能看到舞台上发生的事情。
但是本文仍然是技术性文章,我想在其中分享使用参与者方法实施管理的经验。 并且还分享了使用少数C ++ actor框架之一-sobjectizer的经验 。
为什么是他? 我们一直在关注他很长时间。 在habr上有文章 ,它具有出色的详细文档和示例。 该项目已经相当成熟。 快速浏览示例可以发现,开发人员使用“熟悉的”概念(状态,计时器,事件)进行操作,即 在我们的项目中使用理解和掌握不会出现大问题。 是的,重要的是,开发人员足够友善,乐于助人 (俄语) 。 所以我们决定尝试...
我们在做什么
那么,我们的“控制对象”是什么样的呢? shtanketovy升降系统-这是悬挂在该场景上方舞台整个宽度上的62个小金属片(金属管),距离舞台边缘的深度大约每30-40 cm。 小腿本身悬挂在绳索上,并且可以向上或向下上升到舞台(垂直运动)。 在每场演出(或歌剧或芭蕾舞)中,节的一部分都用于装饰。 在操作期间,将风景挂在它们上并移动(如果脚本需要)。 运动本身是由操作员(他们有特殊的控制面板)使用“发动机-电缆-配重”系统(与房屋中的电梯相同)进行的。 引擎位于舞台的边缘(位于几层)上,因此观看者看不到它们。 所有电动机均分为8组,每组具有三个变频器(IF)。 在每组中,可以同时激活三个电动机,每个电动机都连接到其自己的逆变器。 总共,我们必须控制62个引擎和24个逆变器的系统。
我们的任务是开发用于管理这种经济状况的操作员界面,并实施管理算法。 该系统包括三个控制站。 两个控制柱位于舞台的正上方,一个柱位于机房(控制柜所在的位置)中,用于监视值班电工的工作。 在控制柜中,有一些控制器可以执行命令,PWM控制,为电动机供电,跟踪闸门的位置。 上方的两个遥控器是显示器,这是一个系统单元,控制算法和轨迹球在其中作为“鼠标”旋转。 控制面板之间使用以太网络。 每个控制柜都有来自两个控制面板中每个面板的RS485通道(即8个通道)。 可以同时从两个遥控器(位于舞台上方)进行管理,但是同时只有一个遥控器(由操作员指定为主要操作员)正在与机柜进行交换,此时第二个控制台被视为备份,并且已禁用交换功能。
这里的演员
从算法的角度来看,整个系统是建立在事件之上的。 这些要么是传感器的变化,要么是操作员的动作,或者是某个时间(定时器)的开始。 这样的算法在处理传入事件,形成某种响应以及所有这些都取决于其状态的参与者系统中处于很好的位置。 在sobjectizer中,所有这些机制都是现成的。 这种系统所基于的主要原理可以归因于:参与者之间的交互是通过消息发生的,参与者可以具有状态并在它们之间移动,在每个状态中,参与者仅处理当前感兴趣的消息。 有趣的是,在SObjectizer中,与角色的工作在概念上与与工作流的工作是分开的。 即 您可以描述所需的参与者,实现其逻辑,并通过消息实现其交互。 但是,然后分别解决为其工作分配线程(资源)的问题。 这由负责处理线程的特定策略的所谓“调度程序”来确保。 例如,有一个分配器为每个要使用的actor分配一个单独的线程,有一个分配器提供了一个线程池(即actor比线程更多),并且可以设置最大线程数,有一个分配器为所有线程分配一个线程。 调度员的存在提供了一种非常灵活的机制来设置参与者系统以满足您的需求。 您可以将一组参与者组合在一起,以与其中一个调度程序一起工作,同时将一种类型的调度程序更改为另一种,这实际上是在更改一行代码。 根据该框架的作者所说,编写自己的唯一调度程序也不难。 在我们的项目中不需要这样做,因为我们所需的一切都已经在对象化器中。
另一个有趣的特征是参与者的“合作”概念的存在。 合作是一组参与者,如果合作中至少有一个参与者无法开始工作或完成任务,那么他们可以全部存在,也可以全部销毁(或不发动)。 我什至不怕打个比方( 即使它来自另一个“歌剧” )“合作”的概念就像现在流行的Kubernetes中的“炉床”的概念一样,它似乎只是在对象化器中出现了,它早就出现了……
在创建时,每个参与者都包含在合作中(合作可能由一个参与者组成),成为一个或另一个调度员的成员并开始工作。 同时,可以(轻松)大量动态地创建参与者(和合作者),并且正如开发人员所承诺的那样,这并不昂贵。 所有参与者之间都通过“ 邮箱 ”(mbox)进行交换。 在Sobjectizer中,这也是一个有趣且强大的概念。 它提供了一种非常灵活的机制来处理传入消息。 首先,可能有多个收件人躲在盒子后面。 真的很方便。 例如,创建一个框,其中接收来自外部传感器的事件,并且每个演员都订阅他感兴趣的事件。 这提供了“发布/订阅”操作样式。 其次,开发人员提供了相对容易地创建自己的邮箱实现的机会,该邮箱可以预处理传入的消息(例如,以某种方式过滤它们或以特殊方式在消费者之间分配它们)。 此外,每个参与者都有自己的邮箱,甚至可以通过邮件向其他参与者发送“链接”,例如,以便他们可以发送某种通知作为返回响应。
在我们的项目中,为了确保发动机组之间的独立性,并确保组内发动机的“异步”运行,将所有控制对象分为8个组(根据控制柜的数量),每个组有3个工人流量(一次最多只能同时运行三个引擎)。
还应该说,目标器(在当前版本5.5中)不包含进程间和网络交互机制,并将这部分留给开发人员。 作者非常有意地做到了这一点,因此该框架更加“简单”。 此外,网络交互机制“一次”在以前的版本中已经存在,但被排除在外。 但是,这不会造成任何不便,因为确实网络交互在很大程度上取决于要解决的任务,所使用的交换协议等。 在这里,通用实现并非在所有情况下都是最优的。
在我们的案例中,对于网络和进程间通信,我们使用了我们长期的开发成果之一-libuniset2库。 结果,我们系统的架构看起来像这样:
- libuniset提供网络和进程间通信(基于传感器)
- sobjectizer提供了一个参与者系统的创建,这些参与者彼此交互(在同一地址空间中)以实现控制算法。
因此,让我提醒您,我们有62个引擎。 每个电动机都可以连接到逆变器,可以为相应的支架提供必须到达的坐标和必须移动的速度。 此外,引擎具有以下条件:
- 准备出发
- 已连接
- 运行(旋转)
- 意外
- 连接(瞬态)
- 关机(瞬态)
结果,系统中的每个“引擎”都由执行者来实现,该执行者实现状态之间的转换逻辑,处理来自传感器的事件并发出控制命令。 在sobjectizer中,可以轻松创建actor,只需从基类so_5 :: agent_t继承您的类即可。 在这种情况下,构造函数必须将所谓的::: so_5 :: context_t上下文作为第一个参数,其余参数由开发人员的需求确定。
class Drive_A: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); ... }
因为 本文不是有教育意义的文章,因此在这里我将不提供有关类或方法描述的详细文本。 本文只是想展示在sobjectizer的帮助下(用几行代码)实现所有这一切有多么容易。 让我提醒您,该项目具有出色的详细文档 ,其中包含许多不同的示例。
这些参与者的“状态”是什么? 你在说什么
对于ACS使用状态和状态之间的转换通常是一个自然主题。 这个“概念”非常适合事件处理。 在sobjectizer中,API级别支持此概念。 在演员类中,状态很容易声明
class Drive_A final: public so_5::agent_t { public: Drive_A( context_t ctx, ... ); virtual ~Drive_A();
而且,对于每种状态,开发人员都将确定必要的处理程序。 通常,进入状态和退出状态时需要采取一些措施。 在sobjectizer中也提供了此功能,就像为这些事件(“状态入口”,“状态出口”)定义处理程序一样容易。 可以认为,过去的开发人员具有丰富的ACS-shny经验 ...
事件处理程序
事件处理程序,这是实现应用程序逻辑的地方。 如上所述,订阅是针对特定邮箱以及参与者的特定状态的。 如果参与者没有在代码中显式声明的状态,则它隐式处于特殊状态“ default_state”。 在不同的状态下,您可以为同一事件定义不同的处理程序。 如果您未在此邮箱中指定任何事件的处理程序,则将仅将其忽略(即,对于参与者而言,它将根本不存在)。
定义处理程序的语法非常简单。 足以表明您的功能。 不需要任何类型或模板参数。 一切都是从函数定义中自动推导出的。 例如:
so_subscribe(drv->so_mbox()) .in(st_base) .event( &Drive_A::on_get_info ) .event( &Drive_A::on_control ) .event( &Drive_A::off_control );
这是在st_base状态的特定框中订阅事件的示例。 有趣的是,在此示例中,st_base是其他状态的基本状态,因此,此订阅将对从st_base“继承”的所有状态有效。 这种方法使您可以摆脱“复制粘贴”来为不同状态确定相同的处理程序。 同时,在特定状态下,您可以覆盖指定的处理程序,也可以“禁用”它(抑制)。
还有另一种定义处理程序的方式。 这是lambda函数的直接定义。 这是一种非常方便的方法,因为处理程序通常是几个动作中的短函数,将某物发送给某人或切换状态。
so_subscribe(drv->so_mbox()) .in(st_disconnecting) .event([this](const msg_disconnected_t& m) { ... st_off.activate(); }) .event([this]( const msg_failure_t& m ) { ... st_protection.activate(); });
首先,这种语法似乎很复杂。 但是在几天的积极开发中,您已经习惯了,甚至开始喜欢它。 因为演员在一种状态或另一种状态下的全部逻辑都可以用相当短的代码来表示,而这一切都在您的眼前。 例如,在所示示例中,在断开状态(st_disconnecting)中,如果发生有关某种故障的消息,则将转换为断开状态(st_off。)或发生保护状态(st_protection)。 这样的代码很容易阅读。
顺便说一句,对于简单的情况,当事件只需要进入某种状态时,可以使用更短的语法:
auto mbox = drv->so_mbox(); st_off .just_switch_to<msg_connected_t>(mbox, st_connected) .just_switch_to<msg_failure_t>(mbox, st_protection) .just_switch_to<msg_on_limit_t>(mbox, st_protection) .just_switch_to<msg_on_t>(mbox, st_on);
管理学
所有这些经济的管理如何运作? 如上所述,提供了两个遥控器来直接控制垫片的运动。 在每个遥控器上都有一个监视器,一个操纵器(跟踪球)和一个快速拨号盘(除了隐藏在遥控器中的“计算机”之外,所有东西都在旋转并且堆放着各种转换器)。 该系统具有几种控制垫片运动的模式。 手动和“脚本模式”。 关于“场景模式”的内容将进一步讨论,现在,将对“手动模式”进行一些讨论。 在此模式下,操作员选择所需的垫片,准备运动(将电机连接到变频器),设置垫片的标记(目标位置),一旦将速度设置为大于零,垫片便开始移动。 为了设置速度,使用了一种特殊的物理调节器,形式为“带手柄的电位器”,但也有速度的“屏幕调节器”。 越“转向”, 更大声 走得更快。 最大速度限制为1.5 m / s。 速度旋钮-一应俱全。 即 在手动模式下,所有操作员连接的小腿都以相同的设定速度移动。 尽管它们可以沿不同方向移动(取决于操作员指示它们的位置)。 当然,一个人很难同时跟踪两个或三个以上的垫片,因此通常它们在手动模式下不会移动太多。 从两个站点,操作员可以同时管理他们的每个垫片。 此外,每个控制台(操作员)都有自己的速度控制器。
从实现的角度来看,手动模式不包含任何特殊逻辑。 用于连接引擎的命令来自图形界面,该信息被转换为消息,发送给相应的actor,该actor将在该actor上工作。 通过状态“关闭”->“正在连接”->“已连接”。 设置短节的移动位置和设置速度的方法相同。 所有这些事件以演员对其做出反应的消息的形式到达演员。 除非可以指出,图形界面和控制过程本身是不同的过程,并且它们之间使用libuniset2通过“传感器”进行“过程间”交互。
脚本执行模式(还是这些角色?)
实际上,手动控制模式主要仅用于排练或简单情况下的聚会。 进行控制的主要模式是“脚本执行模式”,或者简称为“脚本模式”。 在此模式下,每个shtank都使用脚本中指定的参数(速度和目标标记)移动到其位置。 对于操作员,此模式下的控制包含两个简单命令:
- 准备好(已连接正确的引擎组)
- 我们走吧(小组开始移动到为每个目标设置的目标位置)。
整个场景分为所谓的“ agendas”。 议程是洗礼小组的一项运动。 即 每个议程都包含一组标签,以及您需要达到的目标速度和品牌。 实际上,剧本分为行为,行为分为绘画,绘画分为传票,传票已经由特定目标的“目标”组成。 但是从管理的角度来看,这种划分并不重要,因为 在议程上最后注明机芯的具体参数。
为了实施这一制度,行动者体系又要尽可能地建立起来。 开发了“剧本播放器”,可以创建一组特殊演员并将其启动。 我们开发了两种类型的演员:演员-演员,旨在执行特定shtanket的任务;演员-协调员,在演员之间分配任务。 此外,如果下一队的时间不是自由的,则根据需要创建表演演员。 协调演员负责创建和维护表演演员库。 结果,管理看起来像这样:
- 语句加载脚本
- 将其“翻转”到所需的议程(通常只是连续进行)。
- 在适当的时候,按下“准备”按钮,通过该按钮,针对当前议程中包含运动参数的每个表格,将命令(消息)发送给协调参与者。
- 演员协调员会查看他的自由表演演员池,选一个免费演员(如果他没有创建新演员),然后给他一个任务(小包数量和移动参数)。
- 收到任务的每个演员角色开始执行“准备就绪”命令。 即 它连接引擎并进入“执行”命令的待机模式。
- 时间到了,操作员会发出“放手”命令
- 团队“出发”来到协调员。 他将其发送给所有当前活跃的表演者,然后他们开始执行。
值得注意的是,议程中还有其他参数。 例如,以N秒的延迟开始运动,或者仅在单独的特殊操作员命令之后才开始运动。 因此,每个表演演员的状态列表非常大:“准备执行下一个命令”,“准备移动”,“动作延迟”,“等待操作员的命令”,“移动”,“执行完成”,“故障” 。
在小程序成功(或未成功)达到指定标记之后,执行者将完成的任务通知协调者。 协调器发出命令以关闭此引擎(如果它不再参与当前议程)或发出新的运动参数。 反过来,演员执行者收到了关闭引擎,关闭引擎并进入等待新命令的状态的命令,或者开始执行新命令。
由于sobjectizer具有经过深思熟虑且方便使用的用于处理状态的API,因此实现代码非常简洁。 例如,在一行中描述了运动的延迟:
st_delay.time_limit( std::chrono::milliseconds{target->delay()}, st_moving ); st_delay.activate(); ...
time_limit函数设置了一个时间限制,该时间限制是在给定状态下可以花费多少,以及在指定时间(st_moving)之后应该通过什么状态。
保护演员
当然,在操作过程中可能会发生故障。 需要系统处理这些情况。 在这里,也有使用演员的地方。 考虑以下几种保护措施:
- 过电流保护
- 测量失败保护
- 防止反方向移动(如果传感器或仪表有问题,则可以这样做)
- 在没有命令的情况下防止移动
- 控制团队的执行(控制标签开始移动)
从实现的角度看,所有这些保护都是独立的(自足的),应该“并行”工作。 即 任何条件都可以工作。 同时,检查每个保护的触发条件的逻辑也各有其逻辑,有时需要一个延时(计时器)来跳闸,有时需要对几个先前的测量值进行初步处理,等等。 因此,事实证明,将每种类型的保护实现为单独的小角色非常方便。 所有这些参与者都是在实现控制逻辑的主要参与者之外(协作)启动的。 通过向组中添加其他参与者,此方法可以轻松添加其他类型的防御。 同时,这样一个参与者的实现仍然相当容易和可以理解,因为 它仅实现一个功能。
保护行动者也有几种状态。 基本上,它们仅在连接发动机或轴正在移动时才打开(进入“打开”状态)。 当触发保护条件时,它们会发布有关触发保护的通知(带有安全代码和一些日志详细信息),主要角色已经在响应该通知,如果有必要,则关闭引擎并切换到保护模式。
作为结论..
...当然,本文不是某种“发现”。 参与者方法早已在许多系统中成功使用。 但是对我来说,这是在一个相对较小的项目中有意识地使用参与者方法来构建控制系统算法的第一次体验。 这次体验非常成功。 我希望我能证明演员很好地叠加在控制算法上,他们在各个地方都找到了位置。
从先前项目的经验来看,很明显,我们正在以一种或另一种方式实现“类似的东西”(状态,消息传递,流控制等),但这不是统一的方法。 使用sobjectizer,我们得到了一个简洁,轻量级的开发工具,该工具可以处理大量问题。 不再需要(明确地)使用同步工具(互斥对象等),不再需要对流进行显式处理,也无需实现状态机。 所有这些都在框架中,在逻辑上相互联系,并以方便的API形式呈现,而又不会失去对细节的控制。 因此,体验很有趣。 对于仍然有疑问的人,我建议特别注意参与者方法和目标对象框架。 他留下积极的情绪。
演员方法确实有效! 特别是在剧院。