Robot Skill UI Components
Every user interface component ARCx provides to a robot skill, what it is for, and the parameters it takes.
ARCx is an ASP.NET Core Blazor Server application using MudBlazor. A robot skill's UI is
one or more Razor components compiled into the skill's own assembly and rendered by ARCx inside
the workspace. Every MudBlazor component is available to a robot skill, plus the ARCx UC*
components listed here, which exist because they do something MudBlazor cannot: pick an EZB
servo port, show a live camera frame, edit a script, draw a lidar map.
This manual is a companion to RobotSkill-Services-And-Core.md, which covers the non-visual
API (EZB hardware, video, audio, kinematics, scripting). Both sit beside ARCx.exe,
ARCx.dll and ARCx.xml. ARCx.xml remains the authoritative per-member reference; this
manual is the map that tells you what to look up.
Contents
- The three component base classes
- Layout rules
- Redrawing the UI
- Settings row components
- Hardware port pickers
- Servo configuration
- Script editing
- Logging and status output
- Video
- Drawing: UCSkillCanvas
- Serving your own images: UCDynamicImage
- Movement and input
- Kinematics
- Page furniture and misc
- Popups: snackbars, dialogs, navigation
- Custom UI widgets
- MudBlazor conventions ARCx expects
1. The three component base classes
Namespace ARCx.Components.InterfaceEntities.
A robot skill has up to three Razor components, each deriving from one of these.
| Base class | File in the template | Where it is shown |
|---|---|---|
RobotSkillUIMainBaseEntity |
RobotSkillUIMain.razor |
The skill's window on the workspace |
RobotSkillUIConfigBaseEntity |
RobotSkillUIConfig.razor |
The skill's configuration page |
RobotSkillUIWidgetBaseEntity |
RobotSkillUIWidget.razor |
A widget on a user's Custom UI screen (optional) |
Declare the base with @inherits at the top of the .razor file:
@inherits ARCx.Components.InterfaceEntities.RobotSkillUIMainBaseEntity
RobotSkillUIMainBaseEntity
The window on the workspace desktop.
| Member | Notes |
|---|---|
CurrentRobotSkillWorker |
[Parameter]. The worker this UI belongs to. Null on the prototype instance ARCx creates at skill load and never renders, so null-guard anything that may run before a browser rendered the skill. |
AnyActiveClients |
True when not disposed and at least one browser is connected. Check it before doing expensive display work. |
IsDisposed |
True once Dispose() has run. Refreshes are ignored afterwards. |
StateHasChanged() |
Hides the Blazor one. Throttled, thread safe, fans out to every connected browser, does nothing when nobody is watching. Use this one. |
StateHasChangedForced() |
Same but bypasses the throttle timer and the rate limit. Not recommended. |
ShouldRender() |
Enforces the RobotSkillUIQueuedUpdateMS rate limit on every render, including Blazor's own after a UI event. If you override it, combine your condition with base.ShouldRender(). |
OnDispose() |
virtual. Raised when the browser navigates away or the skill is removed. Unsubscribe here. Exceptions are only logged. |
OnResizeEnd() |
virtual. Raised after the user finishes dragging the window. New size is in CurrentRobotSkillWorker.Properties.Width / .Height. |
One instance exists per connected browser. State that every browser must agree on belongs on the worker, not in a field on this component.
Cast the worker once, as the template does:
RobotSkillWorker _currentRobotSkillWorker => base.CurrentRobotSkillWorker as RobotSkillWorker;
RobotSkillUIConfigBaseEntity
The configuration page. ARCx supplies the SAVE and CANCEL buttons.
| Member | Notes |
|---|---|
CurrentRobotSkillWorker |
Cascading value, private set. Null on the prototype instance. |
OnSave() |
virtual. Raised on SAVE, before the worker's OnConfigWindowClosed(ConfigChanged = true). Throw to reject the save - the message is shown to the user, the page stays open, the worker is not notified. This is where validation goes. |
OnCancel() |
virtual. Raised on CANCEL. Revert anything you changed on the worker. Throwing also keeps the page open. |
StateHasChanged() |
Thread safe, marshalled through InvokeAsync. Does nothing once disposed or when no browser is connected. |
OnDispose() |
virtual. Raised when the user leaves the page or the skill is removed. |
The convention the template follows is to edit a deep clone of the configuration and copy it
back to the worker in OnSave(), so CANCEL genuinely discards.
While any configuration window is open, RobotSkillWorkerAbstractEntity.AllWorkersPaused is
true for every skill in the project. Worker timers should check it and skip their work.
RobotSkillUIWidgetBaseEntity
An optional compact piece of the skill the user can drop on their own Custom UI screen. If this is not provided, the RobotSkillUIMainBaseEntity will be used on Custom UI screens. See section 16.
| Member | Notes |
|---|---|
CurrentRobotSkillWorker |
[Parameter]. The same worker instance the skill window uses. |
WidgetKey |
[Parameter]. The Key from the worker's GetCustomUIWidgets() entry, for a component that serves several definitions. |
Settings |
[Parameter]. Dictionary<string, string> the user filled in, seeded from DefaultSettings. |
IsEditMode |
[Parameter]. True while shown in the designer. Do not move the robot, run scripts or take any action while true. |
Width / Height |
[Parameter]. The box in design pixels, default 200 x 120. |
GetSetting(key, defaultValue = "") |
A setting, or the default when missing or blank. |
GetSettingNumber(key, double defaultValue) |
As a number, or the default. |
GetSettingBool(key, bool defaultValue) |
Understands true, 1, on, yes. |
AnyActiveClients |
True when not disposed and a browser is connected. |
StateHasChanged() |
Throttled by the RobotSkillUIQueuedUpdateMS preference like the skill window, thread safe. |
OnDispose() |
virtual. Raised when the widget leaves a screen or the browser leaves the page. |
2. Layout rules
The skill window body is a flex column. A direct child given flex: 1 1 0 takes whatever
height the other children leave over, so the window can be dragged to any size with no gap at
the bottom and nothing spilling out.
ARCx ships three helper classes in arcx.css so every robot skill lays out the same way:
| Class | Use it for |
|---|---|
.arcx-skill-fill |
Fill what is left: a table, a list, a control area. flex: 1 1 0; min-height: 0; min-width: 0; |
.arcx-skill-log |
A UCLogBuffered wrapper. Grows with the window, never under 60px. |
.arcx-skill-media |
A camera image or canvas. Fills what is left, clips, never grows the window. min-height: 80px; overflow: hidden; |
Rules:
- Never give the log, a camera view or a canvas a fixed pixel height. They already fill their container; a fixed height is exactly what stops them scaling.
- A
<style>block in a.razorfile is global to the page, and every robot skill on a workspace shares that page. Prefix every class name with something unique to your skill (its initials) so your rules cannot collide with another skill's. .arcx-skill-bodysetsoverflow: hiddenon the window, so anything that overflows is clipped rather than escaping.
The template's main window is the canonical shape:
<style>
.tpl-main {
flex: 1 1 0;
min-height: 0;
min-width: 0;
overflow: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px;
}
.tpl-controls { flex: 0 0 auto; display: flex; flex-direction: column; gap: 8px; }
.tpl-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
</style>
<div class="tpl-main">
<div class="tpl-controls">
@* buttons, readouts, sliders - only as tall as their content *@
</div>
<div class="arcx-skill-log">
<UCLogBuffered InstanceGUID="@(_currentRobotSkillWorker.Properties.InstanceGuid)" Lines="3" />
</div>
</div>
The default window size comes from the worker's Setup() override
(RobotSkillWorkerDefaultParams.Width / .Height, and AllowFullScreen).
3. Redrawing the UI
There are four ways to redraw, and they are not interchangeable.
| Call | From | Reaches |
|---|---|---|
worker.RefreshUI() |
The worker | Every browser showing the skill window, and every Custom UI widget the skill provides |
worker.RefreshCustomUIWidgets() |
The worker | Only the Custom UI widgets |
worker.RefreshUIRegion(name) |
The worker | Only the UCRefreshRegions with that name, on every browser - see below |
this.StateHasChanged() |
Inside a UI component | That component, on every connected browser |
RefreshUI() is the one a worker calls after it changes something the user can see. It is safe
from any thread, safe before anything is rendered (headless / auto-start), and does nothing when
no browser is connected.
Renders are throttled by a timer whose interval is the RobotSkillUIQueuedUpdateMS
preference (Advanced tab, default 250ms), so many calls in quick succession produce one render.
That is why you can call it freely from a timer.
The same interval is a hard rate limit on the component itself. The base classes' ShouldRender()
holds any render that comes sooner than the interval after the last one - including the render
Blazor does on its own after every UI event (dragging a slider, ticking a checkbox) and when the
parent passes new parameters - and draws it once the interval has passed. StateHasChangedForced()
is the only way past it. If you override ShouldRender(), combine your condition with
base.ShouldRender() or your skill loses the limit.
Two components bypass Blazor rendering entirely for efficiency, and have their own refresh path:
UCLogBufferedsets the text with JavaScript. CallUCLogBuffered.AddEntry(...).UCDynamicImagesets theimg srcwith JavaScript. CallRefreshImage().
What a redraw actually costs
A redraw re-renders the whole component, not the part that changed. Blazor then diffs the result against what the browser is showing. That is cheap for a window of a dozen controls and expensive for a window with a long list in it, because the cost is per control per redraw.
Two MudBlazor controls cost far more than they look:
MudTooltipis a popover. It registers itself withMudPopoverProvider, which is a single component in the ARCx page layout shared by every robot skill on the workspace. Each tooltip is an entry in a keyed list that the whole page re-renders.MudFileUploadcarries its own<InputFile>, which captures an element reference back into the render tree. So does anything using@refon an element.
Put either of those inside a row of a list and multiply by the row count. A hundred-row table
with three tooltips and an upload button per row is several hundred popovers and a hundred file
inputs, all torn down and rebuilt on every redraw. Drive that from a timer and Blazor's render
tree can be corrupted outright - the symptom is an InvalidCastException from deep inside
Microsoft.AspNetCore.Components naming ElementReference or ComponentState, followed by the
circuit dropping and the browser losing its connection.
Redraw only the part that changes: UCRefreshRegion
Most windows that redraw often have one small thing that changes - a play position, a counter, a
sensor reading, a status line - sitting in a window that otherwise stays put. Calling RefreshUI()
to move that one number redraws every row and every button around it, several times a second.
Wrap the part that changes in a UCRefreshRegion and have the worker redraw just that:
<UCRefreshRegion Worker="Worker" Name="@RobotSkillWorker.PositionRegion">
<MudText Typo="Typo.caption">@(Worker.IsPlaying ? $"Playing - {Worker.PlaySeconds:0.0}s" : "Stopped")</MudText>
</UCRefreshRegion>
@* ...the long table that does not change while a sound plays... *@
// On the worker
internal const string PositionRegion = "position";
void playbackLoop() {
while (playing) {
PlaySeconds = ...;
if (HasUIClient)
RefreshUIRegion(PositionRegion); // the line above, not the window
Thread.Sleep(250);
}
RefreshUI(); // playback ended - buttons, labels and the rest do change now
}
| Parameter | Notes |
|---|---|
Worker |
Required. The worker whose RefreshUIRegion() redraws it. |
Name |
Required. Several regions can share a name - the status line and a dialog's play marker, say - and are redrawn together. Keep the name in a const on the worker so the two sides cannot drift. |
ChildContent |
The markup to redraw. |
- This is the default for new robot skills. Anything a loop or timer updates while the rest of
the window stays the same goes in a region. Keep
RefreshUI()for the moments the rest of the window really does change: something started or stopped, an item was added or removed. - The region is throttled by
RobotSkillUIQueuedUpdateMSlike the window, reaches every connected browser, and does nothing when no browser is connected. It is also redrawn whenever the window around it is, so it is never out of date after aRefreshUI(). - Read what changes from members, not from a local. The wrapped markup reads fields and
properties when the region draws, so
Worker.PlaySecondsis always current. A local from the window's@{ var tracks = Worker.Tracks; }block is the value it had when the window last drew, and a region redraw will not update it. - A region draws no element of its own, so it can sit anywhere: inside a flex row, inside a table
cell, or inside an
<svg>around a single<line>(the Sound Board's play marker does this, so moving the marker does not redraw the waveform). - Keep regions small. A region wrapping half the window saves half the cost; a region wrapping one label saves nearly all of it.
Rules for a window that redraws often
Put the part that changes in a
UCRefreshRegionand redraw it withRefreshUIRegion()instead ofRefreshUI(). See above.Redraw no faster than the user can read. A play position, a counter or a sensor reading does not need more than four frames a second. The
RobotSkillUIQueuedUpdateMSthrottle sets a floor, not a target - do not ask for a redraw every 50ms and rely on it.Guard the work with
HasAnyUIClient(or the narrowerHasUIClient/HasCustomUIClient) in any loop or timer that only exists to feed the display, so the skill costs nothing when nobody has the window open. See below.No
MudTooltipinside a repeated row. Use the plain HTMLtitleattribute on the control instead - it is a native browser tooltip, costs nothing and reads the same to the user. KeepMudTooltipfor the handful of controls in a toolbar or a settings row.One
MudFileUploadper window, not per row. Put a single picker outside the list, remember which row was pressed, and open it withOpenFilePickerAsync():@* One picker for the whole table. It draws nothing itself - the row buttons are the visible part. *@ <MudFileUpload T="IBrowserFile" @ref="_picker" FilesChanged="filePicked" Accept=".wav,.mp3"> <CustomContent Context="p"><span></span></CustomContent> </MudFileUpload> @foreach (var row in rows) { <MudIconButton Icon="@Icons.Material.Filled.FileOpen" title="Load a file into this slot" OnClick="@(() => pickFor(row))" /> }MudFileUpload<IBrowserFile> _picker; MyRow _target; async Task pickFor(MyRow row) { _target = row; await _picker.OpenFilePickerAsync(); } async Task filePicked(IBrowserFile file) { var target = _target; _target = null; if (target == null || file == null) return; // ... }Anything genuinely high frequency - a waveform, a map, a live plot - belongs on
UCSkillCanvasorUCDynamicImage, which do not re-render at all. See sections 10 and 11.
Skip work nobody will see
RefreshUI() already does nothing when nothing is rendered, but the work a worker does before
calling it - formatting a table, building a preview image, averaging a buffer for a meter - still
runs. On a single board computer that is CPU spent for no one. The worker can ask whether anything
of the skill is actually on a screen and skip that work:
| Worker member | True when |
|---|---|
HasUIClient |
At least one browser is showing this skill's window, on any workspace. Covers the UCRefreshRegions inside it |
HasCustomUIClient |
At least one browser is showing a Custom UI screen that holds one of this skill's widgets |
HasAnyUIClient |
Either of the above. The check that matches RefreshUI(), which redraws both |
AnyActiveClients |
Any browser is connected at all - even one on a different workspace. Broader than the three above |
void sensorTimer_Elapsed(object sender, EventArgs e) {
// Reading the sensor and acting on it is the skill's job - always do it
var reading = readSensor();
actOnReading(reading);
// Building the display text is only for the user - skip it when nobody is looking
if (!HasAnyUIClient)
return;
DisplayText = formatReading(reading);
RefreshUI();
}
- Only skip display work. Anything the robot depends on - reading a sensor, driving a servo, setting a script variable, publishing to another skill - runs whether or not a browser is looking.
- Catch up when the window comes back. When a browser renders the skill's window, it calls the
worker's
UpdateMainUIStateRequested(); set the display fields to their latest values there, so the window does not show stale data until the next tick. Widgets have no such call and pick up the worker's next refresh. - The flags change as browsers switch workspaces, open and close Custom UI screens, and disconnect. They are cheap (a lock and a count) - check them every tick rather than caching them.
Never render straight from a collection the worker mutates
A worker's lists are changed from whatever thread a ControlCommand, a script or another robot
skill arrives on. The browser renders on its own thread. Indexing a live List<T> while drawing
means a render can be handed an item a script removed a microsecond earlier, and an exception
thrown part way through a render leaves the render tree half built, which Blazor cannot recover
from. It is not the same as an exception in an event handler.
Expose a snapshot from the worker and take it once per render:
// On the worker
readonly object _itemsLock = new();
internal MyItem[] Items {
get {
lock (_itemsLock)
return _items.ToArray();
}
}
@* One snapshot for the whole of this render *@
@{
var items = Worker.Items;
}
@for (int i = 0; i < items.Length; i++) {
...
}
Every method on the worker that adds, removes or replaces an item takes the same lock. Call
RefreshUI() after releasing it.
@key on a list row
Give a repeated row a @key so Blazor reuses the row instead of rebuilding it, and make sure the
key is:
- Never null or blank. Blazor silently ignores a null key, which leaves keyed and unkeyed rows
mixed in one list and produces a bad diff. Repair missing keys in
OnSetConfiguration()when loading a project saved by an older build of the skill. - Unique among its siblings. Two rows with the same key is the same problem.
- Stable for the life of the row. Key on the slot or the record, not on its contents. When the user replaces what is in a slot, keep the slot's key so the row survives the change.
A render exception no longer takes the page down
ARCx wraps each robot skill in its own error boundary, so a skill that throws while rendering shows the error in its own window with a Try Again button, and the rest of the workspace keeps running. That is a safety net, not a licence: the window stops drawing until the user presses the button, and the entry lands in the debug log with the skill's name on it. Handle your own errors.
4. Settings row components
The standard way to lay out a configuration page: a title with a help icon on the left, the editor on the right.
UCTitleComponentTableRowColumn
Renders a <tr>, so it must sit inside a <table> / <tbody>. This is the row robot skill
configuration pages use.
| Parameter | Type | Default | Notes |
|---|---|---|---|
ChildContent |
RenderFragment |
- | The editor. A cascading "Disabled" value is provided to it, so ARCx input components inside follow Disabled. |
Title |
string |
"Value" |
The label in the title cell |
HelpText |
string |
"Enter a value for this parameter" |
Tooltip on the help icon |
Href |
string |
empty | When set, the help icon becomes a link opening in a new tab |
Disabled |
bool |
false |
Blurs the row and cascades Disabled |
ContentWidth |
string |
empty | CSS width for the value cell, i.e. "300px" |
TitleWidth |
string |
empty | CSS width for the title cell |
<table>
<UCTitleComponentTableRowColumn Title="Poll interval (ms):"
HelpText="How often the sensor is read. Lower uses more CPU.">
<MudNumericField T="int" Min="50" Max="10000" @bind-Value="@(_configuration.PollMS)" />
</UCTitleComponentTableRowColumn>
<UCTitleComponentTableRowColumn Title="Enabled at start:"
HelpText="Start polling as soon as the project loads.">
<MudCheckBox T="bool" @bind-Value="@(_configuration.AutoStart)" />
</UCTitleComponentTableRowColumn>
</table>
UCTitleComponentRow
The same idea without a table. Use it in a main window or anywhere a <table> would be awkward.
| Parameter | Type | Default | Notes |
|---|---|---|---|
ChildContent |
RenderFragment |
- | The editor |
Title |
string |
"Value" |
|
HelpText |
string |
"Enter a value for this parameter" |
|
Vertical |
bool |
false |
Title above the editor instead of beside it |
Href |
string |
empty | Help icon becomes a link |
Disabled |
bool |
false |
Blurs the row, cascades Disabled |
5. Hardware port pickers
Namespace ARCx.Components.UserComponents. Descriptor types are in ARCx.Config.Helpers.
Each picker edits a port descriptor stored in the skill's configuration. A descriptor holds the EZB board index and the port. The dialog also lets the user name the port; the name goes into the project's port description dictionaries and is then shown by every other picker in the project, and on the Robot Configuration picture.
All of these edit Value in place and never replace the instance. ValueChanged exists only
so @bind-Value compiles; it is not raised. Pass the instance that lives in your configuration.
| Component | Descriptor | Companion *AndLabel |
|---|---|---|
UCPortEditDigitalButton |
DigitalPortDescriptor |
UCPortEditDigitalButtonAndLabel |
UCPortEditADCButton |
ADCPortDescriptor |
UCPortEditADCButtonAndLabel |
UCPortEditPWMButton |
PWMPortDescriptor |
UCPortEditPWMButtonAndLabel |
The bare button shows the selected port and its project description, and opens the picker dialog.
The *AndLabel variant puts a help icon and a title label in front of it as a single row.
| Parameter | Type | Notes |
|---|---|---|
Value |
the descriptor | Must not be null. Edited in place |
Disabled |
bool |
Also disabled while the dialog is open |
Title |
string |
*AndLabel only. Defaults: "Digital Port", "ADC Port", and "Digital Port" for the PWM one - set it to "PWM Port" explicitly |
HelpText |
string |
*AndLabel only |
<UCPortEditDigitalButtonAndLabel Title="Trigger port"
HelpText="The digital port wired to the sensor's trigger pin."
Value="@(_configuration.TriggerPort)" />
Reading the port at run time:
var ezb = EZBManager.TryGetEZB(_Configuration.TriggerPort.BoardIndex);
if (ezb == null || !ezb.IsConnected)
return;
var state = ezb.Digital.GetDigitalPort(_Configuration.TriggerPort.Port);
ToString() on any descriptor gives "board.PORT", i.e. "0.D3" or "0.ADC0".
6. Servo configuration
UCServoSelector
The standard panel for the servo or servos a skill controls: port, min/max position, velocity, acceleration and invert for the first servo, plus a "multiple servos" button opening a dialog to add more servos that follow the first.
Dragging the min or max slider physically moves the servo to that position so the user can see the limit. The single-servo controls are disabled when the collection holds more than one servo; those are edited in the multiple-servo dialog.
| Parameter | Type | Default | Notes |
|---|---|---|---|
Value |
ServoCollection |
- | Must not be null and must contain at least one ServoDescriptor. Servos[0] is the one the panel edits. Modified in place |
Title |
string |
"Servo Details" |
Heading |
HelpText |
string |
"Select the servo parameters. Click for more information." |
|
Disabled |
bool |
false |
Blurs and disables every control |
ValueChanged |
EventCallback<ServoCollection> |
- | For @bind-Value compatibility. Not raised |
<UCServoSelector @bind-Value="@(_configuration.PanServo)"
Title="Pan servo"
HelpText="The servo that turns the head left and right." />
ServoCollection defaults to a single unassigned (NA) servo, so a new configuration already
has a row to edit. Moving it:
// Applies each descriptor's own min, max, invert, multiplier, velocity and acceleration
EZBManager.SetServoPosition(_Configuration.PanServo, 90);
// Map an arbitrary input range onto each servo's own min..max
EZBManager.SetServoPositionScalar(_Configuration.PanServo, 0, 100, 50);
Supporting components in ARCx.Components.UserComponents.UCServoSelectorComponents
(UCMultipleServoSelector, UCPortEditServo) are used by UCServoSelector itself; a skill
normally uses the selector rather than these directly. The template's _Imports.razor already
has a @using for that namespace.
7. Script editing
UCScriptEditButton
A button that opens the full screen script editor dialog - Blockly, JavaScript and Python tabs -
for a ScriptDefinition (ARCx.Config.Helpers). Use one for every script the skill exposes. The
button label shows the script's current type, or "Empty". The dialog provides Save, Cancel
(reverts to the script as it was when the dialog opened) and Panic.
| Parameter | Type | Default | Notes |
|---|---|---|---|
Value |
ScriptDefinition |
- | Must not be null. Edited in place, so pass the instance from your configuration |
HelpText |
string |
"Enter the script for this event. Click for more information" |
Shown in the dialog's help tooltip. Say what the script is for |
Disabled |
bool |
false |
Also disabled while the dialog is open |
Class |
string |
- | Extra CSS classes on the button |
UCScriptEditButtonAndLabel
The same button with a help icon and title label in front, as one row.
| Parameter | Type | Default |
|---|---|---|
Title |
string |
"Script" |
Value |
ScriptDefinition |
- |
HelpText |
string |
"Enter the script for this event. Click for more information" |
Disabled |
bool |
false |
<UCScriptEditButtonAndLabel Title="On object detected"
HelpText="Runs each time the camera reports a new detection."
Value="@(_configuration.OnDetectedScript)" />
Run the script from the worker with an Executor obtained from ScriptManagerService - see the
services manual, section "Scripting".
8. Logging and status output
UCLogBuffered
A read-only console box with a refresh buffer. This is the standard place a robot skill tells the user what it is doing, and the first place a user looks.
It writes the text with JavaScript rather than re-rendering, which makes it far cheaper than
StateHasChanged. Entries are kept in a process-wide cache keyed by InstanceGUID, so the last
entries survive navigating away and back, and every browser showing the same InstanceGUID sees
them. The cache keeps the most recent 100 entries.
| Parameter | Type | Default | Notes |
|---|---|---|---|
InstanceGUID |
string |
empty | Set this. Normally the worker's instance guid. For a second log in the same skill, append a suffix: $"{guid}-console2". Never a static literal, or two skills share one log |
Lines |
int |
0 |
The textarea rows attribute. 0 lets CSS decide the height - preferred |
FontSize |
int |
0 |
px. 0 uses the ARCxConsoleLogFontSize preference |
RefreshBufferMS |
int |
500 |
Buffer before pushing text to the browser. Read once at init |
LineHistoryCount |
int |
50 |
Currently unused; the cache always keeps 100 |
Class / Style |
string |
empty | Extra CSS on the textarea |
Static entry points, callable from any thread:
UCLogBuffered.AddEntry(this, "text"); // from the worker
UCLogBuffered.AddEntry(this, "text"); // from a UIMain component
UCLogBuffered.AddEntry(instanceGuid, "text"); // by guid
UCLogBuffered.ClearLogs(instanceGuid); // drop the cache for one instance
Usage:
<div class="arcx-skill-log">
<UCLogBuffered InstanceGUID="@(_currentRobotSkillWorker.Properties.InstanceGuid)" Lines="3" />
</div>
UCLogBuffered is for the user. ARCLogService is for errors and diagnostics (see the
services manual). A caught exception goes to ARCLogService; "connected to COM3 at 115200" goes
to UCLogBuffered.
UCEmptyState
The blank-slate panel: what would appear here, and what the user would do to make it appear.
| Parameter | Type | Default | Notes |
|---|---|---|---|
Title |
string |
empty | The situation, plainly: "No COM ports are open" |
Description |
string |
- | What would appear here and how to get it |
Icon |
string |
- | A Material icon |
IconColor |
Color |
Color.Default |
Muted by default. An empty state is not a warning |
Actions |
RenderFragment |
- | Buttons offering the way out |
ChildContent |
RenderFragment |
- | |
Class |
string |
empty |
9. Video
Both components display frames from a source registered with VideoRouteService - a camera robot
skill, for example. The service pushes a JPEG to the component when the source produces a frame
and the component is due for one; the browser fetches it from /_videoSource/[component guid].
Frames are dropped, never queued, so a slow browser only ever sees the latest image. A camera
stub image is shown until the first frame arrives.
UCVideoSourceImage
| Parameter | Type | Default | Notes |
|---|---|---|---|
VideoRouteSourceName |
string |
empty | Case-insensitive source name. Empty uses the first registered source (the *Changed callback is not raised for that default) |
DisplayProcessedVideo |
bool |
true |
true shows processed frames (with vision overlays), false the originals |
DelayMSBetweenFrames |
int |
250 |
Minimum ms between frames sent to this browser. Skipped before JPEG encoding, so it is a real CPU saving on small boards |
Width / Height |
int? |
null |
null stretches to 100% of the container |
Class / Style |
string |
- |
The *Changed callbacks exist for @bind support and are not raised by the component.
UCVideoSourceImageAdvanced
The same image with a "Video Source" drop-down listing every registered source, and a "Display
Processed" checkbox above it, so the user picks what to watch without the skill providing its own
controls. Same parameters plus Alt.
<div class="arcx-skill-media">
<UCVideoSourceImage VideoRouteSourceName="@(_currentRobotSkillWorker._Configuration.CameraSource)"
DelayMSBetweenFrames="200" />
</div>
Store the source name, never the guid - guids change every session. In the configuration,
mark the property with [RobotSkillReference("camera source")] if it holds another skill's window
title so the AI agent knows what it must match.
10. Drawing: UCSkillCanvas
Namespace ARCx.Components.UserComponents; the draw list and its types are in
ARCx.Components.UserComponents.UCSkillCanvasComponents.
A drawing surface for skills that generate graphics: lidar maps, audio UV meters, occupancy grids, path plots, spectrum displays.
It sends drawing commands, not a rendered picture, and lets the browser rasterise on the GPU. A 360 point lidar sweep is under 3KB on the wire against roughly 40KB for the same thing as a PNG, with no image encode on the server at all. The server never knows the pixel size of the panel and never needs to, so resizing costs nothing.
Component parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
RobotSkillWorker |
RobotSkillWorkerAbstractEntity |
- | How the skill finds its canvas without holding a component reference |
CanvasName |
string |
empty | Only needed when one skill shows more than one canvas |
RefreshMS |
int |
100 |
Shortest gap between frames. TryBeginDraw returns false until it has passed, so it is a real CPU limit, not just a display throttle |
OnPointer |
EventCallback<SkillCanvasPointerArgs> |
- | Leave unset and no DOM listeners are attached at all |
PointerMove |
SkillCanvasPointerMoveEnum |
WhileDown |
Never, WhileDown, Always. Movement is coalesced to one report per animation frame regardless |
EnableWheel |
bool |
false |
Also stops the wheel scrolling the page while over the canvas. Only turn on if the canvas zooms |
Width / Height |
int? |
null |
null stretches to 100%; the browser redraws the last frame itself on resize |
Class / Style |
string |
- |
Drawing from the worker
if (!UCSkillCanvas.TryBeginDraw(this, out var dl))
return; // nobody is watching, or it is not time yet
using (dl) {
dl.View(SkillCanvasViewModeEnum.World, 0, 0, 10);
dl.Clear();
dl.Points(_scanXY, _scanPointCount, SkillCanvasColors.Green, 2);
dl.Circle(0, 0, 0.25f, SkillCanvasColors.Red, 2);
}
The TryBeginDraw guard is the single most important line for CPU. It is false when no
browser is connected, when no panel is showing this canvas, and when the last frame was too
recent, so a skill never spends anything building a frame that would be thrown away.
Static entry points:
| Method | Notes |
|---|---|
UCSkillCanvas.TryBeginDraw(worker, out SkillCanvasDrawList dl, string canvasName = null) |
Also overloads taking a RobotSkillUIMainBaseEntity or an instance guid string |
UCSkillCanvas.IsDue(worker, string canvasName = null) |
Only needed when gathering the data is expensive and happens before the draw list is wanted |
UCSkillCanvas.ClearFrames(worker) |
Drop the cached last frame |
SkillCanvasDrawList
Disposing the draw list sends the frame (Send() does the same explicitly). MAX_VERTICES is
65535.
| Method | Notes |
|---|---|
View(SkillCanvasViewModeEnum mode, float centerX = 0, float centerY = 0, float spanX = 1) |
Sets the coordinate system for everything that follows |
Clear(uint color = SkillCanvasColors.Transparent) |
|
Line(x1, y1, x2, y2, uint color, float thickness = 1) |
|
Rect(x, y, width, height, uint color, float thickness = 1, bool filled = true) |
|
Circle(centerX, centerY, radius, uint color, float thickness = 1, bool filled = false) |
|
Polyline(float[] interleavedXY, int pointCount, uint color, float thickness = 1, bool closed = false) |
|
Points(float[] interleavedXY, int pointCount, uint color, float size = 2, bool square = true) |
|
Text(x, y, string text, uint color, float sizePixels = 12, SkillCanvasTextAlignEnum align = Left) |
SkillCanvasViewModeEnum:
| Value | Coordinates |
|---|---|
Normalized |
X and Y run 0 to 1 across width and height. X and Y scale independently. Right for meters, bars, gauges - anything that should stretch to fit |
World |
Real world units with Y pointing up, centred on a point you choose. Uniform scaling, so circles stay circular. Right for a lidar map, occupancy grid or path plot |
Pixels |
CSS pixels from the top left, Y down. Only when the drawing genuinely depends on pixel size |
SkillCanvasColors packs colours as 0xAARRGGBB in a uint - four bytes on the wire, zero
allocations. Constants: Transparent, Black, White, Red, Green, Blue, Yellow,
Orange, Cyan, Magenta, Grey, DarkGrey. Helpers: Rgb(r, g, b), Rgba(r, g, b, a),
FromHex(string).
Pointer input
SkillCanvasPointerArgs arrives with X and Y already converted into the units of the first
view the last frame set up - a click on a lidar map is handed to the skill in metres, with no
unprojecting to do.
| Member | Notes |
|---|---|
EventType |
Down, Move, Up, Leave (treat as Up; no Up is coming), Wheel |
X, Y |
In view units |
PixelX, PixelY |
Raw CSS pixels |
Buttons |
Bitmask |
IsButtonDown |
Convenience |
WheelDelta |
Direction and amount |
ShiftKey, CtrlKey, AltKey |
11. Serving your own images: UCDynamicImage
For an image the skill generates or holds in memory but that is a genuine bitmap - a saved
photo, a decoded frame, a map rendered with SkiaSharp. For generated line graphics prefer
UCSkillCanvas, which costs far less.
The browser requests /_dynamic/[instance guid]/[FilePath] and DynamicContentMiddleware routes
that to the worker's override:
public override DynamicContentMiddlewareResponse OnDynamicResourceRequest(DynamicContentMiddlewareRequest request)
| Parameter | Type | Notes |
|---|---|---|
RobotSkillWorker |
RobotSkillWorkerAbstractEntity |
Builds the URL from the instance guid and routes the request to that worker |
FilePath |
string |
The path your override checks for, i.e. "/myImage.jpg" |
Width / Height |
int? |
null leaves it to the image/CSS |
Style |
string |
Appended after the size rules |
Alt |
string |
Defaults to the request URL |
RefreshImage() appends a cache-busting counter and sets img src with client-side JavaScript -
no Blazor re-render. Call it whenever the image content changes. It does nothing when no
clients are connected. Fire and forget (async void); exceptions are logged.
Have the bytes cached and ready in the override; it runs on an HTTP request thread.
12. Movement and input
UCMovementButtons
A forward / left / stop / right / reverse pad plus a small text box that, when focused, drives the
robot with the arrow keys (key up stops). The buttons highlight the direction
MovementManagerService reports, whoever set it.
| Parameter | Type | Notes |
|---|---|---|
OnMovementChange |
EventCallback<MovementManagerService.MovementDirectionEnum> |
The handler is responsible for moving the robot, normally by calling MovementManagerService. The keyboard path calls the service directly |
UCMovementJoyPad
An on-screen joystick wired to MovementManagerService. Forward/back drives, with left and right
speeds blended by the sideways offset; sideways within the dead zone turns; centre stops. The
component drives the robot itself - the skill handles no events.
| Parameter | Type | Default | Notes |
|---|---|---|---|
Sensitivity |
double |
0.2 |
Dead zone as a fraction of range (0 to 1) |
MaxSpeed |
byte |
255 |
Speed at the edge of the range |
Width / Height |
int |
320 / 240 |
Canvas size in pixels |
ButtonRadius |
int |
70 |
Thumb radius |
ReturnToCenterWhenReleased |
bool |
true |
Springs back and stops the robot |
CanvasImage / ButtonImage |
string |
- | Optional replacement artwork |
MaxDriveSpeed |
byte |
MovementManagerService.MAX_SPEED |
Currently unused |
Refresh() |
method | - | No-op, kept so existing skills still compile. The canvas sizes itself and a ResizeObserver redraws it |
UCJoypad
The raw joystick with no movement wiring - use it for anything that is not locomotion (a camera gimbal, a pan/tilt head).
| Parameter | Type | Default | Notes |
|---|---|---|---|
OnJoyPadMove |
EventCallback<JoyPadArgs> |
- | JoyPadArgs.X and .Y run -1 to +1; zero is centre, negative Y is up |
Width / Height |
int |
320 / 240 |
A nominal aspect ratio, not a pixel size - the canvas fills its container |
ButtonRadius |
int |
70 |
Scaled with the canvas |
ReportIntervalMs |
int |
50 |
Minimum ms between C# callbacks (~20Hz) |
ReturnToCenterWhenReleased |
bool |
true |
|
CanvasImage / ButtonImage |
string |
- |
UCNumberSlider
A compact draggable number field.
| Parameter | Type | Default |
|---|---|---|
Value / ValueChanged |
int / EventCallback<int> |
supports @bind-Value |
MinValue / MaxValue |
int |
0 / 100 |
Disabled |
bool |
false |
13. Kinematics
UCKinematicChainSelector
A drop-down of the kinematic chains published in the project, bound to the chain name. The list is live: it refreshes when a provider adds or removes a chain.
| Parameter | Type | Default | Notes |
|---|---|---|---|
Value / ValueChanged |
string / EventCallback<string> |
empty | The chain name. Supports @bind-Value |
OnChainSelected |
EventCallback<KinematicChain> |
- | Raised alongside ValueChanged with the descriptor. Null when "(none)" is chosen |
Label |
string |
"Kinematic configuration" |
|
HelpText |
string |
empty | When empty, the selected chain's provider and joint count are shown instead |
AllowNone |
bool |
false |
Offers a "(none)" entry. Also turns off AutoSelectFirst |
AutoSelectFirst |
bool |
true |
Selects the first chain when nothing is selected, so a freshly added skill works without a visit to its configuration. Also applies when a chain is published after the page rendered |
ShowPose |
bool |
false |
Shows the live end effector pose (re-rendered at most every 100ms) |
Disabled |
bool |
false |
Also honoured when a parent settings row cascades Disabled |
<UCKinematicChainSelector @bind-Value="@(_configuration.ChainName)" ShowPose="true" />
Save the name. At run time pass it to KinematicsService.MoveTo(name, x, y, z) or
GetPose(name).
14. Page furniture and misc
| Component | Purpose | Key parameters |
|---|---|---|
UCPageHeader |
The page's single h1, with an eyebrow, one-sentence description, icon badge and right-aligned actions. For full-page UIs, not skill windows |
Title, Eyebrow, Description, Icon, IconColor (default Color.Primary), Actions, Class |
UCYouTube |
An embedded YouTube player | YouTubeID, Title, Class, Style |
UCNotes |
The project notes editor | - |
UCAiChat (...UserComponents.AiChat) |
The Athena AI chat panel | - |
UCBlockly (...UserComponents.Blockly) |
The Blockly workspace. Normally reached through UCScriptEditButton rather than hosted directly |
- |
UCRobotImageMap, UCRobotPortPanel (...UserComponents.RobotConfig) |
The robot picture with port markers, used by the port pickers | - |
15. Popups: snackbars, dialogs, navigation
ARCx.Components.Layout.UCPopupController. These reach every connected browser.
UCPopupController.Instance is the controller of the most recently connected browser, and is
null when no browser is connected - always null-check it. Use Instance only for things that
must target exactly one browser (streaming a file download). Informational calls broadcast to all
browsers anyway.
Snackbar
ShowSnackbarMessage(Core.SnackBar.SnackBarItem e) shows a short notification in the corner of
every connected browser.
SnackBarItem member |
Default | Notes |
|---|---|---|
Message |
empty | Shortened to its first line and sentence, and to about 120 characters |
Severity |
Severity.Normal |
MudBlazor severity; defines icon and colour |
ButtonText |
"Ok" |
|
OnClick |
null |
Null means the button only dismisses |
Key |
a new guid | Reuse the same key to replace an earlier popup instead of stacking a new one, i.e. when reporting the changing status of one operation |
Dialog
ShowDialogMessageBox(...) shows a modal on every browser and blocks until the first one
responds (or the cancellation token fires); the other dialogs then close. Returns false when no
browser is connected.
DialogMessageItem member |
Default |
|---|---|
Title |
"Title" |
Message |
"Message" |
OKButtonText |
"OK" |
CancelButtonText |
null (no cancel button) |
ResponseValue |
false. True for OK; false for Cancel, no browser, or cancelled |
This path is primarily for the scripting engines, because it uses a cancellation token to close the popup when the script that opened it is stopped. A robot skill UI normally uses a MudBlazor dialog of its own instead.
16. Custom UI widgets
The Custom UI is where a user lays out their own control panel screens for the robot - buttons, sliders, joysticks, camera views - and shows them full screen or on a phone.
Two ways a robot skill appears there:
- The built-in "Robot skill window" widget, which embeds
RobotSkillUIMain.razorexactly as it appears on a workspace. This works with no code at all. - Widgets the skill provides itself: compact, purpose-built pieces of the skill.
Write a widget only when the skill has something a control panel actually wants - a gauge, a reading, one or two big buttons. A widget is not a second copy of the main window; it is the part of it that belongs on a phone in somebody's hand.
Declare them on the worker:
public override List<CustomUIWidgetDefinition> GetCustomUIWidgets() {
return new List<CustomUIWidgetDefinition>() {
new CustomUIWidgetDefinition() {
// Saved in the user's project, so keep it stable once the skill is published
Key = "counter",
Title = "Counter",
Description = "The counter, with start/stop and add buttons.",
ComponentType = typeof(RobotSkillUIWidget),
// The size it is dropped at. The user can resize it afterwards.
DefaultWidth = 200,
DefaultHeight = 140,
DefaultSettings = new Dictionary<string, string>() {
{ "Caption", "Counter" },
{ "ShowButtons", "true" },
},
SettingsHelp = "Caption: the label above the number. ShowButtons: true or false.",
},
};
}
CustomUIWidgetDefinition member |
Default | Notes |
|---|---|---|
Key |
empty | Saved in the project. Keep it stable once published |
Title / Description |
empty | Shown in the designer palette |
ComponentType |
- | A component deriving from RobotSkillUIWidgetBaseEntity |
DefaultWidth / DefaultHeight |
240 / 160 |
Design pixels |
DefaultSettings |
empty | Seeds the Settings dictionary the widget reads |
SettingsHelp |
empty | Explains the settings in the designer |
The designer calls GetCustomUIWidgets() whenever its palette is shown, so keep it quick.
Writing the widget component:
- Give your root element
width: 100%; height: 100%and let it fill the box the user sized. - Read settings with
GetSetting(),GetSettingNumber(),GetSettingBool(). - Honour
IsEditMode: draw a preview, disable the buttons, move nothing. - Call
StateHasChanged()(the base class one) to redraw. The worker'sRefreshUI()already calls it, so a widget usually needs no timer of its own. - When data only a widget shows is expensive to produce, the worker can skip it while
HasCustomUIClientis false. See Skip work nobody will see. - Several browsers may show the same widget at once, each with its own instance. Shared state belongs on the worker.
- Scale text with container query units so the widget reads at any size the user drew:
.tpl-widget { container-type: size; } /* declare on the PARENT */
.tpl-widget-value { font-size: clamp(1.2rem, 30cqh, 5rem); }
If the skill offers no widget, delete both RobotSkillUIWidget.razor and the
GetCustomUIWidgets() override - they go together.
Scripts address the Custom UI itself as a pseudo robot skill:
ControlCommand("Custom UI", "Show", "My screen").
17. MudBlazor conventions ARCx expects
- Give every button an explicit
VariantandColor. Without them aMudButtonrenders as plain text and does not read as a button.Variant.Filledfor the primary action,Variant.Outlinedfor the rest. - Use
UCTitleComponentTableRowColumnrows in a configuration page rather than hand-rolled labels, so every skill's settings look the same. - Wrap anything non-obvious in a
MudTooltip- except inside a repeated row, where the plaintitleattribute says the same thing for none of the cost. See section 3. - Use
Icons.Material.Filled.*for icons. - Theme colours come from MudBlazor CSS variables -
var(--mud-palette-surface),var(--mud-palette-lines-default),var(--mud-palette-text-secondary). Never hard-code a colour, or the skill breaks in the other theme. - Add
@usingdirectives to_Imports.razorrather than repeating them per file. The template already importsARCx.Components.UserComponents,ARCx.Components.UserComponents.UCServoSelectorComponents,ARCx.Core,ARCx.Core.ARCLog,ARCx.Core.DisposableandMudBlazor. - A robot skill project needs
<FrameworkReference Include="Microsoft.AspNetCore.App" />; the Razor SDK does not bring it in on its own and ARCx's public API is Blazor-typed.
Static files in a robot skill
Mark a file as an embedded resource in the skill project and reference it as
/_content/[assembly name]/[resource name]:
<MudImage Src="/_content/PluginRobotSkillTemplate/capture.jpg" />
Where to look next
ARCx.xml- besideARCx.dll, in the folder this copy of ARCx runs from. The full XML documentation for every public class, method, property and event. Search it rather than reading it; every entry is a<member name="...">whereT:is a type,M:a method,P:a property,E:an event,F:a field.RobotSkill-Services-And-Core.md- the non-visual API: EZB servos and I/O, video routing, audio, speech, movement, kinematics, navigation, scripting, logging and configuration.