Skip to content

scan

plantimager.webui.scan Link

Scan Configuration and Execution Components for Plant Imager Web UI.

This module provides the user interface components and functionality for configuring and executing plant scans in the Plant Imager system.

Key Features
  • TOML-based scan configuration editor
  • Dataset name validation with real-time feedback
  • Scan execution controls with status reporting
  • Configuration file upload functionality
  • Comprehensive error handling and user feedback

all_valid_characters Link

all_valid_characters(dataset_name)

Validates if all characters in a given dataset name are permissible.

Parameters:

Name Type Description Default
dataset_name str

The name of the dataset to be validated.

required

Returns:

Type Description
bool

True if all characters in the dataset name are valid otherwise, False.

Source code in plantimager/webui/scan.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def all_valid_characters(dataset_name: str) -> bool:
    """Validates if all characters in a given dataset name are permissible.

    Parameters
    ----------
    dataset_name : str
        The name of the dataset to be validated.

    Returns
    -------
    bool
        ``True`` if all characters in the dataset name are valid otherwise, ``False``.
    """
    return sum([letter in FORBIDDEN_CHAR for letter in dataset_name]) == 0

check_dataset_name_uniqueness Link

check_dataset_name_uniqueness(dataset_name, existing_datasets)

Check if the specified dataset name already exists.

Parameters:

Name Type Description Default
dataset_name str

The name of the dataset input by the user, which needs to be checked for uniqueness.

required
existing_datasets list of str

A list containing the names of datasets that already exist.

required

Returns:

Type Description
Dict[str, str]

A style dictionary for controlling the visibility of a message. If the dataset name already exists, the dictionary will display the message. Otherwise, it will hide the message.

Source code in plantimager/webui/scan.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
@callback(
    Output('dataset-exists-message', 'style'),
    Input('dataset-input-name', 'value'),
    State('dataset-list', 'data')
)
def check_dataset_name_uniqueness(dataset_name: str, existing_datasets: list[str]) -> dict[str, str]:
    """Check if the specified dataset name already exists.

    Parameters
    ----------
    dataset_name : str
        The name of the dataset input by the user, which needs to be checked
        for uniqueness.
    existing_datasets : list of str
        A list containing the names of datasets that already exist.

    Returns
    -------
    Dict[str, str]
        A style dictionary for controlling the visibility of a message. If the
        dataset name already exists, the dictionary will display the message.
        Otherwise, it will hide the message.
    """
    if dataset_name in existing_datasets:
        return {'display': 'block', 'margin-top': '10px'}
    else:
        return {'display': 'none'}

config_scan Link

config_scan(_, url, port, cfg, dataset_name)

Configure a plant scan with the specified configuration.

Parameters:

Name Type Description Default
_ Any

Unused parameter (n_clicks from the button).

required
url str

The hostname or IP address of the PlantDB REST API server.

required
port str

The port number of the PlantDB REST API server.

required
cfg str

The TOML configuration string for the scan.

required
dataset_name str

The name to use for the dataset that will be created.

required

Returns:

Type Description
Tuple[str, str]

A tuple containing two status messages: - First message: Short status for the alert component - Second message: Detailed status for the output component

Raises:

Type Description
RuntimeError

If the Raspberry Pi Controller is not initialized.

Source code in plantimager/webui/scan.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
@callback(
    Output('scan-response', 'children', allow_duplicate=True),
    Output('scan-output', 'children', allow_duplicate=True),
    Input('config-scan-button', 'n_clicks'),
    State('rest-api-host', 'data'),
    State('rest-api-port', 'data'),
    State('scan-cfg-toml', 'value'),
    State('dataset-input-name', 'value'),
    prevent_initial_call=True,
)
def config_scan(_, url: str, port: str, cfg: str, dataset_name: str):
    """Configure a plant scan with the specified configuration.

    Parameters
    ----------
    _ : Any
        Unused parameter (n_clicks from the button).
    url : str
        The hostname or IP address of the PlantDB REST API server.
    port : str
        The port number of the PlantDB REST API server.
    cfg : str
        The TOML configuration string for the scan.
    dataset_name : str
        The name to use for the dataset that will be created.

    Returns
    -------
    Tuple[str, str]
        A tuple containing two status messages:
        - First message: Short status for the alert component
        - Second message: Detailed status for the output component

    Raises
    ------
    RuntimeError
        If the Raspberry Pi Controller is not initialized.
    """
    try:
        controller = RPCController.instance()
    except RuntimeError as e:
        return f"Error: Raspberry Pi Controller not initialized!", str(e)


    controller.set_db_url(f"http://{url}:{port}")
    controller.set_dataset_name(dataset_name)
    print(tomllib.loads(cfg))
    controller.set_config(tomllib.loads(cfg))

    return "Scan configured", "Scan configured, ready to start"

