[AGV] OpenTCS 模組架構解析 | OpenTCS Modular Architecture Overview

本篇文章會說明 OpenTCS 各模組的職責,以及從派車、車輛移動到任務完成的完整流程說明,幫助讀者快速理解整體系統的運作方式。
This article explains the responsibilities of OpenTCS modules and how they work together throughout the workflow, from order dispatching and vehicle movement to order completion.

同時拆解每個模組的運作機制和如何替換,協助需要了解底層和客製化特定功能的讀者,快速定位關鍵模組與擴充切入點。
It also highlights internal mechanisms and module interactions, helping readers identify where to apply customizations when extending the system.

  1. 中文內容
    1. Model Layer
      1. plant model
      2. Transportorder model
      3. VehicleProcessModel
    2. Core Module
      1. Dispatcher
      2. Router
      3. VehicleController
      4. Scheduler
    3. Adapter
      1. Communication Adapter (Vehicle Driver)
      2. Peripheral Communication Adapter (Peripheral Driver)
    4. 運作流程
    5. 核心模組存取協定屬性的設計建議
    6. 補充說明
  2. English Version
    1. Model Layer
      1. Plant Model
      2. Transport Order Model
      3. Vehicle Process Model
    2. Core Modules
      1. Dispatcher
      2. Router
      3. Vehicle Controller
      4. Scheduler
    3. Adapters
      1. Vehicle Communication Adapter (Vehicle Driver)
      2. Peripheral Communication Adapter (Peripheral Driver)
    4. Execution Flow
    5. Design Recommendations for Accessing Protocol Properties in Core Modules
    6. Additional Observations from Integration Experience
  3. Reference

中文內容

Model Layer

主要是 OpenTCS 定義的相關資料格式,包含地圖、任務、車子本身的資料結構和屬性

plant model

地圖的資料格式,包含地圖上定義的所有資訊,除了基礎的 Point, Path, Location, Block 等,每個物件都支援自定義屬性,這些屬性都可以在任何地方被讀取,例如 VDA5050 的 Vehicle Driver 就自定義了許多屬性,讓地圖上可以定義 action,當發送 Command 時,會在依照地圖上的屬性將 action 放入 order 內,達到地圖自定義屬性可以影響車子行爲的效果

對應原始碼 PlantModel.java

Transportorder model

任務的資料格式,包含了任務上定義的所有屬性,像是基礎的名稱、狀態、orderType、指定車輛、目的地等,進階的則是路徑資訊、目前路徑進度以及自定義屬性,這個屬性同樣可以讓 Vehicle Driver 讀取,用來定義行爲,例如 VDA5050 就定義了特定屬性名稱,讓上位在API派車時,可以直接指定目的地要執行的action,TCS 本身也有定義了一個屬性,可以讓上位決定要繞過不走的 Point 或 Path 名稱

針對這些屬性的變化,都可以藉由繼承 eventHandler 來接收到變化的 event,非常方便,不需要一直輪詢屬性

對應原始碼 TransportOrder.java,eventHandler對應原始碼 StatisticsEventLogger.java

VehicleProcessModel

車子本身的屬性,像是狀態、Vehicle Driver 連線狀態、當前運行的任務、是否有載東西、當前的位置角度等,Vehicle Driver收到車子的狀態後,就會更新到此model,Vehicle Driver 可以繼承既有的 Class 擴充自身協定特有的屬性,像是VDA5050定義的各個 topic,接收到會存一份在這個 model 內

與 TransposrOrder 同理,這個 model 的屬性變化也可以用來接收相對應的 event,要對車子任何屬性做監控都非常的方便

對應原始碼 VehicleProcessModel.java,VDA5050 擴充部分對應原始碼 ProcessModelImpl.java

Core Module

這邊會說明 TSC 內建的幾個重要模組的作用,TCS 的抽象化做的很徹底,雖然一開始接觸時覺得模組數量衆多,琳琅滿目不知從何下手,但摸熟大概的架構後,每個模組都可以自行客製,輕鬆替換掉原生的模組,修改的程式碼都非常的少,每次修改完一個功能後,都會令筆者驚嘆,居然只要改這樣就可以了。

Dispatcher

功能如其名,負責指派任務給車子,這個模組會定時執行,預設的時間是10秒一次,這邊有個坑,筆者一開始不熟設定時,發現打API建立任務後,都會不定時 delay 幾秒才派給車子,就是這個原因,當初還以爲是 bug,後來 Member 找出設定後,才發現原來是我們不熟悉導致的誤會

指派任務的策略非常的多元,可以從 config 修改參照條件的順序,例如依照路徑 Cost 最小優先,或是任務設定的 deadline 等等,其他參數建議自行參照說明文件

