Python API
Overview
V-Portal offers standard functions to interface with its features in Python.
Development Environment
The recommended development environment is VSCode with Remote Development using SSHÂ . This allows you to directly program in the same environment as the NUC. The python programs are read from the /home/vikaso/programs directory, so all python programs should be written in there.
Features
Data Fields
Interact with Data Fields programmatically: read values, write values (when allowed), reset to defaults, and subscribe to value changes.
Quick Start
import core.data_fields
# Lookup (raises if not found)
field = core.data_fields.get("field_name")
# Read
current = field.get()
# Write (raises if not modifiable from Python or the incorrect type is provided)
field.set(1)
# Reset to default
field.reset()
# Subscribe to changes
def on_change(value):
print(f"Changed to {value}")
# sub must be maintained and remain in-scope, otherwise the subscription is lost
sub = field.subscribe(on_change)
# Unsubscribe when done
sub.unsubscribe()Module: core.data_fields
get(name: str) -> DataField
Fetch a Data Field by its unique name.
- Parameters
name: The data field identifier (string).
- Returns
DataFieldinstance if found.
- Raises
DataFieldNotFoundErrorif the field does not exist.
Example
import core.data_fields as df
field = df.get("robotCycleTime")Class: DataField
Represents a single data field with read/write/reset and change subscription.
get() -> Any
Read the current value.
- Returns: The field value (type depends on Data Field definition).
- Raises
RuntimeErrorif the field is not readable.
Example
value = field.get()
if isinstance(value, int) and value > 0:
print(value)set(value: Any) -> bool
Write a new value.
- Returns:
bool. True if the value was successfully set, False otherwise. - Validation: Value must match the field’s type.
- Raises
RuntimeErrorif field is not writable from Python.RuntimeErrorType mismatch: attempting to set value of wrong type. If the value fails type validation.
Example
field.set(1) #sets the value to 1 if writablereset() -> None
Reset the field to its default value.
- Raises
RuntimeErrorif field is not writable from Python.
Example
field.reset()last_updated_ms() -> int
Get the epoch time the data field was last updated in milliseconds.
- Returns:
int. The epoch time in milliseconds the data field was updated at. - Validation: The Data Field must be persistent.
- Raises
RuntimeErrorif the Data Field is not persistent.
Example
import time
if field.last_updated_ms() > time.time_ns() - 1000:
print("Data Field updated within 1 second")subscribe(callback: Callable[[Any], None]) -> Subscription
Receive notifications when the field value changes.
- Parameters
callback(value): Function invoked with the new value.
- Returns
Subscriptionobject. Callunsubscribe()to stop receiving updates.
The returned subscription object must remain within scope otherwise the subscription is lost!
- Delivery semantics
- Callbacks are invoked after the field’s value changes.
- Callbacks run on the internal dispatch loop (see Threading & Concurrency).
Example
def on_change(value):
print(f"Changed to {value}")
sub1 = field.subscribe(on_change)
# Lambda functions can also be used
sub2 = field.subscribe(lambda v: print(f"Changed to {v}"))
# Later…
sub1.unsubscribe()
sub2.unsubscribe()Calling field.subscribe(…) without assigning its return value to a variable causes the subscription to go out of scope and removed.
Class: Subscription
Represents an active subscription to field changes.
unsubscribe() -> None
Stops further callbacks for this subscription.
Example
sub = field.subscribe(lambda v: print(v))
sub.unsubscribe()Threading & Concurrency
- Callbacks: Subscription callbacks run on an internal dispatch thread (or event loop). Keep callbacks fast (non‑blocking) and offload heavy work to your own worker thread/pool if needed.
- Reentrancy: Avoid calling
set()on the same field from within its own callback unless your application logic explicitly supports it.
Usage Patterns
Safe read‑modify‑write
f = core.data_fields.get("threshold")
curr = f.get()
new = max(curr, 10) # business rule
f.set(new) # may raise RuntimeErrorGuarded write with fallback
try:
core.data_fields.get("mode").set("auto")
except Exception as E:
# fallback behavior or log
passManaged Subscriptions
cache = {"field" : 0}
def add_subscription(dfield:DataField,
callback: Callable,
error_callback: Callable,
*args, **kwargs):
def wrapper(dfield_value):
try:
return callback(dfield_value, dfield, *args, **kwargs)
except Exception as e:
if error_callback is not None:
error_callback(e)
return dfield.subscribe(wrapper)
def update_internal_map(value, dfield, name):
global cache
cache[name] = value
field = core.data_fields.get("field")
sub = add_subscription(field, update_internal_map, None, name="field")
# ... later
sub.unsubscribe()Complex Data Types
Data Fields can be created with a string type with a max limit of 1000 characters. This allows use for more complex data that can be stored in various forms, e.g. JSON, XML.
import json
from dataclasses import dataclass, asdict
from typing import List, Optional
import core.data_fields as df
# Example target object
@dataclass
class SensorConfig:
name: str
threshold: float
enabled: bool = True
tags: List[str] = None
@staticmethod
def from_dict(d: dict) -> "SensorConfig":
return SensorConfig(
name=d["name"],
threshold=float(d["threshold"]),
enabled=bool(d.get("enabled", True)),
tags=d.get("tags") or [],
)
# 1) Get the field (string, max 1000 chars, storing JSON)
field = df.get("sensorConfig")
# 2) Parse JSON and convert to object
raw = field.get() # expects a JSON string, e.g. {"name":"A1","threshold":12.5,"enabled":true,"tags":["lab"]}
try:
data = json.loads(raw)
except (TypeError, json.JSONDecodeError) as e:
raise ValueError(f"Invalid JSON in Data Field 'sensor.config': {e}")
cfg = SensorConfig.from_dict(data)
# --- Use cfg --- #
print(cfg)
print(cfg.name, cfg.threshold, cfg.enabled, cfg.tags)
# Optional: modify and write back (re‑serialize to JSON)
cfg.threshold = 15.0
field.set(json.dumps(asdict(cfg)))Notes & Best Practices
- Lifecycle: Always call
unsubscribe()before shutdown to avoid dangling callbacks. - Defaults:
reset()sets the value to the field’s configured default, notNoneor zero (unless that is the configured default).
Popups
Display a popup from Python, block until the user responds (like input()), and receive both the pressed action and any data entered/selected in the popup.
Quick Start
import core.popups
# Blocks until user submits; returns PopupResult
result = popups.invoke_generic(
"Get Batch Amount",
"info",
json.dumps({"text_question": "Do you want to proceed with the operation?",}), # Initial values / UI inputs
json.dumps([{"buttonTitle": "Submit", "colour": "#00FF00", "returnValue": "proceed"}]), # Button row
0
)
print(result.wait())
# -> {"actionPressed":"proceed","returnData":{"text_question":""},"sessionId":10,"success":true}