Skip to content

JubileeViewModel API Reference

The JubileeViewModel class coordinates between the GUI and JubileeManager, following an MVVM-inspired architecture pattern.

Overview

JubileeViewModel serves as the coordination layer that:

  • Manages hardware configuration before connection
  • Drives the JubileeManager to execute operations
  • Executes multi-well dispensing jobs systematically
  • Provides callbacks to update the GUI on progress
  • Handles errors and provides user-friendly feedback

Architecture Role

GUI (View) → ViewModel (Coordinator) → JubileeManager (Model) → Hardware

The ViewModel:

  • Receives requests from the GUI
  • Coordinates operations through JubileeManager
  • Notifies GUI of changes via callbacks

Class Reference

JubileeViewModel

JubileeViewModel(on_connection_changed=None, on_weight_changed=None, on_status_changed=None, on_job_progress=None, on_job_completed=None, on_error=None)

ViewModel for coordinating GUI and JubileeManager.

This class acts as the coordination layer between the View (GUI) and Model (JubileeManager). It drives the hardware through the JubileeManager while providing callbacks to update the GUI on progress and state changes.

Responsibilities: - Manage connection to hardware via JubileeManager - Store and update hardware configuration (dispensers, pistons) - Execute dispensing jobs systematically - Provide progress updates via callbacks - Handle errors and provide meaningful feedback

Example
# Create ViewModel with callbacks
view_model = JubileeViewModel(
    on_connection_changed=lambda connected: print(f"Connected: {connected}"),
    on_weight_changed=lambda weight: print(f"Weight: {weight}g"),
    on_status_changed=lambda status: print(status),
    on_job_progress=lambda completed, total, well: print(f"{completed}/{total}")
)

# Configure hardware
view_model.set_hardware_config(num_dispensers=2, pistons_per_dispenser=10)

# Connect to hardware
if view_model.connect():
    # Start a job
    jobs = [
        DispensingJob("0", 50.0),
        DispensingJob("1", 45.0)
    ]
    view_model.start_job(jobs)

Initialize the ViewModel.

PARAMETER DESCRIPTION
on_connection_changed

Callback when connection state changes (connected: bool)

TYPE: Optional[Callable[[bool], None]] DEFAULT: None

on_weight_changed

Callback when scale weight updates (weight: float)

TYPE: Optional[Callable[[float], None]] DEFAULT: None

on_status_changed

Callback when status message changes (status: str)

TYPE: Optional[Callable[[str], None]] DEFAULT: None

on_job_progress

Callback for job progress (completed: int, total: int, current_well: str)

TYPE: Optional[Callable[[int, int, str], None]] DEFAULT: None

on_job_completed

Callback when job finishes successfully

TYPE: Optional[Callable[[], None]] DEFAULT: None

on_error

Callback when an error occurs (error_message: str)

TYPE: Optional[Callable[[str], None]] DEFAULT: None

Attributes

connected property

connected

Check if connected to hardware

job_running property

job_running

Check if a job is currently running

num_dispensers property

num_dispensers

Get current number of dispensers configured

pistons_per_dispenser property

pistons_per_dispenser

Get current number of pistons per dispenser configured

Functions

set_hardware_config

set_hardware_config(num_dispensers, pistons_per_dispenser, feedrate=MEDIUM)

Update hardware configuration.

Sets the hardware configuration that will be used when connecting to the JubileeManager. Can only be called when not connected.

PARAMETER DESCRIPTION
num_dispensers

Number of piston dispensers available

TYPE: int

pistons_per_dispenser

Number of pistons in each dispenser

TYPE: int

feedrate

Movement speed (SLOW, MEDIUM, FAST)

TYPE: FeedRate DEFAULT: MEDIUM

RAISES DESCRIPTION
RuntimeError

If called while connected to hardware

connect

connect(machine_address=None, scale_port='/dev/ttyUSB0')

Connect to hardware using current configuration.

Creates a JubileeManager with the current hardware configuration and connects to the Jubilee machine and scale. This is a blocking operation that can take 30-60 seconds due to homing.

PARAMETER DESCRIPTION
machine_address

IP address of Jubilee (None = use config file)

TYPE: Optional[str] DEFAULT: None

scale_port

Serial port for scale connection

TYPE: str DEFAULT: '/dev/ttyUSB0'

RETURNS DESCRIPTION
bool

True if connection successful, False otherwise

disconnect

disconnect()

Disconnect from hardware and clean up resources.

Stops any running jobs, stops weight monitoring, and disconnects from the JubileeManager.

get_current_weight

get_current_weight()

Get current weight from scale.

RETURNS DESCRIPTION
float

Current weight in grams, or 0.0 if not connected

start_job

start_job(jobs)

Start a dispensing job with the given wells.

Executes dispensing operations for each well in the job list, calling the progress callback after each well is completed.

PARAMETER DESCRIPTION
jobs

List of DispensingJob objects specifying wells and target weights

TYPE: List[DispensingJob]

RETURNS DESCRIPTION
bool

True if job started successfully, False if already running or not connected

stop_job

stop_job()

Stop the currently running job.

Sets a flag to stop the job after the current well is completed. The job will not stop immediately.

