Skip to main content

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:

  1. Raise lift — left motor runs forward (PGD drives eccentric shafts up)
  2. Wait for top sensor — polls inductive sensor until lift is fully raised (or timeout)
  3. Start transport belt — right motor runs in configured direction
  4. Run belt — tote transfers sideways for configured duration
  5. Stop belt + lower lift — left motor reverses to lower the assembly
  6. Wait for lower — timeout-based (no bottom sensor)
  7. 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)

  1. Download EasyRoll+ (Windows only)
  2. Connect via RJ-45, discover the module
  3. Set static IP, subnet mask, gateway, disable DHCP
  4. Set PLC I/O mode (Configuration tab → "Current Mode PLC")
  5. 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

vention_popup_diverter-0.1.0.tar.gz (26.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vention_popup_diverter-0.1.0-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

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

Hashes for vention_popup_diverter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bc8c8a2edccee24a50f1a6badc4339d77db9f0ddc10e1461e03b57c56c977936
MD5 a130aadbd6d5ff12b6d11cd8f7c61a32
BLAKE2b-256 fabaa1e808f4e5f6a91b8d4e1462236c4b92df6c8fb66ec6f579cf99be93208c

See more details on using hashes here.

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

Hashes for vention_popup_diverter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7d3967cc7c8f8a11aef667f6d60b3cc5ff7ebc19ed42d8c1c7f5b062fca23096
MD5 03493a372ae1ebf5d3f433776b42045f
BLAKE2b-256 ad30654cb23882a5e307a26de1ee1888229014c0bf48cf5cb83dd74935f8e5dd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page