5 Commits

8 changed files with 305 additions and 83 deletions

View File

@@ -1,8 +1,9 @@
name: Java CI with Maven name: Java CI with Maven
on: on:
workflow_dispatch:
push: push:
branches: [ "main" ] branches: [ "dev", "cleanup" ]
tags: tags:
- 'v*.*.*' - 'v*.*.*'
pull_request: pull_request:
@@ -11,51 +12,93 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
cache: maven cache: maven
- name: Build with Maven - name: Build with Maven
run: mvn -B package run: mvn -B package
working-directory: main working-directory: main
- name: Upload built JAR - name: Upload built JAR
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: package name: package
path: main/target/*.jar path: main/target/*.jar
- name: Generate dependency graph - name: Generate dependency graph
run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph run: mvn -B -f main/pom.xml com.github.ferstl:depgraph-maven-plugin:4.0.1:graph
- name: Upload dependency graph artifact - name: Upload dependency graph artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: dependency-graph name: dependency-graph
path: main/target/** path: main/target/**
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven (Skip Tests)
run: mvn -B package -DskipTests
working-directory: main
- name: Create JPackage App Image
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "dist"
jpackage --name "DTSS" `
--input main/target `
--main-jar main-1.0-SNAPSHOT.jar `
--dest dist `
--type app-image `
--win-console
- name: Inject java.exe
shell: pwsh
run: |
$javaPath = (Get-Command java).Source
Copy-Item -Path $javaPath -Destination "dist/DTSS/runtime/bin/"
- name: Zip Windows Release
shell: pwsh
run: |
Compress-Archive -Path "dist/DTSS" -DestinationPath "dist/DTSS-Windows.zip"
- name: Upload Windows Artifact
uses: actions/upload-artifact@v4
with:
name: windows-package
path: dist/DTSS-Windows.zip
publish-release: publish-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build] needs: [build, build-windows]
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Download built JAR - name: Download Linux JAR
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: package name: package
path: main/target/ path: main/target/
- name: Download Windows Zip
uses: actions/download-artifact@v4
with:
name: windows-package
path: windows-dist/
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: main/target/*.jar tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'snapshot-build' }}
name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'Manual Snapshot Build' }}
draft: false
prerelease: true
make_latest: false
files: |
main/target/*.jar
windows-dist/*.zip

View File

@@ -49,6 +49,7 @@ public class IntersectionProcess {
private final ExecutorService trafficLightPool; private final ExecutorService trafficLightPool;
private ScheduledExecutorService statsExecutor; private ScheduledExecutorService statsExecutor;
private ScheduledExecutorService departureExecutor;
private volatile boolean running; // Quando uma thread escreve um valor volatile, todas as outras private volatile boolean running; // Quando uma thread escreve um valor volatile, todas as outras
// threads veem a mudança imediatamente. // threads veem a mudança imediatamente.
@@ -86,6 +87,7 @@ public class IntersectionProcess {
this.connectionHandlerPool = Executors.newCachedThreadPool(); this.connectionHandlerPool = Executors.newCachedThreadPool();
this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions this.trafficLightPool = Executors.newFixedThreadPool(4); // Max 4 directions
this.statsExecutor = Executors.newSingleThreadScheduledExecutor(); this.statsExecutor = Executors.newSingleThreadScheduledExecutor();
this.departureExecutor = Executors.newScheduledThreadPool(4);
this.running = false; this.running = false;
this.trafficCoordinationLock = new ReentrantLock(true); // Fair lock to prevent starvation this.trafficCoordinationLock = new ReentrantLock(true); // Fair lock to prevent starvation
this.currentGreenDirection = null; this.currentGreenDirection = null;
@@ -270,32 +272,49 @@ public class IntersectionProcess {
public void sendVehicleToNextDestination(Vehicle vehicle) { public void sendVehicleToNextDestination(Vehicle vehicle) {
String nextDestination = vehicle.getCurrentDestination(); String nextDestination = vehicle.getCurrentDestination();
try { // Calculate travel time
// Get or create connection to next destination double baseTime = config.getBaseTravelTime();
SocketConnection connection = getOrCreateConnection(nextDestination); double multiplier = 1.0;
switch (vehicle.getType()) {
// Create and send message using Message class case BIKE -> multiplier = config.getBikeTravelTimeMultiplier();
MessageProtocol message = new Message( case HEAVY -> multiplier = config.getHeavyTravelTimeMultiplier();
MessageType.VEHICLE_TRANSFER, default -> multiplier = 1.0;
intersectionId,
nextDestination,
vehicle,
System.currentTimeMillis());
connection.sendMessage(message);
System.out.println("[" + intersectionId + "] Sent vehicle " + vehicle.getId() +
" to " + nextDestination);
// Record departure for statistics
recordVehicleDeparture();
// Note: vehicle route is advanced when it arrives at the next intersection
} catch (IOException | InterruptedException e) {
System.err.println("[" + intersectionId + "] Failed to send vehicle " +
vehicle.getId() + " to " + nextDestination + ": " + e.getMessage());
} }
double travelTime = baseTime * multiplier;
long travelTimeMs = (long) (travelTime * 1000);
System.out.printf("[%s] Vehicle %s departing to %s. Travel time: %.2fs%n",
intersectionId, vehicle.getId(), nextDestination, travelTime);
// Record departure immediately as it leaves the intersection
recordVehicleDeparture();
// Schedule the arrival at the next node
departureExecutor.schedule(() -> {
try {
// Get or create connection to next destination
SocketConnection connection = getOrCreateConnection(nextDestination);
// Create and send message using Message class
MessageProtocol message = new Message(
MessageType.VEHICLE_TRANSFER,
intersectionId,
nextDestination,
vehicle,
System.currentTimeMillis());
connection.sendMessage(message);
System.out.println("[" + intersectionId + "] Vehicle " + vehicle.getId() +
" arrived at " + nextDestination + " (msg sent)");
// Note: vehicle route is advanced when it arrives at the next intersection
} catch (IOException | InterruptedException e) {
System.err.println("[" + intersectionId + "] Failed to send vehicle " +
vehicle.getId() + " to " + nextDestination + ": " + e.getMessage());
}
}, travelTimeMs, TimeUnit.MILLISECONDS);
} }
/** /**
@@ -540,6 +559,9 @@ public class IntersectionProcess {
if (statsExecutor != null && !statsExecutor.isShutdown()) { if (statsExecutor != null && !statsExecutor.isShutdown()) {
statsExecutor.shutdownNow(); statsExecutor.shutdownNow();
} }
if (departureExecutor != null && !departureExecutor.isShutdown()) {
departureExecutor.shutdownNow();
}
// 3. Wait briefly for termination (don't block forever) // 3. Wait briefly for termination (don't block forever)
try { try {
@@ -552,6 +574,9 @@ public class IntersectionProcess {
if (statsExecutor != null) { if (statsExecutor != null) {
statsExecutor.awaitTermination(1, TimeUnit.SECONDS); statsExecutor.awaitTermination(1, TimeUnit.SECONDS);
} }
if (departureExecutor != null) {
departureExecutor.awaitTermination(1, TimeUnit.SECONDS);
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }

View File

@@ -373,7 +373,7 @@ public class SimulationConfig {
* @return The multiplier for heavy vehicle travel time. * @return The multiplier for heavy vehicle travel time.
*/ */
public double getHeavyTravelTimeMultiplier() { public double getHeavyTravelTimeMultiplier() {
return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "2.0")); return Double.parseDouble(properties.getProperty("vehicle.travel.time.heavy.multiplier", "4.0"));
} }
// --- Statistics --- // --- Statistics ---

