Skip to content

JubileeManager API Reference

The JubileeManager class is the primary interface for controlling the Jubilee powder dispensing system. It provides high-level methods for common operations and coordinates multiple hardware components.

Overview

JubileeManager is designed to be the main entry point for:

  • Connecting to and managing hardware
  • Performing dispense operations
  • Reading scale weights
  • Coordinating complex multi-step movements

All movements are validated through an internal MotionPlatformStateMachine which cannot be bypassed, ensuring safety and consistency.

Class Reference

JubileeManager

JubileeManager(num_piston_dispensers=0, num_pistons_per_dispenser=0, feedrate=MEDIUM)

High-level manager for Jubilee powder dispensing operations.

JubileeManager provides a simplified interface for controlling the Jubilee for powder dispensing tasks. It coordinates multiple hardware components (machine, scale, dispensers, manipulator) and ensures all operations are safe through state machine validation.

All movements are validated through the MotionPlatformStateMachine, which is owned by this manager and cannot be bypassed. This ensures safety and prevents invalid state transitions.

ATTRIBUTE DESCRIPTION
scale

Connected scale instance for weight measurements, or None if not connected.

TYPE: Optional[Scale]

manipulator

Manipulator tool instance for mold handling, or None if not initialized.

TYPE: Optional[Manipulator]

state_machine

Internal state machine for movement validation, or None before connection.

TYPE: Optional[MotionPlatformStateMachine]

connected

Boolean indicating whether hardware is connected and ready.

TYPE: bool

Example

Basic usage pattern::

manager = JubileeManager(num_piston_dispensers=2, num_pistons_per_dispenser=10)

try:
    if manager.connect():
        weight = manager.get_weight_stable()
        manager.dispense_to_well("0", 50.0)
finally:
    manager.disconnect()
Note
  • Always call disconnect() when done to properly release hardware resources
  • Check connected property before performing operations
  • Use machine_read_only only for queries, never for movements

Initialize the JubileeManager.

Creates a new manager instance with specified dispenser configuration. Does not connect to hardware - call connect() to establish connections.

PARAMETER DESCRIPTION
num_piston_dispensers

Number of piston dispenser units to initialize. Each dispenser can hold multiple pistons. Default is 0.

TYPE: int DEFAULT: 0

num_pistons_per_dispenser

Initial number of pistons in each dispenser. Used to track available pistons. Default is 0.

TYPE: int DEFAULT: 0

feedrate

Default movement speed for operations. Options are SLOW, MEDIUM, or FAST from the FeedRate enum. Default is MEDIUM.

TYPE: FeedRate DEFAULT: MEDIUM

Example
# Create manager with 2 dispensers, 10 pistons each, medium speed
manager = JubileeManager(
    num_piston_dispensers=2,
    num_pistons_per_dispenser=10,
    feedrate=FeedRate.MEDIUM
)
Note
  • No hardware connection is established during initialization
  • Dispenser counts can be zero if pistons are not needed
  • Feedrate affects all subsequent movements after connection

Attributes

machine_read_only property

machine_read_only

Read-only access to the underlying Jubilee Machine instance.

Provides access to the Machine object for read operations only (queries, status checks, position reads). While it's technically possible to perform write operations through this property, doing so bypasses the state machine safety guarantee and should be avoided.

RETURNS DESCRIPTION
Optional[Machine]

The Machine instance if connected, None otherwise.

Warning

This property is named "read_only" as a strong hint that it should ONLY be used for read operations. Performing movements or state changes through this property bypasses the state machine safety guarantee and can lead to:

  • Collisions with labware
  • Invalid state transitions
  • Unsafe operations
  • Loss of state tracking
Example
# GOOD: Query current position
if manager.machine_read_only:
    pos = manager.machine_read_only.get_position()
    print(f"Current position: {pos}")

# BAD: Perform movements (bypasses validation!)
manager.machine_read_only.move_to(x=100, y=100)  # Don't do this!
Note

Always use JubileeManager's high-level methods or the state machine's validated methods for any operations that change machine state.

deck property

deck

Access to the deck configuration and labware layout.

Provides access to the Deck object which contains information about labware positions, well plates, and deck layout.

RETURNS DESCRIPTION
Optional[Deck]

The Deck instance if state machine is initialized, None otherwise.

Example
if manager.deck:
    labware = manager.deck.get_labware()
    print(f"Available labware: {list(labware.keys())}")

piston_dispensers property

piston_dispensers

Access to all configured piston dispensers.

Provides access to the list of PistonDispenser instances managed by the state machine. Each dispenser tracks its piston count and position.

RETURNS DESCRIPTION
List[PistonDispenser]

List of PistonDispenser instances. Empty list if none configured

List[PistonDispenser]

