本文へ進む

進入を承認する係で共有区間を管理する

要求と応答を分け、区間を使う本体を1台ずつ選びます。

必要な本体
2〜3台。原例は最大3台を探索。
線路・準備
内外の2周回と2つの共有区間。3台では外側2台が時計回り、内側1台が反時計回り。黄・紫で入口と出口を示します。
終了方法
Enterで購読解除・各台の停止・共有状態の終了・切断。
保存する場所
matrain-python-async/examples/teamwork/collision_avoidance_2.py
実行する場所
matrain-python-asyncフォルダー内。.venvのPythonを使います。

Mac:.venv/bin/python examples/teamwork/collision_avoidance_2.pyWindows:.venv\Scripts\python.exe examples\teamwork\collision_avoidance_2.py

通常終了の処理を原例のまま掲載しています。例外やCtrl+Cでの停止が保証されないため、異常時は本体のボタンを短く押して止めます。切断だけを停止とみなさないでください。

コード全文

teamwork/collision_avoidance_2.py
# Matrain publication modification: licensing/source notice added.
# Executable behavior is unchanged except for the adaptations documented below.
# Source: https://github.com/intelino-code/intelino-trainlib-async-py
# Source commit: 298e1fd4091e069180c30f338b4c285b13accbd0
# This software is made available by Innokind, Inc. DBA Intelino, under the
# terms of the Intelino Public License Agreement, Version 1.0 located at
# https://intelino.com/intelino-public-license. BY INSTALLING, DOWNLOADING,
# ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE, YOU AGREE TO THE TERMS OF
# SUCH LICENSE AGREEMENT.

"""Teamwork example: Collision avoidance (v2) with arbitration.

Track requirements (same as v1):
- Double loop (inner + outer) with two "shared" sections
  - outer loop is clockwise (2 trains)
  - inner loop is counter-clockwise (1 train)
- Yellow and magenta snaps are used to identify the shared sections.

Changes:
- arbitration (separating actions and state) solves the issue of multiple
  trains waiting at once and introduces transactional logic
- when the section is freed, the waiting trains re-request entry
  - alternatively (not implemented here), the arbiter (shared logic) could
    implement queueing, so re-requesting entry would not be required
    and whichever train started waiting first, would get the "pass" to enter

Notes (corner cases):
- it still doesn't solve queueing when trains are coming from the same branch,
  tm. it doesn't prevent collisions to the back during congestion on a single
  track.

"""
import asyncio
import aioconsole
from dataclasses import dataclass
from enum import Enum
from rx import operators as ops
from rx.core.typing import Disposable
from rx.subject import Subject
from typing import List, NamedTuple
from intelino.trainlib_async import (
    TrainScanner,
    Train,
)
from intelino.trainlib_async.messages import (
    TrainMsgEventFrontColorChanged,
    TrainMsgMovement,
)
from intelino.trainlib_async.enums import SnapColorValue as Color, SteeringDecision
from intelino.trainlib_async.helpers import AsyncObserver

#
# Data types
#

SectionState = Enum("SectionState", "FREE OCCUPIED_BY")
SectionRequest = Enum("SectionState", "ENTER LEAVE")
SectionResponse = Enum("SectionState", "ENTER_APPROVED ENTER_REJECTED SECTION_FREE")

TrainSectionState = Enum("TrainSectionState", "NONE ENTERED WAITING")


class SectionInfo(NamedTuple):
    state: SectionState
    train_id: int


@dataclass(frozen=True)
class Message:
    """The message format to be passed in the shared stream."""

    section_color: Color
    train_id: int


@dataclass(frozen=True)
class MessageRequest(Message):
    request: SectionRequest


@dataclass(frozen=True)
class MessageResponse(Message):
    response: SectionResponse


class SharedState:
    """Class for a global state shared between all train programs and also
    the shared logic.
    """

    def __init__(self):
        self.exit_event = asyncio.Event()
        # critical section request and update stream
        self.stream = Subject()  # type: Subject[Message]
        # critical section state (updated by the SharedLogic from the stream)
        # Note: in this implementation this wouldn't need to be shared
        self.critical_section = {
            Color.YELLOW: SectionInfo(SectionState.FREE, -1),
            Color.MAGENTA: SectionInfo(SectionState.FREE, -1),
        }

    def dispose(self):
        self.stream.on_completed()
        self.stream.dispose()


class LocalState:
    def __init__(self):
        # train-specific critical section state
        self.section: dict[Color, TrainSectionState] = {
            Color.YELLOW: TrainSectionState.NONE,
            Color.MAGENTA: TrainSectionState.NONE,
        }


#
# Shared logic
#


