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

This is an async library: every call uses await, so it must run inside an async def you start with asyncio.run(...). (await at the top level of a script raises SyntaxError: 'await' outside function.)

Quickstart — no hardware (mock mode)

Runs as-is, nothing connected. mode="mock" swaps in an in-memory diverter that completes the full sequence successfully.

import asyncio

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"),
    ],
)
service = DiverterService.from_config(config, mode="mock")


async def main():
    await service.connect_all()
    result = await service.divert("zone-a")   # full raise→belt→lower sequence
    await service.disconnect_all()

    print(result.status)            # DivertStatus.OK | TIMEOUT | ERROR | BUSY
    print(result.lift_reached_top)  # True / False
    print(result.error)             # None or error string


asyncio.run(main())                 # runs everything in main()

divert() is the one-call easy path — it raises the lift, runs the belt for the configured time, lowers, and stops. Use the individual motor controls below only to drive the steps yourself.

Connecting real hardware

Same code — swap mode="mock" for mode="real". host is each diverter's IP; location_id is the name you address it by (in divert() / get_diverter()).

service = DiverterService.from_config(config, mode="real")

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.6.0.tar.gz (25.3 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.6.0-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file vention_popup_diverter-0.6.0.tar.gz.

File metadata

  • Download URL: vention_popup_diverter-0.6.0.tar.gz
  • Upload date:
  • Size: 25.3 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.6.0.tar.gz
Algorithm Hash digest
SHA256 9ff34d767e9eb61436a96c4d6637c086a807f5a867bd2e5a02ee37dd7c1a675b
MD5 546e492f7b1e7663657689aa5810535e
BLAKE2b-256 0b9422644df03c4fbc3443aa6557731714265fcb2b710a6c8d69df6b7b5060d7

See more details on using hashes here.

File details

Details for the file vention_popup_diverter-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: vention_popup_diverter-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 21.9 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.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cbdcf916b8015c0140cf2f6149e342a0898e80af9775b5f86b756913710f7d9c
MD5 45ff8ded659710f1f025518d9f901899
BLAKE2b-256 9124b1f196e6408a84d27dfcf16ad16f04c3e10f315b74e12103f7860751def3

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