or state machine not initialized.

Example
# Check available pistons across all dispensers
for dispenser in manager.piston_dispensers:
    print(f"Dispenser {dispenser.index}: {dispenser.num_pistons} pistons")

# Find first dispenser with available pistons
available = next(
    (d for d in manager.piston_dispensers if d.num_pistons > 0),
    None
)

Functions

connect

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

Connect to all hardware and initialize the system.

Establishes connections to the Jubilee machine controller and scale, initializes the state machine with configuration, sets up dispensers, and performs homing operations to establish a known state.

This method performs the following sequence:

  1. Connect to Jubilee machine (Duet controller)
  2. Connect to precision scale
  3. Initialize state machine with configuration
  4. Initialize deck layout and piston dispensers
  5. Create and configure manipulator tool
  6. Home all machine axes (X, Y, Z, U)
  7. Pick up manipulator tool
  8. Home manipulator axis (V)
PARAMETER DESCRIPTION
machine_address

IP address of the Jubilee's Duet controller. If None, uses the IP address from system configuration file. Examples: "192.168.1.100", "10.0.0.50".

TYPE: Optional[str] DEFAULT: None

scale_port

Serial port path for scale connection. Common values: Linux: "/dev/ttyUSB0", "/dev/ttyACM0" Windows: "COM3", "COM4" macOS: "/dev/tty.usbserial-*"

TYPE: str DEFAULT: '/dev/ttyUSB0'

state_machine_config

Path to JSON file defining state machine positions and transitions. Relative or absolute path accepted.

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
bool

True if all connections and initializations succeeded, False if any

bool

step failed. Check the connected property after calling.

RAISES DESCRIPTION
FileNotFoundError

If state_machine_config file does not exist.

RuntimeError

If homing, tool pickup, or manipulator homing fails.

ConnectionError

If unable to connect to machine or scale.

Example
manager = JubileeManager(num_piston_dispensers=2, num_pistons_per_dispenser=10)

# Connect with explicit IP
if manager.connect(machine_address="192.168.1.100", scale_port="/dev/ttyUSB0"):
    print("Connected successfully!")
else:
    print("Connection failed - check hardware and configuration")

# Connect using config file IP
if manager.connect():  # Uses IP from system_config.json
    print("Connected using configured IP")
Note
  • This operation can take 30-60 seconds due to homing
  • All axes must be clear of obstacles before homing
  • Ensure no tool is already picked up before calling
  • Connection state is stored in self.connected property
  • On failure, partial connections are not cleaned up automatically
Warning

If connection fails partway through (e.g., after machine connects but before homing completes), you may need to manually reset the hardware before attempting to connect again.

disconnect

disconnect()

Disconnect from all hardware components and release resources.

Cleanly disconnects from the Jubilee machine and scale, releasing any held resources. This should always be called when done using the manager.

Example
manager = JubileeManager()
try:
    manager.connect()
    # ... perform operations ...
finally:
    manager.disconnect()  # Always disconnect
Note
  • Safe to call multiple times
  • Safe to call even if not fully connected
  • Does not raise exceptions on disconnection errors
  • Sets connected property to False

get_weight_stable

get_weight_stable()

Get current weight from scale, waiting for stability.

Reads the scale weight, waiting for the reading to stabilize before returning. This is the recommended method for measurements that will be recorded or used for decisions.

RETURNS DESCRIPTION
float

Weight in grams. Returns 0.0 if scale is not connected or on error.

Example
# Get stable reading for recording
weight = manager.get_weight_stable()
print(f"Stable weight: {weight:.3f}g")

# Use in conditional
if manager.get_weight_stable() > 50.0:
    print("Target weight exceeded")
Note
  • Waits for scale to report stable reading (may take 1-3 seconds)
  • More accurate than get_weight_unstable()
  • Returns 0.0 on error rather than raising exceptions
  • Check scale.is_connected if you need to distinguish no scale from zero weight
See Also

get_weight_unstable: For real-time weight monitoring without waiting

get_weight_unstable

get_weight_unstable()

Get instantaneous weight from scale without waiting for stability.

Reads the current scale weight immediately, without waiting for the reading to stabilize. Useful for real-time monitoring but not recommended for recorded measurements.

RETURNS DESCRIPTION
float

Current weight in grams. Returns 0.0 if scale is not connected or on error.