class SharedLogic:
    """The programs' own logic can execute actions based on the shared state
    and the shared data stream. It can manipulate the shared state
    but communication with trains is strictly through message passing.
    """

    def __init__(self, shared_state: SharedState):
        self.shared_state = shared_state

    async def run(self):
        # Any "active" logic (e.g. user inputs, signalling to trains etc.)
        # would be run through this function.
        subscription = self.shared_state.stream.pipe(
            ops.filter(lambda msg: isinstance(msg, MessageRequest))
        ).subscribe(AsyncObserver(self.handle_request))
        await self.waiter()
        subscription.dispose()

    async def handle_request(self, msg: MessageRequest):
        """Arbitration function processing requests as transactions.
        Note: not thread-safe
        """
        section_info = self.shared_state.critical_section[msg.section_color]

        # verify each request, update sections' state and notify all trains
        # through the stream with a response
        if msg.request == SectionRequest.ENTER:
            if section_info.state == SectionState.FREE:
                # allow entry
                self.shared_state.critical_section[msg.section_color] = SectionInfo(
                    SectionState.OCCUPIED_BY, msg.train_id
                )
                self.shared_state.stream.on_next(
                    MessageResponse(
                        msg.section_color, msg.train_id, SectionResponse.ENTER_APPROVED
                    )
                )

            else:
                # reject entry
                self.shared_state.stream.on_next(
                    MessageResponse(
                        msg.section_color, msg.train_id, SectionResponse.ENTER_REJECTED
                    )
                )

        elif msg.request == SectionRequest.LEAVE:
            if section_info == (SectionState.OCCUPIED_BY, msg.train_id):
                # free section
                self.shared_state.critical_section[msg.section_color] = SectionInfo(
                    SectionState.FREE, msg.train_id
                )
                self.shared_state.stream.on_next(
                    MessageResponse(
                        msg.section_color, msg.train_id, SectionResponse.SECTION_FREE
                    )
                )

    async def waiter(self):
        """Let the train program run until the 'enter' key is pressed."""
        await aioconsole.ainput("Press <Enter> to exit at any time...\n")
        self.shared_state.exit_event.set()


#
# Train program (for all trains)
#


async def collision_avoidance(train: Train, index: int, shared_state: SharedState):
    """Multiple trains prevent head-on collision on two shared sections."""
    state = LocalState()

    async def request_entry(color: Color):
        shared_state.stream.on_next(MessageRequest(color, index, SectionRequest.ENTER))

    async def request_leave(color: Color):
        shared_state.stream.on_next(MessageRequest(color, index, SectionRequest.LEAVE))

    async def milestone(msg: TrainMsgEventFrontColorChanged):
        if msg.color in shared_state.critical_section:
            if state.section[msg.color] == TrainSectionState.NONE:
                await request_entry(msg.color)

            elif state.section[msg.color] == TrainSectionState.ENTERED:
                await request_leave(msg.color)

    async def section_enter(color: Color):
        if state.section[color] == TrainSectionState.WAITING:
            await train.drive_at_speed(40)
        state.section[color] = TrainSectionState.ENTERED

    async def section_wait(color: Color):
        if state.section[color] != TrainSectionState.WAITING:
            await train.stop_driving()
        state.section[color] = TrainSectionState.WAITING

    async def section_leave(color: Color):
        state.section[color] = TrainSectionState.NONE

    async def handle_response(msg: MessageResponse):
        if msg.train_id == index:
            # handle all responses addressed to us
            if msg.response == SectionResponse.ENTER_APPROVED:
                await section_enter(msg.section_color)
            elif msg.response == SectionResponse.ENTER_REJECTED:
                await section_wait(msg.section_color)
            elif msg.response == SectionResponse.SECTION_FREE:
                await section_leave(msg.section_color)

        elif msg.response == SectionResponse.SECTION_FREE:
            # listen to all `SECTION_FREE` messages
            if state.section[msg.section_color] == TrainSectionState.WAITING:
                # if we are waiting, we request entry again
                await request_entry(msg.section_color)

    async def update_steering_decision(msg: TrainMsgMovement):
        # since we have 2 loops that are driven in opposite directions,
        # all trains use the same decision side on splits
        await train.set_next_split_steering_decision(SteeringDecision.LEFT)

    # create a list of subscriptions
    subscriptions: List[Disposable] = []

    # start the movement stream
    movement_stream = await train.movement_notification_stream(True)
    sub = movement_stream.pipe(
        # we are interested only if the next decision is not set
        ops.filter(lambda msg: msg.next_split_decision == SteeringDecision.NONE),
    ).subscribe(AsyncObserver(update_steering_decision))
    subscriptions.append(sub)

    sub = train.notifications.pipe(
        # we are interested only in color changes
        ops.filter(lambda msg: isinstance(msg, TrainMsgEventFrontColorChanged)),
    ).subscribe(AsyncObserver(milestone))
    subscriptions.append(sub)

    sub = shared_state.stream.pipe(
        ops.filter(lambda msg: isinstance(msg, MessageResponse)),
    ).subscribe(AsyncObserver(handle_response))
    subscriptions.append(sub)

    await train.drive_at_speed(35)

    # wait for the exit event to happen
    await shared_state.exit_event.wait()

    # cleanup
    for subscription in subscriptions:
        subscription.dispose()
    await train.stop_driving()


async def main():
    # connect to a few trains
    trains = await TrainScanner(timeout=4.0).get_trains(at_most=3)
    shared_state = SharedState()
    shared_logic = SharedLogic(shared_state)

    if len(trains) >= 2:
        print(f"Connected to {len(trains)} trains.")
        # run our train program with all our trains
        await asyncio.gather(
            shared_logic.run(),
            *(
                collision_avoidance(train, idx, shared_state)
                for idx, train in enumerate(trains)
            ),
        )

    else:
        print("Not enough trains found.")

    # after the program finished, clean up and disconnect from the trains
    shared_state.dispose()
    for train in trains:
        await train.disconnect()


if __name__ == "__main__":
    asyncio.run(main())

冒頭に配布条件と出典のコメントを追加しています。表示している全文と、個別保存・ZIP内のこのファイルは同じです。ライセンス・出典を読む

ファイルのダウンロード

ダウンロードの確認

ファイル名:

この端末に保存します。閲覧中のページはそのまま残ります。