diff --git a/src/main/java/telraam/App.java b/src/main/java/telraam/App.java index d7bfdba..4e6eb03 100644 --- a/src/main/java/telraam/App.java +++ b/src/main/java/telraam/App.java @@ -17,9 +17,11 @@ import telraam.database.daos.*; import telraam.database.models.Station; import telraam.healthchecks.TemplateHealthCheck; -import telraam.logic.Lapper; -import telraam.logic.external.ExternalLapper; -import telraam.logic.robust.RobustLapper; +import telraam.logic.lapper.Lapper; +import telraam.logic.lapper.external.ExternalLapper; +import telraam.logic.lapper.robust.RobustLapper; +import telraam.logic.positioner.Positioner; +import telraam.logic.positioner.simple.SimplePositioner; import telraam.station.Fetcher; import telraam.util.AcceptedLapsUtil; import telraam.websocket.WebSocketConnection; @@ -131,10 +133,15 @@ public void run(AppConfiguration configuration, Environment environment) throws lapper.registerAPI(jersey); } + // Set up positioners + Set positioners = new HashSet<>(); + + positioners.add(new SimplePositioner(this.database)); + // Start fetch thread for each station StationDAO stationDAO = this.database.onDemand(StationDAO.class); for (Station station : stationDAO.getAll()) { - new Thread(() -> new Fetcher(this.database, station, lappers).fetch()).start(); + new Thread(() -> new Fetcher(this.database, station, lappers, positioners).fetch()).start(); } } diff --git a/src/main/java/telraam/logic/Lapper.java b/src/main/java/telraam/logic/lapper/Lapper.java similarity index 87% rename from src/main/java/telraam/logic/Lapper.java rename to src/main/java/telraam/logic/lapper/Lapper.java index c3dc674..d999307 100644 --- a/src/main/java/telraam/logic/Lapper.java +++ b/src/main/java/telraam/logic/lapper/Lapper.java @@ -1,4 +1,4 @@ -package telraam.logic; +package telraam.logic.lapper; import io.dropwizard.jersey.setup.JerseyEnvironment; import telraam.database.models.Detection; diff --git a/src/main/java/telraam/logic/external/ExternalLapper.java b/src/main/java/telraam/logic/lapper/external/ExternalLapper.java similarity index 96% rename from src/main/java/telraam/logic/external/ExternalLapper.java rename to src/main/java/telraam/logic/lapper/external/ExternalLapper.java index 3aee9f2..510e28a 100644 --- a/src/main/java/telraam/logic/external/ExternalLapper.java +++ b/src/main/java/telraam/logic/lapper/external/ExternalLapper.java @@ -1,4 +1,4 @@ -package telraam.logic.external; +package telraam.logic.lapper.external; import io.dropwizard.jersey.setup.JerseyEnvironment; import org.jdbi.v3.core.Jdbi; @@ -7,8 +7,8 @@ import telraam.database.models.Detection; import telraam.database.models.Lap; import telraam.database.models.LapSource; -import telraam.logic.Lapper; -import telraam.logic.external.models.ExternalLapperTeamLaps; +import telraam.logic.lapper.Lapper; +import telraam.logic.lapper.external.models.ExternalLapperTeamLaps; import java.sql.Timestamp; import java.util.Comparator; diff --git a/src/main/java/telraam/logic/external/ExternalLapperResource.java b/src/main/java/telraam/logic/lapper/external/ExternalLapperResource.java similarity index 86% rename from src/main/java/telraam/logic/external/ExternalLapperResource.java rename to src/main/java/telraam/logic/lapper/external/ExternalLapperResource.java index 37f8e58..55f6aa0 100644 --- a/src/main/java/telraam/logic/external/ExternalLapperResource.java +++ b/src/main/java/telraam/logic/lapper/external/ExternalLapperResource.java @@ -1,10 +1,10 @@ -package telraam.logic.external; +package telraam.logic.lapper.external; import io.swagger.v3.oas.annotations.Operation; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; -import telraam.logic.external.models.ExternalLapperStats; -import telraam.logic.external.models.ExternalLapperTeamLaps; +import telraam.logic.lapper.external.models.ExternalLapperStats; +import telraam.logic.lapper.external.models.ExternalLapperTeamLaps; import java.util.List; diff --git a/src/main/java/telraam/logic/external/models/ExternalLapperLap.java b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperLap.java similarity index 73% rename from src/main/java/telraam/logic/external/models/ExternalLapperLap.java rename to src/main/java/telraam/logic/lapper/external/models/ExternalLapperLap.java index f343ce1..3623891 100644 --- a/src/main/java/telraam/logic/external/models/ExternalLapperLap.java +++ b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperLap.java @@ -1,4 +1,4 @@ -package telraam.logic.external.models; +package telraam.logic.lapper.external.models; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/telraam/logic/external/models/ExternalLapperStats.java b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperStats.java similarity index 84% rename from src/main/java/telraam/logic/external/models/ExternalLapperStats.java rename to src/main/java/telraam/logic/lapper/external/models/ExternalLapperStats.java index cc31a04..8c875bc 100644 --- a/src/main/java/telraam/logic/external/models/ExternalLapperStats.java +++ b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperStats.java @@ -1,4 +1,4 @@ -package telraam.logic.external.models; +package telraam.logic.lapper.external.models; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/telraam/logic/external/models/ExternalLapperTeamLaps.java b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperTeamLaps.java similarity index 80% rename from src/main/java/telraam/logic/external/models/ExternalLapperTeamLaps.java rename to src/main/java/telraam/logic/lapper/external/models/ExternalLapperTeamLaps.java index c8e8170..fa37f63 100644 --- a/src/main/java/telraam/logic/external/models/ExternalLapperTeamLaps.java +++ b/src/main/java/telraam/logic/lapper/external/models/ExternalLapperTeamLaps.java @@ -1,4 +1,4 @@ -package telraam.logic.external.models; +package telraam.logic.lapper.external.models; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/telraam/logic/robust/RobustLapper.java b/src/main/java/telraam/logic/lapper/robust/RobustLapper.java similarity index 99% rename from src/main/java/telraam/logic/robust/RobustLapper.java rename to src/main/java/telraam/logic/lapper/robust/RobustLapper.java index 2ad64c5..62ae807 100644 --- a/src/main/java/telraam/logic/robust/RobustLapper.java +++ b/src/main/java/telraam/logic/lapper/robust/RobustLapper.java @@ -1,4 +1,4 @@ -package telraam.logic.robust; +package telraam.logic.lapper.robust; import io.dropwizard.jersey.setup.JerseyEnvironment; import org.jdbi.v3.core.Jdbi; @@ -10,7 +10,7 @@ import telraam.database.models.Lap; import telraam.database.models.LapSource; import telraam.database.models.Station; -import telraam.logic.Lapper; +import telraam.logic.lapper.Lapper; import java.sql.Timestamp; import java.util.*; diff --git a/src/main/java/telraam/logic/simple/SimpleLapper.java b/src/main/java/telraam/logic/lapper/simple/SimpleLapper.java similarity index 97% rename from src/main/java/telraam/logic/simple/SimpleLapper.java rename to src/main/java/telraam/logic/lapper/simple/SimpleLapper.java index 9b26f9b..608e724 100644 --- a/src/main/java/telraam/logic/simple/SimpleLapper.java +++ b/src/main/java/telraam/logic/lapper/simple/SimpleLapper.java @@ -1,11 +1,11 @@ -package telraam.logic.simple; +package telraam.logic.lapper.simple; import io.dropwizard.jersey.setup.JerseyEnvironment; import org.jdbi.v3.core.Jdbi; import telraam.database.daos.LapDAO; import telraam.database.daos.LapSourceDAO; import telraam.database.models.*; -import telraam.logic.Lapper; +import telraam.logic.lapper.Lapper; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/telraam/logic/positioner/CircularQueue.java b/src/main/java/telraam/logic/positioner/CircularQueue.java new file mode 100644 index 0000000..773c633 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/CircularQueue.java @@ -0,0 +1,21 @@ +package telraam.logic.positioner; + +import java.util.LinkedList; + +public class CircularQueue extends LinkedList { + + private final int maxSize; + public CircularQueue(int maxSize) { + this.maxSize = maxSize; + } + + @Override + public boolean add(T e) { + if (size() >= this.maxSize) { + removeFirst(); + } + + return super.add(e); + } + +} diff --git a/src/main/java/telraam/logic/positioner/Position.java b/src/main/java/telraam/logic/positioner/Position.java new file mode 100644 index 0000000..3a4218e --- /dev/null +++ b/src/main/java/telraam/logic/positioner/Position.java @@ -0,0 +1,19 @@ +package telraam.logic.positioner; + +import lombok.Getter; +import lombok.Setter; +import telraam.database.models.Team; +import telraam.websocket.WebSocketMessageSingleton; + +@Getter @Setter +public class Position { + private int teamId; + private float progress; // Progress of the lap. Between 0-1 + private float speed; // Current speed. Progress / second + + public Position(int teamId) { + this.teamId = teamId; + this.progress = 0; + this.speed = 0; + } +} diff --git a/src/main/java/telraam/logic/positioner/PositionSender.java b/src/main/java/telraam/logic/positioner/PositionSender.java new file mode 100644 index 0000000..5e779dc --- /dev/null +++ b/src/main/java/telraam/logic/positioner/PositionSender.java @@ -0,0 +1,20 @@ +package telraam.logic.positioner; + +import telraam.websocket.WebSocketMessage; +import telraam.websocket.WebSocketMessageSingleton; + +import java.util.List; + +public class PositionSender { + private final WebSocketMessage> message = new WebSocketMessage<>(); + + public PositionSender() { + this.message.setTopic("position"); + } + + public void send(List positions) { + this.message.setData(positions); + WebSocketMessageSingleton.getInstance().sendToAll(this.message); + } + +} diff --git a/src/main/java/telraam/logic/positioner/Positioner.java b/src/main/java/telraam/logic/positioner/Positioner.java new file mode 100644 index 0000000..4128b36 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/Positioner.java @@ -0,0 +1,8 @@ +package telraam.logic.positioner; + +import telraam.database.models.Detection; + +public interface Positioner { + void handle(Detection detection); + +} diff --git a/src/main/java/telraam/logic/positioner/simple/SimplePositioner.java b/src/main/java/telraam/logic/positioner/simple/SimplePositioner.java new file mode 100644 index 0000000..f8a9022 --- /dev/null +++ b/src/main/java/telraam/logic/positioner/simple/SimplePositioner.java @@ -0,0 +1,104 @@ +package telraam.logic.positioner.simple; + +import org.jdbi.v3.core.Jdbi; +import telraam.database.daos.BatonSwitchoverDAO; +import telraam.database.daos.StationDAO; +import telraam.database.daos.TeamDAO; +import telraam.database.models.BatonSwitchover; +import telraam.database.models.Detection; +import telraam.database.models.Station; +import telraam.database.models.Team; +import telraam.logic.positioner.CircularQueue; +import telraam.logic.positioner.Position; +import telraam.logic.positioner.PositionSender; +import telraam.logic.positioner.Positioner; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class SimplePositioner implements Positioner { + private final int QUEUE_SIZE = 50; + private final int MIN_RSSI = -85; + private final int DEBOUNCE_TIMEOUT = 1; + private boolean debounceScheduled; + private final ScheduledExecutorService scheduler; + private static final Logger logger = Logger.getLogger(SimplePositioner.class.getName()); + private final PositionSender positionSender; + private final Map batonIdToTeam; + private final Map> teamDetections; + private final List stations; + private final Map teamPositions; + + public SimplePositioner(Jdbi jdbi) { + this.debounceScheduled = false; + this.scheduler = Executors.newScheduledThreadPool(1); + this.positionSender = new PositionSender(); + this.batonIdToTeam = new HashMap<>(); + this.teamDetections = new HashMap<>(); + this.teamPositions = new HashMap<>(); + + TeamDAO teamDAO = jdbi.onDemand(TeamDAO.class); + List teams = teamDAO.getAll(); + for (Team team: teams) { + teamDetections.put(team, new CircularQueue<>(QUEUE_SIZE)); + teamPositions.put(team, new Position(team.getId())); + } + List switchovers = jdbi.onDemand(BatonSwitchoverDAO.class).getAll(); + switchovers.sort(Comparator.comparing(BatonSwitchover::getTimestamp)); + + for (BatonSwitchover switchover: switchovers) { + batonIdToTeam.put(switchover.getNewBatonId(), teamDAO.getById(switchover.getTeamId()).get()); + } + + List stationList = jdbi.onDemand(StationDAO.class).getAll(); + stationList.sort(Comparator.comparing(Station::getDistanceFromStart)); + stations = stationList.stream().map(Station::getId).toList(); + } + + public void calculatePositions() { + logger.info("SimplePositioner: Calculating positions..."); + for (Map.Entry> entry: teamDetections.entrySet()) { + List detections = teamDetections.get(entry.getKey()); + detections.sort(Comparator.comparing(Detection::getTimestamp)); + + int currentStationRssi = MIN_RSSI; + int currentStationPosition = 0; + for (Detection detection: detections) { + if (detection.getRssi() > currentStationRssi) { + currentStationRssi = detection.getRssi(); + currentStationPosition = detection.getStationId(); + } + } + + float progress = ((float) 100 / stations.size()) * currentStationPosition; + teamPositions.get(entry.getKey()).setProgress(progress); + } + + positionSender.send(teamPositions.values().stream().toList()); + logger.info("SimplePositioner: Done calculating positions"); + } + + public void handle(Detection detection) { + Team team = batonIdToTeam.get(detection.getBatonId()); + teamDetections.get(team).add(detection); + + if (! debounceScheduled) { + debounceScheduled = true; + scheduler.schedule(() -> { + try { + calculatePositions(); + } catch (Exception e) { + logger.severe(e.getMessage()); + } + debounceScheduled = false; + }, DEBOUNCE_TIMEOUT, TimeUnit.SECONDS); + } + } + +} diff --git a/src/main/java/telraam/station/Fetcher.java b/src/main/java/telraam/station/Fetcher.java index 54e04db..bb0ed5e 100644 --- a/src/main/java/telraam/station/Fetcher.java +++ b/src/main/java/telraam/station/Fetcher.java @@ -7,7 +7,8 @@ import telraam.database.models.Baton; import telraam.database.models.Detection; import telraam.database.models.Station; -import telraam.logic.Lapper; +import telraam.logic.lapper.Lapper; +import telraam.logic.positioner.Positioner; import telraam.station.models.RonnyDetection; import telraam.station.models.RonnyResponse; @@ -29,6 +30,7 @@ public class Fetcher { private final Set lappers; + private final Set positioners; private Station station; private final BatonDAO batonDAO; @@ -48,11 +50,12 @@ public class Fetcher { private final static int IDLE_TIMEOUT_MS = 4000; // Wait 4 seconds - public Fetcher(Jdbi database, Station station, Set lappers) { + public Fetcher(Jdbi database, Station station, Set lappers, Set positioners) { this.batonDAO = database.onDemand(BatonDAO.class); this.detectionDAO = database.onDemand(DetectionDAO.class); this.stationDAO = database.onDemand(StationDAO.class); this.lappers = lappers; + this.positioners = positioners; this.station = station; } @@ -158,7 +161,10 @@ public void fetch() { } if (!new_detections.isEmpty()) { detectionDAO.insertAll(new_detections); - new_detections.forEach((detection) -> lappers.forEach((lapper) -> lapper.handle(detection))); + new_detections.forEach((detection) -> { + lappers.forEach((lapper) -> lapper.handle(detection)); + positioners.forEach((positioner) -> positioner.handle(detection)); + }); } this.logger.finer("Fetched " + detections.size() + " detections from " + station.getName() + ", Saved " + new_detections.size()); diff --git a/src/main/java/telraam/websocket/WebSocketMessage.java b/src/main/java/telraam/websocket/WebSocketMessage.java new file mode 100644 index 0000000..0b4200f --- /dev/null +++ b/src/main/java/telraam/websocket/WebSocketMessage.java @@ -0,0 +1,10 @@ +package telraam.websocket; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class WebSocketMessage { + private String topic; + private T data; +} diff --git a/src/main/java/telraam/websocket/WebSocketMessageSingleton.java b/src/main/java/telraam/websocket/WebSocketMessageSingleton.java index de12115..d073217 100644 --- a/src/main/java/telraam/websocket/WebSocketMessageSingleton.java +++ b/src/main/java/telraam/websocket/WebSocketMessageSingleton.java @@ -1,20 +1,24 @@ package telraam.websocket; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Getter; +import lombok.NoArgsConstructor; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; +@NoArgsConstructor public class WebSocketMessageSingleton { private static final Logger logger = Logger.getLogger(WebSocketMessageSingleton.class.getName()); @Getter private static final WebSocketMessageSingleton instance = new WebSocketMessageSingleton(); private static final Set registeredConnections = new HashSet<>(); - - private WebSocketMessageSingleton() { - } + private final ObjectMapper mapper = new ObjectMapper(); public void registerConnection(WebSocketConnection conn) { boolean modified = registeredConnections.add(conn); @@ -34,4 +38,13 @@ public void sendToAll(String s) { logger.finest("Sending \"%s\" to all registered WebSocketConnection instances".formatted(s)); registeredConnections.forEach(conn -> conn.send(s)); } + + public void sendToAll(WebSocketMessage message) { + try { + String json = mapper.writeValueAsString(message); + this.sendToAll(json); + } catch (JsonProcessingException e) { + logger.severe("Json conversion error for \"%s\"".formatted(message.toString())); + } + } } diff --git a/src/test/java/telraam/logic/simple/SimpleLapperTest.java b/src/test/java/telraam/logic/simple/SimpleLapperTest.java index 838397f..c50e810 100644 --- a/src/test/java/telraam/logic/simple/SimpleLapperTest.java +++ b/src/test/java/telraam/logic/simple/SimpleLapperTest.java @@ -8,8 +8,8 @@ import telraam.database.models.Detection; import telraam.database.models.Lap; import telraam.database.models.LapSource; -import telraam.logic.Lapper; -import telraam.logic.simple.SimpleLapper; +import telraam.logic.lapper.Lapper; +import telraam.logic.lapper.simple.SimpleLapper; import java.sql.Timestamp; import java.util.Optional;