Skip to content

Conversation

@Williangalvani
Copy link
Member

@Williangalvani Williangalvani commented Aug 30, 2025

Helps manage multiple vehicles on the same network. requires that the vehicles are able to see each other

Screen.Recording.2025-08-30.at.18.28.53.mov

Summary by Sourcery

Enable automatic discovery of other vehicles on the local network via mDNS in the backend and add a front-end widget for listing and navigating to those vehicles

New Features:

  • Discover networked vehicles via mDNS in the beacon service and expose them through a /discovered_services API endpoint
  • Add a VehiclePicker Vue component to display and navigate to other detected vehicles
  • Integrate the VehiclePicker into the VehicleBanner UI with updated action buttons

Enhancements:

  • Manage Zeroconf listener lifecycle in the Beacon service to start and stop mDNS discovery

Summary by Sourcery

Enable automatic discovery of other vehicles on the network and provide a front-end widget for listing and navigating to those vehicles.

New Features:

  • Discover other BlueOS vehicles on the local network via mDNS in the backend
  • Expose a /discovered_services API endpoint to return detected services grouped by IP
  • Add a VehiclePicker Vue component to fetch, display, and navigate to discovered vehicles
  • Integrate the VehiclePicker into the VehicleBanner UI

Enhancements:

  • Manage the Zeroconf listener lifecycle in the Beacon service to start and stop mDNS discovery

@sourcery-ai
Copy link

sourcery-ai bot commented Aug 30, 2025

Reviewer's Guide

This PR adds automatic discovery of peer vehicles via mDNS in the backend and a new VehiclePicker widget in the frontend, integrating it into the VehicleBanner with lifecycle-managed Zeroconf listeners and a /discovered_services API endpoint.

Sequence diagram for vehicle discovery and listing in frontend

sequenceDiagram
actor User
participant VehiclePicker
participant Frontend
participant Backend
participant ZeroconfListener
participant OtherVehicle
User->>VehiclePicker: Clicks VehiclePicker button
VehiclePicker->>Frontend: Request /beacon/v1.0/discovered_services
Frontend->>Backend: GET /discovered_services
Backend->>ZeroconfListener: get_discovered_services()
ZeroconfListener->>Backend: Return discovered vehicles
Backend->>Frontend: Return discovered vehicles
Frontend->>VehiclePicker: Display list of vehicles
User->>VehiclePicker: Clicks on vehicle IP
VehiclePicker->>OtherVehicle: Open vehicle UI in new tab
Loading

Class diagram for BeaconServiceListener and Beacon service changes

classDiagram
    class BeaconServiceListener {
      +discovered_services: Dict[str, str]
      +add_service(zc, type_, name)
      +remove_service(zc, type_, name)
      +update_service(zc, type_, name)
      +_process_service(zc, type_, name)
      +get_discovered_services() Dict[str, str]
    }
    class Beacon {
      +zeroconf: Optional[Zeroconf]
      +service_listener: Optional[BeaconServiceListener]
      +service_browser: Optional[ServiceBrowser]
      +start_zeroconf_listener()
      +stop_zeroconf_listener()
      +get_discovered_services() Dict[str, str]
    }
    Beacon "1" -- "1" BeaconServiceListener : manages
    Beacon "1" -- "1" ServiceBrowser : manages
    BeaconServiceListener <|-- ServiceListener
Loading

Class diagram for VehiclePicker Vue component

classDiagram
    class VehiclePicker {
      +expanded: boolean
      +loading: boolean
      +error: string | null
      +availableVehicles: Vehicle[]
      +toggleDropdown()
      +fetchDiscoveredServices()
      +refreshServices()
      +navigateToVehicle(ip: string)
      +allMyIps: string[]
    }
    class Vehicle {
      +ips: string[]
      +hostname: string
      +imagePath: string
    }
    VehiclePicker "*" -- "*" Vehicle : displays
Loading

File-Level Changes

