diff --git a/src/main/java/de/neemann/digital/core/element/Keys.java b/src/main/java/de/neemann/digital/core/element/Keys.java index 80f736f35..d83fb886a 100644 --- a/src/main/java/de/neemann/digital/core/element/Keys.java +++ b/src/main/java/de/neemann/digital/core/element/Keys.java @@ -923,5 +923,17 @@ public final class Keys { public static final Key.KeyEnum TRIGGER = new Key.KeyEnum<>("trigger", ScopeTrigger.Trigger.both, ScopeTrigger.Trigger.values()); + /** + * Selects the telnet port + */ + public static final Key.KeyInteger PORT = + new Key.KeyInteger("port", 23) + .setMin(1) + .setMax((1 << 16) - 1); + /** + * Telnet escape + */ + public static final Key TELNET_ESCAPE = + new Key<>("telnetEscape", true).allowGroupEdit(); } diff --git a/src/main/java/de/neemann/digital/core/io/telnet/ByteBuffer.java b/src/main/java/de/neemann/digital/core/io/telnet/ByteBuffer.java new file mode 100644 index 000000000..fc42687fa --- /dev/null +++ b/src/main/java/de/neemann/digital/core/io/telnet/ByteBuffer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.core.io.telnet; + +/** + * A simple thread save byte queue. + */ +public class ByteBuffer { + private final byte[] data; + private final int size; + private int inBuffer; + private int newest; + private int oldest; + + /** + * Creates a new instance + * + * @param size the size of the buffer + */ + public ByteBuffer(int size) { + data = new byte[size]; + this.size = size; + } + + /** + * Adds a byte at the top of the buffer + * + * @param value the byte value + */ + synchronized public void put(byte value) { + if (inBuffer < size) { + data[newest] = value; + newest = inc(newest); + inBuffer++; + } + } + + /** + * @return the byte at the tail of the buffer + */ + synchronized public byte peek() { + if (inBuffer > 0) { + return data[oldest]; + } else + return -1; + } + + /** + * deletes a byte from the tail of the buffer + */ + synchronized public void delete() { + oldest = inc(oldest); + inBuffer--; + } + + /** + * @return true if there is data available + */ + synchronized public boolean hasData() { + return inBuffer > 0; + } + + private int inc(int n) { + n++; + if (n >= size) + n = 0; + return n; + } + +} diff --git a/src/main/java/de/neemann/digital/core/io/telnet/Server.java b/src/main/java/de/neemann/digital/core/io/telnet/Server.java new file mode 100644 index 000000000..263bd6cc2 --- /dev/null +++ b/src/main/java/de/neemann/digital/core/io/telnet/Server.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2021 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.core.io.telnet; + +import de.neemann.digital.core.Observer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * The telnet server + */ +public class Server { + private final ServerSocket serverSocket; + private final ByteBuffer buffer; + private boolean telnetEscape; + private ClientThread client; + private Observer notify; + + Server(int port) throws IOException { + buffer = new ByteBuffer(1024); + serverSocket = new ServerSocket(port); + ServerThread listener = new ServerThread(); + listener.start(); + } + + void send(int value) { + if (client != null) + client.send(value); + } + + int getData() { + return buffer.peek(); + } + + void delete() { + buffer.delete(); + } + + void setNotify(Observer notify) { + this.notify = notify; + } + + boolean hasData() { + return buffer.hasData(); + } + + private void setClient(ClientThread client) { + this.client = client; + } + + void setTelnetEscape(boolean telnetEscape) { + this.telnetEscape = telnetEscape; + } + + private void dataReceived(int data) { + buffer.put((byte) data); + if (notify != null) + notify.hasChanged(); + } + + private final class ServerThread extends Thread { + + private ServerThread() { + setDaemon(true); + } + + @Override + public void run() { + try { + while (true) { + Socket client = serverSocket.accept(); + ClientThread cl = new ClientThread(client, Server.this); + cl.start(); + setClient(cl); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + + private static final class ClientThread extends Thread { + + private static final int ECHO = 1; + private static final int SGA = 3; + private static final int WILL = 251; + private static final int WONT = 252; + private static final int DO = 253; + private static final int DONT = 254; + private static final int IAC = 255; + + private final InputStream in; + private final OutputStream out; + private final Socket client; + private final Server server; + + private ClientThread(Socket client, Server server) throws IOException { + setDaemon(true); + in = client.getInputStream(); + out = client.getOutputStream(); + if (server.telnetEscape) { + out.write(IAC); + out.write(WILL); + out.write(SGA); + out.write(IAC); + out.write(WILL); + out.write(ECHO); + out.flush(); + } + this.client = client; + this.server = server; + } + + @Override + public void run() { + try { + while (true) { + int data = in.read(); + if (data == IAC && server.telnetEscape) { + int command = in.read(); + int option = in.read(); + } else + server.dataReceived(data); + } + } catch (IOException e) { + e.printStackTrace(); + } + try { + client.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void send(int value) { + try { + out.write(value); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + try { + client.close(); + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } + } + } + +} diff --git a/src/main/java/de/neemann/digital/core/io/telnet/ServerHolder.java b/src/main/java/de/neemann/digital/core/io/telnet/ServerHolder.java new file mode 100644 index 000000000..8f0c3092f --- /dev/null +++ b/src/main/java/de/neemann/digital/core/io/telnet/ServerHolder.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.core.io.telnet; + +import java.io.IOException; +import java.util.HashMap; + +/** + * Simple singleton to hold the server instances. + * Usage of this singleton allows the telnet client to stay connected + * also if the simulation is not running. + */ +public final class ServerHolder { + /** + * The singleton instance + */ + public static final ServerHolder INSTANCE = new ServerHolder(); + + private final HashMap serverMap; + + private ServerHolder() { + serverMap = new HashMap<>(); + } + + /** + * Returns a server. + * + * @param port the port + * @return the server + * @throws IOException IOException + */ + public Server getServer(int port) throws IOException { + Server server = serverMap.get(port); + if (server == null) { + server = new Server(port); + serverMap.put(port, server); + } + return server; + } +} diff --git a/src/main/java/de/neemann/digital/core/io/telnet/Telnet.java b/src/main/java/de/neemann/digital/core/io/telnet/Telnet.java new file mode 100644 index 000000000..edc858e1e --- /dev/null +++ b/src/main/java/de/neemann/digital/core/io/telnet/Telnet.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.core.io.telnet; + +import de.neemann.digital.core.*; +import de.neemann.digital.core.element.Element; +import de.neemann.digital.core.element.ElementAttributes; +import de.neemann.digital.core.element.ElementTypeDescription; +import de.neemann.digital.core.element.Keys; +import de.neemann.digital.draw.elements.PinException; +import de.neemann.digital.lang.Lang; + +import java.io.IOException; + +import static de.neemann.digital.core.element.PinInfo.input; + +/** + * The telnet node + */ +public class Telnet extends Node implements Element { + + /** + * The telnet server description + */ + public static final ElementTypeDescription DESCRIPTION = new ElementTypeDescription(Telnet.class, + input("in"), input("C").setClock(), input("wr"), input("rd")) + .addAttribute(Keys.ROTATE) + .addAttribute(Keys.LABEL) + .addAttribute(Keys.TELNET_ESCAPE) + .addAttribute(Keys.PORT); + + private final ObservableValue dataOut; + private final ObservableValue dataAvail; + private final int port; + private final boolean telnetEscape; + private ObservableValue dataIn; + private ObservableValue clockValue; + private ObservableValue writeEnable; + private ObservableValue readEnableValue; + private Server server; + private boolean lastClock; + private boolean readEnable; + + /** + * Creates a new instance + * + * @param attributes The components attributes + */ + public Telnet(ElementAttributes attributes) { + dataOut = new ObservableValue("out", 8) + .setToHighZ() + .setPinDescription(DESCRIPTION); + dataAvail = new ObservableValue("av", 1) + .setPinDescription(DESCRIPTION); + port = attributes.get(Keys.PORT); + telnetEscape = attributes.get(Keys.TELNET_ESCAPE); + } + + @Override + public void setInputs(ObservableValues inputs) throws NodeException { + dataIn = inputs.get(0).checkBits(8, this, 0); + clockValue = inputs.get(1).checkBits(1, this, 1).addObserverToValue(this); + writeEnable = inputs.get(2).checkBits(1, this, 2); + readEnableValue = inputs.get(3).checkBits(1, this, 3).addObserverToValue(this); + } + + @Override + public void readInputs() throws NodeException { + boolean clock = clockValue.getBool(); + readEnable = readEnableValue.getBool(); + if (clock & !lastClock) { + if (writeEnable.getBool()) + server.send((int) dataIn.getValue()); + if (readEnable) + server.delete(); + } + lastClock = clock; + } + + @Override + public void writeOutputs() throws NodeException { + if (readEnable) + dataOut.setValue(server.getData()); + else + dataOut.setToHighZ(); + + dataAvail.setBool(server.hasData()); + } + + @Override + public ObservableValues getOutputs() throws PinException { + return new ObservableValues(dataOut, dataAvail); + } + + @Override + public void init(Model model) throws NodeException { + try { + server = ServerHolder.INSTANCE.getServer(port); + } catch (IOException e) { + throw new NodeException(Lang.get("err_couldNotCreateServer"), e); + } + server.setTelnetEscape(telnetEscape); + server.setNotify(() -> model.modify(Telnet.this::hasChanged)); + } + +} diff --git a/src/main/java/de/neemann/digital/core/io/telnet/package-info.java b/src/main/java/de/neemann/digital/core/io/telnet/package-info.java new file mode 100644 index 000000000..82ab115d1 --- /dev/null +++ b/src/main/java/de/neemann/digital/core/io/telnet/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2021 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ + +/** + * Implement's a telnet connection to the simulator + */ +package de.neemann.digital.core.io.telnet; diff --git a/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java b/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java index d9b423185..eee62715d 100644 --- a/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java +++ b/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java @@ -15,6 +15,7 @@ import de.neemann.digital.core.extern.External; import de.neemann.digital.core.extern.ExternalFile; import de.neemann.digital.core.flipflops.*; import de.neemann.digital.core.io.*; +import de.neemann.digital.core.io.telnet.Telnet; import de.neemann.digital.core.memory.*; import de.neemann.digital.core.pld.DiodeBackward; import de.neemann.digital.core.pld.DiodeForward; @@ -150,6 +151,7 @@ public class ElementLibrary implements Iterable .add(new LibraryNode(Lang.get("lib_peripherals")) .add(Keyboard.DESCRIPTION) .add(Terminal.DESCRIPTION) + .add(Telnet.DESCRIPTION) .add(VGA.DESCRIPTION) .add(MIDI.DESCRIPTION) ) diff --git a/src/main/resources/lang/lang_de.xml b/src/main/resources/lang/lang_de.xml index ad40db42c..065e2a62e 100644 --- a/src/main/resources/lang/lang_de.xml +++ b/src/main/resources/lang/lang_de.xml @@ -280,6 +280,16 @@ Ein High an diesem Eingang aktiviert den Takteingang. + Telnet + Erlaubt eine Telnet-Verbindung zur Schaltung. + Es können per Telnet Zeichen empfangen und gesendet werden. + Datenausgabe + Gibt eine Eins aus, wenn Daten vorhanden sind. + Die zu sendenden Daten. + Takteingang. + Wenn gesetzt, wird das Eingangsdatenbyte gesendet. + Wenn gesetzt, wird ein empfangenes Byte ausgegeben. + VGA Bildschirm Analysiert die eingehenden Video-Signale und zeigt die entsprechende Grafik an. Da die Simulation nicht in Echtzeit laufen kann, wird zusätzlich zu den Videosignalen der Pixeltakt benötigt. @@ -1240,6 +1250,7 @@ Sind evtl. die Namen der Variablen nicht eindeutig? Zu viel Werte in einer Zeile! Fehler beim Schreiben der Datei {0}. Die Schaltung enthält keine Bauteile! + Der Server konnte nicht gestartet werden! Adress-Bits Anzahl der Adress-Bits, die verwendet werden. @@ -1664,6 +1675,14 @@ Sind evtl. die Namen der Variablen nicht eindeutig? Zähle fallende Flanken Zähle beide Flanken + Steuercodes + Wenn gesetzt, werden die Telnet Steuerkommandos gefiltert. + Zusätzlich werden vom Server die Kommandos SGA uch ECHO gesendet. + Wird diese Option deaktiviert, handelt es sich um einen einfachen TCP server. + + Port + Der vom Server zu öffnende Port. + Farbschema Normal Dunkel diff --git a/src/main/resources/lang/lang_en.xml b/src/main/resources/lang/lang_en.xml index 0239260a7..558d5a734 100644 --- a/src/main/resources/lang/lang_en.xml +++ b/src/main/resources/lang/lang_en.xml @@ -280,6 +280,16 @@ The data to write to the terminal A high at this input enables the clock input. + Telnet + Allows a Telnet connection to the circuit. + It is possible to receive and send characters via Telnet. + Data output + Outputs a one if data is present. + The data to be sent. + Clock input + If set, the input data byte is sent. + If set, a received byte is output. + VGA Monitor Analyzes the incoming video signals and displays the corresponding graphic. Since the simulation cannot run in real time, the pixel clock is required in addition to the video signals. @@ -1227,6 +1237,7 @@ Too many values in one line! Error writing file {0}. The circuit contains no components! + Could not start the server! Address Bits Number of address bits used. @@ -1651,6 +1662,13 @@ Count on Falling Edge Count both Edges + Commands + If set, the Telnet control commands are filtered. + In addition, the server sends the SGA and ECHO commands. If this option is disabled, + the server is a simple TCP server. + Port + The port to be opened by the server. + Color scheme Normal Dark diff --git a/src/test/resources/dig/backtrack/AllComponents.dig b/src/test/resources/dig/backtrack/AllComponents.dig index e77b4c23c..49e49042e 100644 --- a/src/test/resources/dig/backtrack/AllComponents.dig +++ b/src/test/resources/dig/backtrack/AllComponents.dig @@ -695,15 +695,29 @@ 8 - + GenericCode + + Telnet + + + port + 2323 + + + + + + + + @@ -752,6 +766,10 @@ + + + + @@ -864,6 +882,10 @@ + + + + @@ -916,6 +938,10 @@ + + + + @@ -1073,8 +1099,8 @@ - - + + @@ -1181,20 +1207,24 @@ - - - - - - + + - - + + + + + + + + + + @@ -1276,6 +1306,18 @@ + + + + + + + + + + + + @@ -1413,8 +1455,8 @@ - - + + @@ -1644,6 +1686,14 @@ + + + + + + + + @@ -1714,19 +1764,23 @@ - + + + + + - - + + - - + + @@ -2028,6 +2082,10 @@ + + + + @@ -2160,6 +2218,10 @@ + + + + @@ -2170,7 +2232,7 @@ - +