Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nostradamus #142

Merged
merged 25 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
19ee5c0
feat(station): send InitMessage on open connection
NuttyShrimp Mar 20, 2024
4483274
refactor: Use jakarta websockets
NuttyShrimp Mar 29, 2024
d35f66b
feat: re-add http fetcher & move WS to own package
NuttyShrimp Apr 1, 2024
607d73c
fix(ws-fetcher): catch client creation error
NuttyShrimp Apr 1, 2024
de43e91
feat(ws-fetcher): open WS after setting handlers
NuttyShrimp Apr 2, 2024
e8245c3
feat(ws-fetcher): retrieve missing values for detections from DB & tr…
NuttyShrimp Apr 2, 2024
8bcdffa
fix(simplePositioner): make handle function synchronised
NuttyShrimp Apr 13, 2024
6b44e35
tmp
Topvennie Apr 1, 2024
91811a7
refactor
Topvennie Apr 8, 2024
dd4ff53
nostradamus basic logic
Topvennie Apr 8, 2024
0e59e39
chore: bugfixes
Topvennie Apr 12, 2024
b540279
chore: infite speed fix
Topvennie Apr 13, 2024
52fad65
fix math
Topvennie Apr 14, 2024
1f8088a
Smooth out animations
Topvennie Apr 18, 2024
ab0b0cb
chore: seconds to milliseconds
Topvennie Apr 19, 2024
05eb54f
Refactor: Group common data
Topvennie Apr 19, 2024
a762b44
chore: smoothen out some more
Topvennie Apr 19, 2024
ec495ea
chore; remove prints statements
Topvennie Apr 20, 2024
78f1ea5
chore: sync up faster
Topvennie Apr 20, 2024
5a882fc
chore: Send speed 0 when no data is received
Topvennie Apr 20, 2024
6803c63
Docs: Added comments
Topvennie Apr 20, 2024
f23c84c
deleted simplepositioner
Topvennie Apr 23, 2024
74adeb2
renamed the only reference left to average as it somehow still causes…
Topvennie Apr 23, 2024
44ea73f
shorten fetch interval
Topvennie Apr 23, 2024
bdaa067
removed simplepositioner
Topvennie Apr 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor
  • Loading branch information
Topvennie committed Apr 23, 2024
commit 91811a70abcd629277b9bf3d88792a3d73e19080
6 changes: 6 additions & 0 deletions src/main/java/telraam/database/models/Detection.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ public Detection(Integer batonId, Integer stationId, Integer rssi, Float battery
this.timestamp = timestamp;
this.timestampIngestion = timestampIngestion;
}