Example
# Monitor weight in real-time during filling
while filling:
    current = manager.get_weight_unstable()
    print(f"Current: {current:.2f}g", end='
')
    time.sleep(0.1)

# Get final stable reading
final = manager.get_weight_stable()
Note
  • Returns immediately without waiting
  • Reading may still be changing (unstable)
  • Not suitable for decisions or permanent records
  • Use get_weight_stable() for measurements you'll record
  • Returns 0.0 on error rather than raising exceptions
See Also

get_weight_stable: For accurate measurements after stabilization

dispense_to_well

dispense_to_well(well_id, target_weight)

Perform complete powder dispense operation to a well.

This is the primary high-level operation for dispensing powder. It performs a complete workflow including picking up the mold, filling with powder to target weight, retrieving a piston, and returning the mold to its slot.

The operation sequence is:

  1. Move to mold slot position
  2. Pick up empty mold from slot
  3. Move to scale
  4. Place mold on scale
  5. Fill with powder to target weight
  6. Pick up filled mold from scale
  7. Move to piston dispenser
  8. Retrieve piston from dispenser
  9. Move back to mold slot
  10. Place mold (now with powder and piston) back in slot
PARAMETER DESCRIPTION
well_id

Identifier for the target well/mold slot using numerical indexing. Must match an entry in the deck configuration (e.g., "0", "1", "2").

TYPE: str

target_weight

Target weight of powder to dispense, in grams. The system will fill until this weight is reached (within tolerance).

TYPE: float

RETURNS DESCRIPTION
bool

True if the entire operation completed successfully, False if any step

bool

failed or if not connected.

RAISES DESCRIPTION
ToolStateError

If manipulator or scale is not available.

RuntimeError

If state machine is not configured.

ValueError

If well_id is not found in deck configuration.

Example
manager = JubileeManager(num_piston_dispensers=2, num_pistons_per_dispenser=10)

if manager.connect():
    # Dispense 50g of powder to mold 0
    success = manager.dispense_to_well("0", target_weight=50.0)

    if success:
        print("Dispense completed successfully!")
        weight = manager.get_weight_stable()
        print(f"Final weight: {weight}g")
    else:
        print("Dispense failed - check logs for details")

    manager.disconnect()
Note
  • Requires at least one dispenser with available pistons
  • All movements are validated through state machine
  • Operation can take 2-5 minutes depending on target weight
  • If operation fails partway through, system may be in intermediate state
  • Check return value before assuming success
Warning

If the operation fails after picking up the mold but before returning it, the mold may be left at an intermediate position. Manual intervention may be required to return to a safe state.

Usage Examples

Basic Connection and Usage

from src.JubileeManager import JubileeManager

# Create manager instance
manager = JubileeManager(
    num_piston_dispensers=2,
    num_pistons_per_dispenser=10
)

# Connect to hardware
if manager.connect(machine_address="192.168.1.100"):
    print("Connected successfully!")

    # Use the manager
    weight = manager.get_weight_stable()
    print(f"Current weight: {weight}g")

    # Clean up
    manager.disconnect()

Performing Dispense Operations

# After connecting...
success = manager.dispense_to_well(
    well_id="0",
    target_weight=50.0
)

if success:
    print("Dispense completed successfully!")
else:
    print("Dispense failed - check logs for details")

Accessing Hardware Components

# Read-only access to machine (for queries, not movements)
if manager.machine_read_only:
    position = manager.machine_read_only.get_position()
    print(f"Current position: {position}")

# Access deck for labware information
if manager.deck:
    labware = manager.deck.get_labware()
    print(f"Available labware: {labware}")

# Access piston dispensers
for dispenser in manager.piston_dispensers:
    print(f"Dispenser {dispenser.index}: {dispenser.num_pistons} pistons")

Function Call Error Handling

from src.JubileeManager import JubileeManager

manager = JubileeManager()

try:
    if not manager.connect():
        raise ConnectionError("Failed to connect to Jubilee")

    # Perform operations
    success = manager.dispense_to_well("0", 50.0)
    if not success:
        print("Operation failed but system is still connected")

except Exception as e:
    print(f"Error occurred: {e}")

finally:
    # Always disconnect
    manager.disconnect()

Internal Methods

The following methods are primarily for internal use but are documented for developers:

_move_to_mold_slot

_move_to_mold_slot(well_id)

Move to a specific mold slot position.

Internal method that moves to the position where the manipulator can pick up or place a mold in the specified well. The target position is determined by the well's configuration in the deck layout.

PARAMETER DESCRIPTION
well_id

Identifier for the target well using numerical indexing (e.g., "0", "1", "2"). Must exist in the deck configuration's labware definition.

TYPE: str

RETURNS DESCRIPTION
bool

True if movement succeeded.

RAISES DESCRIPTION
RuntimeError

If state machine is not configured or movement validation fails. Validation failure reasons include wrong position, wrong tool, or invalid payload state.

KeyError

If well_id is not found in deck configuration.

Note
  • This is an internal method; typically called by dispense_to_well()
  • Uses the well's ready_pos field from deck configuration
  • Movement is validated through state machine
  • Does not pick up or place the mold, only positions for access

_move_to_scale

_move_to_scale()

Move to the scale ready position.

Internal method that moves the manipulator to the position where it can place or pick up molds on the scale. Movement is validated through the state machine.

RETURNS DESCRIPTION
bool

True if movement succeeded, False if scale is not configured.

RAISES DESCRIPTION
RuntimeError

If state machine is not configured or movement validation fails. Common failure reasons include wrong tool active, invalid payload state, or unable to transition from current position.

Note
  • This is an internal method; typically called by dispense_to_well()
  • Moves to scale_ready position defined in state machine config
  • Does not place or pick up mold, only positions for access
  • Movement is validated against current state

_move_to_dispenser

_move_to_dispenser(dispenser_index)

Move to the ready position for a specific piston dispenser.

Internal method that moves the manipulator to the position where it can retrieve a piston from the specified dispenser. Movement is validated through the state machine.

PARAMETER DESCRIPTION
dispenser_index

Index of the target dispenser (0-based). Must be less than the number of configured dispensers.

TYPE: int

RETURNS DESCRIPTION
bool

True if movement succeeded, False if not connected, no dispensers, or

bool

movement validation failed.

RAISES DESCRIPTION
RuntimeError

If state machine is not configured or movement validation fails.

ValueError

If dispenser_index is out of range.

Note
  • This is an internal method; typically called by dispense_to_well()
  • Movement is validated against current state (tool, payload, position)
  • Does not retrieve the piston, only positions for retrieval

_fill_powder

_fill_powder(target_weight)

Fill mold with powder to target weight.

Internal method that dispenses powder into a mold using the trickler mechanism, monitoring the scale until the target weight is reached. The mold must already be placed on the scale.

PARAMETER DESCRIPTION
target_weight

Target weight of powder to dispense, in grams.

TYPE: float

RETURNS DESCRIPTION
bool

True if filling succeeded, False if scale is not configured.

RAISES DESCRIPTION
RuntimeError

If state machine is not configured or fill operation validation fails. Validation ensures mold is on scale and system is in correct state for powder dispensing.

Note
  • This is an internal method; typically called by dispense_to_well()
  • Mold must already be on the scale before calling
  • Operation continues until target weight is reached (within tolerance)
  • Duration depends on target weight and trickler speed (typically 1-3 min)
  • Continuously monitors scale during filling
Warning

Calling this method without a mold on the scale will result in powder being dispensed directly onto the scale, which is incorrect operation.

get_piston_from_dispenser

get_piston_from_dispenser(dispenser_index)

Retrieve the top piston from a specific dispenser.

Retrieves a piston from the specified dispenser and places it into the mold currently held by the manipulator. The operation is validated through the state machine to ensure safety.

PARAMETER DESCRIPTION
dispenser_index

Index of the dispenser to retrieve from (0-based). Must be less than the number of configured dispensers.

TYPE: int

RETURNS DESCRIPTION
bool

True if piston was successfully retrieved, False if not connected,

bool

no dispensers available, or retrieval failed.

RAISES DESCRIPTION
RuntimeError

If state machine is not configured or retrieval validation fails.

ValueError

If dispenser_index is out of range.

Example
# Manually retrieve piston (typically done by dispense_to_well)
if manager._move_to_dispenser(0):
    if manager.get_piston_from_dispenser(0):
        print("Piston retrieved successfully")
Note
  • Must already be at the dispenser ready position (call _move_to_dispenser() first)
  • Requires mold to be held by manipulator
  • Automatically decrements piston count in the dispenser
  • Validation ensures proper state before and after retrieval
Warning

Calling this method without first moving to the dispenser position will fail validation. Always call _move_to_dispenser() first.

Design Notes

State Machine Ownership

JubileeManager owns the MotionPlatformStateMachine instance. This design ensures:

  • All movements must go through validation
  • No external code can bypass safety checks
  • Consistent state tracking across the system

Read-Only Machine Access

The machine_read_only property provides access to the underlying Machine object for read operations only. While it's technically possible to perform movement operations through this property, this bypasses safety validation without updating the JubileeManager's internal state. Doing so is strongly discouraged.

Use machine_read_only only for: - Querying current position - Reading sensor values - Checking machine state

Never use it for: - Moving axes - Picking/parking tools - Any operation that changes machine state

Connection Sequence

The connect() method performs several initialization steps:

  1. Connects to the Duet controller
  2. Connects to the scale
  3. Initializes the state machine with configuration
  4. Initializes the deck and dispensers
  5. Homes all axes (X, Y, Z, U)
  6. Picks up the manipulator tool
  7. Homes the manipulator axis (V)

This ensures the system is in a known, safe state before operations begin.

See Also