同時若有開啓自動回停車區或充電功能,Dispatcher也會負責檢查可用的站點,建立任務將符合條件的車子派出,Dispatcher 所負責的每個功能,分成不同的 Phase 定義各自的邏輯,若需要調整,只需要找出對應的Phase修改或替換客製版本即可

對應原始碼 DefaultDispatcher.javaPhase位置

Router

主要用來計算路徑,除了常見的最短路徑,也會計算每條 Path 的 Cost,Cost 算法也非常彈性,可以直接從 config 設定,像是距離、時間等,並且可以多選,但需注意預設會將多種 Cost 結果相加,若單位不同就不太有意義,同時有些選項主要是用來卡控哪些路或是哪些情況不能走,像是 bounding box 選項,若車子體積大於 Point 上設定的可通過體積,Cost 會被視爲無限大,該路段就會被捨棄

在 Dispatcher 比較車子較符合哪個待命區或充電站時會使用到,或者在評估哪個任務和哪台車最適合時,也會使用到,同時會參考 TransportOrder 中定義的 tcs:resourcesToAvoid 屬性,算出符合需求的最小 Cost 路徑

對應程式碼 DefaultRouter.java

VehicleController

主要處理車子接收到任務後的流程,會依照任務內的路徑藉由 Scheduler 對 Point、Path、Location 進行佔用,會等待到佔用完成後,發送 Command 給 Vehicle Driver,並且在Vehicle Driver 回報已經過 Point 和 Path 後,再藉由 Scheduler 進行釋放,直到路徑走完爲止

當Path上有週邊設備的設定時,會在佔用 Path 路權後,觸發對應 Peripheral Driver,等到回報週邊設備可通行時,再接續發送 Command 給 Vehicle Driver

當路徑有變更時,VehicleController 也會收到通知,會把尚未發給 Vehicle Driver 的 Command 取消,並取消路權,再重新依照新路徑接續佔用

對應原始碼 DefaultVehicleController.java ,可以自己開發一個客製的 VehicleController class,在這邊修改即可套用

Scheduler

主要負責資源的佔用,包含Point、Path、Location,邏輯是先佔先贏,每次佔用都會開一個 thread,若搶不到會把該次的資源加入一個清單,後續再接續 trigger 佔用一次,如此不斷重複,並且每個查看、佔用和釋放路權的地方,都會使用lock,確保每個資源一次都只被一台車佔用

佔用的同時,還會經過一系列的 Module 的檢查,像是 SingleVehicleBlock 的邏輯,會確保這個資源若在的 SingleVehicleBlock 內,此時整個 Block 內其他資源沒有被其他車佔用,若有的話這個 module 會回報無法佔用,只要任意 Module 回報無法佔用,最終Scheduler就不會佔用這個資源,所有想要客製的佔用規則,可以自行建立 Module,再這邊加入引用即可,需注意此方法會被頻繁呼叫,盡量不要實作費時操作或邏輯,會影響整體效能

對應程式碼 DefaultScheduler.javaModule 位置

Adapter

Communication Adapter (Vehicle Driver)

主要負責實作車子的協定,會繼承固定的 interface 並實作,包含

  • 管理車子的連線、斷線狀態
  • 發送 Command 的方法,並轉成實際的車子控制命令給車子
  • 管理 Command 的 queue 和發送狀態
  • 接收車子即時狀態,設定初始位置,以及更新到 Model 中
  • 判斷當下狀態是否可以執行任務

理論上你知道車子的控制協定後,就可以寫一個 Adapter 讓 TCS 來控制它,一些針對這類型的客製邏輯也都可以實作在這裡,像是讀到地圖中車輛或 Point 的自定義屬性,要執行特定方法等,來達成一些預設行為

對應程式碼 內建模擬車VDA5050 Adapter

Peripheral Communication Adapter (Peripheral Driver)

主要負責檢查自定義邏輯是否通過判斷,是的話就允許 VehicleController 發送 Command 給 Adapter,通常用在自動門或電梯等被動式的簡單週邊設備,因為邏輯是自訂的,你可以用來判斷任意條件,不一定要跟設備掛鉤,例如要有載貨才能通過,在人員放東西上去車子回報後,就會放行了

對應程式碼 內建模擬週邊設備

運作流程

從API呼叫建立任務後,一直到車子移動,完成任務的整個完整流程,筆者憑藉着自己的經驗畫出了這個 流程圖,整理這個流程圖費了不少心力,畢竟模組非常多且流程也不短,雖然無法包含所有完整的細節,但還是希望能讓人清楚的了解整體流程,不會覺得很多地方是黑盒子,無法下手

