HUD: option to override EKF and Vibe#3686
HUD: option to override EKF and Vibe#3686robertlong13 wants to merge 2 commits intoArduPilot:masterfrom
Conversation
Add IHudIconRenderer and CustomEkfRenderer/CustomVibeRenderer properties so plugins can replace the EKF and vibration status icons with custom renderers.
Only stamp lastrepeat when userepeattime is true. Previously CheckValue(false) would update lastrepeat as a side effect, suppressing subsequent alerts.
There was a problem hiding this comment.
Pull request overview
This PR adds a plugin extension point to replace the HUD’s built-in EKF and VIBE indicator “icon slots” with custom rendering and click behavior, enabling richer plugin-driven alerting/annunciator UX.
Changes:
- Introduces
IHudIconRendererto let plugins render and handle clicks for EKF/VIBE HUD slots. - Extends
HUDwithCustomEkfRenderer/CustomVibeRenderer, wiring them into paint + mouse click/move logic, and adds a publicDrawStringhelper for custom renderers. - Adjusts
CustomWarning.CheckValue()repeat-timing behavior whenuserepeattimeis disabled.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| ExtLibs/Utilities/Warnings/CustomWarning.cs | Tweaks repeat-time tracking logic in warning evaluation. |
| ExtLibs/Controls/IHudIconRenderer.cs | Adds the public plugin-facing interface for HUD slot rendering/click handling. |
| ExtLibs/Controls/HUD.cs | Adds custom renderer properties, routes paint/click/hover behavior to them, and exposes a DrawString helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (CustomEkfRenderer != null) | ||
| try { CustomEkfRenderer.OnClick(); } catch { } | ||
| else if (ekfclick != null) |
There was a problem hiding this comment.
OnClick() exceptions from custom renderers are swallowed (catch { }), which can hide plugin failures and make troubleshooting difficult. Consider catching Exception ex and logging it similarly to the Render() path (and potentially falling back to the default click handler).
| if (ekfhitzone.IntersectsWith(new Rectangle(e.X, e.Y, 5, 5))) | ||
| { | ||
| Cursor.Current = Cursors.Hand; | ||
| if (CustomEkfRenderer == null || CustomEkfRenderer.ShowHandCursor) | ||
| Cursor.Current = Cursors.Hand; | ||
| } | ||
| else if (vibehitzone.IntersectsWith(new Rectangle(e.X, e.Y, 5, 5))) | ||
| { | ||
| Cursor.Current = Cursors.Hand; | ||
| if (CustomVibeRenderer == null || CustomVibeRenderer.ShowHandCursor) | ||
| Cursor.Current = Cursors.Hand; | ||
| } |
There was a problem hiding this comment.
ShowHandCursor is accessed without any exception handling. Since this is plugin-provided code, a thrown exception here will break mouse-move handling for the HUD. Consider wrapping the ShowHandCursor getter access in a try/catch (defaulting to showing the hand cursor, or logging and using a safe default).
| public void DrawString(string text, float fontsize, SolidBrush brush, float x, float y) | ||
| { | ||
| drawstring(text, font, fontsize, brush, x, y); |
There was a problem hiding this comment.
DrawString takes a SolidBrush, which forces plugin renderers to cast from Brushes.* (typed as Brush) and prevents use of non-solid brushes. Since this is being exposed as a plugin-facing API, consider accepting Brush (or Color) instead to make the method easier and safer to use.
| public void DrawString(string text, float fontsize, SolidBrush brush, float x, float y) | |
| { | |
| drawstring(text, font, fontsize, brush, x, y); | |
| public void DrawString(string text, float fontsize, Brush brush, float x, float y) | |
| { | |
| // Support any Brush in GDI+ mode; approximate as a solid color in OpenGL mode. | |
| if (!opengl) | |
| { | |
| // Use the standard GDI+ DrawString when not using OpenGL. | |
| graphicsObjectGDIP.DrawString(text, font, brush, x, y); | |
| return; | |
| } | |
| // OpenGL text path expects a solid color; try to extract it. | |
| Color color; | |
| if (brush is SolidBrush solid) | |
| { | |
| color = solid.Color; | |
| } | |
| else | |
| { | |
| // Non-solid brushes cannot be represented exactly as a single color; | |
| // fall back to black as a safe default. | |
| color = Color.Black; | |
| } | |
| using (var solidForGl = new SolidBrush(color)) | |
| { | |
| drawstring(text, font, fontsize, solidForGl, x, y); | |
| } | |
| } | |
| // Backwards-compatible overload for existing callers that use SolidBrush explicitly. | |
| public void DrawString(string text, float fontsize, SolidBrush brush, float x, float y) | |
| { | |
| DrawString(text, fontsize, (Brush)brush, x, y); |
| void Render(HUD hud, Rectangle hitZone, int fontsize, bool useIcons); | ||
| | ||
| /// <summary> | ||
| /// Called when the user clicks this icon slot. | ||
| /// </summary> | ||
| void OnClick(); |
There was a problem hiding this comment.
OnClick() provides no context (e.g., which HUD instance / slot / hitZone was clicked). If multiple HUDs are ever used or a renderer instance is shared, the handler has to rely on state captured during Render(). Consider passing at least the HUD instance (and optionally the hitZone / mouse args) into OnClick for a more robust plugin API.
| if (ekfhitzone.IntersectsWith(new Rectangle(e.X, e.Y, 5, 5))) | ||
| { | ||
| Cursor.Current = Cursors.Hand; | ||
| if (CustomEkfRenderer == null || CustomEkfRenderer.ShowHandCursor) | ||
| Cursor.Current = Cursors.Hand; | ||
| } | ||
| else if (vibehitzone.IntersectsWith(new Rectangle(e.X, e.Y, 5, 5))) | ||
| { | ||
| Cursor.Current = Cursors.Hand; | ||
| if (CustomVibeRenderer == null || CustomVibeRenderer.ShowHandCursor) | ||
| Cursor.Current = Cursors.Hand; | ||
| } |
There was a problem hiding this comment.
When CustomEkfRenderer.ShowHandCursor (or CustomVibeRenderer.ShowHandCursor) is false, the code leaves Cursor.Current unchanged inside the hit-zone branch. This can result in the cursor incorrectly staying as a hand when moving from another clickable zone into the EKF/VIBE zone. Consider explicitly setting Cursor.Current = DefaultCursor when ShowHandCursor is false, or compute the desired cursor first and set it once at the end.
| if (CustomVibeRenderer != null) | ||
| { | ||
| try { CustomVibeRenderer.Render(this, vibehitzone, fontsize, displayicons); } | ||
| catch (Exception ex) { log.Error("Custom vibe renderer", ex); } | ||
| } |
There was a problem hiding this comment.
If CustomVibeRenderer.Render() throws, the catch logs the error but the HUD then skips drawing the default VIBE indicator (leaving the slot blank). Consider falling back to the existing default VIBE rendering logic after logging so a faulty plugin can’t remove the indicator entirely.
| if (CustomEkfRenderer != null) | ||
| { | ||
| try { CustomEkfRenderer.Render(this, ekfhitzone, fontsize, displayicons); } | ||
| catch (Exception ex) { log.Error("Custom ekf renderer", ex); } | ||
| } |
There was a problem hiding this comment.
If CustomEkfRenderer.Render() throws, the catch logs the error but the HUD then skips drawing the default EKF indicator (leaving the slot blank). Consider falling back to the existing default EKF rendering logic after logging so a faulty plugin can’t remove the indicator entirely.
| I was thinking about something like this, but as a bigger change, adding a line of icons/annunciators to the top of the HUD and moving current VIBE/EKF/GPS there, leaving the bottom line to the battery exclusively. |
This adds the ability for plugins to override the EKF and Vibe indicators with anything you want. I'm using it in the Carbonix plugin to make a sophisticated crew-alerting system, but the possibilities are endless.
I've made this small example, replacing Vibe with a sticky warning light which you can click and see all the warnings that have come in since it turned on and then ack them. I can commit it in as part of the PR if we want, or we can come up with an idea for an even smaller thing.
example24-annunciator.cs.txt