Change Details Files
Introduce mDNS-based vehicle discovery in the Beacon service
  • Added BeaconServiceListener class to process and track mDNS services
  • Managed Zeroconf listener lifecycle with start_zeroconf_listener and stop_zeroconf_listener
  • Exposed discovered services via a new /discovered_services API endpoint
core/services/beacon/main.py
Add VehiclePicker component and integrate into the VehicleBanner
  • Created VehiclePicker.vue to fetch, display, and navigate to discovered vehicles
  • Imported and embedded VehiclePicker in VehicleBanner.vue with updated action-buttons layout
  • Adjusted CSS for action-buttons-container visibility and positioning
core/frontend/src/components/app/VehicleBanner.vue
core/frontend/src/components/app/VehiclePicker.vue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Williangalvani Williangalvani marked this pull request as ready for review October 22, 2025 18:09
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • In fetchDiscoveredServices, consider replacing the manual completedRequests counter with Promise.all (or Promise.allSettled) over your per-IP requests so you can more cleanly handle the loading state once all calls finish.
  • BeaconServiceListener currently only pulls the first IPv4 address—think about extending it to support IPv6 addresses (and possibly multiple addresses per service) for more complete network discovery.
  • Right now discovered services only disappear when an explicit remove event fires; you might want to add a TTL or timestamp-based cleanup so stale entries don’t linger indefinitely if a service goes silent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In fetchDiscoveredServices, consider replacing the manual completedRequests counter with Promise.all (or Promise.allSettled) over your per-IP requests so you can more cleanly handle the loading state once all calls finish.
- BeaconServiceListener currently only pulls the first IPv4 address—think about extending it to support IPv6 addresses (and possibly multiple addresses per service) for more complete network discovery.
- Right now discovered services only disappear when an explicit remove event fires; you might want to add a TTL or timestamp-based cleanup so stale entries don’t linger indefinitely if a service goes silent.

## Individual Comments

### Comment 1
<location> `core/services/beacon/main.py:50-56` </location>
<code_context>
+        logger.info(f"Service {name} added")
+        self._process_service(zc, type_, name)
+
+    def _process_service(self, zc: Zeroconf, type_: str, name: str) -> None:
+        """Process a service and extract its IP address"""
+        try:
+            info = zc.get_service_info(type_, name)
+            if info and info.addresses:
+                # Convert the first IP address from bytes to string
+                ip_address = socket.inet_ntoa(info.addresses[0])
+                self.discovered_services[name] = ip_address
+                logger.info(f"Service {name} at {ip_address}")
</code_context>

<issue_to_address>
**issue:** IPv6 addresses are not handled in service discovery.

Currently, only IPv4 addresses are processed via socket.inet_ntoa, so services with IPv6 addresses are ignored. Please add IPv6 support for full compatibility.
</issue_to_address>