public Detection(Integer id, Integer stationId, Integer rssi) {
this.id = id;
this.stationId = stationId;
this.rssi = rssi;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package telraam.logic.positioner.nostradamus;

import java.util.LinkedList;

public class CircularQueue<T> extends LinkedList<T> {

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);
}

}
171 changes: 39 additions & 132 deletions src/main/java/telraam/logic/positioner/nostradamus/Nostradamus.java
Original file line number Diff line number Diff line change
@@ -1,171 +1,78 @@
package telraam.logic.positioner.nostradamus;

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.Position;
import telraam.logic.positioner.PositionSender;
import telraam.logic.positioner.Positioner;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class Nostradamus implements Positioner {
private static final Logger logger = Logger.getLogger(Nostradamus.class.getName());

private final int INTERVAL_FETCH = 10000;
private final int DEBOUNCE_TIMEOUT = 1;
private final int MAX_SIZE = 10000;
private final int MIN_RSSI = -85;
private final int INTERVAL = 2;
private final ScheduledExecutorService scheduler;
private boolean debounceScheduled;
private Lock lock;

private final Jdbi jdbi;
private final TeamDAO teamDAO;
private final StationDAO stationDAO;
private final BatonSwitchoverDAO batonSwitchoverDAO;
private final int INTERVAL_CALCULATE = 500; // How often to handle new detections
private final int INTERVAL_FETCH = 30000; // Interval between fetching all stations, teams, ...
private final int INTERVAL_DETECTIONS = 1; // Amount of seconds to group detections by
private final int AVERAGE_AMOUNT = 10; // Calculate the average running speed the last x intervals
private final int MIN_RSSI = -84;
private final List<Detection> newDetections; // Contains not yet handled detections
private final Lock detectionLock;
private Map<Integer, Team> batonToTeam; // Baton ID to Team
private Map<Team, TeamData> teamData; // All team data
private final PositionSender positionSender;
private Map<Team, CircularPriorityQueue> teamDetections;
private Map<Team, TeamData> teamData;
private Map<Integer, Team> batonIdToTeam;
private Map<Integer, Station> idToStation;

public Nostradamus(Jdbi jdbi) {
this.scheduler = Executors.newScheduledThreadPool(1);
this.debounceScheduled = false;
this.lock = new ReentrantLock();

this.jdbi = jdbi;
this.teamDAO = jdbi.onDemand(TeamDAO.class);
this.stationDAO = jdbi.onDemand(StationDAO.class);
this.batonSwitchoverDAO = jdbi.onDemand(BatonSwitchoverDAO.class);
this.newDetections = new ArrayList<>();
this.detectionLock = new ReentrantLock(); // TODO: Right kind of lock?

this.positionSender = new PositionSender();

new Thread(this::fetch);
new Thread(this::calculatePosition);
}

// Update variables that depend on teams, stations and / or batonswitchover
private void fetch() {
private void calculatePosition() {
Set<Team> changedTeams = new HashSet<>(); // List of teams that have changed station
while (true) {
List<Team> teams = teamDAO.getAll();
List<BatonSwitchover> switchovers = batonSwitchoverDAO.getAll();
List<Station> stations = stationDAO.getAll();

lock.lock();
changedTeams.clear();
detectionLock.lock();
for (Detection detection: newDetections) {
if (batonToTeam.containsKey(detection.getBatonId())) {
Team team = batonToTeam.get(detection.getBatonId());
if (teamData.containsKey(team) && teamData.get(team).addDetection(detection)) {
changedTeams.add(team);
}
}
}
detectionLock.unlock(); // Use lock as short as possible

teamDetections = teams.stream()
.collect(
Collectors.toMap(
team -> team,
team -> teamDetections.getOrDefault(team, new CircularPriorityQueue(MAX_SIZE))
)
);
teamData = teams.stream()
.collect(
Collectors.toMap(
team -> team,
team -> teamData.getOrDefault(team, new TeamData(team.getId()))
)
);
batonIdToTeam = switchovers.stream()
.collect(
Collectors.toMap(
BatonSwitchover::getNewBatonId,
switchover -> teamDAO.getById(switchover.getTeamId()).get()
)
);
idToStation = stations.stream()
.collect(
Collectors.toMap(
Station::getId,
station -> station
)
);
if (!changedTeams.isEmpty()) {
// Update
for (Team team: changedTeams) {
teamData.get(team).updatePosition();
}

lock.unlock();
positionSender.send(
changedTeams.stream().map(team -> teamData.get(team).getPosition()).toList()
);
}

try {
Thread.sleep(INTERVAL_FETCH);
Thread.sleep(INTERVAL_CALCULATE);
} catch (InterruptedException e) {
logger.severe(e.getMessage());
}
}
}

// TODO: Add more detection filtering, high enough rssi, only one detection / timestamp, ...
// TODO: Calculate average times in separate thread
// TODO: If multiple detections come out of order -> restart
// TODO: If something in fetch changes -> restart

// TODO: Start simple, if arrives at new station -> send location and average time. Else send location given speed
private void calculatePositions() {
logger.info("Nostradamus: Calculating positions...");

for (Map.Entry<Team, CircularPriorityQueue> entry: teamDetections.entrySet()) {
Map<Integer, Float> averageTimes = new HashMap<>();

int lastStationid = -1;
long currentStationTime = 0;
int currentStationRssi = MIN_RSSI;
int currentStationId = 0;

for (Detection detection: entry.getValue()) {
if (detection.getTimestamp().getTime() - currentStationTime < INTERVAL) {
// Same interval
// Keep station with the highest RSSI
if (detection.getRssi() > currentStationRssi) {
currentStationId = detection.getStationId();
currentStationRssi = detection.getRssi();
}
} else {
// New interval
// Save old station id
lastStationid = currentStationId;
currentStationTime = detection.getTimestamp().getTime();
currentStationRssi = detection.getRssi();
currentStationId = detection.getStationId();
}
}

// Keep result of last interval if it exists
Station currentStation = idToStation.getOrDefault(lastStationid, idToStation.get(currentStationId));


}

positionSender.send(teamData.values().stream().map(TeamData::getPosition).toList());
logger.info("Nostradamus: Done calculating positions");
}

@Override
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);
if (detection.getRssi() > MIN_RSSI) {
detectionLock.lock();
newDetections.add(detection);
detectionLock.unlock();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package telraam.logic.positioner.nostradamus;

import lombok.Getter;
import telraam.database.models.Detection;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Comparator;

public class PositionList extends ArrayList<Detection> {

private final int interval;
@Getter
private Detection currentPosition;
private Timestamp newestDetection;

public PositionList(int interval) {
this.interval = interval;
this.currentPosition = new Detection(-1, 0, -100);
this.newestDetection = new Timestamp(0);
}

/**
* @param e element to add
* @return True if the current position has changed
*/
@Override
public boolean add(Detection e) {
super.add(e);

if (e.getTimestamp().after(newestDetection)) {
newestDetection = e.getTimestamp();
}

if (!e.getStationId().equals(currentPosition.getStationId())) {
// Possible new position
if (e.getRssi() > currentPosition.getRssi() || !inInterval(currentPosition.getTimestamp(), newestDetection)) {
// Detection stored in currentPosition will change
int oldPosition = currentPosition.getStationId();
// Filter out old detections
removeIf(detection -> !inInterval(detection.getTimestamp(), newestDetection));
// Get new position
currentPosition = stream().max(Comparator.comparing(Detection::getRssi)).get();

return oldPosition != currentPosition.getStationId();
}
}

return false;
}

private boolean inInterval(Timestamp oldest, Timestamp newest) {
return newest.getNanos() - oldest.getNanos() > interval;
}
}
52 changes: 44 additions & 8 deletions src/main/java/telraam/logic/positioner/nostradamus/TeamData.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
package telraam.logic.positioner.nostradamus;

import lombok.Getter;
import lombok.Setter;
import telraam.database.models.Detection;
import telraam.database.models.Station;
import telraam.logic.positioner.Position;

import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Getter @Setter
public class TeamData {
private PositionList detections;
private List<Station> stations; // Station list, ordered by distance from start
private Map<Integer, List<Long>> averageTimes; // List of average times for each station. Going from station 2 to 3 is saved in 3.
private long previousStationArrival; // Arrival time of previous station. Used to calculate the average times
@Getter
private Position position;
private Station lastStation;
private Timestamp lastUpdate;
private Station currentStation;
private int totalDistance;

public TeamData(int teamId) {

public TeamData(int teamId, int interval, List<Station> stations, int averageAmount) {
this.detections = new PositionList(interval);
this.stations = stations;

averageTimes = new HashMap<>();
for (Station station: stations) {
averageTimes.put(station.getId(), new CircularQueue<>(averageAmount));
}

this.previousStationArrival = System.currentTimeMillis();
this.position = new Position(teamId);
this.lastStation = null;
this.lastUpdate = null;
}

public boolean addDetection(Detection e) {
boolean newStation = detections.add(e);
Station previousStation = detections.getCurrentPosition().getStationId()

// TODO: If station missed big problem
if (newStation && averageTimes.containsKey(detections.getCurrentPosition())) {
long now = System.currentTimeMillis();
averageTimes.get(detections.getCurrentPosition()).add(now - previousStationArrival);
previousStationArrival = now;
}

return newStation;
}

// TODO: Requires the last station to be on the finish. Convert to variable
public void updatePosition() {
float progress = (float) (currentStation.getDistanceFromStart() / totalDistance);
Station nextStation = stations.get(stations.indexOf(currentStation) + 1 % stations.size());



position.setProgress(progress);
}
}