disable_scan_button Link

disable_scan_button(valid, n_intervals, previous_state)

Disables the 'Start scanning' button based on dataset validation status and if scanner is connected.

This callback function determines whether the 'start-scan-button' element should be disabled or enabled based on the validity of input provided to the 'dataset-input-name' element. If the input is not valid, the 'start-scan-button' is disabled.

Parameters:

Name Type Description Default
valid bool

Indicates whether the dataset input is valid. True if valid, False otherwise.

required
n_intervals int

Number of times interval is raised

required

Returns:

Type Description
bool

Returns True if the scan button should be disabled, and False if it should be enabled.

Source code in plantimager/webui/scan.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
@callback(
    Output('start-scan-button', 'disabled'),
    Output('config-scan-button', 'disabled'),
    Input('dataset-input-name', 'valid'),
    Input('main-interval', 'n_intervals'),
    State('start-scan-button', 'disabled')
)
def disable_scan_button(valid: bool, n_intervals: int, previous_state: bool) -> tuple[bool, bool]:
    """Disables the 'Start scanning' button based on dataset validation status and if scanner is connected.

    This callback function determines whether the 'start-scan-button' element
    should be disabled or enabled based on the validity of input provided
    to the 'dataset-input-name' element. If the input is not valid, the
    'start-scan-button' is disabled.

    Parameters
    ----------
    valid : bool
        Indicates whether the dataset input is valid.
        ``True`` if valid, ``False`` otherwise.
    n_intervals : int
        Number of times interval is raised

    Returns
    -------
    bool
        Returns ``True`` if the scan button should be disabled, and ``False``
        if it should be enabled.
    """
    try:
        RPCController.instance()
    except RuntimeError as e:
        if previous_state:
            raise PreventUpdate
        return True, True
    if previous_state == (not valid):
        raise PreventUpdate
    return not valid, not valid

is_valid_dataset_name Link

is_valid_dataset_name(dataset_name, existing_datasets)

Check if a dataset name is valid and does not already exist.

Parameters:

Name Type Description Default
dataset_name str

The name of the dataset to be validated.

required
existing_datasets list of str

A list of dataset names that already exist.

required

Returns:

Type Description
bool

True if the dataset name is valid and does not exist in the list of existing datasets, False otherwise.

Source code in plantimager/webui/scan.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def is_valid_dataset_name(dataset_name: str, existing_datasets: list[str]) -> bool:
    """Check if a dataset name is valid and does not already exist.

    Parameters
    ----------
    dataset_name : str
        The name of the dataset to be validated.
    existing_datasets : list of str
        A list of dataset names that already exist.

    Returns
    -------
    bool
        ``True`` if the dataset name is valid and does not exist in the list of
        existing datasets, ``False`` otherwise.
    """
    if dataset_name not in existing_datasets and all_valid_characters(dataset_name):
        return True
    else:
        return False

run_scan Link

run_scan(_, url, port, cfg, dataset_name)

Execute a plant scan with the specified configuration.

Parameters:

Name Type Description Default
_ Any

Unused parameter (n_clicks from the button).

required
url str

The hostname or IP address of the PlantDB REST API server.

required
port str

The port number of the PlantDB REST API server.

required
cfg str

The TOML configuration string for the scan.

required
dataset_name str

The name to use for the dataset that will be created.

required

Returns:

Type Description
Tuple[str, str]

A tuple containing two status messages: - First message: Short status for the alert component - Second message: Detailed status for the output component

Raises:

Type Description
RuntimeError

If the Raspberry Pi Controller is not initialized.

Source code in plantimager/webui/scan.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
@callback(
    Output('scan-response', 'children'),
    Output('scan-output', 'children', allow_duplicate=True),
    Input('start-scan-button', 'n_clicks'),
    State('rest-api-host', 'data'),
    State('rest-api-port', 'data'),
    State('scan-cfg-toml', 'value'),
    State('dataset-input-name', 'value'),
    prevent_initial_call=True,
    running=[
        (Output('start-scan-button', 'disabled', allow_duplicate=True), True, False),
        (Output('config-scan-button', 'disabled'), True, False),
        (Output('scan-progress-interval', 'disabled'), False, True),
        (Output('scan-response', 'children'), 'Scan in progress', ""),
        (Output('scan-output', 'children'), 'Scan in progress', ""),
    ]
)
def run_scan(_, url: str, port: str, cfg: str, dataset_name: str):
    """Execute a plant scan with the specified configuration.

    Parameters
    ----------
    _ : Any
        Unused parameter (n_clicks from the button).
    url : str
        The hostname or IP address of the PlantDB REST API server.
    port : str
        The port number of the PlantDB REST API server.
    cfg : str
        The TOML configuration string for the scan.
    dataset_name : str
        The name to use for the dataset that will be created.

    Returns
    -------
    Tuple[str, str]
        A tuple containing two status messages:
        - First message: Short status for the alert component
        - Second message: Detailed status for the output component

    Raises
    ------
    RuntimeError
        If the Raspberry Pi Controller is not initialized.
    """
    try:
        controller = RPCController.instance()
    except RuntimeError as e:
        return f"Error: Raspberry Pi Controller not initialized!", str(e)

    set_props('scan-response', {'children': "Scan started"})
    set_props('scan-output', {'children': "Scan in progress ..."})

    controller.progressChanged.connect(update_progress)
    controller.maxProgressChanged.connect(update_max_progress)
    update_progress(controller.progress)
    update_max_progress(controller.max_progress)

    controller.set_db_url(f"http://{url}:{port}")
    controller.set_dataset_name(dataset_name)
    print(tomllib.loads(cfg))
    controller.set_config(tomllib.loads(cfg))
    controller.run_scan()

    return "Scan finished", "Scan complete"

