Async popup diverter fleet manager for Vention industrial automation. Supports Flowsort X-Flow90 (ConveyLinx-Ai2 Modbus TCP) and mock diverters.
Project description
vention-popup-diverter
Async popup roller diverter fleet manager for Vention industrial automation. Unified PopupDiverter ABC over Flowsort X-Flow90 (ConveyLinx-Ai2 Modbus TCP) and mock backends.
Diverter Types
| Type | Protocol | Use Case |
|---|---|---|
ConveyLinxDiverter |
Modbus TCP (port 502) | Flowsort X-Flow90 with Pulseroller ConveyLinx-Ai2 controller |
MockDiverter |
In-memory | Simulation and testing |
Usage
from popup_diverter.models import DiverterConfig, DiverterFleetConfig
from popup_diverter.service import DiverterService
config = DiverterFleetConfig(
diverters=[
DiverterConfig(id="d1", host="192.168.1.100", location_id="zone-a"),
DiverterConfig(id="d2", host="192.168.1.101", location_id="zone-b"),
],
)
service = DiverterService.from_config(config, mode="real")
await service.connect_all()
result = await service.divert("zone-a")
await service.disconnect_all()
result.status # DivertStatus.OK | TIMEOUT | ERROR | BUSY
result.lift_reached_top # True / False
result.error # None or error string
Divert Sequence
A single divert() call executes the full mechanical sequence:
- Raise lift — left motor runs forward (PGD drives eccentric shafts up)
- Wait for top sensor — polls inductive sensor until lift is fully raised (or timeout)
- Start transport belt — right motor runs in configured direction
- Run belt — tote transfers sideways for configured duration
- Stop belt + lower lift — left motor reverses to lower the assembly
- Wait for lower — timeout-based (no bottom sensor)
- Stop all
On failure at any step, the diverter safely stops and lowers before returning an error.
Individual Motor Control
For testing, commissioning, or manual operation:
diverter = service.get_diverter("zone-a")
await diverter.raise_lift()
await diverter.start_belt(BeltDirection.FORWARD)
await diverter.stop_belt()
await diverter.lower_lift()
await diverter.stop() # emergency stop all
Diagnostics
state = await service.read_state("zone-a")
state.voltage_mv # 24136 (24.1V)
state.top_sensor # True/False
state.lift_current_ma # 450
state.belt_running # True/False
state.lift_error # None or "stalled, overloaded"
Metrics
metrics = service.get_metrics()["d1"]
metrics.total_diverts # 1234
metrics.success_rate # 0.95
metrics.avg_divert_ms # 8200.0
Structured Logging
from popup_diverter.logger import set_log_callback
def on_diverter_log(code, source, level, message):
mqtt_publish("diverter/logs", {"code": code, "source": source, "message": message})
set_log_callback(on_diverter_log)
Hardware: Flowsort X-Flow90
The X-Flow90 is a 90° pop-up roller transfer. Each unit has a ConveyLinx-Ai2 controller (by Pulseroller) that drives two motors and reads one sensor, all controlled via Modbus TCP.
Components
| Component | Description |
|---|---|
| ConveyLinx-Ai2 | Ethernet-networked motor controller (dual motor, Modbus TCP) |
| PGD lift motor (left port) | Geared drive — raises/lowers the roller assembly |
| Senergy-Ai transport motor (right port) | Motor-driven roller — spins transport belts |
| Top position sensor (left sensor port) | Inductive sensor — detects lift fully raised |
Modbus TCP Registers
The ConveyLinx-Ai2 must be in PLC I/O mode (configured via EasyRoll+). All communication uses assembled register block reads/writes.
| Assembly | Base Address | pymodbus Address | Count |
|---|---|---|---|
| Input (read from module) | M:4:1700 | 1699 | 25 |
| Output (write to module) | M:4:1800 | 1799 | 17 |
Key output registers (offsets from base):
| Offset | Register | Description |
|---|---|---|
| 4 | Left Motor Run | bit0=run, bit8=direction |
| 7 | Right Motor Run | bit0=run, bit8=direction |
| 10 | Left Motor Speed | RPM × 10 (PGD) |
| 11 | Right Motor Speed | mm/s (MDR) |
Key input registers (offsets from base):
| Offset | Register | Description |
|---|---|---|
| 1 | Sensor Inputs | bit0=top position sensor |
| 3 | Motor Voltage | mV |
| 7 | Left Motor Status | Bitwise (running, errors) |
| 11 | Right Motor Status | Bitwise (running, errors) |
Daisy Chaining
ConveyLinx-Ai2 modules can be daisy-chained via RJ-45 (Link Left / Link Right ports). One cable from the network switch to the first module, then chain the rest. Each module needs a unique static IP.
EasyRoll+ Setup (one-time per module)
- Download EasyRoll+ (Windows only)
- Connect via RJ-45, discover the module
- Set static IP, subnet mask, gateway, disable DHCP
- Set PLC I/O mode (Configuration tab → "Current Mode PLC")
- Set "Outputs/Motors On PLC Disconnected" to "Stop All"
API
DiverterService
class DiverterService:
@classmethod
def from_config(cls, config, mode="real") -> DiverterService
async def divert(self, location_id, direction=BeltDirection.FORWARD) -> DivertResult
async def stop(self, location_id) -> None
async def stop_all(self) -> None
async def connect_all(self) -> dict[str, bool]
async def disconnect_all(self) -> None
async def read_state(self, location_id) -> DiverterState | None
async def read_all_states(self) -> dict[str, DiverterState]
def get_metrics(self) -> dict[str, DivertMetrics]
def get_metrics_summary(self) -> dict
DivertResult
class DivertResult:
status: DivertStatus # OK | TIMEOUT | ERROR | BUSY
diverter_id: str
lift_reached_top: bool
error: str | None
PopupDiverter ABC
class PopupDiverter(ABC):
async def connect(self) -> bool
async def disconnect(self) -> None
async def divert(self, direction=BeltDirection.FORWARD) -> DivertResult
async def stop(self) -> None
async def raise_lift(self) -> None
async def lower_lift(self) -> None
async def start_belt(self, direction=BeltDirection.FORWARD) -> None
async def stop_belt(self) -> None
async def stop_lift(self) -> None
async def read_state(self) -> DiverterState
connected: bool # property
Configuration
YAML config with optional config.local.yaml overlay and ${ENV_VAR:-default} substitution.
default_timeout: 5.0
reconnect_delay: 5.0
max_reconnect_attempts: 0 # 0 = unlimited
poll_interval: 0.05 # sensor polling during divert (seconds)
fleet:
- id: diverter-1
host: "192.168.1.100"
location_id: zone-a
belt_direction: forward # or reverse
lift_speed: 850 # RPM × 10
belt_speed: 400 # mm/s
lift_timeout: 5.0 # max seconds to raise lift
belt_run_time: 3.0 # seconds belt runs after lift reaches top
lower_timeout: 5.0 # max seconds to lower lift
Development
cd popup-diverter
uv sync
make test
make lint
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vention_popup_diverter-0.1.0.tar.gz.
File metadata
- Download URL: vention_popup_diverter-0.1.0.tar.gz
- Upload date:
- Size: 26.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc8c8a2edccee24a50f1a6badc4339d77db9f0ddc10e1461e03b57c56c977936
|
|
| MD5 |
a130aadbd6d5ff12b6d11cd8f7c61a32
|
|
| BLAKE2b-256 |
fabaa1e808f4e5f6a91b8d4e1462236c4b92df6c8fb66ec6f579cf99be93208c
|
File details
Details for the file vention_popup_diverter-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vention_popup_diverter-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d3967cc7c8f8a11aef667f6d60b3cc5ff7ebc19ed42d8c1c7f5b062fca23096
|
|
| MD5 |
03493a372ae1ebf5d3f433776b42045f
|
|
| BLAKE2b-256 |
ad30654cb23882a5e307a26de1ee1888229014c0bf48cf5cb83dd74935f8e5dd
|