View File

@@ -333,7 +333,7 @@ public class DashboardUI extends Application {
updateScheduler = Executors.newSingleThreadScheduledExecutor(); updateScheduler = Executors.newSingleThreadScheduledExecutor();
updateScheduler.scheduleAtFixedRate(() -> { updateScheduler.scheduleAtFixedRate(() -> {
Platform.runLater(this::updateUI); Platform.runLater(this::updateUI);
}, 0, 5, TimeUnit.SECONDS); }, 0, 100, TimeUnit.MILLISECONDS);
} }
private void updateUI() { private void updateUI() {

View File

@@ -95,18 +95,24 @@ public class SimulationProcessManager {
builder = new ProcessBuilder(javaBin, "-cp", classpath, className); builder = new ProcessBuilder(javaBin, "-cp", classpath, className);
} }
// this is a linux thing - not sure about windows // get the OS temp folder
// Linux: /tmp/
// Windows: %AppData%\Local\Temp\
String tempDir = System.getProperty("java.io.tmpdir");
String logName = className.substring(className.lastIndexOf('.') + 1) + (arg != null ? "-" + arg : "") + ".log"; String logName = className.substring(className.lastIndexOf('.') + 1) + (arg != null ? "-" + arg : "") + ".log";
File logFile = new File("/tmp/" + logName);
// use the (File parent, String child) constructor to handle slash/backslash
// automatically
File logFile = new File(tempDir, logName);
builder.redirectOutput(logFile); builder.redirectOutput(logFile);
builder.redirectError(logFile); builder.redirectError(logFile);
Process process = builder.start(); Process process = builder.start();
runningProcesses.add(process); runningProcesses.add(process);
System.out.println("Started " + className + (arg != null ? " " + arg : "")); System.out.println("Started " + className + (arg != null ? " " + arg : ""));
} // print where the logs are actually going
System.out.println("Logs redirected to: " + logFile.getAbsolutePath());
public boolean isSimulationRunning() {
return !runningProcesses.isEmpty() && runningProcesses.stream().anyMatch(Process::isAlive);
} }
} }