update_interval Link

update_interval(n_intervals)

Updates various components on a timer

Updates the camera list

Parameters:

Name Type Description Default
n_intervals
required

Returns:

Name Type Description
str

Markdown message which is printed at available-cameras

Source code in plantimager/webui/scan.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@callback(
    Output("available-cameras", "children"),
    Input("main-interval", "n_intervals"))
def update_interval(n_intervals):
    """Updates various components on a timer

    Updates the camera list

    Parameters
    ----------
    n_intervals

    Returns
    -------
    str:
        Markdown message which is printed at available-cameras

    """
    try:
        controller = RPCController.instance()
    except RuntimeError as e:
        return f"Controller not connected", str(e)
    if update_available_cameras not in controller.cameraNamesChanged.connections:
        controller.cameraNamesChanged.connect(update_available_cameras)
        update_available_cameras(controller.camera_names)


    if available_cameras:
        lines = []
        for camera in available_cameras:
            lines.append(f"- {camera}")
        return "\n".join(lines)
    else:
        return "No camera connected"

update_toml_cfg Link

update_toml_cfg(contents)

Updates the TOML configuration text area with the content of the uploaded base64 encoded config file.

Parameters:

Name Type Description Default
contents str

The base64 encoded string of the config file, containing both the content type and the encoded content, separated by a comma.

required

Returns:

Type Description
str

The decoded TOML configuration string extracted from the uploaded base64 encoded config file.

Source code in plantimager/webui/scan.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
@callback(Output('scan-cfg-toml', 'value'),
          Input('cfg-upload', 'contents'),
          prevent_initial_call=True)
def update_toml_cfg(contents: str) -> str:
    """Updates the TOML configuration text area with the content of the uploaded base64 encoded config file.

    Parameters
    ----------
    contents : str
        The base64 encoded string of the config file, containing both the
        content type and the encoded content, separated by a comma.

    Returns
    -------
    str
        The decoded TOML configuration string extracted from the uploaded
        base64 encoded config file.
    """
    # Parse base64 encoded config file contents and update TOML text area
    content_type, content_string = contents.split(',')
    cfg = b64decode(content_string)
    return cfg.decode()

validate_dataset_name Link

validate_dataset_name(dataset_name, existing_datasets)

Callback to validate the selected dataset name.

It should follow two rules: 1. unicity: it should not exist in the database 2. decency: no fancy/weird/impossible characters are allowed!

Parameters:

Name Type Description Default
dataset_name str

The dataset name to validate.

required
existing_datasets list

The dataset indexed dictionary that exists in the database.

required

Returns:

Type Description
bool

The invalid state of the 'dataset-input-name' Input component.

bool

The valid state of the 'dataset-input-name' Input component.

str

The name of the dataset.

Source code in plantimager/webui/scan.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
@callback(
    Output('dataset-input-name', 'invalid'),
    Output('dataset-input-name', 'valid'),
    Output('dataset-id', 'data'),
    Input('dataset-input-name', 'value'),
    State('dataset-list', 'data')
)
def validate_dataset_name(dataset_name: str, existing_datasets: list[str]) -> tuple[bool, bool, str]:
    """Callback to validate the selected dataset name.

    It should follow two rules:
        1. unicity: it should not exist in the database
        2. decency: no fancy/weird/impossible characters are allowed!

    Parameters
    ----------
    dataset_name : str
        The dataset name to validate.
    existing_datasets : list
        The dataset indexed dictionary that exists in the database.

    Returns
    -------
    bool
        The `invalid` state of the 'dataset-input-name' `Input` component.
    bool
        The `valid` state of the 'dataset-input-name' `Input` component.
    str
        The name of the dataset.
    """
    if dataset_name is not None and is_valid_dataset_name(dataset_name, existing_datasets):
        return False, True, dataset_name
    else:
        return True, False, dataset_name