流程圖使用 mermaid 格式,方便讀者自行修改,也方便 AI 讀取跟調整,若覺得有不清楚或可以加強的地方,歡迎不吝指教

Workflow (Mermaid)
flowchart TB

%% ========= API =========
subgraph API
    A["接收到任務"]
end

%% ========= Dispatcher =========
subgraph Dispatcher

    subgraph cycle["cycle"]
        D_confirm["確認新進任務<br/>找出最適合車輛"]
        D_charge["符合充電門檻<br/>指派 charging 任務"]
        D_park["閒置車輛<br/>指派 parking 任務"]
        D_cycle_end(("cycle end"))

        D_confirm --> D_charge
        D_charge --> D_park
        D_park --> D_cycle_end
    end

    subgraph Router
        R_calc["計算路徑與 cost"]
    end

    D_write["將路徑寫入 transportOrder"]
end

%% ========= VehicleController =========
subgraph VehicleController

    V_route["依路徑逐 step 申請路權"]
    V_reserved["佔用成功事件"]
    V_hasPeripheral{"需要週邊設備?"}
    V_toCommand["Step → Command"]
    V_release["完成後釋放路權"]
    V_last{"最後一個 Command?"}
    V_done["TransportOrder 完成"]

    %% ----- Scheduler -----
    subgraph Scheduler
        S_check["確認資源是否可用"]
        S_ok["佔用成功"]
        S_fail["佔用失敗<br/>重試"]

        subgraph BlockModule
            B_rule{"符合 BlockModule 規則?"}
        end
    end

    %% ----- Vehicle Driver -----
    subgraph VehicleDriver
        VD_send["Command → 車輛協定"]
        VD_report["回報 Command 完成"]
    end

    %% ----- Peripheral Driver -----
    subgraph PeripheralDriver
        P_send["發送設備命令"]
        P_done["設備完成回報"]
    end
end

%% ========= Main Flow =========
A --> D_confirm
D_cycle_end --> R_calc
R_calc --> D_write
D_write --> V_route

V_route --> S_check
S_check --> B_rule
B_rule -- 是 --> S_ok
B_rule -- 否 --> S_fail
S_fail --> S_check

S_ok --> V_reserved
V_reserved --> V_hasPeripheral

V_hasPeripheral -- 是 --> P_send
P_send --> P_done
P_done --> V_toCommand

V_hasPeripheral -- 否 --> V_toCommand

V_toCommand --> VD_send
VD_send --> VD_report
VD_report --> V_release
V_release --> V_last

V_last -- 否 --> V_toCommand
V_last -- 是 --> V_done

核心模組存取協定屬性的設計建議

在開發時,經常會遇到一些客製化流程的需求,希望 TCS 的核心模組(例如 VehicleController)能依據特定協定 Adapter 所定義的屬性,觸發不同的行為或流程,但 TCS 核心模組中卻無法直接取得 adapter 內部自行定義的屬性,因為 TCS 的專案架構將模組和 adapter 分離在不同的專案內,模組和 adapter 專案都會引用 opentcs-api-base 專案內的 interface。

實務上在模組的專案內直接 import apdater 專案,會出現編譯錯誤,並且也不該這麼做,會導致核心模組直接和協定耦合,未來擴充其他協定時,或是協定有異動時,都需要再變更核心模組的程式,並且未來在升級時也會出現更多的衝突影響可維護性。

因此建議做法是在 opentcs-api-base 的專案內進行擴充,建立新的 interface,並讓 adapter 繼承,讓 adapter 進行實作,未來有其他 adapter 也可以比照辦理,核心模組只需使用 instanceof 判斷 adapter 物件是否有繼承新的 interface 來決定是否轉型並呼叫 interface 內的方法即可。

補充說明

這邊補充幾個深入修改後發現的幾件事

  1. 雖然許多操作都會另開執行緒運行,但其實背後 thread pool 的數量只有1而已,並且搭配的 lock 也是 global 設計,所有的模組都共用一個鎖,我想這樣設計的原因是想要最大化降低 race condition 的風險,畢竟這類型的bug非常難解
  2. 可以自定義的地方真的太多,大大小小的模組,甚至包含 UI 的部分,只是我這邊主要聚焦在功能面,UI 就不著墨了

English Version

Model Layer

The model layer defines the core data structures used throughout OpenTCS.
It is the foundation for map definition, task execution, and vehicle state tracking, and is heavily used by both internal modules and custom extensions.

Plant Model

The plant model represents the map and all spatial elements defined within it, including points, paths, locations, and blocks.
Beyond the basic topology, each element supports custom properties that can be freely defined and accessed by other components at runtime.

