Custom Movement Panel icon Custom Movement Panel — Custom Movement Panel maps directional commands to scripts, adjusts per-wheel speed (0-255), and integrates with joysticks for unsupported motors Try it →
 

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

  1. The three component base classes
  2. Layout rules
  3. Redrawing the UI
  4. Settings row components
  5. Hardware port pickers
  6. Servo configuration
  7. Script editing
  8. Logging and status output
  9. Video
  10. Drawing: UCSkillCanvas
  11. Serving your own images: UCDynamicImage
  12. Movement and input
  13. Kinematics
  14. Page furniture and misc
  15. Popups: snackbars, dialogs, navigation
  16. Custom UI widgets
  17. 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 .razor file 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-body sets overflow: hidden on 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:

  • UCLogBuffered sets the text with JavaScript. Call UCLogBuffered.AddEntry(...).
  • UCDynamicImage sets the img src with JavaScript. Call RefreshImage().

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:

  • MudTooltip is a popover. It registers itself with MudPopoverProvider, 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.
  • MudFileUpload carries its own <InputFile>, which captures an element reference back into the render tree. So does anything using @ref on 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 RobotSkillUIQueuedUpdateMS like 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 a RefreshUI().
  • Read what changes from members, not from a local. The wrapped markup reads fields and properties when the region draws, so Worker.PlaySeconds is 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 UCRefreshRegion and redraw it with RefreshUIRegion() instead of RefreshUI(). 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 RobotSkillUIQueuedUpdateMS throttle 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 narrower HasUIClient / 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 MudTooltip inside a repeated row. Use the plain HTML title attribute on the control instead - it is a native browser tooltip, costs nothing and reads the same to the user. Keep MudTooltip for the handful of controls in a toolbar or a settings row.

  • One MudFileUpload per window, not per row. Put a single picker outside the list, remember which row was pressed, and open it with OpenFilePickerAsync():

    @* 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 UCSkillCanvas or UCDynamicImage, 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:

  1. The built-in "Robot skill window" widget, which embeds RobotSkillUIMain.razor exactly as it appears on a workspace. This works with no code at all.
  2. 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's RefreshUI() 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 HasCustomUIClient is 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 Variant and Color. Without them a MudButton renders as plain text and does not read as a button. Variant.Filled for the primary action, Variant.Outlined for the rest.
  • Use UCTitleComponentTableRowColumn rows 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 plain title attribute 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 @using directives to _Imports.razor rather than repeating them per file. The template already imports ARCx.Components.UserComponents, ARCx.Components.UserComponents.UCServoSelectorComponents, ARCx.Core, ARCx.Core.ARCLog, ARCx.Core.Disposable and MudBlazor.
  • 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 - beside ARCx.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="..."> where T: 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.