Container architecture¶
redsun leverages an architectural denominated to the Device-View-Presenter (DVP). This is semantically close to the definition of the Model-View-Presenter (MVP) architecture.
block-beta
columns 6
V1["View A"]:2
V2["View B"]:2
V3["View C"]:2
VC["VirtualContainer"]:6
P1["Presenter A"]:3
P2["Presenter B"]:3
D1["Device A"]:2
D2["Device B"]:2
D3["Device C"]:2
style V1 fill:#4caf50,color:#fff,stroke:#388e3c
style V2 fill:#4caf50,color:#fff,stroke:#388e3c
style V3 fill:#4caf50,color:#fff,stroke:#388e3c
style VC fill:#ffc107,color:#000,stroke:#f9a825
style P1 fill:#f44336,color:#fff,stroke:#c62828
style P2 fill:#f44336,color:#fff,stroke:#c62828
style D1 fill:#2196f3,color:#fff,stroke:#1565c0
style D2 fill:#2196f3,color:#fff,stroke:#1565c0
style D3 fill:#2196f3,color:#fff,stroke:#1565c0
The key differences in respect to the MVP architecture are the following:
- In
MVP, the Model layer represents the data the application holds; think for example of a text editor: the content of the text is stored in this layer. - In contrast, the Device layer assumes the role of containing all objects interfacing with real hardware; it is both a semantic and pragmatic difference which, to avoid confusion, has been applied in the renaming of the architecture to make the distinction explicit.
- Additionally, in the
MVPpattern, Presenters and Views are tightly coupled between each other, making it difficult to have one without the other. InDVP, both layers are decoupled via a virtual container to follow an approach of dependency injection in order to maintain all the components separated, allowing to bring only the pieces you need to create an application fully compliant with your specifications.
Overview¶
At the core of Redsun is the AppContainer, which acts as the central registry and build system for all application components. Components are declared as class attributes and instantiated in a well-defined dependency order.
Build order — components are constructed in strict dependency sequence:
graph LR
VC[VirtualContainer]
Devices
Presenters
Views
VC --> Devices
Devices --> Presenters
Presenters --> Views
Provider registration and dependency injection — once all components are built, any presenter or view implementing the relevant protocol participates in registration and injection:
graph LR
VC[VirtualContainer]
subgraph Presenters
P1[Presenter A]
P2[Presenter B]
end
subgraph Views
V1[View A]
V2[View B]
end
P1 -.->|IsProvider: register_providers| VC
V1 -.->|IsProvider: register_providers| VC
VC -.->|IsInjectable: inject_dependencies| P2
VC -.->|IsInjectable: inject_dependencies| V2
The DVP pattern¶
redsun builds three types of components:
- Devices: objects interfacing with real hardware components that implement Bluesky's device protocols via
Device. - View: UI components that implement
Viewto display data and capture user interactions. - Presenter: business logic components that implement
Presenter, sitting between models and views, coordinating device operations and updating the UI throughpsygnal.
This separation ensures that hardware drivers, UI components, and business logic can be developed and tested independently.
Declarative containers¶
redsun operates on a bring-your-own components approach. Each component is intended to be developed separately and in isolation or as part of bundles of multiple components that can be dynamically assembled. In a declarative manner, this means importing the components explicitly and assigning them to a container.
Components are declared as class attributes using the layer-specific field specifiers:
[device()][redsun.device],
presenter(), and
view().
Each accepts the component class as its first positional argument, followed by optional keyword arguments forwarded to the constructor.
When writing a container explicitly, you inherit from the frontend-specific subclass rather than the base AppContainer — for Qt applications that is QtAppContainer:
from redsun.containers import device, presenter, view
from redsun.qt import QtAppContainer
def my_app() -> None:
class MyApp(QtAppContainer):
motor = device(MyMotor, axis=["X", "Y"])
ctrl = presenter(MyController, gain=1.0)
ui = view(MyView)
MyApp().run()
The class is defined inside a function so that the Qt imports and any heavy device imports are deferred until the application is actually launched.
The AppContainerMeta metaclass collects these declarations at class creation time. Because the class is passed directly to the field specifier, no annotation inspection is needed. This declarative approach allows the container to:
- validate component types at class creation time;
- inherit and override components from base classes;
- merge configuration from YAML files with inline keyword arguments.
Component naming¶
Every component receives a name that is used as its key in the container's devices, presenters, or views dictionaries and passed as the first positional argument to the component constructor. The name is resolved with the following priority:
alias— if an explicitaliasis passed todevice(),presenter(), orview(), that value is used regardless of everything else.- attribute name — in the declarative flow, the Python attribute name becomes the component name when no
aliasis provided. - YAML key — in the dynamic flow (
from_config()), the top-level key in thedevices/presenters/viewssection of the configuration file becomes the component name.
Examples in the declarative flow:
class MyApp(QtAppContainer):
motor = device(MyMotor) # name → "motor"
cam = device(MyCamera, alias="detector") # name → "detector"
In the dynamic flow:
Configuration file support¶
Components can pull their keyword arguments from a YAML configuration file by passing config= to the class definition and from_config= to each field specifier call:
from redsun.containers import device, presenter, view
from redsun.qt import QtAppContainer
def my_app() -> None:
class MyApp(QtAppContainer, config="app_config.yaml"):
motor = device(MyMotor, from_config="motor")
ctrl = presenter(MyController, from_config="ctrl")
ui = view(MyView, from_config="ui")
MyApp().run()
# alternatively, you can first build and then run the app
app = MyApp()
app.build()
app.run()
The configuration file provides base keyword arguments for each component. These can be selectively overridden by inline keyword arguments in the field specifier call, allowing the same container class to be reused across different hardware setups by swapping configuration files.
Build order¶
When build() is called, the container proceeds in three phases:
Phase 1 — construction:
VirtualContainer— created and seeded with the application configuration.- Devices — each receives its resolved name and keyword arguments.
- Presenters — each receives its resolved name and the full device dictionary.
- Views — each receives its resolved name.
Phase 2 — provider registration:
Any presenter or view implementing IsProvider calls register_providers() on the VirtualContainer. This is safe to run across both layers simultaneously because no injection occurs here.
Phase 3 — dependency injection:
Any presenter or view implementing IsInjectable calls inject_dependencies() on the VirtualContainer, consuming providers registered in phase 2.
Communication¶
Components communicate through the VirtualContainer, which serves as the single shared data exchange layer for the application. It combines two roles:
- Signal registry: components can register their
psygnalsignals into the container viaregister_signals(), making them discoverable by other components without direct references to each other. Registered signals are accessible through thesignalsproperty. - Dependency injection: built on top of
dependency_injector'sDynamicContainer, it allows any presenter or view implementingIsProviderto register typed providers, and any presenter or view implementingIsInjectableto consume them. This enables components across both layers to share information without direct coupling.
The VirtualContainer is created during [build()][redsun.AppContainer.build] and is accessible via the virtual_container property after the container is built.
Two usage flows¶
redsun supports two distinct approaches for assembling an application, both producing the same result at runtime.
Explicit flow (developer-written containers)¶
The explicit flow is for plugin bundle authors who know exactly which components they need and which frontend they target. The container subclass, component classes, and frontend are all fixed at write time:
from redsun.containers import device, presenter, view
from redsun.qt import QtAppContainer
# these are user-developed classes
# that should reflect the structure
# provided by redsun for each layer
from my_package.device import MyMotor
from my_package.presenter import MyPresenter
from my_package.view import MyView
class MyApp(QtAppContainer, config="config.yaml"):
motor = device(MyMotor, from_config="motor")
ctrl = presenter(MyPresenter, from_config="ctrl")
ui = view(MyView, from_config="ui")
MyApp().run()
Dynamic flow (configuration-driven)¶
The dynamic flow is for end users who point Redsun at a YAML configuration file. Plugins are discovered via entry points and the frontend is resolved from the frontend: key in the file — no Python code needs to be written:
The YAML file drives everything:
schema_version: 1.0
session: "My Experiment"
frontend: "pyqt"
devices:
motor:
plugin_name: my-plugin
plugin_id: my_motor
presenters:
...
views:
...
See the component system documentation for a full description of the dynamic flow.
Frontend support¶
Frontend is intended as the toolkit that deploys the functionalities to implement the Graphical User Interface (GUI).
Qt¶
QtAppContainer extends AppContainer with the full Qt lifecycle:
- Creates the
QApplicationinstance. - Calls
build()to instantiate all components. - Constructs the
QtMainViewmain window and docks all views. - Starts the
psygnalsignal queue bridge for thread-safe signal delivery. - Shows the main window and enters the Qt event loop.
It is imported from the public redsun.qt namespace:
Both PyQt6 or PySide6 wrapped via qtpy are supported.
Other frontends¶
The future expectation is to provide support for other frontends (either desktop or web-based).
While the presenter and device layer are decoupled via the VirtualContainer, the View layer is tied to the frontend selection and plugins will have to implement each View according to the toolkit that the frontend provides. The hope is to find a way to minimize the code required to implement the UI and to simplify this approach across the board, regardless of the specified frontend.