This extensibility is critical in real-world integrations. For example, vehicle drivers such as VDA 5050 rely on custom map properties to define actions.
When a command is generated, these properties are translated into protocol-specific actions and embedded into transport orders, allowing map configuration to directly influence vehicle behavior without hard-coding logic.

Source: PlantModel.java

Transport Order Model

The transport order model defines all task-related information, including name, state, order type, assigned vehicle, destinations, routing data, and execution progress.
In addition to standard fields, transport orders support custom properties that can be interpreted by vehicle drivers at runtime.

This mechanism enables advanced behaviors, such as:

  • Defining actions to be executed at specific destinations
  • Specifying points or paths to be avoided using predefined attributes (e.g. tcs:resourcesToAvoid)

All changes to transport orders are published as events. By subscribing to these events, components can react immediately to state changes without polling, which greatly simplifies monitoring and integration logic.

Source: TransportOrder.java, event handling via StatisticsEventLogger.java

Vehicle Process Model

The vehicle process model represents the real-time state of a vehicle, including operational state, driver connection status, current order, load condition, position, and orientation.
Vehicle drivers continuously update this model based on feedback from the physical vehicle.

Drivers may also extend this model with protocol-specific data.
For example, the VDA 5050 implementation stores received topic data directly in the process model, making it accessible to other modules.

Like transport orders, changes in vehicle state are exposed via events, allowing fine-grained monitoring of any vehicle attribute with minimal effort.

Source: VehicleProcessModel.java, VDA 5050 extensions in ProcessModelImpl.java

Core Modules

OpenTCS follows a strongly modular architecture.
Although the number of modules can be intimidating at first, each module is highly focused and can be customized or replaced independently.
In practice, even non-trivial behavior changes often require only minimal code modifications once the overall structure is understood.

Dispatcher

The dispatcher is responsible for assigning transport orders to vehicles.
It runs periodically (10 seconds by default), which can introduce a short delay between order creation and assignment if not properly configured—an issue that is often mistaken for a system bug by new users.

Assignment strategies are highly configurable and may prioritize factors such as:

  • Route cost
  • Order deadlines
  • Charging or parking behavior

Dispatcher logic is divided into multiple phases. Each phase encapsulates a specific responsibility, allowing targeted customization by replacing or extending only the relevant phase.

Source: DefaultDispatcher.java, Phase

Router

The router calculates feasible routes and their associated costs.
Cost calculation is flexible and configurable, supporting metrics such as distance, time, and various constraints. Multiple cost factors can be combined, though care must be taken when mixing different units.

The router also enforces constraints, such as bounding box limitations.
If a vehicle exceeds the allowed size of a point or path, the corresponding route is treated as infeasible.

Routing is used extensively during:

  • Vehicle selection
  • Evaluation of parking or charging assignments
  • Order feasibility checks, including resources defined to be avoided

Source: DefaultRouter.java

Vehicle Controller

The vehicle controller manages the execution lifecycle after a transport order is assigned.
It coordinates with the scheduler to allocate points, paths, and locations, then issues commands to the vehicle driver once resources are secured.

As the vehicle reports progress, resources are released incrementally.
If peripheral devices are involved (e.g. doors or elevators), the controller triggers the corresponding peripheral drivers and waits for clearance before continuing.

When a route changes, the vehicle controller cancels pending commands, releases allocated resources, and re-executes the allocation process based on the updated route.

Source: DefaultVehicleController.java, Module config

Scheduler

The scheduler is responsible for exclusive resource allocation of points, paths, and locations.
Each allocation attempt runs in its own thread and follows a first-come-first-served strategy, with retries handled internally.

All resource access is synchronized using locks to guarantee that a resource is never occupied by more than one vehicle at a time.

Before allocation is finalized, requests pass through a chain of scheduling modules, such as the single-vehicle block check.
Any module may reject an allocation, in which case the scheduler aborts the attempt.

Custom scheduling rules can be implemented as additional modules, but they must be lightweight, as scheduling logic is executed frequently and directly impacts system performance.

Source: DefaultScheduler.java, Module

Adapters

Vehicle Communication Adapter (Vehicle Driver)

Vehicle drivers implement protocol-specific communication with vehicles.
Their responsibilities include:

  • Managing connection and disconnection states
  • Translating commands into vehicle-specific instructions
  • Handling command queues and execution states
  • Receiving real-time vehicle feedback and updating the process model
  • Determining whether a vehicle is currently capable of executing a task

Most integration-specific logic is implemented here, including behavior driven by custom map or order properties.

Source: Built-In Simulated Vehicle AdapterVDA5050 Adapter

