From e029ef48fc1bf76d0f47c9ff460f4628b8d71a8b Mon Sep 17 00:00:00 2001
From: Geoff Seemueller <28698553+geoffsee@users.noreply.github.com>
Date: Sun, 20 Jul 2025 15:51:33 -0400
Subject: [PATCH] Gpyes integration (#11)
* Introduce core modules: device management, bus communication, and discovery protocol. Adds system device interface, virtual hardware bus, and device discovery logic. Includes tests for all components.
* improve map
- Fix typos in variable and function names (`vessle` to `vessel`).
- Add `update_vessel_data_with_gps` function to enable GPS integration for vessel data updates.
- Integrate real GPS data into vessel systems and UI components (speed, heading, etc.).
- Initialize speed gauge display at 0 kts.
- Include `useEffect` in `MapNext` to log and potentially handle `vesselPosition` changes.
**Add compass heading update system using GPS heading data.**
- Remove `UserLocationMarker` component and related code from `MapNext.tsx`
- Simplify logic for layer selection and navigation within `App.tsx`
- Replace map style 'Bathymetry' with 'OSM' in layer options
improve map
* update image
---------
Co-authored-by: geoffsee <>
---
.aiignore | 14 -
.gitignore | 1 +
Cargo.lock | 23 +
README.md | 2 +-
crates/base-map/map/src/App.tsx | 368 ++++++++++-
crates/base-map/map/src/MapNext.tsx | 35 +-
.../base-map/map/src/user-location-marker.tsx | 77 +++
crates/components/src/instrument_cluster.rs | 2 +-
crates/components/src/vessel_data.rs | 25 +-
crates/datalink-provider/src/gpyes/mod.rs | 564 ++++++++++++++++
crates/hardware/Cargo.toml | 16 +
crates/hardware/README.md | 366 +++++++++++
crates/hardware/src/bus.rs | 338 ++++++++++
crates/hardware/src/device.rs | 430 +++++++++++++
crates/hardware/src/discovery_protocol.rs | 561 ++++++++++++++++
crates/hardware/src/error.rs | 72 +++
crates/hardware/src/gps_device.rs | 534 ++++++++++++++++
crates/hardware/src/lib.rs | 27 +
crates/systems/src/lib.rs | 2 +-
crates/systems/src/vessel/vessel_systems.rs | 4 +-
crates/yachtpit/Cargo.toml | 5 +-
crates/yachtpit/src/lib.rs | 46 +-
crates/yachtpit/src/services/debug_service.rs | 366 +++++++++++
crates/yachtpit/src/services/gps_service.rs | 280 ++++----
.../yachtpit/src/services/gpyes_provider.rs | 603 ++++++++++++++++++
crates/yachtpit/src/services/mod.rs | 3 +
yachtpit.png => yachtpit-og.png | Bin
yachtpit-x.png | Bin 0 -> 1291184 bytes
28 files changed, 4557 insertions(+), 207 deletions(-)
delete mode 100644 .aiignore
create mode 100644 crates/base-map/map/src/user-location-marker.tsx
create mode 100644 crates/datalink-provider/src/gpyes/mod.rs
create mode 100644 crates/hardware/Cargo.toml
create mode 100644 crates/hardware/README.md
create mode 100644 crates/hardware/src/bus.rs
create mode 100644 crates/hardware/src/device.rs
create mode 100644 crates/hardware/src/discovery_protocol.rs
create mode 100644 crates/hardware/src/error.rs
create mode 100644 crates/hardware/src/gps_device.rs
create mode 100644 crates/hardware/src/lib.rs
create mode 100644 crates/yachtpit/src/services/debug_service.rs
create mode 100644 crates/yachtpit/src/services/gpyes_provider.rs
rename yachtpit.png => yachtpit-og.png (100%)
create mode 100644 yachtpit-x.png
diff --git a/.aiignore b/.aiignore
deleted file mode 100644
index de7901f..0000000
--- a/.aiignore
+++ /dev/null
@@ -1,14 +0,0 @@
-# An .aiignore file follows the same syntax as a .gitignore file.
-# .gitignore documentation: https://git-scm.com/docs/gitignore
-# Junie will ask for explicit approval before view or edit the file or file within a directory listed in .aiignore.
-# Only files contents is protected, Junie is still allowed to view file names even if they are listed in .aiignore.
-# Be aware that the files you included in .aiignore can still be accessed by Junie in two cases:
-# - If Brave Mode is turned on.
-# - If a command has been added to the Allowlist — Junie will not ask for confirmation, even if it accesses - files and folders listed in .aiignore.
-target
-**/**/dist
-.idea
-.github
-Cargo.lock
-LICENSE
-yachtpit.png
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3dbe847..02ce09e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
/crates/yachtpit/assets/ui/assets/
/crates/yachtpit/assets/ui/packages/base-map/dist/
+/crates/base-map/map/src/map-upgrade/
diff --git a/Cargo.lock b/Cargo.lock
index 830c279..7b77d43 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4274,6 +4274,26 @@ dependencies = [
"redox_syscall 0.5.13",
]
+[[package]]
+name = "libudev"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
+dependencies = [
+ "libc",
+ "libudev-sys",
+]
+
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -6573,6 +6593,7 @@ dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"io-kit-sys",
+ "libudev",
"mach2",
"nix 0.26.4",
"scopeguard",
@@ -9088,6 +9109,7 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
name = "yachtpit"
version = "0.1.0"
dependencies = [
+ "base-map",
"bevy",
"bevy_asset_loader",
"bevy_flurx",
@@ -9102,6 +9124,7 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_json",
+ "serialport",
"systems",
"tokio",
"wasm-bindgen",
diff --git a/README.md b/README.md
index f19612e..5254b9a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
> Warning: Experimental, incomplete, and unfunded.
-
+
diff --git a/crates/base-map/map/src/App.tsx b/crates/base-map/map/src/App.tsx
index c0887f8..6ef2801 100644
--- a/crates/base-map/map/src/App.tsx
+++ b/crates/base-map/map/src/App.tsx
@@ -1,16 +1,17 @@
-// import Map from 'react-map-gl/mapbox';
-// import {Source, Layer} from 'react-map-gl/maplibre';
import 'mapbox-gl/dist/mapbox-gl.css';
import {Box, Button, HStack, Input} from '@chakra-ui/react';
import {useCallback, useEffect, useState} from "react";
-import MapNext from "@/MapNext.tsx";
-// import type {FeatureCollection} from 'geojson';
-// import type {CircleLayerSpecification} from "mapbox-gl";
+import MapNext, {type Geolocation} from "@/MapNext.tsx";
// public key
const key =
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
+const layers = [
+ { name: 'OSM', value: 'mapbox://styles/mapbox/dark-v11' },
+ { name: 'Satellite', value: 'mapbox://styles/mapbox/satellite-v9' },
+];
+
// const vesselLayerStyle: CircleLayerSpecification = {
@@ -39,6 +40,31 @@ interface VesselStatus {
speed: number;
}
+export type Layer = { name: string; value: string };
+export type Layers = Layer[];
+
+class MyGeolocation implements Geolocation {
+ constructor({clearWatch, getCurrentPosition, watchPosition}: {
+ clearWatch: (watchId: number) => void;
+ getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => void;
+ watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => number;
+ }) {
+ this.clearWatch = clearWatch;
+ this.watchPosition = watchPosition;
+ this.getCurrentPosition = getCurrentPosition;
+ }
+ clearWatch(_watchId: number): void {
+ throw new Error('Method not implemented.');
+ }
+ getCurrentPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): void {
+ throw new Error('Method not implemented.');
+ }
+ watchPosition(_successCallback: PositionCallback, _errorCallback?: PositionErrorCallback | null, _options?: PositionOptions): number {
+ throw new Error('Method not implemented.');
+ }
+
+}
+
// interface MapViewParams {
// latitude: number;
// longitude: number;
@@ -50,9 +76,58 @@ interface VesselStatus {
// token: string | null;
// }
+function LayerSelector(props: { onClick: (e: any) => Promise }) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+
+ {isOpen && (
+
+ {layers.map(layer => (
+ {
+ setIsOpen(false);
+ await props.onClick(e);
+ }}
+ >
+ {layer.name}
+
+ ))}
+
+ )}
+
+ );
+}
+
function App() {
const [isSearchOpen, setIsSearchOpen] = useState(false);
+ const [selectedLayer, setSelectedLayer] = useState(layers[0]);
+ const [searchInput, setSearchInput] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+ const [mapView, setMapView] = useState({
+ longitude: -122.4,
+ latitude: 37.8,
+ zoom: 14
+ });
// Map state that can be updated from Rust
// const [mapView, setMapView] = useState({
@@ -62,7 +137,7 @@ function App() {
// });
// Vessel position state
- // const [vesselPosition, setVesselPosition] = useState(null);
+ const [vesselPosition, setVesselPosition] = useState(null);
// Create vessel geojson data
// const vesselGeojson: FeatureCollection = {
@@ -85,20 +160,51 @@ function App() {
// Button click handlers
- const handleNavigationClick = useCallback(async () => {
- if (typeof window !== 'undefined' && (window as any).__FLURX__) {
- try {
- await (window as any).__FLURX__.invoke("navigation_clicked");
- console.log('Navigation clicked');
- } catch (error) {
- console.error('Failed to invoke navigation_clicked:', error);
- }
- }
+ // const handleNavigationClick = useCallback(async () => {
+ // if (typeof window !== 'undefined' && (window as any).__FLURX__) {
+ // try {
+ // await (window as any).__FLURX__.invoke("navigation_clicked");
+ // console.log('Navigation clicked');
+ // } catch (error) {
+ // console.error('Failed to invoke navigation_clicked:', error);
+ // }
+ // }
+ // }, []);
+
+
+ const selectSearchResult = useCallback(async (searchResult: { lat: string, lon: string }) => {
+ // Navigate to the selected location with zoom
+ console.log(`Navigating to: ${searchResult.lat}, ${searchResult.lon}`);
+ setMapView({
+ longitude: parseFloat(searchResult.lon),
+ latitude: parseFloat(searchResult.lat),
+ zoom: 15
+ });
}, []);
-
const handleSearchClick = useCallback(async () => {
- setIsSearchOpen(!isSearchOpen);
+ if (isSearchOpen && searchInput.length > 1) {
+ try {
+ console.log(`Trying to geocode: ${searchInput}`);
+ const geocode = await fetch('https://geocode.geoffsee.com', {
+ method: 'POST',
+ mode: 'cors',
+ body: JSON.stringify({
+ location: searchInput,
+ }),
+ });
+ const coordinates = await geocode.json();
+ const { lat, lon } = coordinates;
+ console.log(`Got geocode coordinates: ${lat}, ${lon}`);
+ setSearchResults([{ lat, lon }]);
+ } catch (e) {
+ console.error('Geocoding failed:', e);
+ // Continue without results
+ }
+ } else {
+ setIsSearchOpen(!isSearchOpen);
+ }
+
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
try {
await (window as any).__FLURX__.invoke("search_clicked");
@@ -107,6 +213,14 @@ function App() {
console.error('Failed to invoke search_clicked:', error);
}
}
+ }, [isSearchOpen, searchInput]);
+
+ const handleLayerChange = useCallback(async (e: any) => {
+ const newLayer = layers.find(layer => layer.value === e.target.id);
+ if (newLayer) {
+ setSelectedLayer(newLayer);
+ console.log('Layer changed to:', newLayer.name);
+ }
}, []);
// const handleMapViewChange = useCallback(async (evt: any) => {
@@ -135,7 +249,7 @@ function App() {
try {
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
console.log('Vessel status:', vesselStatus);
- // setVesselPosition(vesselStatus);
+ setVesselPosition(vesselStatus);
} catch (error) {
console.error('Failed to get vessel status:', error);
}
@@ -175,6 +289,28 @@ function App() {
return (
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
+ {/* GPS Feed Display — absolutely positioned at bottom-left */}
+ {vesselPosition && (
+
+ GPS Feed
+ Lat: {vesselPosition.latitude.toFixed(6)}°
+ Lon: {vesselPosition.longitude.toFixed(6)}°
+ Heading: {vesselPosition.heading.toFixed(1)}°
+ Speed: {vesselPosition.speed.toFixed(1)} kts
+
+ )}
{/* Button bar — absolutely positioned inside the wrapper */}
setSearchInput(e.target.value)}
+ color="white"
+ bg="rgba(0, 0, 0, 0.8)"
+ border="none"
+ borderRadius="0"
+ _focus={{
+ outline: 'none',
+ }}
_placeholder={{
color: "#d1cfcf"
}}
/>
+ {searchResults.length > 0 && (
+
+ {searchResults.map((result, index) => (
+ {
+ console.log(`Selecting result ${result.lat}, ${result.lon}`);
+ await selectSearchResult(result);
+ setSearchResults([]);
+ setIsSearchOpen(false);
+ }}
+ >
+ {`${result.lat}, ${result.lon}`}
+
+ ))}
+
+ )}
}
-
+
-
+ {
+ if (typeof window !== 'undefined' && (window as any).geolocationWatches) {
+ const interval = (window as any).geolocationWatches.get(watchId);
+ if (interval) {
+ clearInterval(interval);
+ (window as any).geolocationWatches.delete(watchId);
+ }
+ }
+ },
+ watchPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions) => {
+ if (typeof window === 'undefined') return 0;
+
+ // Initialize watches map if it doesn't exist
+ if (!(window as any).geolocationWatches) {
+ (window as any).geolocationWatches = new Map();
+ }
+ if (!(window as any).geolocationWatchId) {
+ (window as any).geolocationWatchId = 0;
+ }
+
+ const watchId = ++(window as any).geolocationWatchId;
+
+ const pollPosition = async () => {
+ if ((window as any).__FLURX__) {
+ try {
+ const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
+ const position: GeolocationPosition = {
+ coords: {
+ latitude: vesselStatus.latitude,
+ longitude: vesselStatus.longitude,
+ altitude: null,
+ accuracy: 10, // Assume 10m accuracy
+ altitudeAccuracy: null,
+ heading: vesselStatus.heading,
+ speed: vesselStatus.speed,
+ toJSON: () => ({
+ latitude: vesselStatus.latitude,
+ longitude: vesselStatus.longitude,
+ altitude: null,
+ accuracy: 10,
+ altitudeAccuracy: null,
+ heading: vesselStatus.heading,
+ speed: vesselStatus.speed
+ })
+ },
+ timestamp: Date.now(),
+ toJSON: () => ({
+ coords: {
+ latitude: vesselStatus.latitude,
+ longitude: vesselStatus.longitude,
+ altitude: null,
+ accuracy: 10,
+ altitudeAccuracy: null,
+ heading: vesselStatus.heading,
+ speed: vesselStatus.speed
+ },
+ timestamp: Date.now()
+ })
+ };
+ successCallback(position);
+ } catch (error) {
+ if (errorCallback) {
+ const positionError: GeolocationPositionError = {
+ code: 2, // POSITION_UNAVAILABLE
+ message: 'Failed to get vessel status: ' + error,
+ PERMISSION_DENIED: 1,
+ POSITION_UNAVAILABLE: 2,
+ TIMEOUT: 3
+ };
+ errorCallback(positionError);
+ }
+ }
+ }
+ };
+
+ // Poll immediately and then at intervals
+ pollPosition();
+ const interval = setInterval(pollPosition, options?.timeout || 5000);
+ (window as any).geolocationWatches.set(watchId, interval);
+
+ return watchId;
+ },
+ getCurrentPosition: (successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, _options?: PositionOptions) => {
+ if (typeof window !== 'undefined' && (window as any).__FLURX__) {
+ (async () => {
+ try {
+ const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
+ const position: GeolocationPosition = {
+ coords: {
+ latitude: vesselStatus.latitude,
+ longitude: vesselStatus.longitude,
+ altitude: null,
+ accuracy: 10, // Assume 10m accuracy
+ altitudeAccuracy: null,
+ heading: vesselStatus.heading,
+ speed: vesselStatus.speed,
+ toJSON: () => ({
+ latitude: vesselStatus.latitude,
+ longitude: vesselStatus.longitude,
+ altitude: null,
+ accuracy: 10,
+ altitudeAccuracy: null,
+ heading: vesselStatus.heading,
+ speed: vesselStatus.speed
+ })
+ },
+ timestamp: Date.now(),
+ toJSON: () => ({
+ coords: {
+ latitude: vesselStatus.latitude,
+ longitude: vesselStatus.longitude,
+ altitude: null,
+ accuracy: 10,
+ altitudeAccuracy: null,
+ heading: vesselStatus.heading,
+ speed: vesselStatus.speed
+ },
+ timestamp: Date.now()
+ })
+ };
+ successCallback(position);
+ } catch (error) {
+ if (errorCallback) {
+ const positionError: GeolocationPositionError = {
+ code: 2, // POSITION_UNAVAILABLE
+ message: 'Failed to get vessel status: ' + error,
+ PERMISSION_DENIED: 1,
+ POSITION_UNAVAILABLE: 2,
+ TIMEOUT: 3
+ };
+ errorCallback(positionError);
+ }
+ }
+ })();
+ } else if (errorCallback) {
+ const positionError: GeolocationPositionError = {
+ code: 2, // POSITION_UNAVAILABLE
+ message: '__FLURX__ not available',
+ PERMISSION_DENIED: 1,
+ POSITION_UNAVAILABLE: 2,
+ TIMEOUT: 3
+ };
+ errorCallback(positionError);
+ }
+ },
+ })}/>
{/*