View File

@@ -31,7 +31,7 @@ dashboard.port=9000
# === SIMULATION CONFIGURATION === # === SIMULATION CONFIGURATION ===
# Total duration in seconds (3600 = 1 hour) # Total duration in seconds (3600 = 1 hour)
simulation.duration=60.0 simulation.duration=3600
# Vehicle arrival model: FIXED or POISSON # Vehicle arrival model: FIXED or POISSON
simulation.arrival.model=POISSON simulation.arrival.model=POISSON
@@ -47,44 +47,33 @@ simulation.arrival.fixed.interval=2.0
# Format: trafficlight.<intersection>.<direction>.<state>=<seconds> # Format: trafficlight.<intersection>.<direction>.<state>=<seconds>
# Intersection 1 (Entry point - balanced) # Intersection 1 (Entry point - balanced)
trafficlight.Cr1.South.green=20.0 trafficlight.Cr1.South.green=60.0
trafficlight.Cr1.South.red=40.0 trafficlight.Cr1.South.red=5.0
trafficlight.Cr1.East.green=20.0 trafficlight.Cr1.East.green=60.0
trafficlight.Cr1.East.red=40.0 trafficlight.Cr1.East.red=5.0
trafficlight.Cr1.West.green=20.0
trafficlight.Cr1.West.red=40.0
# Intersection 2 (Main hub - shorter cycles, favor East-West) # Intersection 2 (Main hub - shorter cycles, favor East-West)
trafficlight.Cr2.South.green=12.0 trafficlight.Cr2.South.green=60.0
trafficlight.Cr2.South.red=36.0 trafficlight.Cr2.South.red=5.0
trafficlight.Cr2.East.green=18.0 trafficlight.Cr2.East.green=60.0
trafficlight.Cr2.East.red=30.0 trafficlight.Cr2.East.red=5.0
trafficlight.Cr2.West.green=18.0 trafficlight.Cr2.West.green=60.0
trafficlight.Cr2.West.red=30.0 trafficlight.Cr2.West.red=5.0
# Intersection 3 (Path to exit - favor East) # Intersection 3 (Path to exit - favor East)
trafficlight.Cr3.South.green=15.0 trafficlight.Cr3.South.green=60.0
trafficlight.Cr3.South.red=30.0 trafficlight.Cr3.South.red=5.0
trafficlight.Cr3.East.green=20.0 trafficlight.Cr3.West.green=60.0
trafficlight.Cr3.East.red=25.0 trafficlight.Cr3.West.red=5.0
trafficlight.Cr3.West.green=15.0
trafficlight.Cr3.West.red=30.0
# Intersection 4 (Favor East toward Cr5) # Intersection 4 (Favor East toward Cr5)
trafficlight.Cr4.South.green=15.0 trafficlight.Cr4.East.green=60.0
trafficlight.Cr4.South.red=30.0 trafficlight.Cr4.East.red=5.0
trafficlight.Cr4.East.green=20.0
trafficlight.Cr4.East.red=25.0
trafficlight.Cr4.West.green=15.0
trafficlight.Cr4.West.red=30.0
# Intersection 5 (Near exit - favor East) # Intersection 5 (Near exit - favor East)
trafficlight.Cr5.South.green=15.0 trafficlight.Cr5.East.green=60.0
trafficlight.Cr5.South.red=30.0 trafficlight.Cr5.East.red=5.0
trafficlight.Cr5.East.green=22.0
trafficlight.Cr5.East.red=23.0
trafficlight.Cr5.West.green=15.0
trafficlight.Cr5.West.red=30.0
# === VEHICLE CONFIGURATION === # === VEHICLE CONFIGURATION ===
# Probability distribution for vehicle types (must sum to 1.0) # Probability distribution for vehicle types (must sum to 1.0)
@@ -99,13 +88,13 @@ vehicle.crossing.time.heavy=4.0
# Travel times between intersections (in seconds) # Travel times between intersections (in seconds)
# Base time for light vehicles (cars) # Base time for light vehicles (cars)
vehicle.travel.time.base=8.0 vehicle.travel.time.base=1.0
# Bike travel time = 0.5 × car travel time # Bike travel time = 0.5 × car travel time
vehicle.travel.time.bike.multiplier=0.5 vehicle.travel.time.bike.multiplier=0.5
# Heavy vehicle travel time = 4 × bike travel time # Heavy vehicle travel time = 4.0 x base travel time
vehicle.travel.time.heavy.multiplier=2.0 vehicle.travel.time.heavy.multiplier=4.0
# === STATISTICS === # === STATISTICS ===
# Interval between dashboard updates (seconds) # Interval between dashboard updates (seconds)
statistics.update.interval=1.0 statistics.update.interval=0.1