get_dispenser_status

get_dispenser_status()

Get status of all piston dispensers.

RETURNS DESCRIPTION
List[Dict[str, any]]

List of dicts with dispenser information:

List[Dict[str, any]]
  • index: Dispenser index
List[Dict[str, any]]
  • pistons_remaining: Number of pistons left

update_dispenser_pistons

update_dispenser_pistons(dispenser_index, num_pistons)

Update the number of pistons in a specific dispenser.

This allows the user to modify the piston count if they manually reload or change dispensers.

PARAMETER DESCRIPTION
dispenser_index

Index of dispenser to update

TYPE: int

num_pistons

New number of pistons

TYPE: int

RETURNS DESCRIPTION
bool

True if update successful, False if invalid index or not connected

DispensingJob

DispensingJob dataclass

DispensingJob(well_id, target_weight, current_weight=0.0, completed=False, error=None)

Represents a single well in a dispensing job

Usage Examples

Basic Setup

from gui.jubilee_view_model import JubileeViewModel, DispensingJob

# Define callbacks for GUI updates
def on_status(status: str):
    print(f"Status: {status}")

def on_progress(completed: int, total: int, current_well: str):
    print(f"Progress: {completed}/{total} - {current_well}")

# Create ViewModel
view_model = JubileeViewModel(
    on_status_changed=on_status,
    on_job_progress=on_progress
)

# Configure hardware before connecting
view_model.set_hardware_config(
    num_dispensers=2,
    pistons_per_dispenser=10
)

Connecting to Hardware

# Connect (this will create JubileeManager with configured settings)
if view_model.connect(machine_address="192.168.1.100"):
    print("Connected successfully!")
    print(f"Dispensers: {view_model.num_dispensers}")
    print(f"Pistons per dispenser: {view_model.pistons_per_dispenser}")
else:
    print("Connection failed")

Running a Dispensing Job

# Define wells to fill
jobs = [
    DispensingJob(well_id="0", target_weight=50.0),
    DispensingJob(well_id="1", target_weight=45.0),
    DispensingJob(well_id="2", target_weight=52.0),
]

# Start job (runs in background thread)
if view_model.start_job(jobs):
    print("Job started")

    # Job runs asynchronously
    # Progress updates come via on_job_progress callback
    # Completion notification via on_job_completed callback

Monitoring Progress

# Callbacks provide real-time updates
def on_connection_changed(connected: bool):
    if connected:
        print("Hardware connected")
    else:
        print("Hardware disconnected")

def on_weight_changed(weight: float):
    print(f"Current weight: {weight:.3f}g")

def on_job_progress(completed: int, total: int, current_well: str):
    print(f"Completed {completed}/{total} wells")
    print(f"Currently processing: {current_well}")

def on_job_completed():
    print("Job finished successfully!")

def on_error(error_msg: str):
    print(f"Error: {error_msg}")

# Create ViewModel with all callbacks
view_model = JubileeViewModel(
    on_connection_changed=on_connection_changed,
    on_weight_changed=on_weight_changed,
    on_status_changed=lambda s: print(s),
    on_job_progress=on_job_progress,
    on_job_completed=on_job_completed,
    on_error=on_error
)

Checking Dispenser Status

# Get status of all dispensers
status = view_model.get_dispenser_status()

for dispenser in status:
    print(f"Dispenser {dispenser['index']}: "
          f"{dispenser['pistons_remaining']} pistons")

Updating Piston Counts

# User manually reloaded dispenser 0 with 20 pistons
success = view_model.update_dispenser_pistons(
    dispenser_index=0,
    num_pistons=20
)

if success:
    print("Dispenser piston count updated")

Stopping a Job

# User wants to stop the current job
view_model.stop_job()

# Job will stop after current well completes
# Status update via on_status_changed callback

Callback System

The ViewModel uses callbacks to notify the GUI of changes. This allows the GUI to remain responsive while operations run in background threads.

Available Callbacks

Callback Parameters When Called
on_connection_changed connected: bool Connection state changes
on_weight_changed weight: float Scale weight updates (500ms)
on_status_changed status: str Status message changes
on_job_progress completed: int, total: int, current_well: str Job progress updates
on_job_completed None Job finishes successfully
on_error error_msg: str Error occurs

Thread Safety

All callbacks are called from background threads. GUI frameworks typically require updates to happen on the main thread. Use your framework's thread-safe update mechanism:

Kivy Example:

from kivy.clock import Clock

def on_status_changed(status: str):
    # Schedule update on main thread
    def update(dt):
        self.status_label.text = status
    Clock.schedule_once(update, 0)

Design Notes

Hardware Configuration

Hardware configuration (dispensers, pistons) is set in the ViewModel before connection. When connect() is called, the ViewModel creates a JubileeManager with these settings. The actual hardware state is then stored in the JubileeManager.

Job Execution

Jobs are executed systematically in a background thread:

  1. Validate piston availability
  2. For each well in order:
  3. Call JubileeManager.dispense_to_well()
  4. Update progress via callback
  5. Check for stop flag
  6. Notify completion via callback

Error Handling

Errors are caught and reported via the on_error callback. Jobs stop on first error to prevent cascading failures.

See Also