Apostolos Fanakis
7 years ago
commit
f0602812cf
15 changed files with 521 additions and 0 deletions
@ -0,0 +1,7 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<classpath> |
||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> |
||||
|
<classpathentry kind="src" path="src"/> |
||||
|
<classpathentry kind="lib" path="libs/ithakimodem.jar"/> |
||||
|
<classpathentry kind="output" path="bin"/> |
||||
|
</classpath> |
@ -0,0 +1,17 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<projectDescription> |
||||
|
<name>finalCode</name> |
||||
|
<comment></comment> |
||||
|
<projects> |
||||
|
</projects> |
||||
|
<buildSpec> |
||||
|
<buildCommand> |
||||
|
<name>org.eclipse.jdt.core.javabuilder</name> |
||||
|
<arguments> |
||||
|
</arguments> |
||||
|
</buildCommand> |
||||
|
</buildSpec> |
||||
|
<natures> |
||||
|
<nature>org.eclipse.jdt.core.javanature</nature> |
||||
|
</natures> |
||||
|
</projectDescription> |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,144 @@ |
|||||
|
import java.io.FileNotFoundException; |
||||
|
import java.io.PrintWriter; |
||||
|
import java.io.UnsupportedEncodingException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Scanner; |
||||
|
import java.util.regex.Matcher; |
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
import ithakimodem.Modem; |
||||
|
|
||||
|
public class ACK { |
||||
|
private final static String regex = ".*PSTART.*<(.*)> ([0-9]*) PSTOP."; |
||||
|
private final static String subst = "$1,$2"; |
||||
|
private final static Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); |
||||
|
|
||||
|
private static final Scanner input = new Scanner(System.in); |
||||
|
|
||||
|
public static void ack() { |
||||
|
System.out.print("Type request code for ack: "); |
||||
|
String ackRequestCode = input.nextLine(); |
||||
|
System.out.print("Type request code for nack: "); |
||||
|
String nackRequestCode = input.nextLine(); |
||||
|
System.out.print("Time to run: "); |
||||
|
int timeToRun = input.nextInt(); |
||||
|
input.nextLine(); |
||||
|
|
||||
|
getACK(ackRequestCode, nackRequestCode, timeToRun); |
||||
|
} |
||||
|
|
||||
|
private static int getACK(String ackRequestCode, String nackRequestCode, int numberOfSecondsToRun) { |
||||
|
long endTime = 0; |
||||
|
boolean lastCallHadErrors = false; |
||||
|
int packageNumber = 0, errorFreePackages = 0, errorPackages = 0, BER = 0; |
||||
|
ArrayList<byte[]> packageBytesWithErrors = new ArrayList<byte[]>(); |
||||
|
ArrayList<Integer> packageResponseTime = new ArrayList<Integer>(); |
||||
|
|
||||
|
PrintWriter writer = null; |
||||
|
try { |
||||
|
writer = new PrintWriter("ARQ_2.txt", "UTF-8"); |
||||
|
} catch (FileNotFoundException e) { |
||||
|
e.printStackTrace(); |
||||
|
return -1; |
||||
|
} catch (UnsupportedEncodingException e) { |
||||
|
e.printStackTrace(); |
||||
|
return -1; |
||||
|
} |
||||
|
|
||||
|
writer.println("[0] #No, [1] HasError, [2] Package, [3] Response time\n\n[0]\t[1]\t\t\t\t\t\t\t\t[2]\t\t\t\t\t\t\t\t[3]"); |
||||
|
|
||||
|
Modem modem = new Modem(); |
||||
|
modem.setSpeed(Utils.modemSpeed); |
||||
|
modem.setTimeout(Utils.modemTimeout); |
||||
|
modem.open(Utils.modemOpen); |
||||
|
|
||||
|
endTime = System.currentTimeMillis() + numberOfSecondsToRun * 1000; |
||||
|
while(System.currentTimeMillis() < endTime) { |
||||
|
long packageStartTime = 0, packageEndTime = 0; |
||||
|
String response = ""; |
||||
|
|
||||
|
packageStartTime = System.currentTimeMillis(); |
||||
|
if (!lastCallHadErrors) { |
||||
|
response = Utils.makeRequest(modem, ackRequestCode); |
||||
|
packageEndTime = System.currentTimeMillis(); |
||||
|
packageResponseTime.add((int)(packageEndTime - packageStartTime - Utils.modemTimeout)); |
||||
|
} else { |
||||
|
response = Utils.makeRequest(modem, nackRequestCode); |
||||
|
packageEndTime = System.currentTimeMillis(); |
||||
|
packageResponseTime.add((int)(packageEndTime - packageStartTime - Utils.modemTimeout)); |
||||
|
} |
||||
|
|
||||
|
Matcher matcher = pattern.matcher(response); |
||||
|
String[] packageAndFCS = (matcher.replaceAll(subst)).split(","); |
||||
|
if (packageAndFCS.length != 2 || packageAndFCS[0] == null || packageAndFCS[0].isEmpty() |
||||
|
|| packageAndFCS[1] == null || packageAndFCS[1].isEmpty()) { |
||||
|
System.out.println("Malformed response: " + response); |
||||
|
continue; |
||||
|
} |
||||
|
byte[] packageBytes = packageAndFCS[0].getBytes(); |
||||
|
byte xor = packageBytes[0]; |
||||
|
for (int i=1; i<packageBytes.length; ++i){ |
||||
|
xor = (byte) (xor^packageBytes[i]); |
||||
|
} |
||||
|
|
||||
|
System.out.print("Package xor = " + xor + " came with xor code: " + packageAndFCS[1]); |
||||
|
if (xor != Integer.parseInt(packageAndFCS[1])) { |
||||
|
System.out.println(" Has errors"); |
||||
|
|
||||
|
writer.print(packageNumber + 1 + "\t >\t"); |
||||
|
writer.print(packageBytes[0]); |
||||
|
for (int i=1; i<packageBytes.length; ++i) { |
||||
|
writer.print("," + packageBytes[i]); |
||||
|
} |
||||
|
if (packageBytes.toString().length() < 56) { |
||||
|
writer.print("\t\t"); |
||||
|
} else { |
||||
|
writer.print("\t"); |
||||
|
} |
||||
|
writer.println(packageResponseTime.get(packageNumber)); |
||||
|
|
||||
|
packageBytesWithErrors.add(packageBytes); |
||||
|
++errorPackages; |
||||
|
lastCallHadErrors = true; |
||||
|
} else { |
||||
|
System.out.println(" No errors"); |
||||
|
|
||||
|
writer.print(packageNumber + 1 + "\t\t"); |
||||
|
writer.print(packageBytes[0]); |
||||
|
for (int i=1; i<packageBytes.length; ++i) { |
||||
|
writer.print("," + packageBytes[i]); |
||||
|
} |
||||
|
if (packageBytes.toString().length() < 56) { |
||||
|
writer.print("\t\t"); |
||||
|
} else { |
||||
|
writer.print("\t"); |
||||
|
} |
||||
|
writer.println(packageResponseTime.get(packageNumber)); |
||||
|
|
||||
|
for (byte[] errorPackage: packageBytesWithErrors) { |
||||
|
for (int i=0; i<packageBytes.length; ++i) { |
||||
|
int temp = errorPackage[i]^packageBytes[i]; |
||||
|
BER += Integer.bitCount(temp); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
packageBytesWithErrors.clear(); |
||||
|
++errorFreePackages; |
||||
|
lastCallHadErrors = false; |
||||
|
} |
||||
|
++packageNumber; |
||||
|
} |
||||
|
|
||||
|
writer.close(); |
||||
|
modem.close(); |
||||
|
|
||||
|
float allPackages = errorFreePackages + errorPackages; |
||||
|
System.out.println("Number of packages aquired: " + allPackages); |
||||
|
System.out.println("Error free packages = " + errorFreePackages + " --> " + (errorFreePackages / allPackages * 100)); |
||||
|
System.out.println("Error packages = " + errorPackages + " --> " + (errorPackages / allPackages * 100)); |
||||
|
System.out.println("Error bits = " + BER); |
||||
|
System.out.println("BER = " + ((float) BER)/((float)((allPackages) * 16 * 8))); |
||||
|
|
||||
|
return 1; |
||||
|
} |
||||
|
} |
@ -0,0 +1,66 @@ |
|||||
|
import java.io.FileNotFoundException; |
||||
|
import java.io.FileOutputStream; |
||||
|
import java.io.IOException; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
import ithakimodem.Modem; |
||||
|
|
||||
|
public class Camera { |
||||
|
private static final Scanner input = new Scanner(System.in); |
||||
|
|
||||
|
public static void camera() { |
||||
|
System.out.print("Type request code: "); |
||||
|
String requestCode = input.nextLine(); |
||||
|
System.out.print("Image filename (if a file with the same name already exists" |
||||
|
+ " it will be overwritten): "); |
||||
|
String filename = input.nextLine(); |
||||
|
|
||||
|
getImage(requestCode, filename); |
||||
|
} |
||||
|
|
||||
|
private static boolean getImage(String requestCode, String imageFilaname) { |
||||
|
String imageString; |
||||
|
|
||||
|
Modem modem = new Modem(); |
||||
|
modem.setSpeed(Utils.modemSpeed); |
||||
|
modem.setTimeout(Utils.modemTimeout); |
||||
|
modem.open(Utils.modemOpen); |
||||
|
|
||||
|
{ |
||||
|
String response = ""; |
||||
|
response = Utils.makeRequest(modem, requestCode); |
||||
|
if (response == null || response.isEmpty()) { |
||||
|
return false; |
||||
|
} |
||||
|
int startIndex = response.indexOf("" + (char) 255 + (char) 216), |
||||
|
endIndex = response.lastIndexOf("" + (char) 255 + (char) 217) + 2; |
||||
|
if (startIndex == -1 || endIndex == -1) { |
||||
|
return false; |
||||
|
} |
||||
|
imageString = response.substring(startIndex, endIndex); |
||||
|
} |
||||
|
|
||||
|
FileOutputStream fileOutputStream; |
||||
|
try { |
||||
|
fileOutputStream = new FileOutputStream(imageFilaname, false); |
||||
|
|
||||
|
char imageCharArray[] = imageString.toCharArray(); |
||||
|
byte imageByteArray[] = new byte[imageCharArray.length]; |
||||
|
for (int i=0; i<imageByteArray.length; ++i) { |
||||
|
imageByteArray[i] = (byte)((int) imageCharArray[i]); |
||||
|
} |
||||
|
fileOutputStream.write(imageByteArray, 0, imageByteArray.length); |
||||
|
fileOutputStream.flush(); |
||||
|
fileOutputStream.close(); |
||||
|
} catch (FileNotFoundException e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} catch (IOException e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
modem.close(); |
||||
|
return true; |
||||
|
} |
||||
|
} |
@ -0,0 +1,37 @@ |
|||||
|
import java.util.Scanner; |
||||
|
|
||||
|
public class ComputerNetworks { |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
final Scanner input = new Scanner(System.in); |
||||
|
String functionChoosen; |
||||
|
|
||||
|
System.out.print("Available functions\n" |
||||
|
+ "\t[1]: Ping ithaki server (ping)\n" |
||||
|
+ "\t[2]: Get camera image with or without error (cam)\n" |
||||
|
+ "\t[3]: Get GPS route image (gps)\n" |
||||
|
+ "\t[4]: Simulate ARQ communication (arq)\n" |
||||
|
+ "\t[0]: Exit (exit)\n" |
||||
|
+ "To choose, type either the number in the brackets or" |
||||
|
+ " the word in parentheses.\n"); |
||||
|
while(true) { |
||||
|
System.out.print("\nSelect funtcion: "); |
||||
|
functionChoosen = input.nextLine(); |
||||
|
if ("ping".equals(functionChoosen) || "1".equals(functionChoosen)) { |
||||
|
Ping.ping(); |
||||
|
} else if ("cam".equals(functionChoosen) || "2".equals(functionChoosen)) { |
||||
|
Camera.camera(); |
||||
|
} else if ("gps".equals(functionChoosen) || "3".equals(functionChoosen)) { |
||||
|
GPS.gps(); |
||||
|
} else if ("arq".equals(functionChoosen) || "4".equals(functionChoosen)) { |
||||
|
ACK.ack(); |
||||
|
} else if ("exit".equals(functionChoosen) || "0".equals(functionChoosen)) { |
||||
|
break; |
||||
|
} else { |
||||
|
System.out.println("Wrong usage! Please be carefull of the syntax."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
input.close(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,162 @@ |
|||||
|
import java.io.FileNotFoundException; |
||||
|
import java.io.FileOutputStream; |
||||
|
import java.io.IOException; |
||||
|
import java.io.PrintWriter; |
||||
|
import java.io.UnsupportedEncodingException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
import java.util.Scanner; |
||||
|
import java.util.regex.Matcher; |
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
import ithakimodem.Modem; |
||||
|
|
||||
|
public class GPS { |
||||
|
private static final String extractGPGGALineRegex = ".*?(\\$.*)\\nSTOP.*"; |
||||
|
private static final String extractGPGGALineSubst = "$1"; |
||||
|
private static final Pattern extractGPGGALinePattern = Pattern.compile(extractGPGGALineRegex, Pattern.DOTALL); |
||||
|
|
||||
|
private static final String extractCoordinateRegex = ".+?,.+?,([0-9]+).(.+?),N,0(.+?).([0-9]+?),.+"; |
||||
|
private static final String extractCoordinateSubst = "$1,$2,$3,$4"; |
||||
|
private static final Pattern extractCoordinatePattern = Pattern.compile(extractCoordinateRegex); |
||||
|
|
||||
|
private static final Scanner input = new Scanner(System.in); |
||||
|
|
||||
|
public static void gps() { |
||||
|
System.out.print("Type request code for the GPS : "); |
||||
|
String requestCode = input.nextLine(); |
||||
|
System.out.print("Type any additional flags for the request: "); |
||||
|
String requestFlags = input.nextLine(); |
||||
|
System.out.print("Enter number of coordinates to extract: "); |
||||
|
int numberOfCoordinates = input.nextInt(); |
||||
|
|
||||
|
List<String> coordinates = getCoordinates(requestCode, requestFlags, numberOfCoordinates); |
||||
|
|
||||
|
if (coordinates == null || coordinates.isEmpty()) { |
||||
|
System.out.println("Came with an empty List!"); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
input.nextLine(); //Clear input buffer
|
||||
|
System.out.print("Should try to fetch the image? (y/n): "); |
||||
|
String fetchImage = input.nextLine(); |
||||
|
while(true) { |
||||
|
if ("y".equals(fetchImage)) { |
||||
|
System.out.print("Image filename (if a file with the same name already exists" |
||||
|
+ " it will be overwritten): "); |
||||
|
String filename = input.nextLine(); |
||||
|
|
||||
|
getImage(requestCode, coordinates, filename); |
||||
|
|
||||
|
return; |
||||
|
} else if ("n".equals(fetchImage)) { |
||||
|
return; |
||||
|
} |
||||
|
System.out.print("Input malformed. Try again: "); |
||||
|
fetchImage = input.nextLine(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static List<String> getCoordinates(String requestCode, String requestFlag, int coordinatesToExtract) { |
||||
|
List<String> coordinates, returnList = new ArrayList<String>(); |
||||
|
String response = ""; |
||||
|
|
||||
|
PrintWriter writer = null; |
||||
|
try { |
||||
|
writer = new PrintWriter("gps.txt", "UTF-8"); |
||||
|
} catch (FileNotFoundException e) { |
||||
|
e.printStackTrace(); |
||||
|
return null; |
||||
|
} catch (UnsupportedEncodingException e) { |
||||
|
e.printStackTrace(); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
Modem modem = new Modem(); |
||||
|
modem.setSpeed(Utils.modemSpeed); |
||||
|
modem.setTimeout(Utils.modemTimeout); |
||||
|
modem.open(Utils.modemOpen); |
||||
|
|
||||
|
response = Utils.makeRequest(modem, requestCode + requestFlag); |
||||
|
modem.close(); |
||||
|
|
||||
|
if (response == null || response.isEmpty()) { |
||||
|
writer.close(); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
final Matcher extractGPGGALineMatcher = extractGPGGALinePattern.matcher(response); |
||||
|
coordinates = new ArrayList<String>(Arrays.asList(extractGPGGALineMatcher.replaceAll(extractGPGGALineSubst).split("\n"))); |
||||
|
for (String coordinate: coordinates) { |
||||
|
writer.println(coordinate); |
||||
|
} |
||||
|
writer.close(); |
||||
|
writer.println(response); |
||||
|
|
||||
|
for (int i=0; i<coordinates.size(); i+=(int)((coordinates.size() - 1)/ coordinatesToExtract)) { |
||||
|
String rawCoordinate = coordinates.get(i); |
||||
|
final Matcher extractCoordinateMatcher = extractCoordinatePattern.matcher(rawCoordinate); |
||||
|
String extractedCoordinate = extractCoordinateMatcher.replaceAll(extractCoordinateSubst); |
||||
|
|
||||
|
String[] coordinateSplitArray = extractedCoordinate.split(","); |
||||
|
String proccessedCoordinate = coordinateSplitArray[2]; |
||||
|
proccessedCoordinate += (int)(Integer.parseInt(coordinateSplitArray[3].substring(0, 4)) * 0.006); |
||||
|
proccessedCoordinate += coordinateSplitArray[0]; |
||||
|
proccessedCoordinate += (int)(Integer.parseInt(coordinateSplitArray[1]) * 0.006); |
||||
|
returnList.add(proccessedCoordinate); |
||||
|
} |
||||
|
|
||||
|
return returnList; |
||||
|
} |
||||
|
|
||||
|
private static boolean getImage(String requestCode, List<String> coordinates, String imageFilename) { |
||||
|
String imageString; |
||||
|
|
||||
|
for (String coordinate : coordinates) { |
||||
|
requestCode += "T=" + coordinate; |
||||
|
} |
||||
|
|
||||
|
Modem modem = new Modem(); |
||||
|
modem.setSpeed(Utils.modemSpeed); |
||||
|
modem.setTimeout(Utils.modemTimeout); |
||||
|
modem.open(Utils.modemOpen); |
||||
|
|
||||
|
{ |
||||
|
String response = ""; |
||||
|
response = Utils.makeRequest(modem, requestCode); |
||||
|
if (response == null || response.isEmpty()) { |
||||
|
return false; |
||||
|
} |
||||
|
int startIndex = response.indexOf("" + (char) 255 + (char) 216), |
||||
|
endIndex = response.lastIndexOf("" + (char) 255 + (char) 217) + 2; |
||||
|
if (startIndex == -1 || endIndex == -1) { |
||||
|
return false; |
||||
|
} |
||||
|
imageString = response.substring(startIndex, endIndex); |
||||
|
} |
||||
|
|
||||
|
FileOutputStream fileOutputStream; |
||||
|
try { |
||||
|
fileOutputStream = new FileOutputStream(imageFilename, false); |
||||
|
|
||||
|
char imageCharArray[] = imageString.toCharArray(); |
||||
|
byte imageByteArray[] = new byte[imageCharArray.length]; |
||||
|
for (int i=0; i<imageByteArray.length; ++i) { |
||||
|
imageByteArray[i] = (byte)((int) imageCharArray[i]); |
||||
|
} |
||||
|
fileOutputStream.write(imageByteArray, 0, imageByteArray.length); |
||||
|
fileOutputStream.flush(); |
||||
|
fileOutputStream.close(); |
||||
|
} catch (FileNotFoundException e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} catch (IOException e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
modem.close(); |
||||
|
return true; |
||||
|
} |
||||
|
} |
@ -0,0 +1,56 @@ |
|||||
|
import java.io.FileNotFoundException; |
||||
|
import java.io.PrintWriter; |
||||
|
import java.io.UnsupportedEncodingException; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
import ithakimodem.Modem; |
||||
|
|
||||
|
public class Ping { |
||||
|
private static final Scanner input = new Scanner(System.in); |
||||
|
|
||||
|
public static void ping() { |
||||
|
System.out.print("Type request code: "); |
||||
|
String requestCode = input.nextLine(); |
||||
|
System.out.print("Enter number of packages to request: "); |
||||
|
int numberOfPackages = input.nextInt(); |
||||
|
input.nextLine(); //Clear input buffer
|
||||
|
|
||||
|
pingServer(requestCode, numberOfPackages); |
||||
|
} |
||||
|
|
||||
|
private static boolean pingServer(String requestCode, int numberOfPackages) { |
||||
|
int[] timeDelayArray = new int[numberOfPackages]; |
||||
|
long startTime = 0, endTime = 0; |
||||
|
|
||||
|
PrintWriter writer = null; |
||||
|
try { |
||||
|
writer = new PrintWriter("ping.txt", "UTF-8"); |
||||
|
} catch (FileNotFoundException e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} catch (UnsupportedEncodingException e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
Modem modem = new Modem(); |
||||
|
modem.setSpeed(Utils.modemSpeed); |
||||
|
modem.setTimeout(Utils.modemTimeout); |
||||
|
modem.open(Utils.modemOpen); |
||||
|
|
||||
|
for(int i=0; i<numberOfPackages; ++i){ |
||||
|
String response = ""; |
||||
|
|
||||
|
startTime = System.currentTimeMillis(); |
||||
|
response = Utils.makeRequest(modem, requestCode); |
||||
|
endTime = System.currentTimeMillis(); |
||||
|
|
||||
|
timeDelayArray[i] = (int)(endTime - startTime); |
||||
|
writer.println(response + "\t" + timeDelayArray[i]); |
||||
|
} |
||||
|
modem.close(); |
||||
|
writer.close(); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
import ithakimodem.Modem; |
||||
|
|
||||
|
public class Utils { |
||||
|
static final int modemSpeed = 80000; |
||||
|
static final int modemTimeout = 1000; |
||||
|
static final String modemOpen = "ithaki"; |
||||
|
|
||||
|
static String makeRequest(Modem modem, String requestCode) { |
||||
|
int responseBuffer; |
||||
|
String response = ""; |
||||
|
|
||||
|
if (!modem.write((requestCode + "\r").getBytes())) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
while (true) { |
||||
|
try { |
||||
|
responseBuffer=modem.read(); |
||||
|
response += (char) responseBuffer; |
||||
|
|
||||
|
if (responseBuffer == -1) { |
||||
|
break; |
||||
|
} |
||||
|
} catch (Exception x) { |
||||
|
x.printStackTrace(); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return response; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue