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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
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

cancel_scan Link

cancel_scan(_)

Placeholder callback for the Cancel Scan button.

Source code in plantimager/webui/scan.py
555
556
557
558
559
560
561
562
563
@callback(
    Output('scan-response', 'children', allow_duplicate=True),
    Input('cancel-scan-button', 'n_clicks'),
    prevent_initial_call=True,
)
def cancel_scan(_):
    """Placeholder callback for the Cancel Scan button."""
    # TODO: implement actual cancellation logic
    return "Cancelling scan..."

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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
@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, prefix, ssl, 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
prefix str

The prefix of the PlantDB REST API server.

required
ssl bool

Whether the PlantDB REST API server is using SSL.

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
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
@callback(
    Output('scan-response', 'children', allow_duplicate=True),
    Output('scan-output', 'children', allow_duplicate=True),
    Input('config-scan-button', 'n_clicks'),
    State('plantdb-host', 'data'),
    State('plantdb-port', 'data'),
    State('plantdb-prefix', 'data'),
    State('plantdb-ssl', 'data'),
    State('scan-cfg-toml', 'value'),
    State('dataset-input-name', 'value'),
    prevent_initial_call=True,
)
def config_scan(_, url: str, port: str, prefix: str, ssl: bool, 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.
    prefix : str
        The prefix of the PlantDB REST API server.
    ssl : bool
        Whether the PlantDB REST API server is using SSL.
    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 "Error: Raspberry Pi Controller not initialized!", str(e)

    res: None | NoResult = controller.set_db_url(plantdb_url(url, port=port, prefix=prefix, ssl=ssl))
    if isinstance(res, NoResult):
        return f"Failed to connect to {'https' if ssl else 'http'}://{url}:{port}{prefix}", res.traceback
    res: None | NoResult = controller.set_dataset_name(dataset_name)
    if isinstance(res, NoResult):
        return f"Failed to set dataset {dataset_name}", res.traceback
    try:
        config_dict = tomllib.loads(cfg)
    except tomllib.TOMLDecodeError:
        return "Failed to parse config file", traceback.format_exc(limit=1)
    res: None | NoResult = controller.set_config(config_dict)
    if isinstance(res, NoResult):
        return "Failed to configure scan", res.traceback

    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
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
@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:
        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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
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(set_progress, _, url, port, prefix, ssl, access_token, refresh_token, 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
prefix str

The prefix of the PlantDB REST API server.

required
ssl bool

Whether the PlantDB REST API server is using SSL.

required
access_token str

The PlantDB REST API access token of the user.

required
refresh_token str

The PlantDB REST API refresh token of the user.

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
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
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
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
@callback(
    Output('scan-response', 'children'),
    Output('scan-output', 'children', allow_duplicate=True),
    Input('start-scan-button', 'n_clicks'),
    State('plantdb-host', 'data'),
    State('plantdb-port', 'data'),
    State('plantdb-prefix', 'data'),
    State('plantdb-ssl', 'data'),
    State('access-token', 'data'),
    State('refresh-token', 'data'),
    State('scan-cfg-toml', 'value'),
    State('dataset-input-name', 'value'),
    background=True,
    manager=background_callback_manager,
    prevent_initial_call=True,
    running=[
        (Output('start-scan-button', 'disabled', allow_duplicate=True), True, False),
        (Output('config-scan-button', 'disabled'), True, False),
        (Output('scan-response', 'children'), 'Scan in progress', ""),
        (Output('scan-output', 'children'), 'Scan in progress', ""),
        (Output('cancel-scan-button', 'disabled', allow_duplicate=True), False, True),
    ],
    progress=[
        Output('scan-progress', 'value'),
        Output('scan-progress', 'max'),
        Output('scan-progress', 'label'),
    ]
)
def run_scan(set_progress, _, url: str, port: str, prefix: str, ssl: bool, access_token: str, refresh_token: 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.
    prefix : str
        The prefix of the PlantDB REST API server.
    ssl : bool
        Whether the PlantDB REST API server is using SSL.
    access_token : str
        The PlantDB REST API access token of the user.
    refresh_token : str
        The PlantDB REST API refresh token of the user.
    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.
    """
    # Background callbacks run in a new process. We must re-init the ZMQ context and Controller proxy.
    ctx = zmq.Context()
    # Using the same URL as app.py
    controller = RPCController(ctx, "tcp://localhost:14567")

    res: None | NoResult = controller.set_db_url(plantdb_url(url, port=port, prefix=prefix, ssl=ssl))
    if isinstance(res, NoResult):
        return f"Failed to connect to {'https' if ssl else 'http'}://{url}:{port}{prefix}", res.traceback

    client = PlantDBClient(plantdb_url(url, port=port, prefix=prefix, ssl=ssl))
    client._access_token = access_token
    client._refresh_token = refresh_token
    if not client.validate_token(access_token):
        return "Failed to authenticate with plantdb.", "Failed to authenticate with plantdb."
    api_token = client.create_api_token(
        1800,
        {dataset_name: (Permission.WRITE, Permission.CREATE, Permission.READ)}
    )
    if not api_token:
        return "Failed to authenticate with plantdb.", "Failed to authenticate with plantdb and create the API token."
    res: None | NoResult = controller.set_api_token(api_token)
    if isinstance(res, NoResult):
        return "Failed to connect to set access token.", res.traceback
    res: None | NoResult = controller.set_dataset_name(dataset_name)
    if isinstance(res, NoResult):
        return f"Failed to set dataset {dataset_name}", res.traceback
    res: None | NoResult = controller.set_config(tomllib.loads(cfg))
    if isinstance(res, NoResult):
        return "Failed to configure scann", res.traceback

    m_prog = controller.max_progress

    def _update_progress(prog):
        set_progress((str(prog), str(m_prog), f"{prog}/{m_prog}"))

    controller.progressChanged.connect(_update_progress)

    res: None | NoResult = controller.run_scan()
    if isinstance(res, NoResult):
        return "Scan Failed", res.traceback

    return "Scan finished", "Scan complete"

update_interval Link

update_interval(n_intervals)

Updates various components on a timer.

Returns:

Type Description
str

Markdown message which is printed at 'available-cameras'.

Source code in plantimager/webui/scan.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
@callback(
    Output('available-cameras', 'children'),
    Input('main-interval', 'n_intervals'))
def update_interval(n_intervals):
    """Updates various components on a timer.

    Returns
    -------
    str
        Markdown message which is printed at 'available-cameras'.
    """
    try:
        controller = RPCController.instance()
    except RuntimeError as e:
        return f"**Controller not connected**: {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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
@callback(Output('scan-cfg-toml', 'value', allow_duplicate=True),
          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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
@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

validate_toml_textarea Link

validate_toml_textarea(toml_text)

Validate the TOML configuration entered by the user.

Source code in plantimager/webui/scan.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
@callback(
    Output('scan-cfg-toml', 'valid'),
    Output('scan-cfg-toml', 'invalid'),
    Input('scan-cfg-toml', 'value'),
)
def validate_toml_textarea(toml_text: str) -> tuple[bool, bool]:
    """Validate the TOML configuration entered by the user."""
    # Empty textarea should not be flagged
    if not toml_text:
        return False, False

    try:
        # Attempt to parse the TOML; we only care about success/failure
        tomllib.loads(toml_text)
        # Valid TOML → keep normal appearance
        return True, False
    except Exception:
        # Invalid TOML → add a red border for visual feedback
        return False, True