Discovery, display consistency, full MQTT mapping, integration tests#32
Discovery, display consistency, full MQTT mapping, integration tests#32markus-lassfolk wants to merge 64 commits into
Conversation
## Summary <!-- Describe your changes in a few sentences. Link the related issue(s). --> Closes #<!-- issue number --> ## Type of change - [ ] 🐛 Bug fix (non-breaking change that fixes an issue) - [ ] ✨ New feature (non-breaking change that adds functionality) - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 Refactor / code cleanup (no behaviour change) - [ ] 📝 Documentation update - [ ] ⚙️ CI/CD / tooling change ## Changes made <!-- Bullet-point summary of what changed and why. --> - - ## Testing <!-- Describe how you tested this. Include commands you ran. --> - [ ] Added / updated Pester tests - [ ] All tests pass locally (`Invoke-Pester ./tests`) - [ ] Tested manually against Yarbo hardware (if applicable — describe setup) ## Quality checklist - [ ] **PSScriptAnalyzer**: Zero Error or Warning findings (`Invoke-ScriptAnalyzer ./src -Settings ./PSScriptAnalyzerSettings.psd1 -Severity Error,Warning`) - [ ] **Tests pass**: `Invoke-Pester ./tests` — all green - [ ] **Module manifest updated** if version changed - [ ] **CHANGELOG.md** updated under `[Unreleased]` - [ ] **Docs updated** (comment-based help, README if needed) - [ ] **No secrets, credentials, or PII** in code, comments, or commit messages - [ ] **Follows coding standards** in [CONTRIBUTING.md](../CONTRIBUTING.md) ## Screenshots / output (optional) <!-- Paste relevant terminal output, before/after if helpful. --> --- > **Reviewer note**: Branch must be up-to-date with `develop` and all CI checks must pass before merge. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes repository automation to create and squash-merge PRs automatically based on branch/author heuristics, which could merge unintended changes if misconfigured. Code changes are limited to GitHub Actions/workflow logic and removal of a module-side error reporting hook. > > **Overview** > Adds a new GitHub Actions workflow (`auto-create-pr.yml`) that automatically opens PRs to `develop` when bot/agent accounts push to fix-style branches and no PR exists. > > Expands and modernizes the existing `copilot-automerge` workflow by updating `actions/github-script` to `v7`, adding needed permissions, and tightening eligibility to bot-authored PRs on fix branch prefixes; it now comments on failed checks and squash-merges when all checks pass. > > Removes PS module auto-initialization for error reporting and deletes the private `ErrorReporter.ps1` implementation. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f64f653. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: markus-lassfolk <markus@lassfolk.fi>
…ation tests Discovery & connection - Find-YarboDevice: discover endpoints, label Rover vs DC (MAC), recommend one per device, failover order - Find-Yarbo: deduplicate by SerialNumber (one robot per device) - ArpHelper.ps1: Get-MacForIp, Test-LocallyAdministeredMac, Invoke-ArpPrime - NetworkHelper.ps1: Get-PSYarboLocalSubnets (use machine's networks when no -Subnet) - YarboEndpoint class; Find-YarboDevice outputs in try-order (recommended first) - Warning when scanning >512 hosts (slow, suggest -MaxHosts) Display consistency (table / fl / select * match) - Script properties Battery, State (and where relevant HeadingDisplay, Time, Areas, SelfOrder, Days, EnabledDisplay, WeatherDisplay, RecommendedDisplay) on YarboRobot, YarboTelemetry, YarboPlan, YarboSchedule, YarboEndpoint - Format and Types.ps1xml updated so same concepts appear everywhere Full MQTT → PowerShell objects (no exceptions) - YarboRobot: all DeviceMSG fields (BatteryMSG, BodyMsg, StateMSG, RTKMSG, RunningStatusMSG, wireless_recharge, ultrasonic_msg, led, route_priority, timestamps); RawMessage (hidden) - YarboTelemetry: same + RechargeState, RtkHeadingStatus, LedRegister, GnggaRaw, DeviceTimestamp, BatteryTimestamp - TelemetryParser: ConvertTo-YarboRobot / ConvertTo-YarboTelemetry map every reversed field - YarboGlobalParams class (MowSpeed, CuttingHeight, RainDelay, ObstacleSensitivity, Timestamp, RawData); Get-YarboGlobalParams returns [YarboGlobalParams] Tests & tooling - Integration tests: Find-Yarbo → Connect-Yarbo → Get-Yarbo* (Status, Robot, Battery, Firmware, GlobalParams, Map, Plan, Schedule, Telemetry, Log) → Disconnect - Class count 11 (YarboGlobalParams added) - tools/MqttSniff.ps1, Invoke-YarboMqttSniff (if present) Other - Various doc/build/issue template updates from earlier work Made-with: Cursor
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
This PR significantly enhances the PSYarbo module's discovery, display consistency, MQTT mapping, and testing capabilities. It introduces intelligent network-based robot discovery with Rover vs DC classification, standardizes display properties across all object types, maps all MQTT fields to PowerShell objects, and adds comprehensive integration tests.
Changes:
- Discovery improvements: Find-YarboDevice distinguishes Rover (direct WiFi) from DC (base station) via MAC address inspection; both Find-Yarbo and Find-YarboDevice auto-detect local subnets when no -Subnet is specified
- Display consistency: Script properties (Battery, State, HeadingDisplay, etc.) standardized across YarboRobot, YarboTelemetry, YarboPlan, YarboSchedule, YarboEndpoint for uniform output
- Full MQTT mapping: All DeviceMSG fields now mapped to YarboRobot/YarboTelemetry properties; new YarboGlobalParams class with typed properties; RawMessage/GnggaRaw/RawData preserved for advanced use
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/PSYarbo/Public/Connection/Find-YarboDevice.ps1 | New cmdlet for endpoint discovery with Rover/DC classification and failover ordering |
| src/PSYarbo/Public/Connection/Find-Yarbo.ps1 | Refactored to use local subnet detection, C# YarboMqttListener, and deduplicate by SerialNumber |
| src/PSYarbo/Public/Connection/Connect-Yarbo.ps1 | Message handler moved before Connect() call per MQTTnet requirements |
| src/PSYarbo/Public/Connection/Invoke-YarboMqttSniff.ps1 | New troubleshooting cmdlet using YarboMqttListener |
| src/PSYarbo/Public/Status/Get-YarboGlobalParams.ps1 | Returns typed YarboGlobalParams instead of raw PSCustomObject |
| src/PSYarbo/Private/NetworkHelper.ps1 | New Get-PSYarboLocalSubnets helper for auto-detecting scan targets |
| src/PSYarbo/Private/ArpHelper.ps1 | New MAC resolution helpers (Get-MacForIp, Test-LocallyAdministeredMac, Invoke-ArpPrime) |
| src/PSYarbo/Private/TelemetryParser.ps1 | Extended to map all DeviceMSG fields including BatteryTimestamp, RechargeState, RtkHeadingStatus, ultrasonic_msg, etc. |
| src/PSYarbo/Classes/YarboGlobalParams.ps1 | New class for typed global parameters |
| src/PSYarbo/Classes/YarboEndpoint.ps1 | New class for discovery results |
| src/PSYarbo/Classes/YarboRobot.ps1 | Added properties for ultrasonic sensors, wireless charging, rain sensor, device timestamp |
| src/PSYarbo/Classes/YarboTelemetry.ps1 | Added properties matching YarboRobot for display consistency |
| src/PSYarbo/PSYarbo.psm1 | Added C# YarboMqttListener helper for reliable MQTT message capture; switched to direct Assembly.LoadFrom |
| src/PSYarbo/PSYarbo.psd1 | Added ScriptsToProcess for class pre-loading; exported Find-YarboDevice and Invoke-YarboMqttSniff |
| src/PSYarbo/PSYarbo.Types.ps1xml | Added script properties (Battery, State, HeadingDisplay, etc.) for consistent display across all types |
| src/PSYarbo/PSYarbo.Format.ps1xml | Refactored to use script properties from Types.ps1xml; added YarboEndpoint table view |
| tests/Integration/FindConnectGetYarbo.Integration.Tests.ps1 | New integration test suite covering Find-Yarbo → Connect-Yarbo → all Get-Yarbo* cmdlets |
| tests/PSYarbo.Tests.ps1 | Updated class count to 11; added Find-YarboDevice to expected exports |
| tests/Unit/PrivateFunctions.Tests.ps1 | Updated test IP addresses for consistency |
| tools/MqttSniff.ps1 | New troubleshooting script wrapper for Invoke-YarboMqttSniff |
| README.md | Updated documentation for discovery alignment with python-yarbo |
| CONTRIBUTING.md | Updated example documentation |
…elftest subscription, thread-safety, and parameter type compatibility
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Inconsistent types for wireless charge properties across classes
- Changed WirelessChargeVoltage and WirelessChargeCurrent from [int] to [double] in YarboTelemetry class and ConvertTo-YarboTelemetry function to match YarboRobot and prevent truncation of decimal values.
- ✅ Fixed: Massive duplicated subnet scanning logic between discovery functions
- Extracted duplicated subnet IP generation logic into a shared Get-PSYarboSubnetIpList helper function in NetworkHelper.ps1 and updated both Find-Yarbo and Find-YarboDevice to use it.
- ✅ Fixed: Type literal throws instead of enabling graceful fallback
- Replaced unsafe type literal [PSYarbo.Mqtt.YarboMqttListener] with safe 'PSYarbo.Mqtt.YarboMqttListener' -as [type] pattern in both Find-Yarbo and Find-YarboDevice to enable graceful fallback when type is unavailable.
Preview (93d3c64514)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -88,7 +88,7 @@
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -165,6 +165,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. Default is 192.168.1.0/24 (typical home LAN); pass `-Subnet` if your network uses a different range so discovery can scan it. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -89,6 +89,11 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient -and [PSYarbo.Mqtt.MessageReceivedAdapter]) {
+ [PSYarbo.Mqtt.MessageReceivedAdapter]::UnregisterCallback($this.MqttClient)
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,16 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -24,25 +29,39 @@
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
[string]$LedRegister
+ [int]$WirelessChargeState
[double]$WirelessChargeVoltage
[double]$WirelessChargeCurrent
+ [int]$WirelessChargeErrorCode
- # Network
+ # Ultrasonic (ultrasonic_msg)
+ [int]$UltrasonicLeftFront
+ [int]$UltrasonicMiddle
+ [int]$UltrasonicRightFront
+
+ # Network (route_priority)
[hashtable]$RoutePriority
- # Timestamps
+ # Timestamps (device timestamp + our LastUpdated)
+ [double]$DeviceTimestamp
[datetime]$LastUpdated
# Connection reference (hidden)
hidden [string]$Broker
hidden [int]$Port
+ # Full DeviceMSG payload (all MQTT keys available)
+ hidden [PSCustomObject]$RawMessage
+
[string] ToString() {
$displayName = if ($this.Name) { $this.Name } else { $this.SerialNumber }
return "$displayName ($($this.SerialNumber)) - Battery: $($this.BatteryCapacity)% - State: $(if($this.WorkingState){'Active'}else{'Idle'})"
diff --git a/src/PSYarbo/Classes/YarboTelemetry.ps1 b/src/PSYarbo/Classes/YarboTelemetry.ps1
--- a/src/PSYarbo/Classes/YarboTelemetry.ps1
+++ b/src/PSYarbo/Classes/YarboTelemetry.ps1
@@ -2,28 +2,33 @@
[datetime]$Timestamp
[string]$SerialNumber
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
[bool]$BatteryTempError
+ [double]$BatteryTimestamp
- # Position
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # Position (CombinedOdom, RTKMSG, combined_odom_confidence)
[double]$X
[double]$Y
[double]$Phi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
[double]$RtkDop
[double]$OdomConfidence
- # Running status
+ # Running status (RunningStatusMSG)
[int]$ChuteAngle
[int]$ChuteSteeringEngineInfo
[double]$HeadGyroPitch
[double]$HeadGyroRoll
[int]$RainSensorData
- # State flags
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -31,23 +36,30 @@
[bool]$IsPaused
[bool]$IsRecharging
- # Sensors
+ # Sensors (ultrasonic_msg)
[int]$UltrasonicLeftFront
[int]$UltrasonicMiddle
[int]$UltrasonicRightFront
- # Wireless charging
+ # Wireless charging (wireless_recharge)
[int]$WirelessChargeState
- [int]$WirelessChargeVoltage
- [int]$WirelessChargeCurrent
+ [double]$WirelessChargeVoltage
+ [double]$WirelessChargeCurrent
[int]$WirelessChargeErrorCode
- # GPS (parsed from rtk_base_data.rover.gngga NMEA sentence)
+ # LED (led)
+ [string]$LedRegister
+
+ # GPS (parsed from rtk_base_data.rover.gngga + raw)
[nullable[double]]$Latitude
[nullable[double]]$Longitude
[nullable[double]]$Altitude
[int]$FixQuality
+ [string]$GnggaRaw
+ # Device message timestamp (root timestamp)
+ [double]$DeviceTimestamp
+
# Raw
hidden [PSCustomObject]$RawMessage
diff --git a/src/PSYarbo/PSYarbo.Format.ps1xml b/src/PSYarbo/PSYarbo.Format.ps1xml
--- a/src/PSYarbo/PSYarbo.Format.ps1xml
+++ b/src/PSYarbo/PSYarbo.Format.ps1xml
@@ -23,10 +23,10 @@
<TableColumnItems>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SerialNumber</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>"$($_.BatteryCapacity)%"</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.WorkingState){'Active'}else{'Idle'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Battery</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>State</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RtkStatus</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Heading, 1)</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>HeadingDisplay</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>ErrorCode</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
@@ -34,7 +34,7 @@
</TableControl>
</View>
- <!-- YarboRobot List View -->
+ <!-- YarboRobot List View (Battery and State match table view; raw fields kept for scripting) -->
<View>
<Name>YarboRobotList</Name>
<ViewSelectedBy>
@@ -46,6 +46,8 @@
<ListItems>
<ListItem><PropertyName>SerialNumber</PropertyName></ListItem>
<ListItem><PropertyName>Name</PropertyName></ListItem>
+ <ListItem><PropertyName>Battery</PropertyName></ListItem>
+ <ListItem><PropertyName>State</PropertyName></ListItem>
<ListItem><PropertyName>HeadType</PropertyName></ListItem>
<ListItem><PropertyName>BatteryCapacity</PropertyName></ListItem>
<ListItem><PropertyName>BatteryStatus</PropertyName></ListItem>
@@ -53,6 +55,7 @@
<ListItem><PropertyName>ChargingStatus</PropertyName></ListItem>
<ListItem><PropertyName>ErrorCode</PropertyName></ListItem>
<ListItem><PropertyName>Heading</PropertyName></ListItem>
+ <ListItem><PropertyName>HeadingDisplay</PropertyName></ListItem>
<ListItem><PropertyName>RtkStatus</PropertyName></ListItem>
<ListItem><PropertyName>OdomConfidence</PropertyName></ListItem>
<ListItem><PropertyName>ChuteAngle</PropertyName></ListItem>
@@ -83,14 +86,14 @@
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
- <TableColumnItem><ScriptBlock>$_.Timestamp.ToString('HH:mm:ss.ff')</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>"$($_.BatteryCapacity)%"</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.WorkingState){'Active'}else{'Idle'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Time</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Battery</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>State</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RtkStatus</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Heading, 1)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.X, 3)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Y, 3)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.OdomConfidence, 3)</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>HeadingDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>XDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>YDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>OdomConfidenceDisplay</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
@@ -129,6 +132,36 @@
</TableControl>
</View>
+ <!-- YarboEndpoint Table View (issue #30: Rover vs DC discovery) -->
+ <View>
+ <Name>YarboEndpoint</Name>
+ <ViewSelectedBy>
+ <TypeName>YarboEndpoint</TypeName>
+ </ViewSelectedBy>
+ <TableControl>
+ <TableHeaders>
+ <TableColumnHeader><Label>IPAddress</Label><Width>16</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Port</Label><Width>6</Width><Alignment>Right</Alignment></TableColumnHeader>
+ <TableColumnHeader><Label>Path</Label><Width>6</Width></TableColumnHeader>
+ <TableColumnHeader><Label>MacAddress</Label><Width>18</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Recommended</Label><Width>12</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Status</Label><Width>10</Width></TableColumnHeader>
+ </TableHeaders>
+ <TableRowEntries>
+ <TableRowEntry>
+ <TableColumnItems>
+ <TableColumnItem><PropertyName>IPAddress</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Port</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Path</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>MacAddress</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>RecommendedDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
+ </TableColumnItems>
+ </TableRowEntry>
+ </TableRowEntries>
+ </TableControl>
+ </View>
+
<!-- YarboPlan Table View -->
<View>
<Name>YarboPlan</Name>
@@ -147,8 +180,8 @@
<TableColumnItems>
<TableColumnItem><PropertyName>Id</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>$_.AreaIds -join ', '</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.EnableSelfOrder){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Areas</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>SelfOrder</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
@@ -178,14 +211,11 @@
<TableColumnItem><PropertyName>Id</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>PlanId</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.Enabled){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>
- if ($_.WeekDay -eq 127) { 'Daily' }
- else { ($_.GetDays() | ForEach-Object { $_.ToString().Substring(0,3) }) -join ',' }
- </ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>EnabledDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Days</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>StartTime</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>EndTime</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.IsWeatherSchedule){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>WeatherDisplay</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
diff --git a/src/PSYarbo/PSYarbo.Types.ps1xml b/src/PSYarbo/PSYarbo.Types.ps1xml
--- a/src/PSYarbo/PSYarbo.Types.ps1xml
+++ b/src/PSYarbo/PSYarbo.Types.ps1xml
@@ -11,13 +11,29 @@
<Type>
<Name>YarboRobot</Name>
<Members>
+ <ScriptProperty>
+ <Name>Battery</Name>
+ <GetScriptBlock>"$($this.BatteryCapacity)%"</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>State</Name>
+ <GetScriptBlock>if($this.WorkingState){'Active'}else{'Idle'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>HeadingDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Heading, 1)</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>SerialNumber</Name>
<Name>Name</Name>
+ <Name>Battery</Name>
+ <Name>State</Name>
+ <Name>HeadingDisplay</Name>
<Name>HeadType</Name>
<Name>BatteryCapacity</Name>
+ <Name>BatteryStatus</Name>
<Name>WorkingState</Name>
<Name>ChargingStatus</Name>
<Name>ErrorCode</Name>
@@ -48,19 +64,51 @@
</Members>
</Type>
- <!-- YarboTelemetry: key telemetry fields -->
+ <!-- YarboTelemetry: key telemetry fields. Battery and State match YarboRobot for consistent display. -->
<Type>
<Name>YarboTelemetry</Name>
<Members>
+ <ScriptProperty>
+ <Name>Battery</Name>
+ <GetScriptBlock>"$($this.BatteryCapacity)%"</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>State</Name>
+ <GetScriptBlock>if($this.WorkingState){'Active'}else{'Idle'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>Time</Name>
+ <GetScriptBlock>$this.Timestamp.ToString('HH:mm:ss.ff')</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>HeadingDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Heading, 1)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>XDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.X, 3)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>YDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Y, 3)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>OdomConfidenceDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.OdomConfidence, 3)</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
+ <Name>Time</Name>
<Name>Timestamp</Name>
<Name>SerialNumber</Name>
+ <Name>Battery</Name>
+ <Name>State</Name>
<Name>BatteryCapacity</Name>
<Name>WorkingState</Name>
<Name>RtkStatus</Name>
<Name>Heading</Name>
+ <Name>HeadingDisplay</Name>
<Name>X</Name>
<Name>Y</Name>
<Name>OdomConfidence</Name>
@@ -69,36 +117,61 @@
</Members>
</Type>
- <!-- YarboPlan: key plan fields -->
+ <!-- YarboPlan: key plan fields. Areas and SelfOrder match table view. -->
<Type>
<Name>YarboPlan</Name>
<Members>
+ <ScriptProperty>
+ <Name>Areas</Name>
+ <GetScriptBlock>if($this.AreaIds){$this.AreaIds -join ', '}else{''}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>SelfOrder</Name>
+ <GetScriptBlock>if($this.EnableSelfOrder){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Id</Name>
<Name>Name</Name>
+ <Name>Areas</Name>
<Name>AreaIds</Name>
+ <Name>SelfOrder</Name>
<Name>EnableSelfOrder</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</Type>
- <!-- YarboSchedule: key schedule fields -->
+ <!-- YarboSchedule: key schedule fields. Days, EnabledDisplay, WeatherDisplay match table view. -->
<Type>
<Name>YarboSchedule</Name>
<Members>
+ <ScriptProperty>
+ <Name>Days</Name>
+ <GetScriptBlock>if($this.WeekDay -eq 127){'Daily'}else{($this.GetDays() | ForEach-Object{$_.ToString().Substring(0,3)}) -join ','}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>EnabledDisplay</Name>
+ <GetScriptBlock>if($this.Enabled){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>WeatherDisplay</Name>
+ <GetScriptBlock>if($this.IsWeatherSchedule){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Id</Name>
<Name>Name</Name>
<Name>PlanId</Name>
+ <Name>EnabledDisplay</Name>
<Name>Enabled</Name>
+ <Name>Days</Name>
<Name>WeekDay</Name>
<Name>StartTime</Name>
<Name>EndTime</Name>
+ <Name>WeatherDisplay</Name>
<Name>IsWeatherSchedule</Name>
</ReferencedProperties>
</PropertySet>
@@ -123,6 +196,29 @@
</Members>
</Type>
+ <!-- YarboEndpoint: discovery result. RecommendedDisplay matches table view. -->
+ <Type>
+ <Name>YarboEndpoint</Name>
+ <Members>
+ <ScriptProperty>
+ <Name>RecommendedDisplay</Name>
+ <GetScriptBlock>if($this.Recommended){'⭐ Yes'}else{''}</GetScriptBlock>
+ </ScriptProperty>
+ <PropertySet>
+ <Name>DefaultDisplayPropertySet</Name>
+ <ReferencedProperties>
+ <Name>IPAddress</Name>
+ <Name>Port</Name>
+ <Name>Path</Name>
+ <Name>MacAddress</Name>
+ <Name>RecommendedDisplay</Name>
+ <Name>Recommended</Name>
+ <Name>Status</Name>
+ </ReferencedProperties>
+ </PropertySet>
+ </Members>
+ </Type>
+
<!-- YarboLightState: all LED channels -->
<Type>
<Name>YarboLightState</Name>
diff --git a/src/PSYarbo/PSYarbo.psd1 b/src/PSYarbo/PSYarbo.psd1
--- a/src/PSYarbo/PSYarbo.psd1
+++ b/src/PSYarbo/PSYarbo.psd1
@@ -10,11 +10,26 @@
PowerShellVersion = '7.4'
# MQTTnet loaded via AssemblyLoadContext in PSYarbo.psm1 — not via RequiredAssemblies
# RequiredAssemblies = @()
+ # Load class definitions before root module so [YarboRobot], [YarboConnection], etc. are visible to OutputType/attributes
+ ScriptsToProcess = @(
+ 'Classes/YarboExceptions.ps1'
+ 'Classes/YarboLightState.ps1'
+ 'Classes/YarboCommandResult.ps1'
+ 'Classes/YarboTelemetry.ps1'
+ 'Classes/YarboPlan.ps1'
+ 'Classes/YarboSchedule.ps1'
+ 'Classes/YarboRobot.ps1'
+ 'Classes/YarboGlobalParams.ps1'
+ 'Classes/YarboEndpoint.ps1'
+ 'Classes/YarboConnection.ps1'
+ 'Classes/YarboCloudSession.ps1'
+ )
FormatsToProcess = @('PSYarbo.Format.ps1xml')
- TypesToProcess = @('PSYarbo.Types.ps1xml')
+ # Types loaded in .psm1 with Update-TypeData -Force to avoid "already present" on re-import
+ # TypesToProcess = @('PSYarbo.Types.ps1xml')
FunctionsToExport = @(
# Connection
- 'Connect-Yarbo', 'Disconnect-Yarbo', 'Find-Yarbo',
+ 'Connect-Yarbo', 'Disconnect-Yarbo', 'Find-Yarbo', 'Find-YarboDevice', 'Invoke-YarboMqttSniff',
# Status
'Get-YarboStatus', 'Get-YarboRobot', 'Get-YarboBattery', 'Get-YarboFirmware',
'Get-YarboGlobalParams',
diff --git a/src/PSYarbo/PSYarbo.psm1 b/src/PSYarbo/PSYarbo.psm1
--- a/src/PSYarbo/PSYarbo.psm1
+++ b/src/PSYarbo/PSYarbo.psm1
@@ -16,37 +16,173 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
-#region — Load MQTTnet via isolated AssemblyLoadContext
+#region — Load MQTTnet and C# listener helper
$libPath = Join-Path $PSScriptRoot 'lib'
$mqttDllPath = Join-Path $libPath 'MQTTnet.dll'
if (Test-Path $mqttDllPath) {
- $alcName = 'PSYarboMqttContext'
- $script:MqttALC = [System.Runtime.Loader.AssemblyLoadContext]::new($alcName, $true)
- $script:MqttAssembly = $script:MqttALC.LoadFromAssemblyPath((Resolve-Path $mqttDllPath).Path)
- Write-Verbose "PSYarbo: Loaded MQTTnet via isolated AssemblyLoadContext '$alcName'"
+ $script:MqttAssembly = [System.Reflection.Assembly]::LoadFrom((Resolve-Path $mqttDllPath).Path)
+ Write-Verbose "PSYarbo: Loaded MQTTnet from $mqttDllPath"
+ # C# helper: connects, subscribes, and receives messages entirely in C# so the MQTTnet
+ # ApplicationMessageReceivedAsync event is attached with += and always fires.
+ $listenerCode = @'
+using System;
+using System.Collections;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PSYarbo.Mqtt {
+ public sealed class YarboMqttListener : IDisposable {
+ private readonly MQTTnet.MqttFactory _factory;
+ private MQTTnet.Client.IMqttClient _client;
+ private readonly ArrayList _messages = ArrayList.Synchronized(new ArrayList());
+ private readonly TaskCompletionSource<ReceivedMessage> _firstMessage = new TaskCompletionSource<ReceivedMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public YarboMqttListener() {
+ _factory = new MQTTnet.MqttFactory();
+ _client = _factory.CreateMqttClient();
+ _client.ApplicationMessageReceivedAsync += e => {
+ var seg = e.ApplicationMessage.PayloadSegment;
+ int len = seg.Array != null ? seg.Count : 0;
+ byte[] copy = len > 0 ? new byte[len] : Array.Empty<byte>();
+ if (len > 0 && seg.Array != null)
+ Buffer.BlockCopy(seg.Array, seg.Offset, copy, 0, len);
+ var msg = new ReceivedMessage(e.ApplicationMessage.Topic ?? "", copy);
+ _messages.Add(msg);
+ if (!_firstMessage.Task.IsCompleted)
+ _firstMessage.TrySetResult(msg);
+ return Task.CompletedTask;
+ };
+ }
+
+ public void Connect(string broker, int port, string clientId, TimeSpan timeout) {
+ var opts = _factory.CreateClientOptionsBuilder()
+ .WithTcpServer(broker, port)
+ .WithClientId(clientId)
+ .WithTimeout(timeout)
+ .Build();
+ _client.ConnectAsync(opts, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public void Subscribe(string[] topicFilters) {
+ var builder = _factory.CreateSubscribeOptionsBuilder();
+ foreach (var topic in topicFilters)
+ builder = builder.WithTopicFilter(topic);
+ var opts = builder.Build();
+ _client.SubscribeAsync(opts, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public void Publish(string topic, byte[] payload) {
+ var msg = _factory.CreateApplicationMessageBuilder()
+ .WithTopic(topic)
+ .WithPayload(payload ?? Array.Empty<byte>())
+ .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce)
+ .Build();
+ _client.PublishAsync(msg, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public ReceivedMessage WaitForFirstMessage(int timeoutMs) {
+ try {
+ if (_firstMessage.Task.IsCompleted)
+ return _firstMessage.Task.Result;
+ return _firstMessage.Task.Wait(timeoutMs) ? _firstMessage.Task.Result : null;
+ } catch { return null; }
+ }
+
+ public ReceivedMessage[] GetReceivedMessages() {
+ var arr = new ReceivedMessage[_messages.Count];
+ _messages.CopyTo(arr);
+ _messages.Clear();
+ return arr;
+ }
+
+ public void Disconnect() {
+ try {
+ _client.DisconnectAsync(_factory.CreateClientDisconnectOptionsBuilder().Build(), CancellationToken.None).GetAwaiter().GetResult();
+ } catch { }
+ }
+
+ public void Dispose() {
+ Disconnect();
+ _client?.Dispose();
+ _client = null;
+ }
+
+ public sealed class ReceivedMessage {
+ public string Topic { get; }
+ public byte[] Payload { get; }
+ public ReceivedMessage(string topic, byte[] payload) { Topic = topic; Payload = payload ?? Array.Empty<byte>(); }
+ }
+ }
+}
+'@
+ try {
+ $runtimeDir = [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()
+ $refAssemblies = @($script:MqttAssembly)
+ foreach ($name in @('netstandard.dll', 'System.Runtime.dll', 'System.Net.Primitives.dll')) {
+ $path = Join-Path $runtimeDir $name
+ if (Test-Path $path) {
+ $refAssemblies += [System.Reflection.Assembly]::LoadFrom((Resolve-Path $path).Path)
+ }
+ }
+ Add-Type -ReferencedAssemblies $refAssemblies -TypeDefinition $listenerCode -ErrorAction Stop
+ Write-Verbose "PSYarbo: YarboMqttListener loaded"
+ } catch {
+ if ($_.Exception.Message -notmatch 'already exists') {
+ Write-Verbose "PSYarbo: YarboMqttListener failed to load: $($_.Exception.Message)"
+ }
+ }
+ # Legacy adapter (kept for Connect-Yarbo / Send-MqttCommand response path if still used)
+ $adapterCode = @'
+using System.Collections.Generic;
+using System.Threading.Tasks;
+namespace PSYarbo.Mqtt {
+ public static class MessageReceivedAdapter {
+ private static readonly Dictionary<object, System.Func<object, Task>> _callbacks = new Dictionary<object, System.Func<object, Task>>();
+ public static void RegisterCallback(object client, System.Func<object, Task> callback) {
+ lock (_callbacks) {
+ _callbacks[client] = callback;
+ }
+ }
+ public static void UnregisterCallback(object client) {
+ lock (_callbacks) {
+ _callbacks.Remove(client);
+ }
+ }
+ public static Task Handler(object sender, MQTTnet.Client.MqttApplicationMessageReceivedEventArgs ea) {
+ System.Func<object, Task> callback = null;
+ lock (_callbacks) {
+ _callbacks.TryGetValue(sender, out callback);
+ }
+ if (callback != null) return callback(ea);
+ return Task.CompletedTask;
+ }
+ }
+}
+'@
+ try {
+ Add-Type -ReferencedAssemblies $script:MqttAssembly -TypeDefinition $adapterCode -ErrorAction Stop
+ } catch {
+ Write-Verbose "PSYarbo: MessageReceivedAdapter already loaded or failed: $_"
... diff truncated: showing 800 of 2534 lines- Fix wireless charge voltage/current types in YarboTelemetry from [int] to [double] for consistency with YarboRobot and to prevent data loss - Extract duplicated subnet scanning logic into shared Get-PSYarboSubnetIpList helper function - Replace unsafe type literals with safe '-as [type]' pattern in Find-Yarbo and Find-YarboDevice to enable graceful fallback
…ceMSG) - Watch-YarboTelemetry is already event-driven (TelemetryQueue, no get_device_msg polling) - IncludeHeartbeat and IncludePlanFeedback already present - Adds -Raw to emit raw decoded DeviceMSG instead of [YarboTelemetry] Fixes #7 Made-with: Cursor
…ToDock Send-YarboReturnToDock already implements cmd_recharge with SupportsShouldProcess, -Connection, Assert-YarboController; exported and documented. Fixes #8 Made-with: Cursor
…e-YarboConfig; Connect-YarboCloud reads Email from env/config - Get-YarboConfig: optional -Path; support defaults.broker/port/serialNumber in config file - Save-YarboConfig: write full config object to path - Merge-YarboConfig: explicit > env > file > defaults - Connect-YarboCloud: -Email optional; resolve from $env:YARBO_EMAIL or ~/.psyarbo/config.json - Unit tests: YARBO_EMAIL resolution, Merge-YarboConfig priority Fixes #9 Made-with: Cursor
…keyed)
- Get-YarboCloudCredential -Email returns @{ Password; RefreshToken }
- Save-YarboCloudCredential -Email -Password -RefreshToken stores keyed by email
- Connect-YarboCloud: -Password optional; when -Email only, try stored credentials
(RefreshToken for token refresh, or Password for login); save credentials after login
- about_PSYarbo.help.txt documents SecretManagement and file fallback
Fixes #10
Made-with: Cursor
#31) - Find-Yarbo / Find-YarboDevice: fallback subnet 192.168.0.0/23 -> 192.0.2.0/24; examples 192.0.2.0/24 - NetworkHelper: example CIDR 192.0.2.0/24 - README: discovery default description (no 192.168.1.0/24) - Tests: broker/test IPs 192.168.x -> 192.0.2.x - MqttSniff example: Broker <rover-ip> Made-with: Cursor
…a port override, remove unused functions
Changed MessageReceivedAdapter to create handlers with correct signature Func<MqttApplicationMessageReceivedEventArgs, Task> instead of Func<object, MqttApplicationMessageReceivedEventArgs, Task> to match MQTTnet's ApplicationMessageReceivedAsync event expectations.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Type literal throws before custom error message executes
- Changed from direct type literal to safe pattern using '-as [type]' operator to allow graceful error handling when type is unavailable.
Preview (3381ac6210)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -88,7 +88,7 @@
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -165,6 +165,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -89,6 +89,11 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient -and [PSYarbo.Mqtt.MessageReceivedAdapter]) {
+ [PSYarbo.Mqtt.MessageReceivedAdapter]::UnregisterCallback($this.MqttClient)
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,16 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -24,25 +29,39 @@
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
[string]$LedRegister
+ [int]$WirelessChargeState
[double]$WirelessChargeVoltage
[double]$WirelessChargeCurrent
+ [int]$WirelessChargeErrorCode
- # Network
+ # Ultrasonic (ultrasonic_msg)
+ [int]$UltrasonicLeftFront
+ [int]$UltrasonicMiddle
+ [int]$UltrasonicRightFront
+
+ # Network (route_priority)
[hashtable]$RoutePriority
- # Timestamps
+ # Timestamps (device timestamp + our LastUpdated)
+ [double]$DeviceTimestamp
[datetime]$LastUpdated
# Connection reference (hidden)
hidden [string]$Broker
hidden [int]$Port
+ # Full DeviceMSG payload (all MQTT keys available)
+ hidden [PSCustomObject]$RawMessage
+
[string] ToString() {
$displayName = if ($this.Name) { $this.Name } else { $this.SerialNumber }
return "$displayName ($($this.SerialNumber)) - Battery: $($this.BatteryCapacity)% - State: $(if($this.WorkingState){'Active'}else{'Idle'})"
diff --git a/src/PSYarbo/Classes/YarboTelemetry.ps1 b/src/PSYarbo/Classes/YarboTelemetry.ps1
--- a/src/PSYarbo/Classes/YarboTelemetry.ps1
+++ b/src/PSYarbo/Classes/YarboTelemetry.ps1
@@ -2,28 +2,33 @@
[datetime]$Timestamp
[string]$SerialNumber
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
[bool]$BatteryTempError
+ [double]$BatteryTimestamp
- # Position
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # Position (CombinedOdom, RTKMSG, combined_odom_confidence)
[double]$X
[double]$Y
[double]$Phi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
[double]$RtkDop
[double]$OdomConfidence
- # Running status
+ # Running status (RunningStatusMSG)
[int]$ChuteAngle
[int]$ChuteSteeringEngineInfo
[double]$HeadGyroPitch
[double]$HeadGyroRoll
[int]$RainSensorData
- # State flags
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -31,23 +36,30 @@
[bool]$IsPaused
[bool]$IsRecharging
- # Sensors
+ # Sensors (ultrasonic_msg)
[int]$UltrasonicLeftFront
[int]$UltrasonicMiddle
[int]$UltrasonicRightFront
- # Wireless charging
+ # Wireless charging (wireless_recharge)
[int]$WirelessChargeState
- [int]$WirelessChargeVoltage
- [int]$WirelessChargeCurrent
+ [double]$WirelessChargeVoltage
+ [double]$WirelessChargeCurrent
[int]$WirelessChargeErrorCode
- # GPS (parsed from rtk_base_data.rover.gngga NMEA sentence)
+ # LED (led)
+ [string]$LedRegister
+
+ # GPS (parsed from rtk_base_data.rover.gngga + raw)
[nullable[double]]$Latitude
[nullable[double]]$Longitude
[nullable[double]]$Altitude
[int]$FixQuality
+ [string]$GnggaRaw
+ # Device message timestamp (root timestamp)
+ [double]$DeviceTimestamp
+
# Raw
hidden [PSCustomObject]$RawMessage
diff --git a/src/PSYarbo/PSYarbo.Format.ps1xml b/src/PSYarbo/PSYarbo.Format.ps1xml
--- a/src/PSYarbo/PSYarbo.Format.ps1xml
+++ b/src/PSYarbo/PSYarbo.Format.ps1xml
@@ -23,10 +23,10 @@
<TableColumnItems>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SerialNumber</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>"$($_.BatteryCapacity)%"</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.WorkingState){'Active'}else{'Idle'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Battery</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>State</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RtkStatus</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Heading, 1)</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>HeadingDisplay</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>ErrorCode</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
@@ -34,7 +34,7 @@
</TableControl>
</View>
- <!-- YarboRobot List View -->
+ <!-- YarboRobot List View (Battery and State match table view; raw fields kept for scripting) -->
<View>
<Name>YarboRobotList</Name>
<ViewSelectedBy>
@@ -46,6 +46,8 @@
<ListItems>
<ListItem><PropertyName>SerialNumber</PropertyName></ListItem>
<ListItem><PropertyName>Name</PropertyName></ListItem>
+ <ListItem><PropertyName>Battery</PropertyName></ListItem>
+ <ListItem><PropertyName>State</PropertyName></ListItem>
<ListItem><PropertyName>HeadType</PropertyName></ListItem>
<ListItem><PropertyName>BatteryCapacity</PropertyName></ListItem>
<ListItem><PropertyName>BatteryStatus</PropertyName></ListItem>
@@ -53,6 +55,7 @@
<ListItem><PropertyName>ChargingStatus</PropertyName></ListItem>
<ListItem><PropertyName>ErrorCode</PropertyName></ListItem>
<ListItem><PropertyName>Heading</PropertyName></ListItem>
+ <ListItem><PropertyName>HeadingDisplay</PropertyName></ListItem>
<ListItem><PropertyName>RtkStatus</PropertyName></ListItem>
<ListItem><PropertyName>OdomConfidence</PropertyName></ListItem>
<ListItem><PropertyName>ChuteAngle</PropertyName></ListItem>
@@ -83,14 +86,14 @@
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
- <TableColumnItem><ScriptBlock>$_.Timestamp.ToString('HH:mm:ss.ff')</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>"$($_.BatteryCapacity)%"</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.WorkingState){'Active'}else{'Idle'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Time</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Battery</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>State</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RtkStatus</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Heading, 1)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.X, 3)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Y, 3)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.OdomConfidence, 3)</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>HeadingDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>XDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>YDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>OdomConfidenceDisplay</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
@@ -129,6 +132,36 @@
</TableControl>
</View>
+ <!-- YarboEndpoint Table View (issue #30: Rover vs DC discovery) -->
+ <View>
+ <Name>YarboEndpoint</Name>
+ <ViewSelectedBy>
+ <TypeName>YarboEndpoint</TypeName>
+ </ViewSelectedBy>
+ <TableControl>
+ <TableHeaders>
+ <TableColumnHeader><Label>IPAddress</Label><Width>16</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Port</Label><Width>6</Width><Alignment>Right</Alignment></TableColumnHeader>
+ <TableColumnHeader><Label>Path</Label><Width>6</Width></TableColumnHeader>
+ <TableColumnHeader><Label>MacAddress</Label><Width>18</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Recommended</Label><Width>12</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Status</Label><Width>10</Width></TableColumnHeader>
+ </TableHeaders>
+ <TableRowEntries>
+ <TableRowEntry>
+ <TableColumnItems>
+ <TableColumnItem><PropertyName>IPAddress</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Port</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Path</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>MacAddress</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>RecommendedDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
+ </TableColumnItems>
+ </TableRowEntry>
+ </TableRowEntries>
+ </TableControl>
+ </View>
+
<!-- YarboPlan Table View -->
<View>
<Name>YarboPlan</Name>
@@ -147,8 +180,8 @@
<TableColumnItems>
<TableColumnItem><PropertyName>Id</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>$_.AreaIds -join ', '</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.EnableSelfOrder){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Areas</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>SelfOrder</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
@@ -178,14 +211,11 @@
<TableColumnItem><PropertyName>Id</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>PlanId</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.Enabled){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>
- if ($_.WeekDay -eq 127) { 'Daily' }
- else { ($_.GetDays() | ForEach-Object { $_.ToString().Substring(0,3) }) -join ',' }
- </ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>EnabledDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Days</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>StartTime</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>EndTime</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.IsWeatherSchedule){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>WeatherDisplay</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
diff --git a/src/PSYarbo/PSYarbo.Types.ps1xml b/src/PSYarbo/PSYarbo.Types.ps1xml
--- a/src/PSYarbo/PSYarbo.Types.ps1xml
+++ b/src/PSYarbo/PSYarbo.Types.ps1xml
@@ -11,13 +11,29 @@
<Type>
<Name>YarboRobot</Name>
<Members>
+ <ScriptProperty>
+ <Name>Battery</Name>
+ <GetScriptBlock>"$($this.BatteryCapacity)%"</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>State</Name>
+ <GetScriptBlock>if($this.WorkingState){'Active'}else{'Idle'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>HeadingDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Heading, 1)</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>SerialNumber</Name>
<Name>Name</Name>
+ <Name>Battery</Name>
+ <Name>State</Name>
+ <Name>HeadingDisplay</Name>
<Name>HeadType</Name>
<Name>BatteryCapacity</Name>
+ <Name>BatteryStatus</Name>
<Name>WorkingState</Name>
<Name>ChargingStatus</Name>
<Name>ErrorCode</Name>
@@ -48,19 +64,51 @@
</Members>
</Type>
- <!-- YarboTelemetry: key telemetry fields -->
+ <!-- YarboTelemetry: key telemetry fields. Battery and State match YarboRobot for consistent display. -->
<Type>
<Name>YarboTelemetry</Name>
<Members>
+ <ScriptProperty>
+ <Name>Battery</Name>
+ <GetScriptBlock>"$($this.BatteryCapacity)%"</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>State</Name>
+ <GetScriptBlock>if($this.WorkingState){'Active'}else{'Idle'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>Time</Name>
+ <GetScriptBlock>$this.Timestamp.ToString('HH:mm:ss.ff')</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>HeadingDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Heading, 1)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>XDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.X, 3)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>YDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Y, 3)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>OdomConfidenceDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.OdomConfidence, 3)</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
+ <Name>Time</Name>
<Name>Timestamp</Name>
<Name>SerialNumber</Name>
+ <Name>Battery</Name>
+ <Name>State</Name>
<Name>BatteryCapacity</Name>
<Name>WorkingState</Name>
<Name>RtkStatus</Name>
<Name>Heading</Name>
+ <Name>HeadingDisplay</Name>
<Name>X</Name>
<Name>Y</Name>
<Name>OdomConfidence</Name>
@@ -69,36 +117,61 @@
</Members>
</Type>
- <!-- YarboPlan: key plan fields -->
+ <!-- YarboPlan: key plan fields. Areas and SelfOrder match table view. -->
<Type>
<Name>YarboPlan</Name>
<Members>
+ <ScriptProperty>
+ <Name>Areas</Name>
+ <GetScriptBlock>if($this.AreaIds){$this.AreaIds -join ', '}else{''}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>SelfOrder</Name>
+ <GetScriptBlock>if($this.EnableSelfOrder){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Id</Name>
<Name>Name</Name>
+ <Name>Areas</Name>
<Name>AreaIds</Name>
+ <Name>SelfOrder</Name>
<Name>EnableSelfOrder</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</Type>
- <!-- YarboSchedule: key schedule fields -->
+ <!-- YarboSchedule: key schedule fields. Days, EnabledDisplay, WeatherDisplay match table view. -->
<Type>
<Name>YarboSchedule</Name>
<Members>
+ <ScriptProperty>
+ <Name>Days</Name>
+ <GetScriptBlock>if($this.WeekDay -eq 127){'Daily'}else{($this.GetDays() | ForEach-Object{$_.ToString().Substring(0,3)}) -join ','}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>EnabledDisplay</Name>
+ <GetScriptBlock>if($this.Enabled){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>WeatherDisplay</Name>
+ <GetScriptBlock>if($this.IsWeatherSchedule){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Id</Name>
<Name>Name</Name>
<Name>PlanId</Name>
+ <Name>EnabledDisplay</Name>
<Name>Enabled</Name>
+ <Name>Days</Name>
<Name>WeekDay</Name>
<Name>StartTime</Name>
<Name>EndTime</Name>
+ <Name>WeatherDisplay</Name>
<Name>IsWeatherSchedule</Name>
</ReferencedProperties>
</PropertySet>
@@ -123,6 +196,29 @@
</Members>
</Type>
+ <!-- YarboEndpoint: discovery result. RecommendedDisplay matches table view. -->
+ <Type>
+ <Name>YarboEndpoint</Name>
+ <Members>
+ <ScriptProperty>
+ <Name>RecommendedDisplay</Name>
+ <GetScriptBlock>if($this.Recommended){'⭐ Yes'}else{''}</GetScriptBlock>
+ </ScriptProperty>
+ <PropertySet>
+ <Name>DefaultDisplayPropertySet</Name>
+ <ReferencedProperties>
+ <Name>IPAddress</Name>
+ <Name>Port</Name>
+ <Name>Path</Name>
+ <Name>MacAddress</Name>
+ <Name>RecommendedDisplay</Name>
+ <Name>Recommended</Name>
+ <Name>Status</Name>
+ </ReferencedProperties>
+ </PropertySet>
+ </Members>
+ </Type>
+
<!-- YarboLightState: all LED channels -->
<Type>
<Name>YarboLightState</Name>
diff --git a/src/PSYarbo/PSYarbo.psd1 b/src/PSYarbo/PSYarbo.psd1
--- a/src/PSYarbo/PSYarbo.psd1
+++ b/src/PSYarbo/PSYarbo.psd1
@@ -10,11 +10,26 @@
PowerShellVersion = '7.4'
# MQTTnet loaded via AssemblyLoadContext in PSYarbo.psm1 — not via RequiredAssemblies
# RequiredAssemblies = @()
+ # Load class definitions before root module so [YarboRobot], [YarboConnection], etc. are visible to OutputType/attributes
+ ScriptsToProcess = @(
+ 'Classes/YarboExceptions.ps1'
+ 'Classes/YarboLightState.ps1'
+ 'Classes/YarboCommandResult.ps1'
+ 'Classes/YarboTelemetry.ps1'
+ 'Classes/YarboPlan.ps1'
+ 'Classes/YarboSchedule.ps1'
+ 'Classes/YarboRobot.ps1'
+ 'Classes/YarboGlobalParams.ps1'
+ 'Classes/YarboEndpoint.ps1'
+ 'Classes/YarboConnection.ps1'
+ 'Classes/YarboCloudSession.ps1'
+ )
FormatsToProcess = @('PSYarbo.Format.ps1xml')
- TypesToProcess = @('PSYarbo.Types.ps1xml')
+ # Types loaded in .psm1 with Update-TypeData -Force to avoid "already present" on re-import
+ # TypesToProcess = @('PSYarbo.Types.ps1xml')
FunctionsToExport = @(
# Connection
- 'Connect-Yarbo', 'Disconnect-Yarbo', 'Find-Yarbo',
+ 'Connect-Yarbo', 'Disconnect-Yarbo', 'Find-Yarbo', 'Find-YarboDevice', 'Invoke-YarboMqttSniff',
# Status
'Get-YarboStatus', 'Get-YarboRobot', 'Get-YarboBattery', 'Get-YarboFirmware',
'Get-YarboGlobalParams',
@@ -41,7 +56,7 @@
)
CmdletsToExport = @()
VariablesToExport = @()
- AliasesToExport = @('Return-YarboToDock')
+ AliasesToExport = @('Return-YarboToDock', 'Start-YarboRecharge')
PrivateData = @{
PSData = @{
Tags = @('Yarbo', 'Robot', 'Mower', 'MQTT', 'IoT', 'Automation', 'SnowBlower')
diff --git a/src/PSYarbo/PSYarbo.psm1 b/src/PSYarbo/PSYarbo.psm1
--- a/src/PSYarbo/PSYarbo.psm1
+++ b/src/PSYarbo/PSYarbo.psm1
@@ -16,37 +16,182 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
-#region — Load MQTTnet via isolated AssemblyLoadContext
+#region — Load MQTTnet and C# listener helper
$libPath = Join-Path $PSScriptRoot 'lib'
$mqttDllPath = Join-Path $libPath 'MQTTnet.dll'
if (Test-Path $mqttDllPath) {
- $alcName = 'PSYarboMqttContext'
- $script:MqttALC = [System.Runtime.Loader.AssemblyLoadContext]::new($alcName, $true)
- $script:MqttAssembly = $script:MqttALC.LoadFromAssemblyPath((Resolve-Path $mqttDllPath).Path)
- Write-Verbose "PSYarbo: Loaded MQTTnet via isolated AssemblyLoadContext '$alcName'"
+ $script:MqttAssembly = [System.Reflection.Assembly]::LoadFrom((Resolve-Path $mqttDllPath).Path)
+ Write-Verbose "PSYarbo: Loaded MQTTnet from $mqttDllPath"
+ # C# helper: connects, subscribes, and receives messages entirely in C# so the MQTTnet
+ # ApplicationMessageReceivedAsync event is attached with += and always fires.
+ $listenerCode = @'
+using System;
+using System.Collections;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PSYarbo.Mqtt {
+ public sealed class YarboMqttListener : IDisposable {
+ private readonly MQTTnet.MqttFactory _factory;
+ private MQTTnet.Client.IMqttClient _client;
+ private readonly ArrayList _messages = ArrayList.Synchronized(new ArrayList());
+ private readonly TaskCompletionSource<ReceivedMessage> _firstMessage = new TaskCompletionSource<ReceivedMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public YarboMqttListener() {
+ _factory = new MQTTnet.MqttFactory();
+ _client = _factory.CreateMqttClient();
+ _client.ApplicationMessageReceivedAsync += e => {
+ var seg = e.ApplicationMessage.PayloadSegment;
+ int len = seg.Array != null ? seg.Count : 0;
+ byte[] copy = len > 0 ? new byte[len] : Array.Empty<byte>();
+ if (len > 0 && seg.Array != null)
+ Buffer.BlockCopy(seg.Array, seg.Offset, copy, 0, len);
+ var msg = new ReceivedMessage(e.ApplicationMessage.Topic ?? "", copy);
+ _messages.Add(msg);
+ if (!_firstMessage.Task.IsCompleted)
+ _firstMessage.TrySetResult(msg);
+ return Task.CompletedTask;
+ };
+ }
+
+ public void Connect(string broker, int port, string clientId, TimeSpan timeout) {
+ var opts = _factory.CreateClientOptionsBuilder()
+ .WithTcpServer(broker, port)
+ .WithClientId(clientId)
+ .WithTimeout(timeout)
+ .Build();
+ _client.ConnectAsync(opts, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public void Subscribe(string[] topicFilters) {
+ var builder = _factory.CreateSubscribeOptionsBuilder();
+ foreach (var topic in topicFilters)
+ builder = builder.WithTopicFilter(topic);
+ var opts = builder.Build();
+ _client.SubscribeAsync(opts, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public void Publish(string topic, byte[] payload) {
+ var msg = _factory.CreateApplicationMessageBuilder()
+ .WithTopic(topic)
+ .WithPayload(payload ?? Array.Empty<byte>())
+ .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce)
+ .Build();
+ _client.PublishAsync(msg, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public ReceivedMessage WaitForFirstMessage(int timeoutMs) {
+ try {
+ if (_firstMessage.Task.IsCompleted)
+ return _firstMessage.Task.Result;
+ return _firstMessage.Task.Wait(timeoutMs) ? _firstMessage.Task.Result : null;
+ } catch { return null; }
+ }
+
+ public ReceivedMessage[] GetReceivedMessages() {
+ var arr = new ReceivedMessage[_messages.Count];
+ _messages.CopyTo(arr);
+ _messages.Clear();
+ return arr;
+ }
+
+ public void Disconnect() {
+ try {
+ _client.DisconnectAsync(_factory.CreateClientDisconnectOptionsBuilder().Build(), CancellationToken.None).GetAwaiter().GetResult();
+ } catch { }
+ }
+
+ public void Dispose() {
+ Disconnect();
+ _client?.Dispose();
+ _client = null;
+ }
+
+ public sealed class ReceivedMessage {
+ public string Topic { get; }
+ public byte[] Payload { get; }
+ public ReceivedMessage(string topic, byte[] payload) { Topic = topic; Payload = payload ?? Array.Empty<byte>(); }
+ }
+ }
+}
+'@
+ try {
+ $runtimeDir = [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()
+ $refAssemblies = @($script:MqttAssembly)
+ foreach ($name in @('netstandard.dll', 'System.Runtime.dll', 'System.Net.Primitives.dll')) {
+ $path = Join-Path $runtimeDir $name
+ if (Test-Path $path) {
+ $refAssemblies += [System.Reflection.Assembly]::LoadFrom((Resolve-Path $path).Path)
+ }
+ }
+ Add-Type -ReferencedAssemblies $refAssemblies -TypeDefinition $listenerCode -ErrorAction Stop
+ Write-Verbose "PSYarbo: YarboMqttListener loaded"
+ } catch {
+ if ($_.Exception.Message -notmatch 'already exists') {
+ Write-Verbose "PSYarbo: YarboMqttListener failed to load: $($_.Exception.Message)"
+ }
+ }
+ # Legacy adapter (kept for Connect-Yarbo / Send-MqttCommand response path if still used)
+ $adapterCode = @'
+using System.Collections.Generic;
+using System.Threading.Tasks;
+namespace PSYarbo.Mqtt {
+ public static class MessageReceivedAdapter {
+ private static readonly Dictionary<object, System.Func<object, Task>> _callbacks = new Dictionary<object, System.Func<object, Task>>();
+ private static readonly Dictionary<object, System.Func<MQTTnet.Client.MqttApplicationMessageReceivedEventArgs, Task>> _handlers = new Dictionary<object, System.Func<MQTTnet.Client.MqttApplicationMessageReceivedEventArgs, Task>>();
+ public static void RegisterCallback(object client, System.Func<object, Task> callback) {
+ lock (_callbacks) {
+ _callbacks[client] = callback;
+ _handlers[client] = ea => {
+ System.Func<object, Task> cb = null;
+ lock (_callbacks) {
+ _callbacks.TryGetValue(client, out cb);
+ }
+ if (cb != null) return cb(ea);
+ return Task.CompletedTask;
+ };
+ }
+ }
+ public static void UnregisterCallback(object client) {
+ lock (_callbacks) {
... diff truncated: showing 800 of 2901 lines…MqttSniff Changed from direct type literal [PSYarbo.Mqtt.YarboMqttListener] to safe pattern 'PSYarbo.Mqtt.YarboMqttListener' -as [type] to allow graceful error handling when type is not available. This matches the pattern used in Find-Yarbo and Find-YarboDevice functions.
…ixes #11) - MockMqttClient: document §8.3 (PublishedMessages, SimulateResponse) - Fixtures: read_schedules, read_global_params, get_connect_wifi_name; cloud-api-responses (login, getUserRobotBindVos, getLatestPubVersion) - Unit: ZlibCodec, ErrorHelper, YarboSchedule, TelemetryParser, ControllerGuard - Integration (mocked): Get-YarboStatus, Get-YarboPlan (Tag Mocked) - CI: run Unit, Integration (exclude real-robot tag), root tests separately; install MQTTnet so module loads in tests Made-with: Cursor
- Fix Battery assertion in Get-YarboStatus.Tests.ps1 to expect '83%' string instead of 83 integer - Set Run.Exit = true for unit and integration test steps in CI to properly fail on test failures
- Remove invalid Run.ExitCode property from Pester configuration (lines 87, 102 in ci.yml) - Add Invoke-YarboMqttSniff to expected exports test list - Fix ARP IP lookup to use word boundaries preventing prefix matches (e.g., 192.168.1.1 vs 192.168.1.10)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Handler delegate never removed from MQTTnet event on unregister
- Added remove_ApplicationMessageReceivedAsync call in YarboConnection.Dispose() to symmetrically detach the event handler before calling UnregisterCallback.
Preview (eedf378b5e)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -72,20 +72,53 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $true
+ $intConfig.Filter.ExcludeTag = 'Integration'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ $intResult = Invoke-Pester -Configuration $intConfig
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -88,7 +88,7 @@
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -165,6 +165,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -89,6 +89,15 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient -and [PSYarbo.Mqtt.MessageReceivedAdapter]) {
+ $handlerDelegate = [PSYarbo.Mqtt.MessageReceivedAdapter]::GetHandler($this.MqttClient)
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ [PSYarbo.Mqtt.MessageReceivedAdapter]::UnregisterCallback($this.MqttClient)
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,16 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -24,25 +29,39 @@
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
[string]$LedRegister
+ [int]$WirelessChargeState
[double]$WirelessChargeVoltage
[double]$WirelessChargeCurrent
+ [int]$WirelessChargeErrorCode
- # Network
+ # Ultrasonic (ultrasonic_msg)
+ [int]$UltrasonicLeftFront
+ [int]$UltrasonicMiddle
+ [int]$UltrasonicRightFront
+
+ # Network (route_priority)
[hashtable]$RoutePriority
- # Timestamps
+ # Timestamps (device timestamp + our LastUpdated)
+ [double]$DeviceTimestamp
[datetime]$LastUpdated
# Connection reference (hidden)
hidden [string]$Broker
hidden [int]$Port
+ # Full DeviceMSG payload (all MQTT keys available)
+ hidden [PSCustomObject]$RawMessage
+
[string] ToString() {
$displayName = if ($this.Name) { $this.Name } else { $this.SerialNumber }
return "$displayName ($($this.SerialNumber)) - Battery: $($this.BatteryCapacity)% - State: $(if($this.WorkingState){'Active'}else{'Idle'})"
diff --git a/src/PSYarbo/Classes/YarboTelemetry.ps1 b/src/PSYarbo/Classes/YarboTelemetry.ps1
--- a/src/PSYarbo/Classes/YarboTelemetry.ps1
+++ b/src/PSYarbo/Classes/YarboTelemetry.ps1
@@ -2,28 +2,33 @@
[datetime]$Timestamp
[string]$SerialNumber
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
[bool]$BatteryTempError
+ [double]$BatteryTimestamp
- # Position
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # Position (CombinedOdom, RTKMSG, combined_odom_confidence)
[double]$X
[double]$Y
[double]$Phi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
[double]$RtkDop
[double]$OdomConfidence
- # Running status
+ # Running status (RunningStatusMSG)
[int]$ChuteAngle
[int]$ChuteSteeringEngineInfo
[double]$HeadGyroPitch
[double]$HeadGyroRoll
[int]$RainSensorData
- # State flags
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -31,23 +36,30 @@
[bool]$IsPaused
[bool]$IsRecharging
- # Sensors
+ # Sensors (ultrasonic_msg)
[int]$UltrasonicLeftFront
[int]$UltrasonicMiddle
[int]$UltrasonicRightFront
- # Wireless charging
+ # Wireless charging (wireless_recharge)
[int]$WirelessChargeState
- [int]$WirelessChargeVoltage
- [int]$WirelessChargeCurrent
+ [double]$WirelessChargeVoltage
+ [double]$WirelessChargeCurrent
[int]$WirelessChargeErrorCode
- # GPS (parsed from rtk_base_data.rover.gngga NMEA sentence)
+ # LED (led)
+ [string]$LedRegister
+
+ # GPS (parsed from rtk_base_data.rover.gngga + raw)
[nullable[double]]$Latitude
[nullable[double]]$Longitude
[nullable[double]]$Altitude
[int]$FixQuality
+ [string]$GnggaRaw
+ # Device message timestamp (root timestamp)
+ [double]$DeviceTimestamp
+
# Raw
hidden [PSCustomObject]$RawMessage
diff --git a/src/PSYarbo/PSYarbo.Format.ps1xml b/src/PSYarbo/PSYarbo.Format.ps1xml
--- a/src/PSYarbo/PSYarbo.Format.ps1xml
+++ b/src/PSYarbo/PSYarbo.Format.ps1xml
@@ -23,10 +23,10 @@
<TableColumnItems>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>SerialNumber</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>"$($_.BatteryCapacity)%"</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.WorkingState){'Active'}else{'Idle'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Battery</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>State</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RtkStatus</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Heading, 1)</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>HeadingDisplay</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>ErrorCode</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
@@ -34,7 +34,7 @@
</TableControl>
</View>
- <!-- YarboRobot List View -->
+ <!-- YarboRobot List View (Battery and State match table view; raw fields kept for scripting) -->
<View>
<Name>YarboRobotList</Name>
<ViewSelectedBy>
@@ -46,6 +46,8 @@
<ListItems>
<ListItem><PropertyName>SerialNumber</PropertyName></ListItem>
<ListItem><PropertyName>Name</PropertyName></ListItem>
+ <ListItem><PropertyName>Battery</PropertyName></ListItem>
+ <ListItem><PropertyName>State</PropertyName></ListItem>
<ListItem><PropertyName>HeadType</PropertyName></ListItem>
<ListItem><PropertyName>BatteryCapacity</PropertyName></ListItem>
<ListItem><PropertyName>BatteryStatus</PropertyName></ListItem>
@@ -53,6 +55,7 @@
<ListItem><PropertyName>ChargingStatus</PropertyName></ListItem>
<ListItem><PropertyName>ErrorCode</PropertyName></ListItem>
<ListItem><PropertyName>Heading</PropertyName></ListItem>
+ <ListItem><PropertyName>HeadingDisplay</PropertyName></ListItem>
<ListItem><PropertyName>RtkStatus</PropertyName></ListItem>
<ListItem><PropertyName>OdomConfidence</PropertyName></ListItem>
<ListItem><PropertyName>ChuteAngle</PropertyName></ListItem>
@@ -83,14 +86,14 @@
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
- <TableColumnItem><ScriptBlock>$_.Timestamp.ToString('HH:mm:ss.ff')</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>"$($_.BatteryCapacity)%"</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.WorkingState){'Active'}else{'Idle'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Time</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Battery</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>State</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>RtkStatus</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Heading, 1)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.X, 3)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.Y, 3)</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>[math]::Round($_.OdomConfidence, 3)</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>HeadingDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>XDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>YDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>OdomConfidenceDisplay</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
@@ -129,6 +132,36 @@
</TableControl>
</View>
+ <!-- YarboEndpoint Table View (issue #30: Rover vs DC discovery) -->
+ <View>
+ <Name>YarboEndpoint</Name>
+ <ViewSelectedBy>
+ <TypeName>YarboEndpoint</TypeName>
+ </ViewSelectedBy>
+ <TableControl>
+ <TableHeaders>
+ <TableColumnHeader><Label>IPAddress</Label><Width>16</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Port</Label><Width>6</Width><Alignment>Right</Alignment></TableColumnHeader>
+ <TableColumnHeader><Label>Path</Label><Width>6</Width></TableColumnHeader>
+ <TableColumnHeader><Label>MacAddress</Label><Width>18</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Recommended</Label><Width>12</Width></TableColumnHeader>
+ <TableColumnHeader><Label>Status</Label><Width>10</Width></TableColumnHeader>
+ </TableHeaders>
+ <TableRowEntries>
+ <TableRowEntry>
+ <TableColumnItems>
+ <TableColumnItem><PropertyName>IPAddress</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Port</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Path</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>MacAddress</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>RecommendedDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Status</PropertyName></TableColumnItem>
+ </TableColumnItems>
+ </TableRowEntry>
+ </TableRowEntries>
+ </TableControl>
+ </View>
+
<!-- YarboPlan Table View -->
<View>
<Name>YarboPlan</Name>
@@ -147,8 +180,8 @@
<TableColumnItems>
<TableColumnItem><PropertyName>Id</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>$_.AreaIds -join ', '</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.EnableSelfOrder){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>Areas</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>SelfOrder</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
@@ -178,14 +211,11 @@
<TableColumnItem><PropertyName>Id</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>Name</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>PlanId</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.Enabled){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
- <TableColumnItem><ScriptBlock>
- if ($_.WeekDay -eq 127) { 'Daily' }
- else { ($_.GetDays() | ForEach-Object { $_.ToString().Substring(0,3) }) -join ',' }
- </ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>EnabledDisplay</PropertyName></TableColumnItem>
+ <TableColumnItem><PropertyName>Days</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>StartTime</PropertyName></TableColumnItem>
<TableColumnItem><PropertyName>EndTime</PropertyName></TableColumnItem>
- <TableColumnItem><ScriptBlock>if($_.IsWeatherSchedule){'✓'}else{'✗'}</ScriptBlock></TableColumnItem>
+ <TableColumnItem><PropertyName>WeatherDisplay</PropertyName></TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
diff --git a/src/PSYarbo/PSYarbo.Types.ps1xml b/src/PSYarbo/PSYarbo.Types.ps1xml
--- a/src/PSYarbo/PSYarbo.Types.ps1xml
+++ b/src/PSYarbo/PSYarbo.Types.ps1xml
@@ -11,13 +11,29 @@
<Type>
<Name>YarboRobot</Name>
<Members>
+ <ScriptProperty>
+ <Name>Battery</Name>
+ <GetScriptBlock>"$($this.BatteryCapacity)%"</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>State</Name>
+ <GetScriptBlock>if($this.WorkingState){'Active'}else{'Idle'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>HeadingDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Heading, 1)</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>SerialNumber</Name>
<Name>Name</Name>
+ <Name>Battery</Name>
+ <Name>State</Name>
+ <Name>HeadingDisplay</Name>
<Name>HeadType</Name>
<Name>BatteryCapacity</Name>
+ <Name>BatteryStatus</Name>
<Name>WorkingState</Name>
<Name>ChargingStatus</Name>
<Name>ErrorCode</Name>
@@ -48,19 +64,51 @@
</Members>
</Type>
- <!-- YarboTelemetry: key telemetry fields -->
+ <!-- YarboTelemetry: key telemetry fields. Battery and State match YarboRobot for consistent display. -->
<Type>
<Name>YarboTelemetry</Name>
<Members>
+ <ScriptProperty>
+ <Name>Battery</Name>
+ <GetScriptBlock>"$($this.BatteryCapacity)%"</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>State</Name>
+ <GetScriptBlock>if($this.WorkingState){'Active'}else{'Idle'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>Time</Name>
+ <GetScriptBlock>$this.Timestamp.ToString('HH:mm:ss.ff')</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>HeadingDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Heading, 1)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>XDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.X, 3)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>YDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.Y, 3)</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>OdomConfidenceDisplay</Name>
+ <GetScriptBlock>[math]::Round($this.OdomConfidence, 3)</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
+ <Name>Time</Name>
<Name>Timestamp</Name>
<Name>SerialNumber</Name>
+ <Name>Battery</Name>
+ <Name>State</Name>
<Name>BatteryCapacity</Name>
<Name>WorkingState</Name>
<Name>RtkStatus</Name>
<Name>Heading</Name>
+ <Name>HeadingDisplay</Name>
<Name>X</Name>
<Name>Y</Name>
<Name>OdomConfidence</Name>
@@ -69,36 +117,61 @@
</Members>
</Type>
- <!-- YarboPlan: key plan fields -->
+ <!-- YarboPlan: key plan fields. Areas and SelfOrder match table view. -->
<Type>
<Name>YarboPlan</Name>
<Members>
+ <ScriptProperty>
+ <Name>Areas</Name>
+ <GetScriptBlock>if($this.AreaIds){$this.AreaIds -join ', '}else{''}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>SelfOrder</Name>
+ <GetScriptBlock>if($this.EnableSelfOrder){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Id</Name>
<Name>Name</Name>
+ <Name>Areas</Name>
<Name>AreaIds</Name>
+ <Name>SelfOrder</Name>
<Name>EnableSelfOrder</Name>
</ReferencedProperties>
</PropertySet>
</Members>
</Type>
- <!-- YarboSchedule: key schedule fields -->
+ <!-- YarboSchedule: key schedule fields. Days, EnabledDisplay, WeatherDisplay match table view. -->
<Type>
<Name>YarboSchedule</Name>
<Members>
+ <ScriptProperty>
+ <Name>Days</Name>
+ <GetScriptBlock>if($this.WeekDay -eq 127){'Daily'}else{($this.GetDays() | ForEach-Object{$_.ToString().Substring(0,3)}) -join ','}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>EnabledDisplay</Name>
+ <GetScriptBlock>if($this.Enabled){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
+ <ScriptProperty>
+ <Name>WeatherDisplay</Name>
+ <GetScriptBlock>if($this.IsWeatherSchedule){'✓'}else{'✗'}</GetScriptBlock>
+ </ScriptProperty>
<PropertySet>
<Name>DefaultDisplayPropertySet</Name>
<ReferencedProperties>
<Name>Id</Name>
<Name>Name</Name>
<Name>PlanId</Name>
+ <Name>EnabledDisplay</Name>
<Name>Enabled</Name>
+ <Name>Days</Name>
<Name>WeekDay</Name>
<Name>StartTime</Name>
<Name>EndTime</Name>
+ <Name>WeatherDisplay</Name>
<Name>IsWeatherSchedule</Name>
</ReferencedProperties>
</PropertySet>
@@ -123,6 +196,29 @@
</Members>
</Type>
+ <!-- YarboEndpoint: discovery result. RecommendedDisplay matches table view. -->
+ <Type>
+ <Name>YarboEndpoint</Name>
+ <Members>
+ <ScriptProperty>
+ <Name>RecommendedDisplay</Name>
+ <GetScriptBlock>if($this.Recommended){'⭐ Yes'}else{''}</GetScriptBlock>
+ </ScriptProperty>
+ <PropertySet>
+ <Name>DefaultDisplayPropertySet</Name>
+ <ReferencedProperties>
+ <Name>IPAddress</Name>
+ <Name>Port</Name>
+ <Name>Path</Name>
+ <Name>MacAddress</Name>
+ <Name>RecommendedDisplay</Name>
+ <Name>Recommended</Name>
+ <Name>Status</Name>
+ </ReferencedProperties>
+ </PropertySet>
+ </Members>
+ </Type>
+
<!-- YarboLightState: all LED channels -->
<Type>
<Name>YarboLightState</Name>
diff --git a/src/PSYarbo/PSYarbo.psd1 b/src/PSYarbo/PSYarbo.psd1
--- a/src/PSYarbo/PSYarbo.psd1
+++ b/src/PSYarbo/PSYarbo.psd1
@@ -10,11 +10,26 @@
PowerShellVersion = '7.4'
# MQTTnet loaded via AssemblyLoadContext in PSYarbo.psm1 — not via RequiredAssemblies
# RequiredAssemblies = @()
+ # Load class definitions before root module so [YarboRobot], [YarboConnection], etc. are visible to OutputType/attributes
+ ScriptsToProcess = @(
+ 'Classes/YarboExceptions.ps1'
+ 'Classes/YarboLightState.ps1'
+ 'Classes/YarboCommandResult.ps1'
+ 'Classes/YarboTelemetry.ps1'
+ 'Classes/YarboPlan.ps1'
+ 'Classes/YarboSchedule.ps1'
+ 'Classes/YarboRobot.ps1'
+ 'Classes/YarboGlobalParams.ps1'
+ 'Classes/YarboEndpoint.ps1'
+ 'Classes/YarboConnection.ps1'
+ 'Classes/YarboCloudSession.ps1'
+ )
FormatsToProcess = @('PSYarbo.Format.ps1xml')
- TypesToProcess = @('PSYarbo.Types.ps1xml')
+ # Types loaded in .psm1 with Update-TypeData -Force to avoid "already present" on re-import
+ # TypesToProcess = @('PSYarbo.Types.ps1xml')
FunctionsToExport = @(
# Connection
- 'Connect-Yarbo', 'Disconnect-Yarbo', 'Find-Yarbo',
+ 'Connect-Yarbo', 'Disconnect-Yarbo', 'Find-Yarbo', 'Find-YarboDevice', 'Invoke-YarboMqttSniff',
# Status
'Get-YarboStatus', 'Get-YarboRobot', 'Get-YarboBattery', 'Get-YarboFirmware',
'Get-YarboGlobalParams',
@@ -41,7 +56,7 @@
)
CmdletsToExport = @()
VariablesToExport = @()
- AliasesToExport = @('Return-YarboToDock')
+ AliasesToExport = @('Return-YarboToDock', 'Start-YarboRecharge')
PrivateData = @{
PSData = @{
Tags = @('Yarbo', 'Robot', 'Mower', 'MQTT', 'IoT', 'Automation', 'SnowBlower')
diff --git a/src/PSYarbo/PSYarbo.psm1 b/src/PSYarbo/PSYarbo.psm1
--- a/src/PSYarbo/PSYarbo.psm1
+++ b/src/PSYarbo/PSYarbo.psm1
@@ -16,37 +16,182 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
-#region — Load MQTTnet via isolated AssemblyLoadContext
+#region — Load MQTTnet and C# listener helper
$libPath = Join-Path $PSScriptRoot 'lib'
$mqttDllPath = Join-Path $libPath 'MQTTnet.dll'
if (Test-Path $mqttDllPath) {
- $alcName = 'PSYarboMqttContext'
- $script:MqttALC = [System.Runtime.Loader.AssemblyLoadContext]::new($alcName, $true)
- $script:MqttAssembly = $script:MqttALC.LoadFromAssemblyPath((Resolve-Path $mqttDllPath).Path)
- Write-Verbose "PSYarbo: Loaded MQTTnet via isolated AssemblyLoadContext '$alcName'"
+ $script:MqttAssembly = [System.Reflection.Assembly]::LoadFrom((Resolve-Path $mqttDllPath).Path)
+ Write-Verbose "PSYarbo: Loaded MQTTnet from $mqttDllPath"
+ # C# helper: connects, subscribes, and receives messages entirely in C# so the MQTTnet
+ # ApplicationMessageReceivedAsync event is attached with += and always fires.
+ $listenerCode = @'
+using System;
+using System.Collections;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PSYarbo.Mqtt {
+ public sealed class YarboMqttListener : IDisposable {
+ private readonly MQTTnet.MqttFactory _factory;
+ private MQTTnet.Client.IMqttClient _client;
+ private readonly ArrayList _messages = ArrayList.Synchronized(new ArrayList());
+ private readonly TaskCompletionSource<ReceivedMessage> _firstMessage = new TaskCompletionSource<ReceivedMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public YarboMqttListener() {
+ _factory = new MQTTnet.MqttFactory();
+ _client = _factory.CreateMqttClient();
+ _client.ApplicationMessageReceivedAsync += e => {
+ var seg = e.ApplicationMessage.PayloadSegment;
+ int len = seg.Array != null ? seg.Count : 0;
+ byte[] copy = len > 0 ? new byte[len] : Array.Empty<byte>();
+ if (len > 0 && seg.Array != null)
+ Buffer.BlockCopy(seg.Array, seg.Offset, copy, 0, len);
+ var msg = new ReceivedMessage(e.ApplicationMessage.Topic ?? "", copy);
+ _messages.Add(msg);
+ if (!_firstMessage.Task.IsCompleted)
+ _firstMessage.TrySetResult(msg);
+ return Task.CompletedTask;
+ };
+ }
+
+ public void Connect(string broker, int port, string clientId, TimeSpan timeout) {
+ var opts = _factory.CreateClientOptionsBuilder()
+ .WithTcpServer(broker, port)
+ .WithClientId(clientId)
+ .WithTimeout(timeout)
+ .Build();
+ _client.ConnectAsync(opts, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public void Subscribe(string[] topicFilters) {
+ var builder = _factory.CreateSubscribeOptionsBuilder();
+ foreach (var topic in topicFilters)
+ builder = builder.WithTopicFilter(topic);
+ var opts = builder.Build();
+ _client.SubscribeAsync(opts, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public void Publish(string topic, byte[] payload) {
+ var msg = _factory.CreateApplicationMessageBuilder()
+ .WithTopic(topic)
+ .WithPayload(payload ?? Array.Empty<byte>())
+ .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtMostOnce)
+ .Build();
+ _client.PublishAsync(msg, CancellationToken.None).GetAwaiter().GetResult();
+ }
+
+ public ReceivedMessage WaitForFirstMessage(int timeoutMs) {
+ try {
+ if (_firstMessage.Task.IsCompleted)
... diff truncated: showing 800 of 3365 linesSymmetrically call remove_ApplicationMessageReceivedAsync in YarboConnection.Dispose() to properly detach the event handler before calling UnregisterCallback. This prevents unnecessary callback invocations between unregister and client disposal.
- YarboConnection.Dispose: resolve MessageReceivedAdapter at runtime so module loads when ScriptsToProcess runs before .psm1 (fixes CI/import failure) - Invoke-YarboMqttSniff: -RecordPath to save full payloads (Topic, PayloadBase64) for later analysis; capture start time per message - Get-YarboMqttRecordingReport: new cmdlet; reads recording JSON, decodes zlib, reports topic coverage vs cmdlets - Record-YarboMqtt.ps1: 60s real MQTT record + report; -DiscoverBroker support - Analyze-MqttRecordingGaps.ps1: standalone gap analysis (payload keys not exposed) - YarboRobot/YarboTelemetry: add all DeviceMSG fields as typed properties (Battery current/voltage, RTK/RTCM, RunningStatus, State, LedInfo, EletricMSG, SystemInfo, DebugMsg, switches, etc.); TelemetryParser maps all from payload - tools/MqttSniff.ps1: -RecordPath, doc updates - Analyze ExposedKeys updated so real recording shows full coverage Made-with: Cursor
- Thread 2: Replace ArrayList.Synchronized with explicit lock object in YarboMqttListener to make CopyTo+Clear compound operation atomic - Thread 3/4: Rewrite >512-host warning to recommend -Subnet or reducing -MaxHosts instead of confusingly suggesting -MaxHosts to scan more - CI PSUseSingularNouns: Rename Get-PSYarboLocalSubnets → Get-PSYarboLocalSubnet in NetworkHelper.ps1, Find-Yarbo.ps1, and Find-YarboDevice.ps1 - CI PSUseConsistentIndentation: Fix ValidateScript block indentation in Find-Yarbo.ps1 and Find-YarboDevice.ps1 (16-space body, 12-space closing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminated ~150 lines of duplication between ConvertTo-YarboTelemetry and ConvertTo-YarboRobot by creating Set-YarboCommonFields helper function. This consolidates parsing logic for BatteryMSG, StateMSG, RTKMSG, RunningStatusMSG, wireless_recharge, EletricMSG, ultrasonic_msg, LED, and misc fields into a single location, reducing maintenance burden and preventing inconsistencies when adding or fixing field mappings.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: CI integration step may silently pass with zero tests
- Changed Run.Exit to false and added manual checks to ensure at least one test runs and fail the step if zero tests are executed or any tests fail.
Preview (dbf3c28e99)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,61 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Filter.ExcludeTag = 'Integration'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
+ [int]$SnowRollerMotor
... diff truncated: showing 800 of 5314 linesThe integration test step was silently passing even when no tests ran because Run.Exit was true but there was no ErrorOnNoTests safeguard. Changed Run.Exit to false and added manual checks to ensure at least one test runs and fail the step if any tests fail or if zero tests are executed.
…ver/DC message per device - TelemetryParser: Add SuppressMessageAttribute for Set-YarboCommonFields (PSUseShouldProcessForStateChangingFunctions, PSUseSingularNouns) so PSScriptAnalyzer tests pass; attributes placed inside function body. - Find-YarboDevice: Show 'Both endpoints reach the same broker' only when at least one device (per SerialNumber) has both Rover and DC endpoints, fixing misleading message when multiple devices (e.g. one Rover, one DC). Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: CI integration tests silently pass due to missing PassThru
- Added $intConfig.Run.PassThru = $true to enable Invoke-Pester to return the result object needed for TotalCount and FailedCount checks.
Preview (28f929e057)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.ExcludeTag = 'Integration'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
... diff truncated: showing 800 of 5322 linesWithout PassThru=true, Invoke-Pester returns null instead of a result object, causing TotalCount and FailedCount checks to always pass silently.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Redundant
ToUInt32call inside IP generation loop- Replaced redundant ToUInt32 call with direct use of the baseVal variable already computed before the loop.
- ✅ Fixed: Callback parameter shadows automatic
$argsvariable- Renamed callback parameter from $args to $eventArgs to avoid shadowing PowerShell's automatic variable.
Preview (a2f6e6152c)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.ExcludeTag = 'Integration'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
... diff truncated: showing 800 of 5322 lines| # Build connection options | ||
| # Attach message handler before connecting (MQTTnet requirement) | ||
| $callback = [Func[object, System.Threading.Tasks.Task]] { | ||
| param($args) |
There was a problem hiding this comment.
Callback parameter shadows automatic $args variable
Low Severity
The MQTT message handler scriptblock declares param($args) which shadows PowerShell's automatic $args variable. While this works correctly at runtime (the Func[object, Task] delegate argument binds to it), the naming is misleading and fragile — any future maintainer adding code that relies on the automatic $args would get surprising behavior. A descriptive name like $eventArgs or $ea would be clearer and safer.
- Remove redundant ToUInt32 call in IP generation loop by reusing baseVal variable - Rename callback parameter from $args to $eventArgs to avoid shadowing automatic variable
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: CI integration test step will always fail with zero tests error
- Changed CI filter from ExcludeTag='Integration' to Tag='Mocked' to explicitly run only mocked tests and prevent unintentional execution of future untagged tests.
- ✅ Fixed: Dead
$baseBytesreassignment after masking in subnet IP generation- Removed the dead assignment of $baseBytes at line 62 which was never used after being reassigned.
Preview (43d6df4fe3)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
... diff truncated: showing 800 of 5321 lines| } | ||
| if ($intResult.FailedCount -gt 0) { | ||
| exit 1 | ||
| } |
There was a problem hiding this comment.
CI integration test step will always fail with zero tests error
Medium Severity
The CI integration test step runs ./tests/Integration with ExcludeTag = 'Integration', expecting mocked tests tagged Mocked to run. However, the Get-YarboPlan.Tests.ps1 and Get-YarboStatus.Tests.ps1 files use InModuleScope PSYarbo with BeforeAll that directly calls Import-Module, meaning their Describe blocks (tagged Mocked) run outside Pester's tag-filtering flow. If Pester discovers them but the module fails to load (e.g. missing MQTTnet in CI), the step could fail unexpectedly. More critically, the error message at line 111 says "check that mocked test files exist and are tagged 'Mocked'" but the ExcludeTag filter is Integration — this doesn't include only Mocked, it runs everything except Integration. If in the future someone adds a non-tagged test to tests/Integration, it would also run unintentionally.
| $hostBits = 32 - $prefixLen | ||
| $baseVal = [System.BitConverter]::ToUInt32($baseBytes, 0) | ||
| $baseVal = [uint32]($baseVal -band ([uint32]::MaxValue -shl $hostBits)) | ||
| $baseBytes = [System.BitConverter]::GetBytes($baseVal) |
There was a problem hiding this comment.
Dead $baseBytes reassignment after masking in subnet IP generation
Low Severity
In Get-PSYarboSubnetIpList, $baseBytes is reassigned from [System.BitConverter]::GetBytes($baseVal) at line 62, but is never read again — the loop on lines 65–70 uses $baseVal directly. This dead store is likely a leftover from the original Find-Yarbo code (which used $baseBytes in the loop). It adds confusion about which variable holds the canonical network address value.
- Change CI integration test filter from ExcludeTag='Integration' to Tag='Mocked' to explicitly run only mocked tests, preventing unintentional execution of future untagged tests - Remove dead assignment of $baseBytes after subnet masking in Get-PSYarboSubnetIpList (variable was never used after line 62)
| $session.RefreshAuth() | ||
| if ($script:YarboCloudSession) { $script:YarboCloudSession.Dispose() } | ||
| $script:YarboCloudSession = $session | ||
| return $session |
There was a problem hiding this comment.
Stored refresh token rotation not persisted back
Medium Severity
The early-return path for stored refresh-token authentication skips the credential-persistence block. When RefreshAuth() succeeds and the server rotates the refresh token (common for security), $session.RefreshToken is updated in memory but never saved back via Save-YarboCloudCredential or Save-YarboCredential. The next session attempt using stored credentials will send the stale (old) token, which will likely fail with an auth error, breaking the "seamless reconnect without re-entering password" workflow this feature is meant to provide.
Additional Locations (1)
- PSYarbo.psm1: initialize \$script:MqttResolvingHandler = \$null in the else branch (MQTTnet.dll absent) so the OnRemove cleanup handler never throws under Set-StrictMode -Version Latest. Fixes 94 test container failures across all test suites. - DebugAndSupportBundle.Tests.ps1: align TelemetryLog mock entry with the actual property names used by Connect-Yarbo (Timestamp / MessageType / Direction / Topic) instead of the incorrect At / Source that caused a PropertyNotFoundException under StrictMode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Typed property changes silently lost when saving params
- Added code to sync typed properties (MowSpeed, CuttingHeight, RainDelay, ObstacleSensitivity) back to the payload hashtable before sending to the device, preventing silent data loss when users modify typed properties.
Preview (7a4a85e039)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
... diff truncated: showing 800 of 5330 lines| $Parameters | ||
| } else { | ||
| throw "Parameters must be a hashtable or YarboGlobalParams object." | ||
| } |
There was a problem hiding this comment.
Typed property changes silently lost when saving params
Medium Severity
When Set-YarboGlobalParams receives a YarboGlobalParams object, it exclusively reads from RawData to build the MQTT payload, completely ignoring the typed properties (MowSpeed, CuttingHeight, RainDelay, ObstacleSensitivity). Since Get-YarboGlobalParams populates both RawData and the typed properties independently, a user who modifies $params.MowSpeed = 0.3 will have that change silently discarded — only RawData.mow_speed (still the original value) is sent to the device.
Additional Locations (1)
When Set-YarboGlobalParams receives a YarboGlobalParams object, it now syncs the typed properties (MowSpeed, CuttingHeight, RainDelay, ObstacleSensitivity) back to the payload hashtable before sending to the device. This prevents silent data loss when users modify typed properties instead of RawData directly. Fixes bug e1860387-4993-47cc-8858-2cc2682247ef
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Value-type null checks always true, silently overwriting RawData
- Removed the null checks and sync lines that unconditionally overwrote RawData values with typed property values, allowing user modifications to RawData to be preserved as documented.
Preview (0957680cf9)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
... diff truncated: showing 800 of 5323 lines| if ($null -ne $Parameters.MowSpeed) { $ht['mow_speed'] = $Parameters.MowSpeed } | ||
| if ($null -ne $Parameters.CuttingHeight) { $ht['cutting_height'] = $Parameters.CuttingHeight } | ||
| if ($null -ne $Parameters.RainDelay) { $ht['rain_delay'] = $Parameters.RainDelay } | ||
| if ($null -ne $Parameters.ObstacleSensitivity) { $ht['obstacle_sensitivity'] = $Parameters.ObstacleSensitivity } |
There was a problem hiding this comment.
Value-type null checks always true, silently overwriting RawData
High Severity
The $null -ne guards on MowSpeed ([double]), CuttingHeight ([int]), RainDelay ([int]), and ObstacleSensitivity ([int]) always evaluate to $true because PowerShell value types are never $null — they default to 0. This means the typed properties always overwrite the corresponding keys in the hashtable built from RawData. The documented usage pattern ($params.RawData.mow_speed = 0.5) is broken: the user's RawData edit is silently replaced by the stale typed property value, causing silent data loss.
Remove lines that unconditionally overwrote RawData values with typed properties. Value types (int, double) cannot be null in PowerShell and default to 0, making the null checks always true. This caused silent data loss when users modified RawData as documented.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Bugbot Autofix resolved 3 of the 4 issues found in the latest run.
- ✅ Fixed: Test class loading missing two new class files
- Added YarboGlobalParams and YarboEndpoint to the explicit class loading list in the test file to match the module's 11 classes.
- ✅ Fixed: Early return bypasses session output logging in cloud auth
- Wrapped the refresh token attempt in try-catch to allow fallback to password authentication when the stored refresh token is stale or revoked.
- ✅ Fixed: Subnet IP generation silently skips /31 user input
- Extended validation range to accept /31 subnets and implemented RFC 3021 point-to-point link handling with both addresses usable.
Preview (0da87b4fcb)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
+ [string]$RtkVersion
+ [int]$RtkSatNum
+ [double]$RtkTimestamp
+ [double]$RtcmAge
+ [object]$RtcmInfo
+ [string]$RoverHeading
+ [string]$BaseGngga
- # Hardware
+ # Hardware (RunningStatusMSG, led, wireless_recharge)
[int]$ChuteAngle
+ [int]$RainSensorData
+ [int]$ChuteSteeringEngineInfo
+ [int]$ChuteSteeringRunStatus
+ [double]$HeadGyroPitch
+ [double]$HeadGyroRoll
+ [int]$PushPodStatus
+ [int]$PushRodPlace
+ [int]$SnowPipeRunStatus
... diff truncated: showing 800 of 5342 lines| if (-not $toRecommend) { $toRecommend = $rovers[0] } | ||
| foreach ($r in $rovers) { $r.Recommended = ($r -eq $toRecommend) } | ||
| } | ||
| } |
There was a problem hiding this comment.
Recommendation logic inverted: DC preferred over Rover
Medium Severity
The Find-YarboDevice recommendation logic prefers DC (locally-administered MAC) as the recommended endpoint, but the description says "Rover = direct WiFi to the mower" and "DC = base station / data center bridge." The code comment at line 213 says "Prefer DC" and recommends DC first. However, if the locally-administered MAC heuristic is wrong for a given network (e.g. some routers assign locally-administered MACs to all clients), every endpoint gets labeled DC and the first one is recommended — potentially the wrong one. More importantly, an unknown MAC (not resolved) defaults to Rover classification, so a real DC whose MAC can't be resolved would be misclassified and a real Rover whose MAC can't be resolved would be correctly labeled — creating an asymmetric failure mode.
There was a problem hiding this comment.
Bugbot Autofix determined this is a false positive.
The logic correctly prefers DC over Rover as documented; defaulting unknown MACs to Rover is intentional conservative behavior to avoid wrong recommendations.
| It 'Classes directory exists' { | ||
| Join-Path $moduleRoot 'Classes' | Should -Exist | ||
| (Get-ChildItem -Path (Join-Path $moduleRoot 'Classes') -Filter '*.ps1').Count | Should -Be 9 | ||
| (Get-ChildItem -Path (Join-Path $moduleRoot 'Classes') -Filter '*.ps1').Count | Should -Be 11 |
There was a problem hiding this comment.
Test class loading missing two new class files
Low Severity
The Classes test block loads only 9 class files directly (missing YarboGlobalParams and YarboEndpoint), while the module now has 11 classes. Although the using module directive at line 3 loads all classes via ScriptsToProcess, this direct-loading list is inconsistent with the module's class count assertion at line 104 (Should -Be 11) and could cause failures if the using module path changes or if future tests in this block need the missing classes.
| $session.RefreshAuth() | ||
| if ($script:YarboCloudSession) { $script:YarboCloudSession.Dispose() } | ||
| $script:YarboCloudSession = $session | ||
| return $session |
There was a problem hiding this comment.
Early return bypasses session output logging in cloud auth
Medium Severity
When Connect-YarboCloud succeeds via stored RefreshToken (lines 67–73), it returns early without reaching the code at lines 172–175 that disposes a previous $script:YarboCloudSession before overwriting. The early-return path does its own dispose-then-store (lines 71–72), but if RefreshAuth() at line 70 throws, the exception is caught by the outer catch which disposes the new session — however the stored RefreshToken failure never falls through to try the stored Password at line 75, meaning a user with both a stale refresh token and a valid password saved will get an error instead of a graceful fallback.
Additional Locations (1)
| ) | ||
| } | ||
| } | ||
| continue |
There was a problem hiding this comment.
Subnet IP generation silently skips /31 user input
Low Severity
Get-PSYarboSubnetIpList rejects prefixes outside /16–/30 range. When ValidateUserInput is $true, it throws for /prefixLen < 16 and /prefixLen > 30, but the second if at line 48 is unreachable after the first if throws at line 43 for the same condition branch. More importantly, /31 point-to-point links are valid and usable in some network configurations but are silently skipped without user feedback when ValidateUserInput is $false (auto-detected subnets).
- Fix test class loading to include YarboGlobalParams and YarboEndpoint (bug 82802596) - Add try-catch for refresh token with password fallback in Connect-YarboCloud (bug 943ed285) - Add support for /31 point-to-point subnets per RFC 3021 (bug 8beebbd0)
| $session.RefreshAuth() | ||
| if ($script:YarboCloudSession) { $script:YarboCloudSession.Dispose() } | ||
| $script:YarboCloudSession = $session | ||
| return $session |
There was a problem hiding this comment.
Refreshed tokens not persisted on early return
Medium Severity
When Connect-YarboCloud succeeds via the stored refresh token path, it returns early without persisting the potentially-rotated tokens back to the credential store. The Save-YarboCredential / Save-YarboCloudCredential calls only exist in the normal email/password login flow. If the cloud API rotates refresh tokens on use, the stored token becomes stale, causing the next auto-login attempt to fail and fall back to password — which may also not be stored, resulting in a PASSWORD_REQUIRED error.
Additional Locations (1)
…, HeadType docs - Stop-YarboPlan: use stop_plan; Resume-YarboPlan: use resume_plan - Start-YarboPlan: add -Percent (0-100) for start_plan percent - Get-YarboSavedWifiList: get_saved_wifi_list - Set-YarboBladeSpeed, Set-YarboChargeLimit: new cmdlets - YarboLightState.BodyOn(): body accent preset (lights_body) - about_PSYarbo_Heads.help.txt: HeadType enum + 7-channel LED reference - Commands-and-Properties: HeadType values, new exports in tests Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Refresh token not persisted on stored-credential early return
- Added credential-saving logic after RefreshAuth() in the stored-credential early return path to persist potentially-rotated refresh tokens, mirroring the behavior of the normal email/password login flow.
Preview (e7dd0d58dc)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier (0=NoHead, 1=SnowBlower, 2=LeafBlower, 3=LawnMower, 4=SmartCover, 5=LawnMowerPro, 99=Trimmer). See `Get-Help about_PSYarbo_Heads` for full list. |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboLightState.ps1 b/src/PSYarbo/Classes/YarboLightState.ps1
--- a/src/PSYarbo/Classes/YarboLightState.ps1
+++ b/src/PSYarbo/Classes/YarboLightState.ps1
@@ -31,6 +31,14 @@
return [YarboLightState]::new()
}
+ # Body accent lights only (red channels; matches python-yarbo lights_body())
+ static [YarboLightState] BodyOn() {
+ $s = [YarboLightState]::new()
+ $s.BodyLeftRed = 255
+ $s.BodyRightRed = 255
+ return $s
+ }
+
[string] ToString() {
return "Head:$($this.HeadLight) L:$($this.LeftWhite) R:$($this.RightWhite) BL:$($this.BodyLeftRed) BR:$($this.BodyRightRed) TL:$($this.TailLeftRed) TR:$($this.TailRightRed)"
}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
... diff truncated: showing 800 of 5684 lines| $session.RefreshAuth() | ||
| if ($script:YarboCloudSession) { $script:YarboCloudSession.Dispose() } | ||
| $script:YarboCloudSession = $session | ||
| return $session |
There was a problem hiding this comment.
Refresh token not persisted on stored-credential early return
Medium Severity
When Connect-YarboCloud authenticates via a stored refresh token (the early return path), it calls $session.RefreshAuth() which may receive a new rotated refresh token from the API (line 131–133 of YarboCloudSession.ps1), but returns at line 74 without persisting the updated token via Save-YarboCredential or Save-YarboCloudCredential. The normal email/password login path at lines 164–173 does save tokens. If the Yarbo API rotates refresh tokens on use, the next session will attempt to use the now-stale token, causing authentication failure and forcing the user to re-enter their password.
Additional Locations (1)
When Connect-YarboCloud authenticates via a stored refresh token, the RefreshAuth() method may receive a new rotated refresh token from the API. The early return path was not persisting this updated token, causing authentication failures in subsequent sessions. This fix adds credential-saving logic after RefreshAuth() in the stored-credential path, mirroring the behavior of the normal email/password login flow.
…e, motor/trimmer, OTA, vision, fence, maps, camera) - Control: Set-YarboTurnType, Push-YarboSnowDir, Set-YarboChuteSteeringWork, Set-YarboRollerSpeed, Set-YarboBladeHeight, Set-YarboMotorProtect, Set-YarboTrimmer, Set-YarboEdgeBlowing, Set-YarboSmartBlowing, Set-YarboHeatingFilm, Set-YarboModuleLock, Set-YarboFollowState, Start-YarboDrawCmd, Set-YarboGreengrassUpdateSwitch, Set-YarboIpcameraOtaSwitch, Update-YarboFirmware, Set-YarboSmartVision, Set-YarboVideoRecord, Set-YarboBagRecord, Set-YarboChildLock, Set-YarboGeoFence, Set-YarboElecFence, Set-YarboNgzEdge, Invoke-YarboCameraCalibration - Plans: Clear-YarboMap, Restore-YarboMap, Save-YarboCurrentMap - Status: Get-YarboCameraStatus - All new cmdlets have .SYNOPSIS and .EXAMPLE for help tests Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Null password passed to mandatory SecureString parameter
- Made Password parameter optional in Save-YarboCloudCredential and added conditional check to only save when provided, preventing parameter binding error when null.
Preview (bb0fd24a03)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier (0=NoHead, 1=SnowBlower, 2=LeafBlower, 3=LawnMower, 4=SmartCover, 5=LawnMowerPro, 99=Trimmer). See `Get-Help about_PSYarbo_Heads` for full list. |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboLightState.ps1 b/src/PSYarbo/Classes/YarboLightState.ps1
--- a/src/PSYarbo/Classes/YarboLightState.ps1
+++ b/src/PSYarbo/Classes/YarboLightState.ps1
@@ -31,6 +31,14 @@
return [YarboLightState]::new()
}
+ # Body accent lights only (red channels; matches python-yarbo lights_body())
+ static [YarboLightState] BodyOn() {
+ $s = [YarboLightState]::new()
+ $s.BodyLeftRed = 255
+ $s.BodyRightRed = 255
+ return $s
+ }
+
[string] ToString() {
return "Head:$($this.HeadLight) L:$($this.LeftWhite) R:$($this.RightWhite) BL:$($this.BodyLeftRed) BR:$($this.BodyRightRed) TL:$($this.TailLeftRed) TR:$($this.TailRightRed)"
}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
# Position / Navigation
[double]$OdometryX
[double]$OdometryY
[double]$OdometryPhi
[double]$Heading
+ [int]$RtkHeadingStatus
[string]$RtkStatus
+ [double]$RtkHeadingDop
[double]$OdomConfidence
+ [double]$RtkGgaAtnDis
+ [double]$RtkHeadingAtnDis
+ [int]$RtkHeadingMulti
+ [int]$RtkHeadingObs
+ [double]$RtkPre4Timestamp
... diff truncated: showing 800 of 6583 lines| try { | ||
| Save-YarboCredential -Name 'CloudRefreshToken' -Value $session.RefreshToken | ||
| if ($session.Email) { | ||
| Save-YarboCloudCredential -Email $session.Email -Password $stored.Password -RefreshToken $session.RefreshToken |
There was a problem hiding this comment.
Null password passed to mandatory SecureString parameter
Low Severity
When a stored refresh token succeeds, Save-YarboCloudCredential is called with $stored.Password which can be $null (if only a refresh token was stored, not a password). Since Save-YarboCloudCredential declares Password as [Parameter(Mandatory)] [SecureString], this causes a parameter binding error. The error is caught by the surrounding try/catch so it doesn't crash, but it silently prevents the rotated refresh token from being persisted to email-keyed storage.
When a refresh token succeeds without a stored password, Save-YarboCloudCredential was called with a null Password parameter, causing a parameter binding error since Password was mandatory. This prevented the rotated refresh token from being saved. Made Password parameter optional and conditionally save it only when provided, matching the existing pattern for RefreshToken.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Single-line function bodies reduce readability and maintainability
- Reformatted both functions to use multi-line format consistent with other cmdlets in the codebase.
- ✅ Fixed: Auto-credential refresh path omits BoundSerialNumbers from session
- Updated RefreshAuth() to populate BoundSerialNumbers if available in API response, and added fallback to fetch device list in Connect-YarboCloud auto-credential path.
Preview (9d87f59d12)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier (0=NoHead, 1=SnowBlower, 2=LeafBlower, 3=LawnMower, 4=SmartCover, 5=LawnMowerPro, 99=Trimmer). See `Get-Help about_PSYarbo_Heads` for full list. |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboCloudSession.ps1 b/src/PSYarbo/Classes/YarboCloudSession.ps1
--- a/src/PSYarbo/Classes/YarboCloudSession.ps1
+++ b/src/PSYarbo/Classes/YarboCloudSession.ps1
@@ -131,6 +131,9 @@
if ($result.data.refreshToken) {
$this.RefreshToken = ConvertTo-SecureString -String $result.data.refreshToken -AsPlainText -Force
}
+ if ($result.data.snList) {
+ $this.BoundSerialNumbers = @($result.data.snList)
+ }
} else {
$codeStr = if ($null -ne $result.code) { $result.code.ToString() } else { 'UNKNOWN' }
throw [YarboCloudAuthException]::new("Token refresh failed: $($result.message)", $codeStr)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboLightState.ps1 b/src/PSYarbo/Classes/YarboLightState.ps1
--- a/src/PSYarbo/Classes/YarboLightState.ps1
+++ b/src/PSYarbo/Classes/YarboLightState.ps1
@@ -31,6 +31,14 @@
return [YarboLightState]::new()
}
+ # Body accent lights only (red channels; matches python-yarbo lights_body())
+ static [YarboLightState] BodyOn() {
+ $s = [YarboLightState]::new()
+ $s.BodyLeftRed = 255
+ $s.BodyRightRed = 255
+ return $s
+ }
+
[string] ToString() {
return "Head:$($this.HeadLight) L:$($this.LeftWhite) R:$($this.RightWhite) BL:$($this.BodyLeftRed) BR:$($this.BodyRightRed) TL:$($this.TailLeftRed) TR:$($this.TailRightRed)"
}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
... diff truncated: showing 800 of 6642 lines| .EXAMPLE | ||
| Set-YarboGreengrassUpdateSwitch -State 1 | ||
| #> | ||
| [CmdletBinding(SupportsShouldProcess)] [OutputType([YarboCommandResult])] param([Parameter(Mandatory,Position=0)][ValidateSet(0,1)][int]$State,[Parameter(ValueFromPipeline)][YarboConnection]$Connection) process { $conn = Resolve-YarboConnection -Connection $Connection; if ($PSCmdlet.ShouldProcess($conn.SerialNumber, "Set Greengrass update switch $State")) { Assert-YarboController -Connection $conn; $cmd = 'set_greengrass_auto_update_switch'; return Send-MqttCommand -Connection $conn -Command $cmd -Payload @{ state = $State } } } } |
There was a problem hiding this comment.
Single-line function bodies reduce readability and maintainability
Low Severity
Set-YarboGreengrassUpdateSwitch and Set-YarboIpcameraOtaSwitch compress their entire [CmdletBinding], [OutputType], param(), and process {} blocks into a single line. Every other cmdlet added in this PR uses proper multi-line formatting. This makes these two functions extremely hard to read, review, and maintain — a diff or merge conflict here would be very painful.
Additional Locations (1)
| } | ||
| if ($script:YarboCloudSession) { $script:YarboCloudSession.Dispose() } | ||
| $script:YarboCloudSession = $session | ||
| return $session |
There was a problem hiding this comment.
Auto-credential refresh path omits BoundSerialNumbers from session
Low Severity
The new auto-credential refresh path in the Credential parameter set returns a YarboCloudSession without populating BoundSerialNumbers. The normal email+password login sets $session.BoundSerialNumbers = @($result.data.snList), but RefreshAuth() doesn't set it. Users inspecting the session (e.g., to check which robots are bound) will see an empty array instead of the expected serial list.
- Reformat Set-YarboGreengrassUpdateSwitch and Set-YarboIpcameraOtaSwitch to use multi-line format consistent with other cmdlets - Update RefreshAuth() to populate BoundSerialNumbers from API response if available - Fetch BoundSerialNumbers in auto-credential refresh path when not provided by refresh token API
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Misleading indentation hides control flow in credential logic
- Corrected indentation to properly reflect the nested control flow structure, making the if ($stored.RefreshToken) block clearly nested within if ($stored) and aligning the if ($stored.Password) fallback at the correct level.
Preview (13b1673e14)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -43,7 +43,7 @@
render: powershell
placeholder: |
# Example:
- Connect-Yarbo -MowerSerial 'YB-XXXX' -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
Get-YarboStatus | Format-List
- type: checkboxes
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,6 +5,7 @@
branches: [develop]
pull_request:
branches: [main, develop]
+ workflow_dispatch:
permissions:
contents: read
@@ -72,20 +73,62 @@
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name PSScriptAnalyzer -MinimumVersion 1.21.0 -Scope CurrentUser -Force
- - name: Run Pester tests
+ - name: Install MQTTnet (for module load in tests)
shell: pwsh
+ run: ./build/Install-Dependencies.ps1
+
+ - name: Run Unit tests
+ shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0 -Force
- $config = New-PesterConfiguration
- $config.Run.Path = './tests'
- $config.Run.Exit = $true
- $config.Output.Verbosity = 'Detailed'
- $config.TestResult.Enabled = $true
- $config.TestResult.OutputPath = 'output/test-results/pester-${{ matrix.os }}.xml'
- $config.TestResult.OutputFormat = 'NUnitXml'
New-Item -ItemType Directory -Path 'output/test-results' -Force | Out-Null
- Invoke-Pester -Configuration $config
+ $unitConfig = New-PesterConfiguration
+ $unitConfig.Run.Path = './tests/Unit'
+ $unitConfig.Run.Exit = $true
+ $unitConfig.Output.Verbosity = 'Detailed'
+ $unitConfig.Filter.ExcludeTag = 'Integration'
+ $unitConfig.TestResult.Enabled = $true
+ $unitConfig.TestResult.OutputPath = 'output/test-results/pester-unit-${{ matrix.os }}.xml'
+ $unitConfig.TestResult.OutputFormat = 'NUnitXml'
+ $unitResult = Invoke-Pester -Configuration $unitConfig
+ - name: Run Integration tests (mocked; excludes real-robot Integration tag)
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $intConfig = New-PesterConfiguration
+ $intConfig.Run.Path = './tests/Integration'
+ $intConfig.Run.Exit = $false
+ $intConfig.Run.PassThru = $true
+ $intConfig.Filter.Tag = 'Mocked'
+ $intConfig.Output.Verbosity = 'Detailed'
+ $intConfig.TestResult.Enabled = $true
+ $intConfig.TestResult.OutputPath = 'output/test-results/pester-integration-${{ matrix.os }}.xml'
+ $intConfig.TestResult.OutputFormat = 'NUnitXml'
+ # ErrorOnNoTests removed — not available in all Pester versions
+ $intResult = Invoke-Pester -Configuration $intConfig
+ if ($intResult.TotalCount -eq 0) {
+ Write-Error "Integration test step ran 0 tests — check that mocked test files exist and are tagged 'Mocked'"
+ exit 1
+ }
+ if ($intResult.FailedCount -gt 0) {
+ exit 1
+ }
+
+ - name: Run root tests (PSYarbo.Tests, PSScriptAnalyzer) and exit on failure
+ shell: pwsh
+ run: |
+ Import-Module Pester -MinimumVersion 5.0.0 -Force
+ $rootConfig = New-PesterConfiguration
+ $rootConfig.Run.Path = './tests/PSYarbo.Tests.ps1', './tests/PSScriptAnalyzer.Tests.ps1'
+ $rootConfig.Run.Exit = $true
+ $rootConfig.Filter.ExcludeTag = 'Integration'
+ $rootConfig.Output.Verbosity = 'Detailed'
+ $rootConfig.TestResult.Enabled = $true
+ $rootConfig.TestResult.OutputPath = 'output/test-results/pester-root-${{ matrix.os }}.xml'
+ $rootConfig.TestResult.OutputFormat = 'NUnitXml'
+ Invoke-Pester -Configuration $rootConfig
+
- name: Upload test results
if: always()
uses: actions/upload-artifact@v6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -114,17 +114,21 @@
Brief one-line description.
.DESCRIPTION
Longer description.
- .PARAMETER MqttServer
- MQTT broker hostname or IP.
+ .PARAMETER Broker
+ MQTT broker hostname or IP (use Find-YarboDevice to discover).
+ .PARAMETER SerialNumber
+ Robot serial number.
.EXAMPLE
- Connect-Yarbo -MqttServer '192.168.1.100'
+ Connect-Yarbo -Broker '<rover-ip>' -SerialNumber '<serial-number>'
.NOTES
Author: Your Name
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
- [string] $MqttServer
+ [string] $Broker,
+ [Parameter(Mandatory)]
+ [string] $SerialNumber
)
# ...
}
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@
$conn = $robot | Connect-Yarbo
# — OR — connect directly
-$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+$conn = Connect-Yarbo -Broker <rover-ip> -SerialNumber <serial-number>
# Check status
Get-YarboStatus
@@ -82,13 +82,21 @@
---
+## Commands and properties (how to get information)
+
+For detailed examples of **which cmdlets to use** and **which properties to read** (battery, RTK, LEDs, `SystemInfo.cpu.Temperature`, `LedInfo`, telemetry, etc.), see:
+
+📖 **[Commands and Properties](docs/Commands-and-Properties.md)** — status vs telemetry, property reference, and example snippets.
+
+---
+
## Cmdlet Reference
### Connection
| Cmdlet | Description |
|--------|-------------|
-| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT `heart_beat`. Returns `YarboRobot[]`. |
+| `Find-Yarbo` | Auto-discover Yarbo robots via MQTT (DeviceMSG, data_feedback, heart_beat; aligned with [python-yarbo](https://github.com/markus-lassfolk/python-yarbo)). Returns `YarboRobot[]`. |
| `Connect-Yarbo` | Connect to a robot's MQTT broker. Returns `YarboConnection`. |
| `Disconnect-Yarbo` | Disconnect and dispose resources. |
| `Test-YarboConnection` | Test if a connection is alive. |
@@ -155,8 +163,54 @@
| `Get-YarboVideo` | Get video token/URL. |
| `Get-YarboPlanHistory` | Get plan execution history from cloud. |
+### Utility
+
+| Cmdlet | Description |
+|--------|-------------|
+| `Test-YarboConnection` | Test if a connection is alive. |
+| `Get-YarboLog` | View CommandLog and TelemetryLog (sent/received MQTT). |
+| `Get-YarboMqttRecordingReport` | Report topic coverage from an MQTT recording. |
+| `Export-YarboSupportBundle` | Export a redacted MQTT/connection bundle for support (e.g. GlitchTip). |
+
---
+## Debug logging and support bundles
+
+To see what is being **sent and received** over MQTT (useful for troubleshooting firmware or unsupported heads/attachments), use either:
+
+- **`-Debug`** — Pass the common `-Debug` switch to any cmdlet that uses MQTT. Debug output shows topic, payload (human-readable JSON), and responses.
+- **Environment variable `PSYARBO_DEBUG`** — Set `$env:PSYARBO_DEBUG = "1"` (or `"true"`) so that debug output is shown for all MQTT traffic without passing `-Debug` every time. Output is written as information stream so it appears in the console.
+
+For **raw payloads** (base64 of the zlib-compressed bytes) instead of human-readable JSON, set **`PSYARBO_DEBUG_RAW=1`** (or `"true"`).
+
+```powershell
+# One-off debug for a single command
+Get-YarboStatus -Debug
+
+# Session-wide debug (no -Debug needed)
+$env:PSYARBO_DEBUG = "1"
+Get-YarboStatus
+Send-YarboCommand -Command read_global_params
+
+# Raw payload format
+$env:PSYARBO_DEBUG_RAW = "1"
+Get-YarboStatus -Debug
+```
+
+To provide a **full MQTT dump** for support (e.g. GlitchTip or when opening an issue), use **`Export-YarboSupportBundle`**. It produces a redacted JSON file from a recording and/or the current connection log. You can upload that file to your issue tracker.
+
+```powershell
+# From an MQTT recording (Invoke-YarboMqttSniff -RecordPath ...)
+Export-YarboSupportBundle -Path ./support-bundle.json -RecordingPath ./mqtt-recording.json
+
+# From the current connection (recent CommandLog + TelemetryLog)
+Export-YarboSupportBundle -Path ./support-bundle.json -Connection $conn
+```
+
+Full in-product documentation: `Get-Help about_PSYarbo_Debug`. This aligns with [python-yarbo #59 — Debug Logging](https://github.com/markus-lassfolk/python-yarbo/issues/59).
+
+---
+
## Protocol
PSYarbo uses the `snowbot/{SN}/...` MQTT topic hierarchy discovered by reverse engineering. Payloads are zlib-compressed UTF-8 JSON. Full documentation:
@@ -165,6 +219,8 @@
See also: `Get-Help about_PSYarbo_MQTT`
+**Discovery:** Find-Yarbo and Find-YarboDevice use the same strategy as [python-yarbo](https://github.com/markus-lassfolk/python-yarbo): anonymous MQTT client, subscribe to `snowbot/+/device/DeviceMSG`, `snowbot/+/device/data_feedback`, and `snowbot/+/device/heart_beat`, and derive the robot serial number from the first message received. By default, discovery scans all networks this machine has an IP on; pass `-Subnet` (e.g. `192.0.2.0/24`) if you want to limit the scan. If discovery finds no robots, run from a host on the same LAN as the Yarbo and ensure the robot is powered and connected.
+
---
## Contributing
diff --git a/build/build.ps1 b/build/build.ps1
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -76,6 +76,8 @@
foreach ($file in $psFiles) {
$lintFindings += Invoke-ScriptAnalyzer -Path $file.FullName -Settings $settings -Severity Error, Warning
}
+# Only fail on Error/Warning (exclude Information e.g. TypeNotFound in class files loaded via ScriptsToProcess)
+$lintFindings = @($lintFindings | Where-Object { $_.Severity -in 'Error', 'Warning' })
if ($lintFindings.Count -gt 0) {
$lintFindings | Format-Table -AutoSize
diff --git a/docs/Commands-and-Properties.md b/docs/Commands-and-Properties.md
new file mode 100644
--- /dev/null
+++ b/docs/Commands-and-Properties.md
@@ -1,0 +1,398 @@
+# Commands and Properties — Getting Information from Your Robot
+
+This guide shows which cmdlets to use and which properties to read for status, telemetry, battery, RTK, LEDs, system info, and more.
+
+---
+
+## Connect first
+
+```powershell
+# Discover and connect (recommended)
+$robot = Find-Yarbo | Select-Object -First 1
+$conn = $robot | Connect-Yarbo
+
+# Or connect directly if you know broker and serial number
+$conn = Connect-Yarbo -Broker 192.168.1.24 -SerialNumber 24400102L8HO5227
+```
+
+After connecting, `Get-YarboStatus` and `Get-YarboTelemetry` use the current default connection unless you pass `-Connection $conn`.
+
+---
+
+## Status vs telemetry
+
+| What you want | Cmdlet | Returns |
+|---------------|--------|--------|
+| **Full robot status** (one snapshot) | `Get-YarboStatus` or `Get-YarboRobot` | `YarboRobot` |
+| **Single telemetry snapshot** | `Get-YarboTelemetry` | `YarboTelemetry` |
+| **Streaming telemetry** | `Watch-YarboTelemetry -Duration 00:01:00` | Events (YarboTelemetry objects) |
+| **Battery only** | `Get-YarboBattery` | Integer 0–100 |
+
+`YarboRobot` and `YarboTelemetry` share most of the same properties (both are built from the same MQTT DeviceMSG). Use **Get-YarboStatus** when you want the full status object to inspect or script against.
+
+---
+
+## Getting status and storing it
+
+```powershell
+$status = Get-YarboStatus # or: Get-YarboRobot
+# $status is a YarboRobot with many properties (see below)
+```
+
+---
+
+## Property reference
+
+### Identity
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `SerialNumber` | string | Robot serial number |
+| `HeadSerialNumber` | string | Attached head serial |
+| `Name` | string | Robot name (if set) — **from cloud only, not MQTT** |
+| `HeadType` | int | Head type identifier (0=NoHead, 1=SnowBlower, 2=LeafBlower, 3=LawnMower, 4=SmartCover, 5=LawnMowerPro, 99=Trimmer). See `Get-Help about_PSYarbo_Heads` for full list. |
+
+**Robot name (the one you gave it):** Local MQTT does **not** provide the robot’s friendly name. That name comes from the **cloud API**. Use `Get-YarboDevice` (after `Connect-YarboCloud`) to get it; each device has a `.name` property (e.g. `"My Yarbo"`). You can match by `SerialNumber` and assign it to your status object if you want:
+
+```powershell
+Connect-YarboCloud -Email you@example.com -Password $pw
+$devices = Get-YarboDevice
+$status = Get-YarboStatus
+$status.Name = ($devices | Where-Object { $_.sn -eq $status.SerialNumber }).name
+# Now $status.Name is e.g. "My Yarbo"
+```
+
+**Example:** `$status.SerialNumber` → `24400102L8HO5227`
+
+---
+
+### Battery (BatteryMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `BatteryCapacity` | int | Charge percentage (0–100) |
+| `BatteryStatus` | int | Status code |
+| `BatteryTempError` | bool | Temperature error flag |
+| `BatteryTimestamp` | double | Device timestamp |
+| `BatteryCurrent` | double | Current (when present) |
+| `BatteryVoltage` | double | Voltage (when present) |
+
+**Examples:**
+
+```powershell
+$status.BatteryCapacity # e.g. 83
+$status.BatteryCurrent # current draw
+$status.BatteryVoltage # voltage
+Get-YarboBattery # quick 0–100 only
+```
+
+---
+
+### State (StateMSG)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `WorkingState` | int | 0 = idle, non-zero = active |
+| `ChargingStatus` | int | Charging state |
+| `ErrorCode` | int | Error code (0 = none) |
+| `IsPlanning` | bool | Plan in progress |
+| `IsPaused` | bool | Plan paused |
+| `IsRecharging` | bool | Recharge in progress |
+| `CarController` | bool | Car controller flag |
+| `MachineController` | int | Machine controller state |
+| `AdjustAngleStatus` | int | Adjust angle state |
+| `AutoDrawWaitingState` | int | Auto-draw waiting state |
+| `EnStateLed` | int | State LED enable |
+| `EnWarnLed` | int | Warning LED enable |
+| `OnGoingToStartPoint` | bool | Going to start point |
+| `OnMulPoints` | bool | Multi-point state |
+| `RobotFollowState` | int | Follow state |
+| `ScheduleCancel` | int | Schedule cancel state |
+| `VisionAutoDrawState` | int | Vision auto-draw state |
+
+**Examples:**
+
+```powershell
+$status.WorkingState # 0 = idle
+$status.IsPlanning # true if plan running
+$status.ErrorCode # 0 = no error
+```
+
+---
+
+### Position / navigation (RTK, odometry)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `OdometryX`, `OdometryY`, `OdometryPhi` | double | Odometry position |
+| `Heading` | double | Heading (degrees) |
+| `RtkHeadingStatus` | int | RTK heading status |
+| `RtkStatus` | string | RTK status string |
+| `RtkHeadingDop` | double | Heading DOP |
+| `OdomConfidence` | double | Odometry confidence |
+| `RtkGgaAtnDis` | double | GGA attenuation |
+| `RtkHeadingAtnDis` | double | Heading attenuation |
+| `RtkHeadingMulti` | int | Heading multi |
+| `RtkHeadingObs` | int | Heading observations |
+| `RtkPre4Timestamp` | double | Pre4 timestamp |
+| `RtkVersion` | string | RTK firmware version |
+| `RtkSatNum` | int | Number of satellites |
+| `RtkTimestamp` | double | RTK timestamp |
+| `RtcmAge` | double | RTCM age |
+| `RtcmInfo` | object | RTCM info (nested) |
+| `RoverHeading` | string | Rover heading from rtk_base_data |
+| `BaseGngga` | string | Base station GNGGA sentence |
+
+**Examples:**
+
+```powershell
+$status.Heading # e.g. 339.45
+$status.RtkStatus # e.g. "4"
+$status.RtkVersion # RTK version string
+$status.RtkSatNum # satellite count
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.RoverHeading
+```
+
+---
+
+### Hardware (RunningStatusMSG, LED, wireless charge)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `ChuteAngle` | int | Chute angle (snow blower) |
+| `RainSensorData` | int | Rain sensor value |
+| `ChuteSteeringEngineInfo` | int | Chute steering engine |
+| `ChuteSteeringRunStatus` | int | Chute run status |
+| `HeadGyroPitch` | double | Head gyro pitch |
+| `HeadGyroRoll` | double | Head gyro roll |
+| `PushPodStatus` | int | Push pod status |
+| `PushRodPlace` | int | Push rod place |
+| `SnowPipeRunStatus` | int | Snow pipe run status |
+| `SnowRollerMotor` | int | Snow roller motor |
+| `ElecNavigationFrontRightSensor` | int | Nav sensor front-right |
+| `ElecNavigationRearRightSensor` | int | Nav sensor rear-right |
+| `LedRegister` | string | Raw LED register value |
+| `LedInfo` | object | LED info (body_left_r/g/b, body_right_*, led_head, led_left_w, led_right_w, tail_left_r, tail_right_r) |
+| `HeadLedBrightness` | int | Head LED brightness |
+| `WirelessChargeState` | int | Wireless charge state |
+| `WirelessChargeVoltage` | double | Charge voltage |
+| `WirelessChargeCurrent` | double | Charge current |
+| `WirelessChargeErrorCode` | int | Charge error code |
+| `BrushlessMotorCurrent` | double | Brushless motor current (EletricMSG) |
+| `NtcTemperature` | double | NTC temperature |
+| `PushPodCurrent` | double | Push pod current |
+
+**Examples:**
+
+```powershell
+$status.ChuteAngle
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+$tel = Get-YarboTelemetry
+$tel.ChuteSteeringRunStatus
+$tel.HeadGyroPitch
+```
+
+---
+
+### Ultrasonic sensors
+
+| Property | Type |
+|----------|------|
+| `UltrasonicLeftFront` | int |
+| `UltrasonicMiddle` | int |
+| `UltrasonicRightFront` | int |
+
+**Example:** `$status.UltrasonicLeftFront`
+
+---
+
+### Network and misc
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `RoutePriority` | hashtable | Route priority (e.g. hg0, wlan0, wwan0) |
+| `BaseStatus` | object | base_status payload |
+| `Bds`, `Bs`, `Ms`, `S`, `Sbs`, `Tms` | object | Misc MQTT fields (when present) |
+| `GreenGrassUpdateSwitch` | int | Green grass update switch |
+| `IpcameraOtaSwitch` | int | IPC camera OTA switch |
+| `SystemInfo` | object | system_info (cpu.Frequency, cpu.Temperature, cpu.Usage, mem.*, userdata.disk.*, topProc) |
+| `DebugMsg` | object | Debug message payload |
+| `DeviceTimestamp` | double | Device message timestamp |
+| `LastUpdated` | datetime | When we last updated this object |
+
+**Examples:**
+
+```powershell
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+$status.RoutePriority.wlan0
+```
+
+---
+
+### YarboTelemetry-only (position as X/Y/Phi)
+
+`Get-YarboTelemetry` returns a `YarboTelemetry` object. It has the same MQTT-derived properties as above, plus:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `Timestamp` | datetime | When the snapshot was taken |
+| `X`, `Y`, `Phi` | double | Position (CombinedOdom) |
+| `Latitude`, `Longitude`, `Altitude` | double? | Parsed from GPS (rtk_base_data.rover.gngga) |
+| `FixQuality` | int | GPS fix quality |
+| `GnggaRaw` | string | Raw GNGGA NMEA sentence |
+
+**Example:**
+
+```powershell
+$tel = Get-YarboTelemetry
+$tel.X
+$tel.Latitude
+$tel.GnggaRaw
+```
+
+---
+
+## Full MQTT payload (RawMessage)
+
+Every property above is taken from the robot’s MQTT DeviceMSG. The **entire** decoded payload is also available so you can access any field that isn’t mapped to a typed property:
+
+```powershell
+$status = Get-YarboStatus
+$status.RawMessage # full DeviceMSG PSCustomObject
+$status.RawMessage.SomeKey # any key from the JSON
+```
+
+---
+
+## Example snippets
+
+```powershell
+# Connect and get status
+$robot = Find-Yarbo | Select-Object -First 1
+$robot | Connect-Yarbo | Out-Null
+$status = Get-YarboStatus
+
+# Battery and basic state
+$status.BatteryCapacity
+$status.BatteryCurrent
+$status.BatteryVoltage
+$status.WorkingState
+
+# RTK / GPS
+$status.RtkVersion
+$status.RtkSatNum
+$status.Heading
+$status.RtcmAge
+
+# System (CPU, memory, disk)
+$status.SystemInfo.cpu.Temperature
+$status.SystemInfo.cpu.Usage
+$status.SystemInfo.mem.MemAvailable
+$status.SystemInfo.userdata.disk.availableSize
+
+# LEDs
+$status.LedInfo.led_head
+$status.LedInfo.body_left_r
+$status.HeadLedBrightness
+
+# Telemetry snapshot (same kind of properties)
+$tel = Get-YarboTelemetry
+$tel.RtkSatNum
+$tel.ChuteSteeringRunStatus
+$tel.BatteryCurrent
+```
+
+---
+
+## Recording MQTT and checking coverage
+
+To record real MQTT from your robot and see which topics map to which cmdlets:
+
+```powershell
+.\tools\Record-YarboMqtt.ps1 -DiscoverBroker
+# Or with a known broker:
+.\tools\Record-YarboMqtt.ps1 -Broker 192.168.1.24 -DurationSeconds 60
+```
+
+Then open the generated report or run:
+
+```powershell
+Get-YarboMqttRecordingReport -RecordingPath .\PSYarbo-MqttRecording-<timestamp>.json
+```
+
+See [README](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md) for full cmdlet lists and installation.
+
+---
+
+## Robot offline or sleeping — what MQTT info do we get?
+
+The MQTT broker runs **on the robot**. What you get depends on whether the robot is reachable and whether it is awake or sleeping.
+
+### Robot completely offline (powered off or not on the network)
+
+- **No MQTT at all.** There is no broker to connect to.
+- `Find-Yarbo` / `Find-YarboDevice` will not return this robot (no TCP, or TCP works but no `heart_beat`/DeviceMSG).
+- `Connect-Yarbo` will fail (TCP connect timeout or connection refused).
+- **You get:** no connection, no status, no telemetry. Use your network tools or cloud app to confirm the robot is on and on the same LAN.
+
+### Robot “sleeping” (suspended / low power, broker still reachable)
+
+If the robot is in sleep mode (e.g. after `Suspend-Yarbo` or auto-sleep), the broker may still accept TCP connections and subscriptions, but the robot often **stops or greatly reduces** publishing:
+
+- **device/DeviceMSG** — full status (battery, RTK, state, etc.) — typically **not published** while sleeping.
+- **device/heart_beat** — simple keepalive with `working_state` — may stop or be rare.
+
+So in practice:
+
+- You **can** often still **connect** (`Connect-Yarbo` succeeds).
+- **get_controller** usually **times out** (robot not responding). You get a warning and the connection is left in **Connected** state without **ControllerAcquired**.
+- **No new DeviceMSG** (and possibly no new heart_beat) until the robot wakes.
+- Commands that send a request and wait for a response (e.g. `Get-YarboStatus` → get_device_msg) will **time out**; you do **not** get fresh status from MQTT.
+
+**What you *do* get in this situation:**
+
+| Source | What you get |
+|--------|-------------------------------|
+| **Connection object** | `State` = Connected, `ControllerAcquired` = false, `Broker`, `SerialNumber`, `ConnectedAt`. |
+| **LastHeartbeat** | Last time a `heart_beat` was received (or `DateTime.MinValue` if none). Stale once the robot is asleep. |
+| **LastWorkingState** | `working_state` from that last heart_beat (0 = idle/sleep). |
+| **Connection.Robot** | Only if at least one **DeviceMSG** was received earlier (e.g. before sleep). That last snapshot is stale. |
+| **Get-YarboStatus** | Times out (no response to get_device_msg). No new data. |
+| **Test-YarboConnection -Detailed** | `Connected` (MQTT client still connected), `HeartbeatAge` (time since last heart_beat), `ControllerAcquired` = false. |
+
+**Example: check if the robot is likely sleeping**
+
+```powershell
+$conn = $conn ?? (Get-YarboConnection) # your connection
+$detail = Test-YarboConnection -Connection $conn -Detailed
+$detail.Connected # true = MQTT still connected
+$detail.ControllerAcquired # false = we don't have controller (often means robot sleeping)
+$detail.HeartbeatAge # time since last heart_beat (e.g. 00:05:00 = 5 minutes)
+$conn.LastWorkingState # 0 = idle/sleep when we last heard
+# If HeartbeatAge is large and ControllerAcquired is false → robot probably sleeping
+Resume-Yarbo -Connection $conn # wake the robot, then retry Get-YarboStatus
+```
+
+### Summary
+
+| Robot state | MQTT connection | New DeviceMSG / status | What you can use |
+|-------------|-----------------|------------------------|------------------|
+| **Offline** (off or not on LAN) | No | No | Nothing; fix network/power. |
+| **Sleeping** (broker up, robot not responding) | Yes (usually) | No (timeouts) | Connection metadata, last `Robot` if any, `Test-YarboConnection -Detailed`, `Resume-Yarbo` to wake. |
+| **Active** | Yes | Yes | Full status and telemetry as in the rest of this doc. |
+
+---
+
+## Debug logging and support bundles
+
+To see exactly what is sent and received over MQTT (e.g. for unsupported firmware or heads):
+
+- Use **`-Debug`** on any MQTT cmdlet, or set **`$env:PSYARBO_DEBUG = "1"`** for the whole session. Set **`$env:PSYARBO_DEBUG_RAW = "1"`** for raw (base64) payloads instead of JSON.
+- Use **`Export-YarboSupportBundle -Path ./bundle.json -RecordingPath ./recording.json`** (and/or **`-Connection $conn`**) to produce a redacted JSON file you can attach to an issue or upload to GlitchTip. See README section *Debug logging and support bundles*.
diff --git a/docs/index.md b/docs/index.md
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,6 +5,8 @@
## Quick Links
- 📖 [Full Documentation (README)](https://github.com/markus-lassfolk/PSYarbo/blob/main/README.md)
+- 📋 [Commands and Properties](Commands-and-Properties.md) — how to get status, telemetry, battery, RTK, LEDs, system info
+- 🔧 **In-product help** — After installing: `Get-Help about_PSYarbo`, `Get-Help about_PSYarbo_MQTT`, `Get-Help about_PSYarbo_Debug` (debug logging and support bundles)
- 🐛 [Report a Bug](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=bug_report.yml)
- 💡 [Request a Feature](https://github.com/markus-lassfolk/PSYarbo/issues/new?template=feature_request.yml)
- 💬 [Discussions](https://github.com/markus-lassfolk/PSYarbo/discussions)
diff --git a/src/PSYarbo/Classes/YarboCloudSession.ps1 b/src/PSYarbo/Classes/YarboCloudSession.ps1
--- a/src/PSYarbo/Classes/YarboCloudSession.ps1
+++ b/src/PSYarbo/Classes/YarboCloudSession.ps1
@@ -131,6 +131,9 @@
if ($result.data.refreshToken) {
$this.RefreshToken = ConvertTo-SecureString -String $result.data.refreshToken -AsPlainText -Force
}
+ if ($result.data.snList) {
+ $this.BoundSerialNumbers = @($result.data.snList)
+ }
} else {
$codeStr = if ($null -ne $result.code) { $result.code.ToString() } else { 'UNKNOWN' }
throw [YarboCloudAuthException]::new("Token refresh failed: $($result.message)", $codeStr)
diff --git a/src/PSYarbo/Classes/YarboConnection.ps1 b/src/PSYarbo/Classes/YarboConnection.ps1
--- a/src/PSYarbo/Classes/YarboConnection.ps1
+++ b/src/PSYarbo/Classes/YarboConnection.ps1
@@ -88,7 +88,20 @@
# Implement IDisposable — dispose all owned IDisposable members.
# Disconnect-Yarbo calls this indirectly; classes holding a YarboConnection can also call it directly.
+ # Resolve adapter type at runtime so the class can load when ScriptsToProcess runs before the .psm1 defines it.
[void] Dispose() {
+ try {
+ if ($null -ne $this.MqttClient) {
+ $adapterType = [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object { $_.GetType('PSYarbo.Mqtt.MessageReceivedAdapter') } | Where-Object { $_ } | Select-Object -First 1
+ if ($null -ne $adapterType) {
+ $handlerDelegate = $adapterType.GetMethod('GetHandler').Invoke($null, @($this.MqttClient))
+ if ($null -ne $handlerDelegate) {
+ $this.MqttClient.GetType().GetMethod('remove_ApplicationMessageReceivedAsync').Invoke($this.MqttClient, @($handlerDelegate)) | Out-Null
+ }
+ $adapterType.GetMethod('UnregisterCallback').Invoke($null, @($this.MqttClient))
+ }
+ }
+ } catch { $null = $_ }
try { if ($null -ne $this.CancellationSource) { $this.CancellationSource.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.CommandSemaphore) { $this.CommandSemaphore.Dispose() } } catch { $null = $_ }
try { if ($null -ne $this.ResponseSignal) { $this.ResponseSignal.Dispose() } } catch { $null = $_ }
diff --git a/src/PSYarbo/Classes/YarboEndpoint.ps1 b/src/PSYarbo/Classes/YarboEndpoint.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboEndpoint.ps1
@@ -1,0 +1,15 @@
+class YarboEndpoint {
+ [string]$IPAddress
+ [int]$Port
+ [string]$Path
+ [string]$MacAddress
+ [bool]$Recommended
+ [string]$Hostname
+ [string]$SerialNumber
+ [string]$Status
+
+ [string] ToString() {
+ $rec = if ($this.Recommended) { ' (recommended)' } else { '' }
+ return "$($this.IPAddress):$($this.Port) $($this.Path)$rec"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboGlobalParams.ps1 b/src/PSYarbo/Classes/YarboGlobalParams.ps1
new file mode 100644
--- /dev/null
+++ b/src/PSYarbo/Classes/YarboGlobalParams.ps1
@@ -1,0 +1,20 @@
+<#
+.SYNOPSIS
+ Global robot parameters from read_global_params MQTT response.
+ All fields from the data_feedback payload (MQTT reversing).
+ RawData holds the full decoded payload for any extra keys the device sends.
+#>
+class YarboGlobalParams {
+ [double]$MowSpeed
+ [int]$CuttingHeight
+ [int]$RainDelay
+ [int]$ObstacleSensitivity
+ [double]$Timestamp
+
+ # Full MQTT payload so no keys are lost (e.g. future device fields)
+ [PSCustomObject]$RawData
+
+ [string] ToString() {
+ return "MowSpeed=$($this.MowSpeed) CuttingHeight=$($this.CuttingHeight) RainDelay=$($this.RainDelay) ObstacleSensitivity=$($this.ObstacleSensitivity)"
+ }
+}
diff --git a/src/PSYarbo/Classes/YarboLightState.ps1 b/src/PSYarbo/Classes/YarboLightState.ps1
--- a/src/PSYarbo/Classes/YarboLightState.ps1
+++ b/src/PSYarbo/Classes/YarboLightState.ps1
@@ -31,6 +31,14 @@
return [YarboLightState]::new()
}
+ # Body accent lights only (red channels; matches python-yarbo lights_body())
+ static [YarboLightState] BodyOn() {
+ $s = [YarboLightState]::new()
+ $s.BodyLeftRed = 255
+ $s.BodyRightRed = 255
+ return $s
+ }
+
[string] ToString() {
return "Head:$($this.HeadLight) L:$($this.LeftWhite) R:$($this.RightWhite) BL:$($this.BodyLeftRed) BR:$($this.BodyRightRed) TL:$($this.TailLeftRed) TR:$($this.TailRightRed)"
}
diff --git a/src/PSYarbo/Classes/YarboRobot.ps1 b/src/PSYarbo/Classes/YarboRobot.ps1
--- a/src/PSYarbo/Classes/YarboRobot.ps1
+++ b/src/PSYarbo/Classes/YarboRobot.ps1
@@ -5,11 +5,18 @@
[string]$Name
[int]$HeadType
- # Battery
+ # Battery (BatteryMSG)
[int]$BatteryCapacity
[int]$BatteryStatus
+ [bool]$BatteryTempError
+ [double]$BatteryTimestamp
+ [double]$BatteryCurrent
+ [double]$BatteryVoltage
- # State
+ # Body (BodyMsg)
+ [int]$RechargeState
+
+ # State (StateMSG)
[int]$WorkingState
[int]$ChargingStatus
[int]$ErrorCode
@@ -18,31 +25,99 @@
[bool]$IsRecharging
[bool]$CarController
[int]$MachineController
+ [int]$AdjustAngleStatus
+ [int]$AutoDrawWaitingState
+ [int]$EnStateLed
+ [int]$EnWarnLed
+ [bool]$OnGoingToStartPoint
+ [bool]$OnMulPoints
+ [int]$RobotFollowState
+ [int]$ScheduleCancel
+ [int]$VisionAutoDrawState
... diff truncated: showing 800 of 6642 lines| } | ||
| } | ||
| if ($stored.Password) { $Password = $stored.Password } | ||
| } |
There was a problem hiding this comment.
Misleading indentation hides control flow in credential logic
Medium Severity
The if ($stored.RefreshToken) block on line 67 is indented at the same level as the opening brace of if ($stored) on line 66, and the password fallback on line 100 has extra indentation. While the brace structure is technically correct, this makes the nested control flow extremely confusing — it looks like if ($stored.RefreshToken) is a sibling rather than a child of if ($stored). This is a maintenance hazard that could easily lead to logic bugs when future contributors modify this block.
Corrected indentation in Connect-YarboCloud.ps1 lines 66-101 to properly reflect the nested control flow structure. The if (.RefreshToken) block is now properly indented as a child of if (), and the if (.Password) fallback is aligned at the correct nesting level.
| if ($IsWindows) { | ||
| $null = Test-Connection -ComputerName $IPAddress -Count 1 -Quiet -ErrorAction SilentlyContinue | ||
| } else { | ||
| $null = ping -c 1 -W 1 $IPAddress 2>$null |
There was a problem hiding this comment.
macOS ping timeout uses wrong units for ARP priming
Low Severity
Invoke-ArpPrime uses ping -c 1 -W 1 for both Linux and macOS, but the -W flag takes seconds on Linux and milliseconds on macOS. On macOS, -W 1 gives a 1ms timeout, making ARP priming almost always fail. This causes Get-MacForIp to return $null for macOS users, so Find-YarboDevice classifies all endpoints as "Rover" regardless of actual path, potentially giving incorrect DC-vs-Rover recommendations.


Summary
This PR consolidates discovery improvements, display consistency, full MQTT-to-object mapping, and integration tests for review.
Discovery & connection
-Subnet).Display consistency
Format-Table,Format-List, andSelect-Object *show the same concepts.Full MQTT → PowerShell objects
RawMessage/GnggaRawwhere applicable.Get-YarboGlobalParamsreturns[YarboGlobalParams].Tests & tooling
tests/Integration/FindConnectGetYarbo.Integration.Tests.ps1).Checklist
Invoke-Pester ./tests/Integration/FindConnectGetYarbo.Integration.Tests.ps1 -Tag Integration)Made with Cursor
Note
Medium Risk
Moderate risk: changes core MQTT module loading/connection and network discovery paths, plus expands cloud credential handling; regressions could break connect/discovery or message routing. Adds multiple new robot-control cmdlets (some destructive) but generally gated behind
SupportsShouldProcess/ConfirmImpact.Overview
Discovery and MQTT reliability are expanded.
Find-Yarbonow auto-scans local interface subnets (or-Subnet), verifies brokers via a new C#YarboMqttListener, and deduplicates results by serial number; a newFind-YarboDevicecommand discovers multiple endpoints per robot, resolves MACs, labels Rover vs DC, and outputs a recommended failover order.Status/telemetry data and formatting are broadened.
YarboRobot/YarboTelemetrygain many additional DeviceMSG-backed fields plusRawMessage, parsing is refactored via a sharedSet-YarboCommonFields, and type/format data is updated to use consistent display script properties (e.g.Battery,State, display-friendly heading/time).Get-YarboGlobalParamsnow returns a typedYarboGlobalParams(withRawData), andSet-YarboGlobalParamsaccepts either a hashtable or that object.New tooling, commands, and pipeline updates. Adds env-driven MQTT debug logging (
PSYARBO_DEBUG,PSYARBO_DEBUG_RAW), MQTT sniff/record support (Invoke-YarboMqttSniff), and exports additional utility/control cmdlets (camera, maps, firmware update, mower/head settings, etc.). CI is updated to install dependencies and split Pester into unit/integration/root runs, and docs/README are expanded with a new “Commands and Properties” guide and improved examples.Written by Cursor Bugbot for commit 13b1673. This will update automatically on new commits. Configure here.