### Comment 2
<location> `core/frontend/src/components/app/VehiclePicker.vue:168-177` </location>
<code_context>
+        ips.forEach(async (ip) => {
</code_context>

<issue_to_address>
**issue (bug_risk):** Mixing async/await with forEach may cause race conditions.

forEach does not handle async functions as expected, which can result in unpredictable increments of completedRequests. Use Promise.all or a for...of loop with await to ensure proper sequencing and concurrency control.
</issue_to_address>

### Comment 3
<location> `core/frontend/src/components/app/VehiclePicker.vue:195` </location>
<code_context>
+            )
+
+            if (existingVehicleIndex >= 0) {
+              this.availableVehicles[existingVehicleIndex].ips.push(...newVehicle.ips)
+              if (newVehicle.imagePath && !this.availableVehicles[existingVehicleIndex].imagePath) {
+                this.availableVehicles[existingVehicleIndex].imagePath = newVehicle.imagePath
+              }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Duplicate IPs may be added to availableVehicles.

Currently, IPs are merged without checking for duplicates. Use a Set or filter out existing IPs before adding to prevent redundant entries.

```suggestion
              const existingIps = new Set(this.availableVehicles[existingVehicleIndex].ips)
              const uniqueNewIps = newVehicle.ips.filter(ip => !existingIps.has(ip))
              this.availableVehicles[existingVehicleIndex].ips.push(...uniqueNewIps)
```
</issue_to_address>

### Comment 4
<location> `core/services/beacon/main.py:293-303` </location>
<code_context>
    def stop_zeroconf_listener(self) -> None:
        """
        Stop the zeroconf listener and clean up resources
        """
        if self.zeroconf is not None:
            try:
                if self.service_browser is not None:
                    self.service_browser.cancel()
                    self.service_browser = None
                self.zeroconf.close()
                self.zeroconf = None
                self.service_listener = None
                logger.info("Zeroconf listener stopped")
            except Exception as e:
                logger.warning(f"Error stopping zeroconf listener: {e}")

</code_context>

<issue_to_address>
**suggestion (code-quality):** Add guard clause ([`last-if-guard`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/last-if-guard/))

```suggestion
        if self.zeroconf is None:
            return
        try:
            if self.service_browser is not None:
                self.service_browser.cancel()
                self.service_browser = None
            self.zeroconf.close()
            self.zeroconf = None
            self.service_listener = None
            logger.info("Zeroconf listener stopped")
        except Exception as e:
            logger.warning(f"Error stopping zeroconf listener: {e}")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +50 to +56
def _process_service(self, zc: Zeroconf, type_: str, name: str) -> None:
"""Process a service and extract its IP address"""
try:
info = zc.get_service_info(type_, name)
if info and info.addresses:
# Convert the first IP address from bytes to string
ip_address = socket.inet_ntoa(info.addresses[0])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: IPv6 addresses are not handled in service discovery.

Currently, only IPv4 addresses are processed via socket.inet_ntoa, so services with IPv6 addresses are ignored. Please add IPv6 support for full compatibility.

Comment on lines +293 to +303
if self.zeroconf is not None:
try:
if self.service_browser is not None:
self.service_browser.cancel()
self.service_browser = None
self.zeroconf.close()
self.zeroconf = None
self.service_listener = None
logger.info("Zeroconf listener stopped")
except Exception as e:
logger.warning(f"Error stopping zeroconf listener: {e}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Add guard clause (last-if-guard)

Suggested change
if self.zeroconf is not None:
try:
if self.service_browser is not None:
self.service_browser.cancel()
self.service_browser = None
self.zeroconf.close()
self.zeroconf = None
self.service_listener = None
logger.info("Zeroconf listener stopped")
except Exception as e:
logger.warning(f"Error stopping zeroconf listener: {e}")
if self.zeroconf is None:
return
try:
if self.service_browser is not None:
self.service_browser.cancel()
self.service_browser = None
self.zeroconf.close()
self.zeroconf = None
self.service_listener = None
logger.info("Zeroconf listener stopped")
except Exception as e:
logger.warning(f"Error stopping zeroconf listener: {e}")

Copy link
Member

@patrickelectric patrickelectric left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear how this discovery BlueOS on the network since we do not have an global identifier for BlueOS in mdns, besides the mavlink one. IIRC.

this.availableVehicles = []
try {
const response = await axios.get('/beacon/v1.0/discovered_services', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this here and not in the beacon store ?

@patrickelectric
Copy link
Member

It's not clear how this discovery BlueOS on the network since we do not have an global identifier for BlueOS in mdns, besides the mavlink one. IIRC.

@Williangalvani can you answer this comment ?

@Williangalvani
Copy link
Member Author

It's not clear how this discovery BlueOS on the network since we do not have an global identifier for BlueOS in mdns, besides the mavlink one. IIRC.

@Williangalvani can you answer this comment ?

I'm not done with this PR yet, I probably pushed it to build on CI (due to 429 isssues) and switched tasks.

@Williangalvani Williangalvani marked this pull request as draft October 30, 2025 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants