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.5.0.tar.gz (25.2 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.5.0-py3-none-any.whl (21.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: vention_popup_diverter-0.5.0.tar.gz
  • Upload date:
  • Size: 25.2 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.5.0.tar.gz
Algorithm Hash digest
SHA256 0934de5fe90713d056783399bb0786a0ccfc959afba36da0dc1ab03ab24a1dad
MD5 5d3e9aa87b21582d87857fefd4cd5c94
BLAKE2b-256 6be64fd2c942207e61943b5f75678858c615ff0324af11d2648f380033f49eaf

See more details on using hashes here.

File details

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

File metadata

  • Download URL: vention_popup_diverter-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 21.8 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.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6ccdad6473a06ee171acb8ab7fad7b914f761969e232ec58f66b490c95d40adc
MD5 5e4047effade657f49fc3901ece170c0
BLAKE2b-256 4fcfccec10182b10ced58e88c2bd7ed2654db6f0017a294be342c72e60ecb3d9

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