Peripheral Communication Adapter (Peripheral Driver)

Peripheral drivers control access to passive or logical resources such as doors, elevators, or custom conditions.
They determine whether a vehicle may proceed, based on user-defined logic that does not necessarily correspond to a physical device.

Source: Built-In Simulated Peripheral Adapter

This flexibility allows peripheral drivers to enforce arbitrary conditions, such as requiring a vehicle to be loaded before entering a specific area.

Execution Flow

From task creation via the API to vehicle movement and task completion, OpenTCS involves a long chain of interacting modules.
A workflow diagram is provided using Mermaid syntax to make the process explicit, modifiable, and easy to reason about.

Workflow (Mermaid)
flowchart TB

%% ========= API =========
subgraph API
    A["Task received"]
end

%% ========= Dispatcher =========
subgraph Dispatcher

    subgraph cycle["cycle"]
        D_confirm["Validate new task<br/>Select suitable vehicle"]
        D_charge["Meets charging threshold<br/>Assign charging task"]
        D_park["Idle vehicle<br/>Assign parking task"]
        D_cycle_end(("cycle end"))

        D_confirm --> D_charge
        D_charge --> D_park
        D_park --> D_cycle_end
    end

    subgraph Router
        R_calc["Calculate route and cost"]
    end

    D_write["Write route into TransportOrder"]
end

%% ========= VehicleController =========
subgraph VehicleController

    V_route["Request resource allocation step by step<br/>based on route"]
    V_reserved["Resource reserved event"]
    V_hasPeripheral{"Peripheral required?"}
    V_toCommand["Step → Command"]
    V_release["Release resources after completion"]
    V_last{"Last command?"}
    V_done["TransportOrder completed"]

    %% ----- Scheduler -----
    subgraph Scheduler
        S_check["Check resource availability"]
        S_ok["Reservation successful"]
        S_fail["Reservation failed<br/>Retry"]

        subgraph BlockModule
            B_rule{"Satisfies BlockModule rules?"}
        end
    end

    %% ----- Vehicle Driver -----
    subgraph VehicleDriver
        VD_send["Command → Vehicle protocol"]
        VD_report["Report command completion"]
    end

    %% ----- Peripheral Driver -----
    subgraph PeripheralDriver
        P_send["Send peripheral command"]
        P_done["Peripheral completion report"]
    end
end

%% ========= Main Flow =========
A --> D_confirm
D_cycle_end --> R_calc
R_calc --> D_write
D_write --> V_route

V_route --> S_check
S_check --> B_rule
B_rule -- Yes --> S_ok
B_rule -- No --> S_fail
S_fail --> S_check

S_ok --> V_reserved
V_reserved --> V_hasPeripheral

V_hasPeripheral -- Yes --> P_send
P_send --> P_done
P_done --> V_toCommand

V_hasPeripheral -- No --> V_toCommand

V_toCommand --> VD_send
VD_send --> VD_report
VD_report --> V_release
V_release --> V_last

V_last -- No --> V_toCommand
V_last -- Yes --> V_done

Design Recommendations for Accessing Protocol Properties in Core Modules

During development, it is common to encounter customized workflow requirements where TCS core modules (such as VehicleController) need to trigger different behaviors or processing logic based on properties defined by a specific protocol adapter. However, core modules in TCS cannot directly access adapter-defined custom properties.

This limitation is intentional and stems from the TCS project architecture: core modules and adapters are separated into different projects, and both depend only on the interfaces defined in the opentcs-api-base project.

In practice, directly importing an adapter project into a core module project will result in compilation errors due to circular dependencies. More importantly, it is fundamentally a bad design choice. Doing so tightly couples core modules to a specific protocol implementation, meaning that any future protocol changes or the addition of new protocols would require modifications to the core module itself. This significantly reduces maintainability and increases the risk of conflicts during future upgrades.

To address this properly, the recommended approach is to extend the opentcs-api-base project by defining a new interface that represents the required capability or behavior. The adapter can then implement this interface and provide the corresponding logic. If additional adapters are introduced in the future, they can follow the same pattern.

The core module only needs to check whether a given object implements the interface (for example, using instanceof). If so, it can safely cast the object and invoke the interface methods. This approach allows core modules to react to protocol-specific capabilities without being directly coupled to any particular protocol implementation.

Additional Observations from Integration Experience

  • Although many operations appear asynchronous, OpenTCS relies on a very limited thread pool combined with global locking to minimize race conditions.
  • Extension points are abundant across backend modules and even the UI, though this article focuses on functional components.

Reference

發表留言

在 WordPress.com 建立網站或部落格

向上 ↑