Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

<img src="https://raw.githubusercontent.com/vanous/PollToMVR/refs/heads/master/images/PollToMVR_icon.png" width="128px">

[PollToMVR](https://github.com/vanous/PollToMVR) - a tool to perform network
discovery via ArtNet - ArtPoll and to save found devices in an
[PollToMVR](https://github.com/vanous/PollToMVR) - a tool to perform device
discovery via ArtNet or RDM E1.20 and to save found devices in an
[MVR](https://gdtf-share.com/) (My Virtual Rig) scene file.

<img src="https://raw.githubusercontent.com/vanous/PollToMVR/refs/heads/master/images/polltomvr_00.png">
Expand All @@ -13,13 +13,14 @@ discovery via ArtNet - ArtPoll and to save found devices in an
- Uses [pymvr](https://pypi.org/project/pymvr/) to write fixtures to MVR files
- Provides Graphical [Terminal User Interface](https://textual.textualize.io/)
- Uses ArtPoll based device network discovery, can parse DMX Address and Universe from device description if present
- Uses RDM ANSI E1.20 via Robe RUNIT USB Interfaces for DMX line device discovery
- Uses the [GDTF Share](https://gdtf-share.com) Fixture Library for fixture definitions in MVR

## FAQ

### What is this

A tool to quickly create an MVR file based on network scan.
A tool to quickly create an MVR file based on ArtPoll or RDM scan.

### What this is not

Expand All @@ -36,6 +37,12 @@ No, it only uses ArtPoll and ArtPollReply. Some manufacturers put DMX
Address/Universe into `long_name`. If present and parsed, these values will
then be used in the MVR export.

### Can devices be discovered via RDM?

Yes, PollToMVR can use ANSI E1.20 for device discovery and to query the device
for Device Info and Device Model Description. Supported USB interfaces are the
Robe Lighting: `Universal Interface` and `RUNIT WTX` interfaces.

## Instalation

Binary releases for Linux, macOS and Windows are available from the
Expand All @@ -55,11 +62,11 @@ Hub](https://gdtf.eu/).
## Quick Start

- Start the PollToMVR
- Click Discover to run discovery
- Click Discover to run network discovery
- After discovery, press the "Add devices to MVR Layer", click Add
- Click `Save Devices` to store the result as an MVR file

This will create a barebone MVR file with device names and their IP addresses
This will create a bare-bone MVR file with device names and their IP addresses
(and Universes, DMX address, if also discovered). For more featured MVR: set
user login credential in the Config, download some GDTF files and after network
discovery, link the GDTFs to the discovered fixtures. After saving, the MVR
Expand All @@ -78,8 +85,9 @@ will also contain the full GDTF definitions
file will then be used for the created MVR file. To download the GDTF
files, use the GDTF Files button on the main screen.
- ### Discover
- Discover devices on the local network
- Discover devices on the local network or on DMX line
- Select a network interface on which the discovery will run
- Select a USB based Runit interface for RDM based discovery
- ### Add Discovered Devices
- Add the discovered devices to a selected MVR layer
- Create a new named MVR Layer
Expand Down Expand Up @@ -159,6 +167,14 @@ uv run textual console
uv run textual run --dev run.py
```

## Code formatting

All python code is to be formatted with ruff:

```
uv tool run ruff format
``

## Packaging

Initial pyinstaller setup
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
[project]
name = "PolltoMVR"
version = "0.1.0"
description = "A tool to discover network devices via Art-Net ArtPoll and to save them into [MVR](https://gdtf-share.com/) (My Virtual Rig) scene file"
version = "0.2.0"
description = "A tool to discover network devices via Art-Net or RDM E1.20 and to save them into [MVR](https://gdtf-share.com/) (My Virtual Rig) scene file"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"ifaddr>=0.2.0",
"pymvr>=1.0.4",
"pyserial>=3.5",
"requests>=2.32.5",
"textual>=6.5.0",
"textual-fspicker>=0.6.0",
Expand Down
4 changes: 2 additions & 2 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from tui.app import ArtPollToMVR
from tui.app import PollToMVR

if __name__ == "__main__":
app = ArtPollToMVR()
app = PollToMVR()
app.run()
3 changes: 3 additions & 0 deletions tui/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ SelectCurrent:focus {
border: none;
}

GDTFMapping {
height: 2;
}
GDTFMapping SelectCurrent,
SelectCurrent:focus {
border: none;
Expand Down
16 changes: 4 additions & 12 deletions tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import functools
import json
import os
import random
import traceback
import subprocess
from types import SimpleNamespace
from textual.app import App, ComposeResult
from textual import on, work
from textual.containers import Horizontal, Vertical, VerticalScroll, Grid
from textual.widgets import Header, Footer, Input, Button, Static, Select
from textual.worker import Worker, WorkerState
from textual.widgets import Header, Button, Static, Select
from tui.screens import ArtNetScreen, QuitScreen, ConfigScreen, ImportDiscovery
from textual.message import Message
from textual.reactive import reactive
from tui.messages import MvrParsed, Errors
import uuid as py_uuid
from tui.create_mvr import create_mvr
from textual_fspicker import FileSave, Filters
Expand All @@ -47,7 +39,7 @@ def update_items(self, items):
for fixture in fixtures:
self.mount(
Static(
f"[green]{fixture.short_name}[/green] {fixture.universe or ''} {fixture.address or ''} {fixture.ip_address} "
f"[green]{fixture.short_name}[/green] {f'IP Address: {fixture.ip_address}' if fixture.ip_address else ''} {f'Universe: {fixture.universe}' if fixture.universe else ''} {f'DMX: {fixture.address}' if fixture.address else ''}"
)
)

Expand Down Expand Up @@ -123,7 +115,7 @@ def on_select_changed(self, event: Select.Changed):
self.app.gdtf_map[self.fixture] = event.value


class ArtPollToMVR(App):
class PollToMVR(App):
"""A Textual app to manage Uptime Kuma MVR."""

CSS_PATH = [
Expand Down Expand Up @@ -272,5 +264,5 @@ async def save_a_file(self, event: Button.Pressed) -> None:


if __name__ == "__main__":
app = ArtPollToMVR()
app = PollToMVR()
app.run()
5 changes: 4 additions & 1 deletion tui/create_mvr.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ def create_mvr(devices, mvr_layers, gdtf_map, save_to):
if net_fixture.short_name in gdtf_map:
fixture.gdtf_spec = gdtf_map[net_fixture.short_name]

fixture.addresses.network.append(pymvr.Network(ipv4=net_fixture.ip_address))
if net_fixture.ip_address:
fixture.addresses.network.append(
pymvr.Network(ipv4=net_fixture.ip_address)
)

if fixture.gdtf_spec:
files_to_pack.append(
Expand Down
12 changes: 2 additions & 10 deletions tui/gdtf_share/gdtf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from types import SimpleNamespace
from textual.message import Message
from textual.screen import ModalScreen
from textual.app import ComposeResult
from textual.containers import (
Grid,
Horizontal,
Vertical,
HorizontalGroup,
VerticalScroll,
)
from textual.widgets import Button, Static, Input, Label, Checkbox, Select, Switch
from textual import on, work, events
from textual_fspicker import FileOpen, Filters
from tui.messages import Errors, DevicesDiscovered
from tui.network import get_network_cards
from tui.artnet import ArtNetDiscovery
from textual.widgets import Button, Static, Input, Select
from textual import work, events
from tui.share_api_client import update_data, download_files
import re
import sys
from pathlib import Path
import json
import asyncio
Expand Down
27 changes: 26 additions & 1 deletion tui/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from textual.message import Message
from dataclasses import dataclass


class MvrParsed(Message):
Expand All @@ -35,10 +36,34 @@ def __init__(self, error: str | None = None) -> None:
super().__init__()


class DevicesDiscovered(Message):
class NetworkDevicesDiscovered(Message):
"""Message sent when monitors are fetched from the API."""

def __init__(self, devices: list | None = None, error: str = "") -> None:
self.devices = devices
self.error = error
super().__init__()


@dataclass
class RdmDevicesDiscovered(Message):
"""Message with discovered devices."""

devices: list | None = None
error: str = ""


@dataclass
class RdmDeviceDetailDiscovered(Message):
"""Message with discovered device details."""

data: dict | None = None
error: str = ""


@dataclass
class RdmDiscoveryMessage(Message):
"""Message to signal RDM discovery is complete."""

label: str = ""
disabled: bool = False
4 changes: 2 additions & 2 deletions tui/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@


def get_network_cards():
all_cards = [("All interfaces 0.0.0.0", "0.0.0.0")]
all_cards = [("All Network Interfaces: 0.0.0.0", "0.0.0.0")]
for adapter in ifaddr.get_adapters():
for ip in adapter.ips:
if isinstance(ip.ip, tuple): # Skip IPv6
continue
if ip.ip.startswith("169.254."): # Skip link-local
continue

label = f"{adapter.nice_name} ({ip.ip})"
label = f"{adapter.nice_name}: {ip.ip}"
value = ip.ip
all_cards.append((label, value))
return all_cards
Loading