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:
- 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
Metadata
Release files for vention-popup-diverter 0.9.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| vention_popup_diverter-0.9.5.tar.gz | 26.9 kB | Without provenance | Details for vention_popup_diverter-0.9.5.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| vention_popup_diverter-0.9.5-py3-none-any.whl | Python 3 | none | any | Details for vention_popup_diverter-0.9.5-py3-none-any.whl |
Total release size: 50.3 kB
Release files / vention_popup_diverter-0.9.5.tar.gz
| Download URL | vention_popup_diverter-0.9.5.tar.gz |
|---|---|
| Size | 26.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7cac190c30546d66b736c2489b099a62600e6898309f10651426ad7326038f20
|
|
BLAKE2b-256 checksum How to use checksums |
1dfcc1cf1bd83829cedee3b40b8e80644e80dfd844c7325b9943bd1dd85beef5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","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}
|
Release files / vention_popup_diverter-0.9.5-py3-none-any.whl
| Download URL | vention_popup_diverter-0.9.5-py3-none-any.whl |
|---|---|
| Size | 23.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef820ba3ba355a4b672255185e2df8c53209fbf357ac71f78d43505baad7efc4
|
|
BLAKE2b-256 checksum How to use checksums |
5d37187514c6ffabe50d897e87c73c2f406c1886302063b959392a34443787ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","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}
|
Security for vention-popup-diverter 0.9.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.