diff --git a/.github/workflows/containerize_fleet_analysis_backend.yaml b/.github/workflows/containerize_fleet_analysis_backend.yaml new file mode 100644 index 0000000..9c4ffa2 --- /dev/null +++ b/.github/workflows/containerize_fleet_analysis_backend.yaml @@ -0,0 +1,61 @@ +# /******************************************************************************** +# * Copyright (c) 2026 Contributors to the Eclipse Foundation +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * This program and the accompanying materials are made available under the +# * terms of the Apache License 2.0 which is available at +# * https://www.apache.org/licenses/LICENSE-2.0 +# * +# * SPDX-License-Identifier: Apache-2.0 +# ********************************************************************************/ +name: Containerize Fleet Analysis Backend and Push to Container Registry + +on: + push: + branches: + - main + paths: + - "components/backend-fleet-analysis-java/**" + - ".github/workflows/containerize_fleet_analysis_backend.yaml" + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }}/fleet-analysis-backend + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: components/backend-fleet-analysis-java + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/README.md b/README.md index ec114c4..57dcd3a 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ docker compose -f ./fms-blueprint-compose.yaml -f ./fms-blueprint-compose-zenoh. This will pull (or build if necessary) the container images and create and start all components. +The stack includes the Jakarta EE fleet analysis backend and exposes it on `http://127.0.0.1:8082/fleet-analysis/api`. + Once all services have been started, the current vehicle status can be viewed on a [Grafana dashboard](http://127.0.0.1:3000), using *admin*/*admin* as username and password for logging in. @@ -114,6 +116,8 @@ setup manually. Additional information can be found in the components' corresponding subfolders. +The fleet analysis backend component lives in `components/backend-fleet-analysis-java`. + # Contributing We are looking forward to your ideas and PRs. Each PRs triggers a GitHub action which checks the formating, performs diff --git a/components/backend-fleet-analysis-java/.dockerignore b/components/backend-fleet-analysis-java/.dockerignore new file mode 100644 index 0000000..612c5bc --- /dev/null +++ b/components/backend-fleet-analysis-java/.dockerignore @@ -0,0 +1,3 @@ +target +.idea +*.iml diff --git a/components/backend-fleet-analysis-java/Dockerfile b/components/backend-fleet-analysis-java/Dockerfile new file mode 100644 index 0000000..2cafa10 --- /dev/null +++ b/components/backend-fleet-analysis-java/Dockerfile @@ -0,0 +1,34 @@ +# /******************************************************************************** +# * Copyright (c) 2026 Contributors to the Eclipse Foundation +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * This program and the accompanying materials are made available under the +# * terms of the Apache License 2.0 which is available at +# * https://www.apache.org/licenses/LICENSE-2.0 +# * +# * SPDX-License-Identifier: Apache-2.0 +# ********************************************************************************/ + +FROM maven:3.9.6-eclipse-temurin-21 AS build + +WORKDIR /workspace +COPY pom.xml . +COPY src ./src +RUN mvn -q -DskipTests package + +FROM eclipse-temurin:21-jre + +ARG PAYARA_MICRO_VERSION=6.2024.2 +WORKDIR /opt/payara +ADD https://repo1.maven.org/maven2/fish/payara/extras/payara-micro/${PAYARA_MICRO_VERSION}/payara-micro-${PAYARA_MICRO_VERSION}.jar /opt/payara/payara-micro.jar + +COPY --from=build /workspace/target/*.war /opt/payara/fleet-analysis-backend.war + +ENV TMPDIR=/tmp + +RUN mkdir -p /tmp && chmod 1777 /tmp + +EXPOSE 8080 +ENTRYPOINT ["java","-Djava.io.tmpdir=/tmp","-jar","/opt/payara/payara-micro.jar","--deploy","/opt/payara/fleet-analysis-backend.war","--contextRoot","/fleet-analysis"] diff --git a/components/backend-fleet-analysis-java/README.md b/components/backend-fleet-analysis-java/README.md new file mode 100644 index 0000000..8c2cdd6 --- /dev/null +++ b/components/backend-fleet-analysis-java/README.md @@ -0,0 +1,157 @@ +# Fleet Analysis Backend (Jakarta EE 11) + +This service provides a small Jakarta EE 11 backend to analyze Fleet Management telemetry and +return summary statistics for dashboards or alerts. + +## Responsibilities + +- Accepts fleet telemetry snapshots as JSON. +- Returns computed summary metrics (fleet size, average speed, battery SOC range, braking count). +- Writes header/snapshot telemetry to InfluxDB when requested. +- Reads InfluxDB telemetry and writes fleet statistics periodically. + +## Build + +```bash +mvn package +``` + +## Run (example with Payara Micro) + +1. Download [Payara Micro 6](https://www.payara.fish/downloads/payara-platform-community-edition/). +2. Deploy the WAR: + +```bash +java -jar payara-micro.jar --deploy target/fleet-analysis-backend.war --contextRoot /fleet-analysis +``` + +The API will be available at `http://localhost:8080/fleet-analysis/api`. + +## Build Docker image (without Docker Compose) + +From the repository root: + +```bash +docker build -t fleet-analysis-backend:local components/backend-fleet-analysis-java +``` + +Run it manually (example): + +```bash +docker run --rm -p 8082:8080 --name fleet-analysis-backend \ + -e INFLUXDB_TOKEN_FILE=/tmp/fms-demo.token \ + -e INFLUXDB_STATS_INTERVAL_SECONDS=30 \ + -v /path/to/fms-demo.token:/tmp/fms-demo.token:ro \ + fleet-analysis-backend:local +``` + +## Accessing output + +Variant 1: Docker container logs + +```bash +docker logs -f fleet-analysis-backend +``` + +Variant 2: API + +```bash +curl -s http://localhost:8082/fleet-analysis/api/analysis/stats | jq +``` + +## API + +### `POST /api/analysis/summary` + +Request body (example): + +```json +[ + { + "vehicleId": "bike-001", + "speedKph": 42.3, + "batterySoc": 0.78, + "brakeActive": false, + "updatedAt": "2024-06-10T10:15:30Z" + }, + { + "vehicleId": "bike-002", + "speedKph": 12.4, + "batterySoc": 0.52, + "brakeActive": true, + "updatedAt": "2024-06-10T10:15:32Z" + } +] +``` + +Response: + +```json +{ + "vehicleCount": 2, + "averageSpeedKph": 27.35, + "minBatterySoc": 0.52, + "maxBatterySoc": 0.78, + "brakingVehicles": 1 +} +``` + +### `POST /api/telemetry/ingest` + +Writes header and/or snapshot measurements into InfluxDB. Configure the client using: + +- `INFLUXDB_URI` (default: `http://influxdb:8086`) +- `INFLUXDB_ORG` (default: `sdv`) +- `INFLUXDB_BUCKET` (default: `demo`) +- `INFLUXDB_TOKEN` or `INFLUXDB_TOKEN_FILE` + +Request body (example): + +```json +{ + "vin": "truck-001", + "trigger": "periodic", + "createdDateTime": 1737940602000, + "header": { + "hrTotalVehicleDistance": 12345.6, + "grossCombinationVehicleWeight": 18100.2, + "totalEngineHours": 82.5, + "engineTotalFuelUsed": 221.9, + "driver1Id": "driver-01", + "driver1IdCardIssuer": "fleet" + }, + "snapshot": { + "latitude": 37.7749, + "longitude": -122.4194, + "speed": 54.2, + "positionDateTime": 1737940602, + "wheelBasedSpeed": 53.7, + "fuelLevel1": 0.42, + "parkingBrakeSwitch": false + } +} +``` + +### Fleet stats (InfluxDB) + +The service periodically computes fleet statistics from the `header`/`snapshot` measurements and +writes them into a `fleet_stats` measurement. + +Default schedule: every 30 seconds (configure with `INFLUXDB_STATS_INTERVAL_SECONDS` as an env var +or Java system property). +Initial delay before the first stats run defaults to 10 seconds (configure with +`INFLUXDB_STATS_INITIAL_DELAY_SECONDS`). + +**Measurement: `fleet_stats`** + +Fields: + +- `vehicleCount` (unique `vin` across all time) +- `headerCount` (total number of header points) +- `snapshotCount` (total number of snapshot points) +- `totalCount` (header + snapshot points) +- `generatedAt` (ms since Unix epoch) + +### `GET /api/analysis/stats` + +Returns the latest stats snapshot (and triggers a refresh if none exist yet). diff --git a/components/backend-fleet-analysis-java/pom.xml b/components/backend-fleet-analysis-java/pom.xml new file mode 100644 index 0000000..5300cfa --- /dev/null +++ b/components/backend-fleet-analysis-java/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + org.eclipse.sdv + fleet-analysis-backend + 1.0.0-SNAPSHOT + war + + + 21 + 21 + UTF-8 + + + + + jakarta.platform + jakarta.jakartaee-web-api + 10.0.0 + provided + + + com.influxdb + influxdb-client-java + 7.0.0 + + + diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/AnalysisApplication.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/AnalysisApplication.java new file mode 100644 index 0000000..cf44516 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/AnalysisApplication.java @@ -0,0 +1,21 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +@ApplicationPath("/api") +public class AnalysisApplication extends Application { +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisResource.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisResource.java new file mode 100644 index 0000000..638a2d0 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisResource.java @@ -0,0 +1,53 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import java.util.List; + +@Path("/analysis") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class FleetAnalysisResource { + + @POST + @Path("/summary") + public FleetAnalysisSummary summarize(List telemetry) { + FleetAnalysisSummary summary = new FleetAnalysisSummary(); + if (telemetry == null || telemetry.isEmpty()) { + summary.setVehicleCount(0); + summary.setAverageSpeedKph(0.0); + summary.setMinBatterySoc(0.0); + summary.setMaxBatterySoc(0.0); + summary.setBrakingVehicles(0); + return summary; + } + + summary.setVehicleCount(telemetry.size()); + summary.setAverageSpeedKph( + telemetry.stream().mapToDouble(FleetTelemetry::getSpeedKph).average().orElse(0.0)); + summary.setMinBatterySoc( + telemetry.stream().mapToDouble(FleetTelemetry::getBatterySoc).min().orElse(0.0)); + summary.setMaxBatterySoc( + telemetry.stream().mapToDouble(FleetTelemetry::getBatterySoc).max().orElse(0.0)); + summary.setBrakingVehicles(telemetry.stream().filter(FleetTelemetry::isBrakeActive).count()); + + return summary; + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisSummary.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisSummary.java new file mode 100644 index 0000000..2665010 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetAnalysisSummary.java @@ -0,0 +1,62 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +public class FleetAnalysisSummary { + private int vehicleCount; + private double averageSpeedKph; + private double minBatterySoc; + private double maxBatterySoc; + private long brakingVehicles; + + public int getVehicleCount() { + return vehicleCount; + } + + public void setVehicleCount(int vehicleCount) { + this.vehicleCount = vehicleCount; + } + + public double getAverageSpeedKph() { + return averageSpeedKph; + } + + public void setAverageSpeedKph(double averageSpeedKph) { + this.averageSpeedKph = averageSpeedKph; + } + + public double getMinBatterySoc() { + return minBatterySoc; + } + + public void setMinBatterySoc(double minBatterySoc) { + this.minBatterySoc = minBatterySoc; + } + + public double getMaxBatterySoc() { + return maxBatterySoc; + } + + public void setMaxBatterySoc(double maxBatterySoc) { + this.maxBatterySoc = maxBatterySoc; + } + + public long getBrakingVehicles() { + return brakingVehicles; + } + + public void setBrakingVehicles(long brakingVehicles) { + this.brakingVehicles = brakingVehicles; + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsResource.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsResource.java new file mode 100644 index 0000000..49801a5 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsResource.java @@ -0,0 +1,35 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/analysis") +@Produces(MediaType.APPLICATION_JSON) +public class FleetStatsResource { + + @Inject + private InfluxStatsService statsService; + + @GET + @Path("/stats") + public FleetStatsSummary getStats() { + FleetStatsSummary stats = statsService.getLatestStats(true); + return stats == null ? new FleetStatsSummary() : stats; + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsSummary.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsSummary.java new file mode 100644 index 0000000..c620b6f --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetStatsSummary.java @@ -0,0 +1,62 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +public class FleetStatsSummary { + private long vehicleCount; + private long headerCount; + private long snapshotCount; + private long totalCount; + private long generatedAt; + + public long getVehicleCount() { + return vehicleCount; + } + + public void setVehicleCount(long vehicleCount) { + this.vehicleCount = vehicleCount; + } + + public long getHeaderCount() { + return headerCount; + } + + public void setHeaderCount(long headerCount) { + this.headerCount = headerCount; + } + + public long getSnapshotCount() { + return snapshotCount; + } + + public void setSnapshotCount(long snapshotCount) { + this.snapshotCount = snapshotCount; + } + + public long getTotalCount() { + return totalCount; + } + + public void setTotalCount(long totalCount) { + this.totalCount = totalCount; + } + + public long getGeneratedAt() { + return generatedAt; + } + + public void setGeneratedAt(long generatedAt) { + this.generatedAt = generatedAt; + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetTelemetry.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetTelemetry.java new file mode 100644 index 0000000..3e31101 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/FleetTelemetry.java @@ -0,0 +1,64 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import java.time.Instant; + +public class FleetTelemetry { + private String vehicleId; + private double speedKph; + private double batterySoc; + private boolean brakeActive; + private Instant updatedAt; + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + public double getSpeedKph() { + return speedKph; + } + + public void setSpeedKph(double speedKph) { + this.speedKph = speedKph; + } + + public double getBatterySoc() { + return batterySoc; + } + + public void setBatterySoc(double batterySoc) { + this.batterySoc = batterySoc; + } + + public boolean isBrakeActive() { + return brakeActive; + } + + public void setBrakeActive(boolean brakeActive) { + this.brakeActive = brakeActive; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbConfig.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbConfig.java new file mode 100644 index 0000000..fc6e444 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbConfig.java @@ -0,0 +1,95 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class InfluxDbConfig { + private static final String DEFAULT_URI = "http://influxdb:8086"; + private static final String DEFAULT_ORG = "sdv"; + private static final String DEFAULT_BUCKET = "demo"; + + private final String uri; + private final String org; + private final String bucket; + private final String token; + + private InfluxDbConfig(String uri, String org, String bucket, String token) { + this.uri = uri; + this.org = org; + this.bucket = bucket; + this.token = token; + } + + public static InfluxDbConfig fromEnv() { + String uri = envOrDefault("INFLUXDB_URI", DEFAULT_URI); + String org = envOrDefault("INFLUXDB_ORG", DEFAULT_ORG); + String bucket = envOrDefault("INFLUXDB_BUCKET", DEFAULT_BUCKET); + String token = env("INFLUXDB_TOKEN"); + + if (isBlank(token)) { + String tokenFile = env("INFLUXDB_TOKEN_FILE"); + if (!isBlank(tokenFile)) { + token = readTokenFile(tokenFile); + } + } + + if (isBlank(token)) { + throw new IllegalStateException( + "Missing InfluxDB token. Set INFLUXDB_TOKEN or INFLUXDB_TOKEN_FILE."); + } + + return new InfluxDbConfig(uri, org, bucket, token); + } + + public String getUri() { + return uri; + } + + public String getOrg() { + return org; + } + + public String getBucket() { + return bucket; + } + + public String getToken() { + return token; + } + + private static String env(String key) { + return System.getenv(key); + } + + private static String envOrDefault(String key, String fallback) { + String value = env(key); + return isBlank(value) ? fallback : value; + } + + private static boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + private static String readTokenFile(String tokenFile) { + try { + return Files.readString(Path.of(tokenFile), StandardCharsets.UTF_8).trim(); + } catch (IOException ex) { + throw new IllegalStateException("Failed to read InfluxDB token file: " + tokenFile, ex); + } + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbWriter.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbWriter.java new file mode 100644 index 0000000..cc39a27 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxDbWriter.java @@ -0,0 +1,136 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.InfluxDBClientFactory; +import com.influxdb.client.WriteApi; +import com.influxdb.client.domain.WritePrecision; +import com.influxdb.client.write.Point; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class InfluxDbWriter { + private InfluxDBClient client; + private WriteApi writeApi; + + @PostConstruct + void init() { + InfluxDbConfig config = InfluxDbConfig.fromEnv(); + client = + InfluxDBClientFactory.create( + config.getUri(), config.getToken().toCharArray(), config.getOrg(), config.getBucket()); + writeApi = client.getWriteApi(); + } + + @PreDestroy + void shutdown() { + if (writeApi != null) { + writeApi.close(); + } + if (client != null) { + client.close(); + } + } + + public void writeHeader( + String vin, String trigger, long createdDateTime, InfluxTelemetryPayload.Header header) { + Point point = + Point.measurement("header") + .addTag("vin", vin) + .addTag("trigger", trigger) + .addField("createdDateTime", createdDateTime) + .time(createdDateTime, WritePrecision.MS); + + if (header != null) { + addFieldIfPresent(point, "hrTotalVehicleDistance", header.getHrTotalVehicleDistance()); + addFieldIfPresent(point, "grossCombinationVehicleWeight", header.getGrossCombinationVehicleWeight()); + addFieldIfPresent(point, "totalEngineHours", header.getTotalEngineHours()); + addFieldIfPresent(point, "totalElectricMotorHours", header.getTotalElectricMotorHours()); + addFieldIfPresent(point, "engineTotalFuelUsed", header.getEngineTotalFuelUsed()); + addFieldIfPresent(point, "driver1Id", header.getDriver1Id()); + addFieldIfPresent(point, "driver1IdCardIssuer", header.getDriver1IdCardIssuer()); + } + + writeApi.writePoint(point); + } + + public void writeSnapshot( + String vin, String trigger, long createdDateTime, InfluxTelemetryPayload.Snapshot snapshot) { + Point point = + Point.measurement("snapshot") + .addTag("vin", vin) + .addTag("trigger", trigger) + .addField("createdDateTime", createdDateTime) + .time(createdDateTime, WritePrecision.MS); + + if (snapshot != null) { + addFieldIfPresent(point, "latitude", snapshot.getLatitude()); + addFieldIfPresent(point, "longitude", snapshot.getLongitude()); + addFieldIfPresent(point, "heading", snapshot.getHeading()); + addFieldIfPresent(point, "altitude", snapshot.getAltitude()); + addFieldIfPresent(point, "speed", snapshot.getSpeed()); + addFieldIfPresent(point, "positionDateTime", snapshot.getPositionDateTime()); + addFieldIfPresent(point, "wheelBasedSpeed", snapshot.getWheelBasedSpeed()); + addFieldIfPresent(point, "tachographSpeed", snapshot.getTachographSpeed()); + addFieldIfPresent(point, "engineSpeed", snapshot.getEngineSpeed()); + addFieldIfPresent(point, "fuelType", snapshot.getFuelType()); + addFieldIfPresent(point, "catalystFuelLevel", snapshot.getCatalystFuelLevel()); + addFieldIfPresent(point, "fuelLevel1", snapshot.getFuelLevel1()); + addFieldIfPresent(point, "fuelLevel2", snapshot.getFuelLevel2()); + addFieldIfPresent(point, "driver1WorkingState", snapshot.getDriver1WorkingState()); + addFieldIfPresent(point, "driver2WorkingState", snapshot.getDriver2WorkingState()); + addFieldIfPresent(point, "ambientAirTemperature", snapshot.getAmbientAirTemperature()); + addFieldIfPresent(point, "parkingBrakeSwitch", snapshot.getParkingBrakeSwitch()); + addFieldIfPresent(point, "estimatedDistanceToEmptyFuel", snapshot.getEstimatedDistanceToEmptyFuel()); + addFieldIfPresent(point, "estimatedDistanceToEmptyTotal", snapshot.getEstimatedDistanceToEmptyTotal()); + addFieldIfPresent(point, "driver2Id", snapshot.getDriver2Id()); + addFieldIfPresent(point, "driver2IdCardIssuer", snapshot.getDriver2IdCardIssuer()); + } + + writeApi.writePoint(point); + } + + private static void addFieldIfPresent(Point point, String name, Double value) { + if (value != null) { + point.addField(name, value); + } + } + + private static void addFieldIfPresent(Point point, String name, Long value) { + if (value != null) { + point.addField(name, value); + } + } + + private static void addFieldIfPresent(Point point, String name, Integer value) { + if (value != null) { + point.addField(name, value); + } + } + + private static void addFieldIfPresent(Point point, String name, Boolean value) { + if (value != null) { + point.addField(name, value); + } + } + + private static void addFieldIfPresent(Point point, String name, String value) { + if (value != null && !value.isBlank()) { + point.addField(name, value); + } + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsConfig.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsConfig.java new file mode 100644 index 0000000..86812f3 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsConfig.java @@ -0,0 +1,72 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +public class InfluxStatsConfig { + private static final long DEFAULT_INTERVAL_SECONDS = 30; + private static final long DEFAULT_INITIAL_DELAY_SECONDS = 10; + private final long intervalSeconds; + private final long initialDelaySeconds; + + private InfluxStatsConfig(long intervalSeconds, long initialDelaySeconds) { + this.intervalSeconds = intervalSeconds; + this.initialDelaySeconds = initialDelaySeconds; + } + + public static InfluxStatsConfig fromEnv() { + String raw = + System.getProperty( + "INFLUXDB_STATS_INTERVAL_SECONDS", + System.getenv("INFLUXDB_STATS_INTERVAL_SECONDS")); + long interval = parseInterval(raw); + String rawDelay = + System.getProperty( + "INFLUXDB_STATS_INITIAL_DELAY_SECONDS", + System.getenv("INFLUXDB_STATS_INITIAL_DELAY_SECONDS")); + long initialDelay = parseInitialDelay(rawDelay); + return new InfluxStatsConfig(interval, initialDelay); + } + + public long getIntervalSeconds() { + return intervalSeconds; + } + + public long getInitialDelaySeconds() { + return initialDelaySeconds; + } + + private static long parseInterval(String raw) { + if (raw == null || raw.isBlank()) { + return DEFAULT_INTERVAL_SECONDS; + } + try { + long value = Long.parseLong(raw.trim()); + return value > 0 ? value : DEFAULT_INTERVAL_SECONDS; + } catch (NumberFormatException ex) { + return DEFAULT_INTERVAL_SECONDS; + } + } + + private static long parseInitialDelay(String raw) { + if (raw == null || raw.isBlank()) { + return DEFAULT_INITIAL_DELAY_SECONDS; + } + try { + long value = Long.parseLong(raw.trim()); + return value >= 0 ? value : DEFAULT_INITIAL_DELAY_SECONDS; + } catch (NumberFormatException ex) { + return DEFAULT_INITIAL_DELAY_SECONDS; + } + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsService.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsService.java new file mode 100644 index 0000000..f75f8af --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxStatsService.java @@ -0,0 +1,242 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.InfluxDBClientFactory; +import com.influxdb.client.QueryApi; +import com.influxdb.client.WriteApi; +import com.influxdb.client.domain.WritePrecision; +import com.influxdb.client.write.Point; +import com.influxdb.query.FluxRecord; +import com.influxdb.query.FluxTable; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ApplicationScoped +public class InfluxStatsService { + private static final Logger LOGGER = Logger.getLogger(InfluxStatsService.class.getName()); + + private InfluxDbConfig influxConfig; + private InfluxStatsConfig statsConfig; + private InfluxDBClient client; + private WriteApi writeApi; + private ScheduledExecutorService scheduler; + + @PostConstruct + void init() { + influxConfig = InfluxDbConfig.fromEnv(); + statsConfig = InfluxStatsConfig.fromEnv(); + client = + InfluxDBClientFactory.create( + influxConfig.getUri(), + influxConfig.getToken().toCharArray(), + influxConfig.getOrg(), + influxConfig.getBucket()); + writeApi = client.getWriteApi(); + + scheduler = + Executors.newSingleThreadScheduledExecutor( + runnable -> { + Thread thread = new Thread(runnable, "fleet-stats-scheduler"); + thread.setDaemon(true); + return thread; + }); + scheduler.scheduleAtFixedRate( + this::refreshAndPersistStats, + statsConfig.getInitialDelaySeconds(), + statsConfig.getIntervalSeconds(), + TimeUnit.SECONDS); + } + + @PreDestroy + void shutdown() { + if (scheduler != null) { + scheduler.shutdownNow(); + } + if (writeApi != null) { + writeApi.close(); + } + if (client != null) { + client.close(); + } + } + + public FleetStatsSummary getLatestStats(boolean computeIfMissing) { + FleetStatsSummary latest = queryLatestStats(); + if (latest == null && computeIfMissing) { + latest = computeStats(); + writeStats(latest); + } + return latest; + } + + private void refreshAndPersistStats() { + try { + FleetStatsSummary stats = computeStats(); + writeStats(stats); + } catch (Exception ex) { + LOGGER.log(Level.WARNING, "Failed to refresh fleet stats.", ex); + } + } + + private FleetStatsSummary computeStats() { + long vehicleCount = queryVehicleCount(); + long headerCount = queryMeasurementCount("header"); + long snapshotCount = queryMeasurementCount("snapshot"); + + FleetStatsSummary summary = new FleetStatsSummary(); + summary.setVehicleCount(vehicleCount); + summary.setHeaderCount(headerCount); + summary.setSnapshotCount(snapshotCount); + summary.setTotalCount(headerCount + snapshotCount); + summary.setGeneratedAt(System.currentTimeMillis()); + return summary; + } + + private void writeStats(FleetStatsSummary summary) { + Point point = + Point.measurement("fleet_stats") + .addField("vehicleCount", summary.getVehicleCount()) + .addField("headerCount", summary.getHeaderCount()) + .addField("snapshotCount", summary.getSnapshotCount()) + .addField("totalCount", summary.getTotalCount()) + .addField("generatedAt", summary.getGeneratedAt()) + .time(summary.getGeneratedAt(), WritePrecision.MS); + writeApi.writePoint(point); + } + + private FleetStatsSummary queryLatestStats() { + String flux = + "from(bucket: \"" + + influxConfig.getBucket() + + "\")" + + " |> range(start: 0)" + + " |> filter(fn: (r) => r._measurement == \"fleet_stats\")" + + " |> last()"; + + List tables = client.getQueryApi().query(flux); + if (tables == null || tables.isEmpty()) { + return null; + } + + FleetStatsSummary summary = new FleetStatsSummary(); + boolean hasData = false; + for (FluxTable table : tables) { + for (FluxRecord record : table.getRecords()) { + Object field = record.getField(); + Object value = record.getValue(); + if (field == null || value == null) { + continue; + } + String name = field.toString(); + long numeric = toLong(value); + switch (name) { + case "vehicleCount" -> summary.setVehicleCount(numeric); + case "headerCount" -> summary.setHeaderCount(numeric); + case "snapshotCount" -> summary.setSnapshotCount(numeric); + case "totalCount" -> summary.setTotalCount(numeric); + case "generatedAt" -> summary.setGeneratedAt(numeric); + default -> { + } + } + hasData = true; + } + } + + return hasData ? summary : null; + } + + private long queryVehicleCount() { + String flux = + "import \"influxdata/influxdb/schema\"\n" + + "schema.tagValues(bucket: \"" + + influxConfig.getBucket() + + "\", tag: \"vin\")" + + " |> group(columns: [])" + + " |> count(column: \"_value\")"; + return querySingleLong(flux); + } + + private long queryMeasurementCount(String measurement) { + String flux = + "from(bucket: \"" + + influxConfig.getBucket() + + "\")" + + " |> range(start: 0)" + + " |> filter(fn: (r) => r._measurement == \"" + + measurement + + "\")" + + " |> keep(columns: [\"_time\", \"vin\"])" + + " |> map(fn: (r) => ({_value: string(v: r._time) + \":\" + r.vin}))" + + " |> distinct(column: \"_value\")" + + " |> group(columns: [])" + + " |> count(column: \"_value\")"; + return querySingleLong(flux); + } + + private long querySingleLong(String flux) { + QueryApi queryApi = client.getQueryApi(); + List tables = queryApi.query(flux); + if (tables == null || tables.isEmpty()) { + return 0L; + } + for (FluxTable table : tables) { + for (FluxRecord record : table.getRecords()) { + Object value = record.getValue(); + if (value == null) { + value = record.getValueByKey("count"); + } + if (value != null) { + return toLong(value); + } + } + } + return 0L; + } + + private long querySumLong(String flux) { + QueryApi queryApi = client.getQueryApi(); + List tables = queryApi.query(flux); + if (tables == null || tables.isEmpty()) { + return 0L; + } + long sum = 0L; + for (FluxTable table : tables) { + for (FluxRecord record : table.getRecords()) { + Object value = record.getValue(); + if (value != null) { + sum += toLong(value); + } + } + } + return sum; + } + + private long toLong(Object value) { + if (value instanceof Number number) { + return number.longValue(); + } + return Long.parseLong(Objects.toString(value, "0")); + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryPayload.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryPayload.java new file mode 100644 index 0000000..e574125 --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryPayload.java @@ -0,0 +1,320 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +public class InfluxTelemetryPayload { + private String vin; + private String trigger; + private Long createdDateTime; + private Header header; + private Snapshot snapshot; + + public String getVin() { + return vin; + } + + public void setVin(String vin) { + this.vin = vin; + } + + public String getTrigger() { + return trigger; + } + + public void setTrigger(String trigger) { + this.trigger = trigger; + } + + public Long getCreatedDateTime() { + return createdDateTime; + } + + public void setCreatedDateTime(Long createdDateTime) { + this.createdDateTime = createdDateTime; + } + + public Header getHeader() { + return header; + } + + public void setHeader(Header header) { + this.header = header; + } + + public Snapshot getSnapshot() { + return snapshot; + } + + public void setSnapshot(Snapshot snapshot) { + this.snapshot = snapshot; + } + + public static class Header { + private Double hrTotalVehicleDistance; + private Double grossCombinationVehicleWeight; + private Double totalEngineHours; + private Double totalElectricMotorHours; + private Double engineTotalFuelUsed; + private String driver1Id; + private String driver1IdCardIssuer; + + public Double getHrTotalVehicleDistance() { + return hrTotalVehicleDistance; + } + + public void setHrTotalVehicleDistance(Double hrTotalVehicleDistance) { + this.hrTotalVehicleDistance = hrTotalVehicleDistance; + } + + public Double getGrossCombinationVehicleWeight() { + return grossCombinationVehicleWeight; + } + + public void setGrossCombinationVehicleWeight(Double grossCombinationVehicleWeight) { + this.grossCombinationVehicleWeight = grossCombinationVehicleWeight; + } + + public Double getTotalEngineHours() { + return totalEngineHours; + } + + public void setTotalEngineHours(Double totalEngineHours) { + this.totalEngineHours = totalEngineHours; + } + + public Double getTotalElectricMotorHours() { + return totalElectricMotorHours; + } + + public void setTotalElectricMotorHours(Double totalElectricMotorHours) { + this.totalElectricMotorHours = totalElectricMotorHours; + } + + public Double getEngineTotalFuelUsed() { + return engineTotalFuelUsed; + } + + public void setEngineTotalFuelUsed(Double engineTotalFuelUsed) { + this.engineTotalFuelUsed = engineTotalFuelUsed; + } + + public String getDriver1Id() { + return driver1Id; + } + + public void setDriver1Id(String driver1Id) { + this.driver1Id = driver1Id; + } + + public String getDriver1IdCardIssuer() { + return driver1IdCardIssuer; + } + + public void setDriver1IdCardIssuer(String driver1IdCardIssuer) { + this.driver1IdCardIssuer = driver1IdCardIssuer; + } + } + + public static class Snapshot { + private Double latitude; + private Double longitude; + private Double heading; + private Double altitude; + private Double speed; + private Long positionDateTime; + private Double wheelBasedSpeed; + private Double tachographSpeed; + private Double engineSpeed; + private String fuelType; + private Double catalystFuelLevel; + private Double fuelLevel1; + private Double fuelLevel2; + private String driver1WorkingState; + private String driver2WorkingState; + private Double ambientAirTemperature; + private Boolean parkingBrakeSwitch; + private Double estimatedDistanceToEmptyFuel; + private Double estimatedDistanceToEmptyTotal; + private String driver2Id; + private String driver2IdCardIssuer; + + public Double getLatitude() { + return latitude; + } + + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + public Double getLongitude() { + return longitude; + } + + public void setLongitude(Double longitude) { + this.longitude = longitude; + } + + public Double getHeading() { + return heading; + } + + public void setHeading(Double heading) { + this.heading = heading; + } + + public Double getAltitude() { + return altitude; + } + + public void setAltitude(Double altitude) { + this.altitude = altitude; + } + + public Double getSpeed() { + return speed; + } + + public void setSpeed(Double speed) { + this.speed = speed; + } + + public Long getPositionDateTime() { + return positionDateTime; + } + + public void setPositionDateTime(Long positionDateTime) { + this.positionDateTime = positionDateTime; + } + + public Double getWheelBasedSpeed() { + return wheelBasedSpeed; + } + + public void setWheelBasedSpeed(Double wheelBasedSpeed) { + this.wheelBasedSpeed = wheelBasedSpeed; + } + + public Double getTachographSpeed() { + return tachographSpeed; + } + + public void setTachographSpeed(Double tachographSpeed) { + this.tachographSpeed = tachographSpeed; + } + + public Double getEngineSpeed() { + return engineSpeed; + } + + public void setEngineSpeed(Double engineSpeed) { + this.engineSpeed = engineSpeed; + } + + public String getFuelType() { + return fuelType; + } + + public void setFuelType(String fuelType) { + this.fuelType = fuelType; + } + + public Double getCatalystFuelLevel() { + return catalystFuelLevel; + } + + public void setCatalystFuelLevel(Double catalystFuelLevel) { + this.catalystFuelLevel = catalystFuelLevel; + } + + public Double getFuelLevel1() { + return fuelLevel1; + } + + public void setFuelLevel1(Double fuelLevel1) { + this.fuelLevel1 = fuelLevel1; + } + + public Double getFuelLevel2() { + return fuelLevel2; + } + + public void setFuelLevel2(Double fuelLevel2) { + this.fuelLevel2 = fuelLevel2; + } + + public String getDriver1WorkingState() { + return driver1WorkingState; + } + + public void setDriver1WorkingState(String driver1WorkingState) { + this.driver1WorkingState = driver1WorkingState; + } + + public String getDriver2WorkingState() { + return driver2WorkingState; + } + + public void setDriver2WorkingState(String driver2WorkingState) { + this.driver2WorkingState = driver2WorkingState; + } + + public Double getAmbientAirTemperature() { + return ambientAirTemperature; + } + + public void setAmbientAirTemperature(Double ambientAirTemperature) { + this.ambientAirTemperature = ambientAirTemperature; + } + + public Boolean getParkingBrakeSwitch() { + return parkingBrakeSwitch; + } + + public void setParkingBrakeSwitch(Boolean parkingBrakeSwitch) { + this.parkingBrakeSwitch = parkingBrakeSwitch; + } + + public Double getEstimatedDistanceToEmptyFuel() { + return estimatedDistanceToEmptyFuel; + } + + public void setEstimatedDistanceToEmptyFuel(Double estimatedDistanceToEmptyFuel) { + this.estimatedDistanceToEmptyFuel = estimatedDistanceToEmptyFuel; + } + + public Double getEstimatedDistanceToEmptyTotal() { + return estimatedDistanceToEmptyTotal; + } + + public void setEstimatedDistanceToEmptyTotal(Double estimatedDistanceToEmptyTotal) { + this.estimatedDistanceToEmptyTotal = estimatedDistanceToEmptyTotal; + } + + public String getDriver2Id() { + return driver2Id; + } + + public void setDriver2Id(String driver2Id) { + this.driver2Id = driver2Id; + } + + public String getDriver2IdCardIssuer() { + return driver2IdCardIssuer; + } + + public void setDriver2IdCardIssuer(String driver2IdCardIssuer) { + this.driver2IdCardIssuer = driver2IdCardIssuer; + } + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryResource.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryResource.java new file mode 100644 index 0000000..242187d --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxTelemetryResource.java @@ -0,0 +1,78 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +import jakarta.inject.Inject; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/telemetry") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class InfluxTelemetryResource { + + @Inject + private InfluxDbWriter influxDbWriter; + + @POST + @Path("/ingest") + public InfluxWriteResult ingest(InfluxTelemetryPayload payload) { + if (payload == null) { + throw new BadRequestException("Payload is required."); + } + + if (payload.getVin() == null || payload.getVin().isBlank()) { + throw new BadRequestException("vin is required."); + } + + if (payload.getTrigger() == null || payload.getTrigger().isBlank()) { + throw new BadRequestException("trigger is required."); + } + + if (payload.getCreatedDateTime() == null) { + throw new BadRequestException("createdDateTime is required."); + } + + boolean wroteHeader = false; + boolean wroteSnapshot = false; + + if (payload.getHeader() != null) { + influxDbWriter.writeHeader( + payload.getVin(), + payload.getTrigger(), + payload.getCreatedDateTime(), + payload.getHeader()); + wroteHeader = true; + } + + if (payload.getSnapshot() != null) { + influxDbWriter.writeSnapshot( + payload.getVin(), + payload.getTrigger(), + payload.getCreatedDateTime(), + payload.getSnapshot()); + wroteSnapshot = true; + } + + if (!wroteHeader && !wroteSnapshot) { + throw new BadRequestException("Provide header and/or snapshot data to write."); + } + + return new InfluxWriteResult(wroteHeader, wroteSnapshot); + } +} diff --git a/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxWriteResult.java b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxWriteResult.java new file mode 100644 index 0000000..91ed27b --- /dev/null +++ b/components/backend-fleet-analysis-java/src/main/java/org/eclipse/sdv/fleet/analysis/InfluxWriteResult.java @@ -0,0 +1,43 @@ +// ******************************************************************************** +// * Copyright (c) 2026 Contributors to the Eclipse Foundation +// * +// * See the NOTICE file(s) distributed with this work for additional +// * information regarding copyright ownership. +// * +// * This program and the accompanying materials are made available under the +// * terms of the Apache License 2.0 which is available at +// * https://www.apache.org/licenses/LICENSE-2.0 +// * +// * SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************** + +package org.eclipse.sdv.fleet.analysis; + +public class InfluxWriteResult { + private boolean headerWritten; + private boolean snapshotWritten; + + public InfluxWriteResult() { + } + + public InfluxWriteResult(boolean headerWritten, boolean snapshotWritten) { + this.headerWritten = headerWritten; + this.snapshotWritten = snapshotWritten; + } + + public boolean isHeaderWritten() { + return headerWritten; + } + + public void setHeaderWritten(boolean headerWritten) { + this.headerWritten = headerWritten; + } + + public boolean isSnapshotWritten() { + return snapshotWritten; + } + + public void setSnapshotWritten(boolean snapshotWritten) { + this.snapshotWritten = snapshotWritten; + } +} diff --git a/docs/docusaurus.md b/docs/docusaurus.md index 6bd11de..c63dc44 100644 --- a/docs/docusaurus.md +++ b/docs/docusaurus.md @@ -22,7 +22,7 @@ plugins:[ [ // the base directory to output to. outDir: "docs/fleet-management", // the file names to download - documents: ["introduction.md"], + documents: ["introduction.md", "fleet-analysis-backend.md"], }, ], [ "docusaurus-plugin-remote-content", diff --git a/docs/fleet-analysis-backend.md b/docs/fleet-analysis-backend.md new file mode 100644 index 0000000..25f1b98 --- /dev/null +++ b/docs/fleet-analysis-backend.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 2 +title: Fleet Analysis Backend +--- + +## Fleet Analysis Backend + +The Fleet Management Blueprint includes a Jakarta EE backend service that provides fleet-level analytics on top of telemetry stored in InfluxDB. + +## Overview + +The backend runs in the same Docker Compose stack as the Fleet Management services and exposes REST APIs for: + +- Computing summary statistics from telemetry snapshots +- Ingesting telemetry payloads into InfluxDB +- Reading periodically refreshed fleet statistics + +## Endpoints + +### POST /api/analysis/summary + +Accepts a JSON array of vehicle snapshots and returns aggregate values such as fleet size, average speed and battery range. + +### POST /api/telemetry/ingest + +Writes header and snapshot telemetry to InfluxDB. + +### GET /api/analysis/stats + +Returns the latest precomputed fleet statistics snapshot. + +## Configuration + +The component supports these environment variables: + +- INFLUXDB_URI (default: `http://influxdb:8086`) +- INFLUXDB_ORG (default: sdv) +- INFLUXDB_BUCKET (default: demo) +- INFLUXDB_TOKEN or INFLUXDB_TOKEN_FILE +- INFLUXDB_STATS_INTERVAL_SECONDS (default: 30) +- INFLUXDB_STATS_INITIAL_DELAY_SECONDS (default: 10) + +## Build and Run + +Build and run from the Fleet Management repository root: + +```bash +docker compose -f ./fms-blueprint-compose.yaml -f ./fms-blueprint-compose-zenoh.yaml up --detach +``` + +The service is exposed at: + +- `http://127.0.0.1:8082/fleet-analysis/api` + +The source code is located in components/backend-fleet-analysis-java. diff --git a/docs/introduction.md b/docs/introduction.md index b4dab22..ff92b6a 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -8,8 +8,8 @@ title: Introduction | | | |------------------|---| |Short Summary | This repository contains the **Fleet Management Blueprint** which is a close to *real-life* showcase for truck fleet management where trucks run an SDV software stack so that logistics fleet operators can manage apps, data and services for a diverse set of vehicles. | -|What is in the showcase | In-vehicle data collection using VSS Signal Specification, data transfer to cloud back end, rFMS server| +|What is in the showcase | In-vehicle data collection using VSS Signal Specification, data transfer to cloud back end, rFMS server, fleet analysis REST API | |SDV Projects Involved| Eclipse Leda, Eclipse Kuksa | -|Other interesting Technologies|InfluxDB, Eclipse Hono, Eclipse Kanto, Eclipse Paho | +|Other interesting Technologies|InfluxDB, Eclipse Hono, Eclipse Kanto, Eclipse Paho, Jakarta EE | | Architecture Overview | ![FleetArchSDV](../img/architecture.drawio.svg)| | Distro | TBD | diff --git a/fms-blueprint-compose-zenoh.yaml b/fms-blueprint-compose-zenoh.yaml index 76e86fe..e3c0681 100644 --- a/fms-blueprint-compose-zenoh.yaml +++ b/fms-blueprint-compose-zenoh.yaml @@ -51,7 +51,7 @@ services: fleet-analysis-backend: build: - context: "../../devices/backend-fleet-analysis-java" + context: "./components/backend-fleet-analysis-java" dockerfile: "Dockerfile" container_name: "fleet-analysis-backend" networks: