Skip to content

dummy_cnc

plantimager.controller.scanner.dummy_cnc Link

Dummy CNC Implementation for Plant Imaging Systems.

A simulated CNC controller that mimics the behavior of a real GRBL-based CNC machine without requiring actual hardware. This is useful for development, testing, and demonstration purposes when physical hardware is unavailable.

Key Features: - Simulates all standard CNC operations (movement, homing, positioning) - Implements the AbstractCNC interface for compatibility with scanner code - Adds realistic delays and position noise to mimic real hardware behavior - Provides status reporting and command handling like a real GRBL controller - Enforces position limits and validates movement commands - Thread-safe asynchronous operation support

Usage Examples:

>>> from plantimager.controller.scanner.dummy_cnc import DummyCNC
>>> # Initialize the dummy CNC
>>> cnc = DummyCNC()
>>> # Perform operations just like with a real CNC
>>> cnc.home()
>>> cnc.moveto(100, 100, 45)
>>> x, y, z = cnc.get_position()
>>> print(f"Current position: ({x}, {y}, {z})")
>>> # Async movement
>>> cnc.moveto_async(200, 200, 90)
>>> # Do other things...
>>> cnc.wait()  # Wait for movement to complete

DummyCNC Link

DummyCNC(x_limits=(0, 740), y_limits=(0, 740), z_limits=(0, 360))

Bases: AbstractCNC

A dummy implementation of CNC machine control for testing purposes.

This class mimics the behavior of the real CNC class without connecting to actual hardware. It simulates basic CNC operations like movement, positioning, and homing.

Attributes:

Name Type Description
x_lims tuple[float, float]

Allowed range for X-axis movement

y_lims tuple[float, float]

Allowed range for Y-axis movement

z_lims tuple[float, float]

Allowed range for Z-axis movement (rotational axis)

_position tuple[float, float, float]

Current position (x, y, z)

_busy bool

Flag indicating if the CNC is currently moving

Initialize the dummy CNC controller with default axis limits.

Source code in plantimager/controller/scanner/dummy_cnc.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def __init__(self, x_limits: tuple[float, float] = (0, 740), y_limits: tuple[float, float] = (0, 740),
             z_limits: tuple[float, float] = (0, 360)) -> None:
    """Initialize the dummy CNC controller with default axis limits."""
    super().__init__()
    self.x_lims = x_limits
    self.y_lims = y_limits
    self.z_lims = z_limits
    self._position = (0, 0, 0)  # Initial position
    self._busy = False
    self.has_started = True

    # Simulate initialization delay
    time.sleep(0.2)
    logger.info("Dummy CNC initialized")

x property Link

x

Get the current X-axis position.

y property Link

y

Get the current Y-axis position.

z property Link

z

Get the current Z-axis position.

get_position Link

get_position()

Get the current XYZ position of the dummy CNC machine.

Returns:

Type Description
length_mm

Current X-axis position in millimeters

length_mm

Current Y-axis position in millimeters

deg

Current Z-axis position in degrees

Source code in plantimager/controller/scanner/dummy_cnc.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def get_position(self) -> tuple[length_mm, length_mm, deg]:
    """Get the current XYZ position of the dummy CNC machine.

    Returns
    -------
    length_mm
        Current X-axis position in millimeters
    length_mm
        Current Y-axis position in millimeters
    deg
        Current Z-axis position in degrees
    """
    # Add a small amount of noise to simulate real sensor readings
    noise_x = random.uniform(-0.01, 0.01)
    noise_y = random.uniform(-0.01, 0.01)
    noise_z = random.uniform(-0.01, 0.01)

    return (
        self._position[0] + noise_x,
        self._position[1] + noise_y,
        self._position[2] + noise_z
    )

get_status Link

get_status()

Simulate querying the current status of the GRBL controller.

Returns:

Type Description
dict

A dictionary containing simulated status information

Source code in plantimager/controller/scanner/dummy_cnc.py
279
280
281
282
283
284
285
286
287
288
289
290
291
def get_status(self) -> dict:
    """Simulate querying the current status of the GRBL controller.

    Returns
    -------
    dict
        A dictionary containing simulated status information
    """
    state = "Idle" if not self._busy else "Run"
    return {
        'status': state,
        'position': self._position
    }

home Link

home()

Simulate performing a homing cycle.

Sets the current position to a starting point near the origin, simulating the behavior of the real CNC homing procedure.

