From 4643c08cc270825ddc821fa555b12042febba51f Mon Sep 17 00:00:00 2001 From: Dmitry Isaenko Date: Sat, 9 May 2020 03:54:48 +0300 Subject: [PATCH] DarkMatterCore/nxdumptool support --- pom.xml | 2 +- src/main/java/nsusbloader/AppPreferences.java | 3 + .../Controllers/NSLMainController.java | 5 + .../Controllers/NxdtController.java | 133 +++++++ .../java/nsusbloader/MediatorControl.java | 1 + .../nsusbloader/NSLDataTypes/EModule.java | 3 +- src/main/java/nsusbloader/NSLMain.java | 2 +- .../java/nsusbloader/Utilities/NxdtTask.java | 60 ++++ .../nsusbloader/Utilities/NxdtUsbAbi1.java | 327 ++++++++++++++++++ src/main/resources/NSLMain.fxml | 8 + src/main/resources/NXDTab.fxml | 78 +++++ src/main/resources/locale.properties | 1 + src/main/resources/locale_rus.properties | 1 + src/main/resources/locale_ukr.properties | 3 +- src/main/resources/res/app_dark.css | 9 + src/main/resources/res/app_light.css | 10 +- src/main/resources/res/nxdt_icon.jpg | Bin 0 -> 32049 bytes 17 files changed, 641 insertions(+), 5 deletions(-) create mode 100644 src/main/java/nsusbloader/Controllers/NxdtController.java create mode 100644 src/main/java/nsusbloader/Utilities/NxdtTask.java create mode 100644 src/main/java/nsusbloader/Utilities/NxdtUsbAbi1.java create mode 100644 src/main/resources/NXDTab.fxml create mode 100644 src/main/resources/res/nxdt_icon.jpg diff --git a/pom.xml b/pom.xml index fd5943d..e529882 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ NS-USBloader ns-usbloader - 2.2.1-SNAPSHOT + 3.0-SNAPSHOT https://github.com/developersu/ns-usbloader/ diff --git a/src/main/java/nsusbloader/AppPreferences.java b/src/main/java/nsusbloader/AppPreferences.java index 4eaca73..3e009b8 100644 --- a/src/main/java/nsusbloader/AppPreferences.java +++ b/src/main/java/nsusbloader/AppPreferences.java @@ -152,4 +152,7 @@ public class AppPreferences { // RCM // public String getRecentRcm(int num){ return preferences.get(String.format("RCM_%02d", num), ""); } public void setRecentRcm(int num, String value){ preferences.put(String.format("RCM_%02d", num), value); } + // NXDT // + public String getNXDTSaveToLocation(){ return preferences.get("nxdt_saveto", System.getProperty("user.home")); } + public void setNXDTSaveToLocation(String value){ preferences.put("nxdt_saveto", value); } } diff --git a/src/main/java/nsusbloader/Controllers/NSLMainController.java b/src/main/java/nsusbloader/Controllers/NSLMainController.java index 07c800a..495be84 100644 --- a/src/main/java/nsusbloader/Controllers/NSLMainController.java +++ b/src/main/java/nsusbloader/Controllers/NSLMainController.java @@ -48,6 +48,8 @@ public class NSLMainController implements Initializable { private SplitMergeController SplitMergeTabController; @FXML private RcmController RcmTabController; + @FXML + private NxdtController NXDTabController; @Override public void initialize(URL url, ResourceBundle rb) { @@ -110,6 +112,8 @@ public class NSLMainController implements Initializable { } public RcmController getRcmCtrlr(){ return RcmTabController; } + + public NxdtController getNXDTabController(){ return NXDTabController; } /** * Save preferences before exit * */ @@ -135,5 +139,6 @@ public class NSLMainController implements Initializable { SplitMergeTabController.updatePreferencesOnExit(); // NOTE: This shit above should be re-written to similar pattern RcmTabController.updatePreferencesOnExit(); + NXDTabController.updatePreferencesOnExit(); } } diff --git a/src/main/java/nsusbloader/Controllers/NxdtController.java b/src/main/java/nsusbloader/Controllers/NxdtController.java new file mode 100644 index 0000000..ecdc0f8 --- /dev/null +++ b/src/main/java/nsusbloader/Controllers/NxdtController.java @@ -0,0 +1,133 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of NS-USBloader. + + NS-USBloader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NS-USBloader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NS-USBloader. If not, see . +*/ +package nsusbloader.Controllers; + +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.stage.DirectoryChooser; +import nsusbloader.AppPreferences; +import nsusbloader.MediatorControl; +import nsusbloader.NSLDataTypes.EModule; +import nsusbloader.Utilities.NxdtTask; + +import java.io.File; +import java.net.URL; +import java.util.ResourceBundle; + +public class NxdtController implements Initializable { + @FXML + private Label saveToLocationLbl, statusLbl; + + @FXML + private Button injectPldBtn; + + private ResourceBundle rb; + + private Region btnDumpStopImage; + + private Task NxdtTask; + private Thread workThread; + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + this.rb = resourceBundle; + + saveToLocationLbl.setText(AppPreferences.getInstance().getNXDTSaveToLocation()); + + btnDumpStopImage = new Region(); + btnDumpStopImage.getStyleClass().add("regionDump"); + + injectPldBtn.getStyleClass().add("buttonUp"); + injectPldBtn.setGraphic(btnDumpStopImage); + + injectPldBtn.setOnAction(event -> startDumpProcess()); + } + + @FXML + private void bntSelectSaveTo(){ + DirectoryChooser dc = new DirectoryChooser(); + dc.setTitle(rb.getString("tabSplMrg_Btn_SelectFolder")); + dc.setInitialDirectory(new File(saveToLocationLbl.getText())); + File saveToDir = dc.showDialog(saveToLocationLbl.getScene().getWindow()); + if (saveToDir != null) + saveToLocationLbl.setText(saveToDir.getAbsolutePath()); + } + /** + * Start reading commands from NXDT button handler + * */ + private void startDumpProcess(){ + if ((workThread == null || ! workThread.isAlive())){ + MediatorControl.getInstance().getContoller().logArea.clear(); + + NxdtTask = new NxdtTask(saveToLocationLbl.getText()); + NxdtTask.setOnSucceeded(event -> { + if (NxdtTask.getValue()) + statusLbl.setText(rb.getString("done_txt")); + else + statusLbl.setText(rb.getString("failure_txt")); + }); + + workThread = new Thread(NxdtTask); + workThread.setDaemon(true); + workThread.start(); + } + } + + /** + * Interrupt thread NXDT button handler + * */ + private void stopBtnAction(){ + //TODO + } + + public void notifyThreadStarted(boolean isActive, EModule type){ + if (! type.equals(EModule.NXDT)){ + injectPldBtn.setDisable(isActive); + return; + } + + if (isActive) { + btnDumpStopImage.getStyleClass().clear(); + btnDumpStopImage.getStyleClass().add("regionStop"); + + injectPldBtn.setOnAction(e-> stopBtnAction()); + injectPldBtn.setText(rb.getString("btn_Stop")); + injectPldBtn.getStyleClass().remove("buttonUp"); + injectPldBtn.getStyleClass().add("buttonStop"); + return; + } + btnDumpStopImage.getStyleClass().clear(); + btnDumpStopImage.getStyleClass().add("regionDump"); + + injectPldBtn.setOnAction(e-> startDumpProcess()); + injectPldBtn.setText(rb.getString("tabNXDT_Btn_Start")); + injectPldBtn.getStyleClass().remove("buttonStop"); + injectPldBtn.getStyleClass().add("buttonUp"); + } + /** + * Save application settings on exit + * */ + public void updatePreferencesOnExit(){ + AppPreferences.getInstance().setNXDTSaveToLocation(saveToLocationLbl.getText()); + } +} diff --git a/src/main/java/nsusbloader/MediatorControl.java b/src/main/java/nsusbloader/MediatorControl.java index 6a8ccd6..c0af1ab 100644 --- a/src/main/java/nsusbloader/MediatorControl.java +++ b/src/main/java/nsusbloader/MediatorControl.java @@ -44,6 +44,7 @@ public class MediatorControl { mainCtrler.getFrontCtrlr().notifyTransmThreadStarted(isActive, appModuleType); mainCtrler.getSmCtrlr().notifySmThreadStarted(isActive, appModuleType); mainCtrler.getRcmCtrlr().notifySmThreadStarted(isActive, appModuleType); + mainCtrler.getNXDTabController().notifyThreadStarted(isActive, appModuleType); } public synchronized boolean getTransferActive() { return this.isTransferActive.get(); } } diff --git a/src/main/java/nsusbloader/NSLDataTypes/EModule.java b/src/main/java/nsusbloader/NSLDataTypes/EModule.java index 21360f0..b8331a0 100644 --- a/src/main/java/nsusbloader/NSLDataTypes/EModule.java +++ b/src/main/java/nsusbloader/NSLDataTypes/EModule.java @@ -21,5 +21,6 @@ package nsusbloader.NSLDataTypes; public enum EModule { USB_NET_TRANSFERS, SPLIT_MERGE_TOOL, - RCM + RCM, + NXDT } diff --git a/src/main/java/nsusbloader/NSLMain.java b/src/main/java/nsusbloader/NSLMain.java index e222ae3..8e2585d 100644 --- a/src/main/java/nsusbloader/NSLMain.java +++ b/src/main/java/nsusbloader/NSLMain.java @@ -31,7 +31,7 @@ import java.util.ResourceBundle; public class NSLMain extends Application { - public static final String appVersion = "v2.2.1"; + public static final String appVersion = "v3.0"; @Override public void start(Stage primaryStage) throws Exception{ diff --git a/src/main/java/nsusbloader/Utilities/NxdtTask.java b/src/main/java/nsusbloader/Utilities/NxdtTask.java new file mode 100644 index 0000000..4b3907f --- /dev/null +++ b/src/main/java/nsusbloader/Utilities/NxdtTask.java @@ -0,0 +1,60 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of NS-USBloader. + + NS-USBloader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NS-USBloader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NS-USBloader. If not, see . +*/ +package nsusbloader.Utilities; + +import javafx.concurrent.Task; +import nsusbloader.COM.USB.UsbConnect; +import nsusbloader.ModelControllers.LogPrinter; +import nsusbloader.NSLDataTypes.EModule; +import nsusbloader.NSLDataTypes.EMsgType; +import org.usb4java.DeviceHandle; + +public class NxdtTask extends Task { + + private LogPrinter logPrinter; + private String saveToLocation; + + public NxdtTask(String saveToLocation){ + this.logPrinter = new LogPrinter(EModule.NXDT); + this.saveToLocation = saveToLocation; + } + + @Override + protected Boolean call() { + logPrinter.print("Save to location: "+ saveToLocation, EMsgType.INFO); + logPrinter.print("=============== nxdumptool ===============", EMsgType.INFO); + + UsbConnect usbConnect = UsbConnect.connectHomebrewMode(logPrinter); + + if (! usbConnect.isConnected()){ + logPrinter.close(); + return false; + } + + DeviceHandle handler = usbConnect.getNsHandler(); + + new NxdtUsbAbi1(handler, this, logPrinter, saveToLocation); + + logPrinter.print(".:: Complete ::.", EMsgType.PASS); + + usbConnect.close(); + logPrinter.close(); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/nsusbloader/Utilities/NxdtUsbAbi1.java b/src/main/java/nsusbloader/Utilities/NxdtUsbAbi1.java new file mode 100644 index 0000000..ceb6af6 --- /dev/null +++ b/src/main/java/nsusbloader/Utilities/NxdtUsbAbi1.java @@ -0,0 +1,327 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of NS-USBloader. + + NS-USBloader is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NS-USBloader is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NS-USBloader. If not, see . +*/ +package nsusbloader.Utilities; + +import javafx.concurrent.Task; +import nsusbloader.COM.USB.UsbErrorCodes; +import nsusbloader.ModelControllers.LogPrinter; +import nsusbloader.NSLDataTypes.EMsgType; +import org.usb4java.DeviceHandle; +import org.usb4java.LibUsb; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +class NxdtUsbAbi1 { + private LogPrinter logPrinter; + private DeviceHandle handlerNS; + private Task task; + private String saveToPath; + + private boolean isWindows; + + private static final int NXDT_COMMAND_SIZE = 0x1000; + private static final int NXDT_FILE_CHUNK_SIZE = 0x800000; + + private static final byte ABI_VERSION = 1; + private static final byte[] MAGIC_NXDT = { 0x4e, 0x58, 0x44, 0x54 }; + + private static final byte CMD_HANDSHAKE = 0; + private static final byte CMD_SEND_FILE_PROPERTIES = 1; + private static final byte CMD_ENDSESSION = 3; + + // Standard set of possible replies + private static final byte[] USBSTATUS_SUCCESS = { 0x4e, 0x58, 0x44, 0x54, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + private static final byte[] USBSTATUS_INVALID_MAGIC = { 0x4e, 0x58, 0x44, 0x54, + 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + private static final byte[] USBSTATUS_UNSUPPORTED_ABI = { 0x4e, 0x58, 0x44, 0x54, + 0x06, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + private static final byte[] USBSTATUS_UNSUPPORTED_CMD = { 0x4e, 0x58, 0x44, 0x54, + 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + private static final byte[] USBSTATUS_HOSTIOERROR = { 0x4e, 0x58, 0x44, 0x54, + 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 }; + + public NxdtUsbAbi1(DeviceHandle handler, + Task task, + LogPrinter logPrinter, + String saveToPath + ){ + this.handlerNS = handler; + this.task = task; + this.logPrinter = logPrinter; + this.isWindows = System.getProperty("os.name").toLowerCase().contains("windows"); + + if (! saveToPath.endsWith(File.separator)) + this.saveToPath = saveToPath + File.separator; + else + this.saveToPath = saveToPath; + + readLoop(); + } + + private void readLoop(){ + logPrinter.print("Awaiting for handshake", EMsgType.INFO); + try { + byte[] deviceCommand; + + while (true){ + deviceCommand = readUsbCmd(); + + if (isInvalidCommand(deviceCommand)) + continue; + + switch (deviceCommand[4]){ + case CMD_HANDSHAKE: + performHandshake(deviceCommand); + break; + case CMD_SEND_FILE_PROPERTIES: + handleSendFileProperties(deviceCommand); + break; + case CMD_ENDSESSION: + logPrinter.print("Session successfully ended", EMsgType.PASS); + return; + } + } + } + catch (InterruptedException ioe){ + logPrinter.print("Execution interrupted", EMsgType.INFO); + } + catch (Exception e){ + e.printStackTrace(); + logPrinter.print(e.getMessage(), EMsgType.INFO); + logPrinter.print("Terminating now", EMsgType.FAIL); + } + }; + + private boolean isInvalidCommand(byte[] message) throws Exception{ + boolean returnValue = false; + if (! Arrays.equals(Arrays.copyOfRange(message, 0,4), MAGIC_NXDT)){ + writeUsb(USBSTATUS_INVALID_MAGIC); + logPrinter.print("Invalid magic command", EMsgType.INFO); + returnValue = true; + } + if (message.length != NXDT_COMMAND_SIZE){ + writeUsb(USBSTATUS_UNSUPPORTED_CMD); + logPrinter.print("Invalid command size. Expected size is 4096 while received is "+message.length, EMsgType.INFO); + returnValue = true; + } + return returnValue; + } + + private void performHandshake(byte[] message) throws Exception{ + logPrinter.print("nxdumptool v"+message[8]+"."+message[9]+"."+message[10]+" ABI v"+message[11], EMsgType.INFO); + + if (ABI_VERSION != message[11]){ + writeUsb(USBSTATUS_UNSUPPORTED_ABI); + throw new Exception("ABI v"+message[11]+" is not supported in current version."); + } + writeUsb(USBSTATUS_SUCCESS); + // consider refactoring: create sub-classes for various ABI versions and check if it's supported or not + } + + private void handleSendFileProperties(byte[] message) throws Exception{ + long fileSize = getLElong(message, 8); + int fileNameLen = getLEint(message, 12); + String filename = new String(Arrays.copyOfRange(message, 16, fileNameLen), StandardCharsets.UTF_8); + + // TODO: In here should be also a field with NSP Header size that would be transmitted after the end of main data transfer; NOTE: Handle with RandomAccessFile. + + logPrinter.print("Write request for: '"+filename+"' ("+fileSize+" bytes)", EMsgType.INFO); + // If RomFs related + if (filename.startsWith("/")) { + if (isWindows) + filename = saveToPath + filename.replaceAll("/", "\\\\"); + else + filename = saveToPath + filename; + + try { + createPath(filename); + } + catch (Exception e){ + writeUsb(USBSTATUS_HOSTIOERROR); + logPrinter.print("Unable to create dir(s) for file in "+filename+ + "\n Returned: "+e.getMessage(), EMsgType.FAIL); + return; + } + + } + else + filename = saveToPath + filename; + + File fileToDump = new File(filename); + + // Check if enough space + if (fileToDump.getFreeSpace() <= fileSize){ + writeUsb(USBSTATUS_HOSTIOERROR); + logPrinter.print("Not enough space on selected volume. Need: "+fileSize+" while available: "+fileToDump.getFreeSpace(), EMsgType.FAIL); + return; + } + // Check if FS is NOT read-only + if (! fileToDump.canWrite()){ + writeUsb(USBSTATUS_HOSTIOERROR); + logPrinter.print("Unable to write into selected volume: "+fileToDump.getAbsolutePath(), EMsgType.FAIL); + return; + } + + dumpFile(fileToDump, fileSize); + + writeUsb(USBSTATUS_SUCCESS); + + // TODO: check if NSP_SIZE != 0 then go dump header + } + + private void createPath(String path) throws Exception{ + File resultingFile = new File(path); + File folderForTheFile = resultingFile.getParentFile(); + folderForTheFile.mkdirs(); + } + + private void dumpFile(File file, long size) throws Exception{ + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file, false)); + + byte[] readBuffer; + long received = 0; + int bufferSize; + + while (received < size){ + readBuffer = readUsbFile(); + bos.write(readBuffer); + bufferSize = readBuffer.length; + received += bufferSize; + + logPrinter.updateProgress((received + bufferSize) / (size / 100.0) / 100.0); + } + + logPrinter.updateProgress(1.0); + bos.close(); + } + + + public static int getLEint(byte[] bytes, int fromOffset){ + return ByteBuffer.wrap(bytes, fromOffset, 0x4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + public static long getLElong(byte[] bytes, int fromOffset){ + return ByteBuffer.wrap(bytes, fromOffset, 0x8).order(ByteOrder.LITTLE_ENDIAN).getLong(); + } + + /** + * Sending any byte array to USB device + * @return 'false' if no issues + * 'true' if errors happened + * */ + private void writeUsb(byte[] message) throws Exception{ + ByteBuffer writeBuffer = ByteBuffer.allocateDirect(message.length); + writeBuffer.put(message); + IntBuffer writeBufTransferred = IntBuffer.allocate(1); + int result; + + while (! task.isCancelled()) { + result = LibUsb.bulkTransfer(handlerNS, (byte) 0x01, writeBuffer, writeBufTransferred, 5050); + + switch (result){ + case LibUsb.SUCCESS: + if (writeBufTransferred.get() == message.length) + return; + throw new Exception("Data transfer issue [write]" + + "\n Requested: "+message.length+ + "\n Transferred: "+writeBufTransferred.get()); + case LibUsb.ERROR_TIMEOUT: + continue; + default: + throw new Exception("Data transfer issue [write]" + + "\n Returned: "+ UsbErrorCodes.getErrCode(result) + + "\n (execution stopped)"); + } + } + throw new InterruptedException("Execution interrupted"); + } + /** + * Reading what USB device responded (command). + * @return byte array if data read successful + * 'null' if read failed + * */ + private byte[] readUsbCmd() throws Exception{ + ByteBuffer readBuffer = ByteBuffer.allocateDirect(NXDT_COMMAND_SIZE); + // We can limit it to 32 bytes, but there is a non-zero chance to got OVERFLOW from libusb. + IntBuffer readBufTransferred = IntBuffer.allocate(1); + int result; + while (! task.isCancelled()) { + result = LibUsb.bulkTransfer(handlerNS, (byte) 0x81, readBuffer, readBufTransferred, 1000); // last one is TIMEOUT. 0 stands for unlimited. Endpoint IN = 0x81 + + switch (result) { + case LibUsb.SUCCESS: + int trans = readBufTransferred.get(); + byte[] receivedBytes = new byte[trans]; + readBuffer.get(receivedBytes); + return receivedBytes; + case LibUsb.ERROR_TIMEOUT: + continue; + default: + throw new Exception("Data transfer issue [read command]" + + "\n Returned: " + UsbErrorCodes.getErrCode(result)+ + "\n (execution stopped)"); + } + } + throw new InterruptedException(); + } + /** + * Reading what USB device responded (file). + * @return byte array if data read successful + * 'null' if read failed + * */ + private byte[] readUsbFile() throws Exception{ + ByteBuffer readBuffer = ByteBuffer.allocateDirect(NXDT_FILE_CHUNK_SIZE); + IntBuffer readBufTransferred = IntBuffer.allocate(1); + int result; + while (! task.isCancelled()) { + result = LibUsb.bulkTransfer(handlerNS, (byte) 0x81, readBuffer, readBufTransferred, 1000); + + switch (result) { + case LibUsb.SUCCESS: + int trans = readBufTransferred.get(); + byte[] receivedBytes = new byte[trans]; + readBuffer.get(receivedBytes); + return receivedBytes; + case LibUsb.ERROR_TIMEOUT: + continue; + default: + throw new Exception("Data transfer issue [read file]" + + "\n Returned: " + UsbErrorCodes.getErrCode(result)+ + "\n (execution stopped)"); + } + } + throw new InterruptedException(); + } +} diff --git a/src/main/resources/NSLMain.fxml b/src/main/resources/NSLMain.fxml index 017fe2f..6fb2355 100644 --- a/src/main/resources/NSLMain.fxml +++ b/src/main/resources/NSLMain.fxml @@ -31,6 +31,14 @@ + + + + + + + + diff --git a/src/main/resources/NXDTab.fxml b/src/main/resources/NXDTab.fxml new file mode 100644 index 0000000..dd720f0 --- /dev/null +++ b/src/main/resources/NXDTab.fxml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+ + + + + + + + +