View File

@@ -0,0 +1,159 @@
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;
import sd.IntersectionProcess;
import sd.model.Message;
import sd.model.MessageType;
import sd.model.Vehicle;
import sd.model.VehicleType;
import sd.protocol.SocketConnection;
public class TravelTimeTest {
@TempDir
Path tempDir;
private Path configFile;
private IntersectionProcess intersectionProcess;
private Thread serverThread;
@BeforeEach
public void setUp() throws IOException {
configFile = tempDir.resolve("test-simulation.properties");
String configContent = """
intersection.Cr1.host=localhost
intersection.Cr1.port=19001
intersection.Cr2.host=localhost
intersection.Cr2.port=19002
# Base travel time = 1.0s for testing
vehicle.travel.time.base=1.0
vehicle.travel.time.bike.multiplier=0.5
vehicle.travel.time.heavy.multiplier=4.0
# Dummy values for others
dashboard.host=localhost
dashboard.port=19100
exit.host=localhost
exit.port=19099
""";
Files.writeString(configFile, configContent);
}
@AfterEach
public void tearDown() {
if (intersectionProcess != null) {
intersectionProcess.shutdown();
}
if (serverThread != null) {
try {
serverThread.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Test
@Timeout(10)
public void testVariableTravelTimes() throws IOException, InterruptedException {
// Start Intersection Cr1
intersectionProcess = new IntersectionProcess("Cr1", configFile.toString());
// Mock network config for Cr1 to know about Cr2
// Since we can't easily inject network config without file, we rely on
// IntersectionProcess
// using the properties file we created. But wait, IntersectionProcess loads
// network_config.json
// from classpath. This might be an issue if we need custom routing.
// However, sendVehicleToNextDestination just looks up host/port from
// properties.
// We need to ensure getOrCreateConnection works.
// Let's manually inject the connection or just rely on properties.
// The properties file has intersection.Cr2.host/port, so it should work.
// Start a "fake" Cr2 server to receive the vehicle
BlockingQueue<Long> arrivalTimes = new LinkedBlockingQueue<>();
ServerSocket fakeCr2 = new ServerSocket(19002);
Thread cr2Thread = new Thread(() -> {
try {
Socket socket = fakeCr2.accept();
SocketConnection conn = new SocketConnection(socket);
while (!Thread.currentThread().isInterrupted()) {
try {
conn.receiveMessage();
arrivalTimes.offer(System.currentTimeMillis());
} catch (Exception e) {
break;
}
}
} catch (IOException e) {
// End
}
});
cr2Thread.start();
// Send vehicles from Cr1
// We need to call sendVehicleToNextDestination directly.
// But we need to initialize Cr1 first (at least the executor).
// We can't easily call initialize() because it tries to connect to dashboard
// etc.
// But the constructor initializes the executors!
// 1. Light Vehicle (Base = 1.0s)
Vehicle lightVehicle = new Vehicle("V_LIGHT", VehicleType.LIGHT, 0, Arrays.asList("Cr2"));
long startLight = System.currentTimeMillis();
intersectionProcess.sendVehicleToNextDestination(lightVehicle);
Long arrivalLight = arrivalTimes.poll(2000, TimeUnit.MILLISECONDS);
assertNotNull(arrivalLight, "Light vehicle should arrive");
long durationLight = arrivalLight - startLight;
System.out.println("Light Duration: " + durationLight + "ms");
assertTrue(durationLight >= 1000, "Light vehicle should take at least 1000ms");
assertTrue(durationLight < 1500, "Light vehicle should be close to 1000ms");
// 2. Bike (0.5 * 1.0 = 0.5s)
Vehicle bikeVehicle = new Vehicle("V_BIKE", VehicleType.BIKE, 0, Arrays.asList("Cr2"));
long startBike = System.currentTimeMillis();
intersectionProcess.sendVehicleToNextDestination(bikeVehicle);
Long arrivalBike = arrivalTimes.poll(2000, TimeUnit.MILLISECONDS);
assertNotNull(arrivalBike, "Bike should arrive");
long durationBike = arrivalBike - startBike;
System.out.println("Bike Duration: " + durationBike + "ms");
assertTrue(durationBike >= 500, "Bike should take at least 500ms");
assertTrue(durationBike < 1000, "Bike should be close to 500ms");
// 3. Heavy (4.0 * 1.0 = 4.0s)
Vehicle heavyVehicle = new Vehicle("V_HEAVY", VehicleType.HEAVY, 0, Arrays.asList("Cr2"));
long startHeavy = System.currentTimeMillis();
intersectionProcess.sendVehicleToNextDestination(heavyVehicle);
Long arrivalHeavy = arrivalTimes.poll(5000, TimeUnit.MILLISECONDS);
assertNotNull(arrivalHeavy, "Heavy vehicle should arrive");
long durationHeavy = arrivalHeavy - startHeavy;
System.out.println("Heavy Duration: " + durationHeavy + "ms");
assertTrue(durationHeavy >= 4000, "Heavy vehicle should take at least 4000ms");
assertTrue(durationHeavy < 4500, "Heavy vehicle should be close to 4000ms");
// Cleanup
fakeCr2.close();
cr2Thread.interrupt();
}
}

View File

@@ -133,8 +133,8 @@ public class TrafficLightCoordinationTest {
List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights(); List<TrafficLight> lights = intersectionProcess.getIntersection().getTrafficLights();
boolean[] hasBeenGreen = new boolean[lights.size()]; boolean[] hasBeenGreen = new boolean[lights.size()];
// Monitor for 60 seconds (enough time for all lights to cycle: 18+18+12 = 48s) // Monitor for 10 seconds (enough time for all lights to cycle: 18+18+12 = 48s)
long endTime = System.currentTimeMillis() + 60000; long endTime = System.currentTimeMillis() + 10000;
while (System.currentTimeMillis() < endTime) { while (System.currentTimeMillis() < endTime) {
for (int i = 0; i < lights.size(); i++) { for (int i = 0; i < lights.size(); i++) {