Source code in plantimager/controller/scanner/dummy_cnc.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def home(self) -> None:
    """Simulate performing a homing cycle.

    Sets the current position to a starting point near the origin,
    simulating the behavior of the real CNC homing procedure.
    """
    logger.info("Dummy CNC homing...")
    self._busy = True
    # Simulate homing time
    time.sleep(1.5)
    # Set position to a slight offset from zero, simulating a real homing result
    self._position = (20, 20, 0)
    self._busy = False
    logger.info("Dummy CNC homing completed")

moveto Link

moveto(x, y, z)

Move the dummy CNC machine to specified coordinates and wait until the target position is reached.

Parameters:

Name Type Description Default
x length_mm

Target position along the X-axis in millimeters

required
y length_mm

Target position along the Y-axis in millimeters

required
z deg

Target position along the Z-axis in degrees

required

Raises:

Type Description
ValueError

If any of the target coordinates are outside the allowed limits

Source code in plantimager/controller/scanner/dummy_cnc.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def moveto(self, x: length_mm, y: length_mm, z: deg) -> None:
    """Move the dummy CNC machine to specified coordinates and wait until the target position is reached.

    Parameters
    ----------
    x : length_mm
        Target position along the X-axis in millimeters
    y : length_mm
        Target position along the Y-axis in millimeters
    z : deg
        Target position along the Z-axis in degrees

    Raises
    ------
    ValueError
        If any of the target coordinates are outside the allowed limits
    """
    self.moveto_async(x, y, z)
    self.wait()

moveto_async Link

moveto_async(x, y, z)

Asynchronously move the dummy CNC machine to specified coordinates.

Parameters:

Name Type Description Default
x length_mm

Target position along the X-axis in millimeters

required
y length_mm

Target position along the Y-axis in millimeters

required
z deg

Target position along the Z-axis in degrees

required

Raises:

Type Description
ValueError

If any of the target coordinates are outside the allowed limits

Source code in plantimager/controller/scanner/dummy_cnc.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def moveto_async(self, x: length_mm, y: length_mm, z: deg) -> None:
    """Asynchronously move the dummy CNC machine to specified coordinates.

    Parameters
    ----------
    x : length_mm
        Target position along the X-axis in millimeters
    y : length_mm
        Target position along the Y-axis in millimeters
    z : deg
        Target position along the Z-axis in degrees

    Raises
    ------
    ValueError
        If any of the target coordinates are outside the allowed limits
    """
    # Convert to float to ensure compatibility
    x, y, z = float(x), float(y), float(z)

    # Check if the move is within limits
    self._check_move(x, y, z)

    # Set busy flag
    self._busy = True

    # Log the movement
    logger.info(f"Dummy CNC moving to (x={x}, y={y}, z={z})")

    # Start simulated movement in a separate thread
    threading.Thread(
        target=self._simulate_movement,
        args=(x, y, z),
        daemon=True
    ).start()

send_cmd Link

send_cmd(cmd)

Simulate sending a command to the GRBL controller.

Parameters:

Name Type Description Default
cmd str

A GRBL-compatible G-code command or system command

required

Returns:

Type Description
str

Simulated response from the controller

Source code in plantimager/controller/scanner/dummy_cnc.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def send_cmd(self, cmd: str) -> str:
    """Simulate sending a command to the GRBL controller.

    Parameters
    ----------
    cmd : str
        A GRBL-compatible G-code command or system command

    Returns
    -------
    str
        Simulated response from the controller
    """
    logger.debug(f"Dummy CNC received command: {cmd}")
    # Simulate response delay
    time.sleep(0.1)
    return "ok"

stop Link

stop()

Stop the dummy CNC and clean up resources.

Source code in plantimager/controller/scanner/dummy_cnc.py
256
257
258
259
def stop(self) -> None:
    """Stop the dummy CNC and clean up resources."""
    logger.info("Stopping dummy CNC")
    self._busy = False

wait Link

wait(timeout=60)

Wait for the dummy CNC machine to complete any ongoing operations.

Parameters:

Name Type Description Default
timeout int

Maximum time to wait in seconds, by default 60

60

Raises:

Type Description
TimeoutError

If the simulated movement doesn't complete within the timeout

Source code in plantimager/controller/scanner/dummy_cnc.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
def wait(self, timeout: int=60) -> None:
    """Wait for the dummy CNC machine to complete any ongoing operations.

    Parameters
    ----------
    timeout : int, optional
        Maximum time to wait in seconds, by default 60

    Raises
    ------
    TimeoutError
        If the simulated movement doesn't complete within the timeout
    """
    start_time = time.time()

    while self._busy:
        time.sleep(0.1)  # Small delay to prevent CPU overload
        if time.time() - start_time > timeout:
            raise TimeoutError("Timeout waiting for CNC movement to complete")