/* * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA */ package jace.hardware; import jace.EmulatorUILogic; import jace.config.ConfigurableField; import jace.config.Name; import jace.config.Reconfigurable; import jace.core.Card; import jace.core.Computer; import jace.core.RAMEvent; import jace.core.RAMEvent.TYPE; import jace.core.Utility; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import javafx.scene.control.Label; /** * Super Serial Card with serial-over-tcp/ip support. This is fully compatible * with the SSC ROM and supported applications. * * @author Brendan Robert (BLuRry) brendan.robert@gmail.com */ @Name("Super Serial Card") public class CardSSC extends Card implements Reconfigurable { @ConfigurableField(name = "TCP/IP Port", shortName = "port") public short IP_PORT = 1977; protected ServerSocket socket; protected Socket clientSocket; protected BufferedReader socketInput; protected Thread listenThread; private int lastInputByte = 0; private boolean FULL_ECHO = true; private final boolean RECV_ACTIVE = true; private boolean TRANS_ACTIVE = true; // private boolean RECV_STRIP_LF = true; // private boolean TRANS_ADD_LF = true; @ConfigurableField(name = "Strip LF (recv)", shortName = "stripLF", defaultValue = "false", description = "Strip incoming linefeeds") public boolean RECV_STRIP_LF = false; @ConfigurableField(name = "Add LF (send)", shortName = "addLF", defaultValue = "false", description = "Append linefeeds after outgoing carriage returns") public boolean TRANS_ADD_LF = false; private boolean DTR = true; public int SW1 = 0x01; // Read = Jumper block SW1 //Bit 0 = !SW1-6 //Bit 1 = !SW1-5 //Bit 4 = !SW1-4 //Bit 5 = !SW1-3 //Bit 6 = !SW1-2 //Bit 7 = !SW1-1 // 19200 baud (SW1-1,2,3,4 off) // Communications mode (SW1-5,6 on) public int SW1_SETTING = 0x0F0; public int SW2_CTS = 0x02; // Read = Jumper block SW2 and CTS //Bit 0 = !CTS //SW2-6 = Allow interrupts (disable in ][, ][+) //Bit 1 = !SW2-5 -- Generate LF after CR //Bit 2 = !SW2-4 //Bit 3 = !SW2-3 //Bit 5 = !SW2-2 //Bit 7 = !SW2-1 // 1 stop bit (SW2-1 on) // 8 data bits (SW2-2 on) // No parity (SW2-3 don't care, SW2-4 off) private final int SW2_SETTING = 0x04; public int ACIA_Data = 0x08; // Read=Receive / Write=transmit public int ACIA_Status = 0x09; // Read=Status / Write=Reset public int ACIA_Command = 0x0A; public int ACIA_Control = 0x0B; public boolean PORT_CONNECTED = false; public boolean RECV_IRQ_ENABLED = false; public boolean TRANS_IRQ_ENABLED = false; public boolean IRQ_TRIGGERED = false; // Bitmask for stop bits (FF = 8, 7F = 7, etc) private int DATA_BITS = 0x07F; public CardSSC(Computer computer) { super(computer); } @Override public String getDeviceName() { return "Super Serial Card"; } Label activityIndicator; @Override public void setSlot(int slot) { try { loadRom("jace/data/SSC.rom"); } catch (IOException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); } super.setSlot(slot); Utility.loadIconLabel("network-wired.png").ifPresent(icon->{ activityIndicator = icon; activityIndicator.setText("Slot " + slot); }); } boolean newInputAvailable = false; public void socketMonitor() { try { socket = new ServerSocket(IP_PORT); socket.setReuseAddress(true); socket.setSoTimeout(0); } catch (IOException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); suspend(); return; } while (socket != null && !socket.isClosed()) { try { Logger.getLogger(CardSSC.class.getName()).log(Level.INFO, "Slot " + getSlot() + " listening on port " + IP_PORT, (Throwable) null); while ((clientSocket = socket.accept()) != null) { socketInput = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); clientConnected(); clientSocket.setTcpNoDelay(true); while (isConnected()) { try { Thread.sleep(10); if (socketInput.ready()) { newInputAvailable = true; } } catch (InterruptedException ex) { // Do nothing } } clientDisconnected(); hangUp(); socketInput = null; } Thread.yield(); } catch (SocketTimeoutException ex) { // Do nothing } catch (IOException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.FINE, null, ex); } } socket = null; } // Called when a client first connects via telnet public void clientConnected() { System.err.println("Client connected"); } // Called when a client disconnects public void clientDisconnected() { System.out.println("Client disconnected"); } public void loadRom(String path) throws IOException { // Load rom file, first 0x0700 bytes are C8 rom, last 0x0100 bytes are CX rom // CF00-CFFF are unused by the SSC InputStream romFile = CardSSC.class.getClassLoader().getResourceAsStream(path); final int cxRomLength = 0x0100; final int c8RomLength = 0x0700; byte[] romxData = new byte[cxRomLength]; byte[] rom8Data = new byte[c8RomLength]; try { if (romFile.read(rom8Data) != c8RomLength) { throw new IOException("Bad SSC rom size"); } getC8Rom().loadData(rom8Data); if (romFile.read(romxData) != cxRomLength) { throw new IOException("Bad SSC rom size"); } getCxRom().loadData(romxData); } catch (IOException ex) { throw ex; } } @Override public void reset() { Thread resetThread = new Thread(() -> { try { Thread.sleep(50); } catch (InterruptedException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); } suspend(); resume(); }); resetThread.start(); } @Override protected void handleIOAccess(int register, TYPE type, int value, RAMEvent e) { try { int newValue = -1; switch (type) { case EXECUTE: case READ_OPERAND: case READ_DATA: case READ: if (register == SW1) { newValue = SW1_SETTING; } if (register == SW2_CTS) { newValue = SW2_SETTING & 0x0FE; // if port is connected and ready to send another byte, set CTS bit on newValue |= (PORT_CONNECTED && inputAvailable()) ? 0x00 : 0x01; } if (register == ACIA_Data) { EmulatorUILogic.addIndicator(this, activityIndicator); newValue = getInputByte(); } if (register == ACIA_Status) { newValue = 0; // 0 = Parity error (1) // 1 = Framing error (1) // 2 = Overrun error (1) // 3 = ACIA Receive Register full (1) if (newInputAvailable || inputAvailable()) { newValue |= 0x08; } // 4 = ACIA Transmit Register empty (1) newValue |= 0x010; // 5 = Data Carrier Detect (DCD) true (0) // 6 = Data Set Ready (DSR) true (0) // 7 = Interrupt (IRQ) has occurred if (IRQ_TRIGGERED) { newValue |= 0x080; } IRQ_TRIGGERED = false; } if (register == ACIA_Command) { newValue = DTR ? 1 : 0; // 0 = DTR Enable (1) / Disable (0) receiver and IRQ // 1 = Allow IRQ (1) when status bit 3 is true if (RECV_IRQ_ENABLED) { newValue |= 2; } // 2,3 = Control transmit IRQ, RTS level and transmitter newValue |= 12; // 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0) if (FULL_ECHO) { newValue |= 16; } // 5 = Control parity } if (register == ACIA_Control) { // 0-3 = Baud Rate // 4 = Use baud rate generator (1) / Use external clock (0) // 5-6 = Number of data bits (00 = 8, 10 = 7, 01 = 6, 11 = 5) // 7 = Number of stop bits (0 = 1 stop bit, 1 = 1-1/2 (with 5 data bits no parity), 1 (8 data plus parity) or 2) newValue = 0; } break; case WRITE: if (register == ACIA_Data) { EmulatorUILogic.addIndicator(this, activityIndicator); sendOutputByte(value & 0x0FF); if (TRANS_IRQ_ENABLED) { triggerIRQ(); } } if (register == ACIA_Command) { // 0 = DTR Enable (1) / Disable (0) receiver and IRQ DTR = ((value & 1) == 0); // 0 = Allow IRQ (0) when status bit 3 is true if ((value & 2) == 0) { RECV_IRQ_ENABLED = !DTR; } else { RECV_IRQ_ENABLED = false; } // 2,3 = Control transmit IRQ, RTS level and transmitter // 0 0 = Transmit interrupt off, RTS high, Transmitter off // 1 0 = Transmit interrupt ON, RTS low, Transmitter on // 0 1 = Transmit interrupt off, RTS low, Transmitter on // 1 1 = Transmit interrupt off, RTS low, Transmit BRK switch ((value >> 2) & 3) { case 0: TRANS_IRQ_ENABLED = false; TRANS_ACTIVE = false; break; case 1: TRANS_IRQ_ENABLED = true; TRANS_ACTIVE = true; break; case 2: TRANS_IRQ_ENABLED = false; TRANS_ACTIVE = true; break; case 3: TRANS_IRQ_ENABLED = false; TRANS_ACTIVE = true; break; } // 4 = Normal mode 0, or Echo mode 1 (bits 2 and 3 must be 0) FULL_ECHO = ((value & 16) > 0); // System.out.println("Echo set to " + FULL_ECHO); // 5 = Control parity } if (register == ACIA_Control) { // 0-3 = Baud Rate // 4 = Use baud rate generator (1) / Use external clock (0) // 5-6 = Number of data bits (00 = 8, 01 = 7, 10 = 6, 11 = 5) // 7 = Number of stop bits (0 = 1 stop bit, 1 = 1-1/2 (with 5 data bits no parity), 1 (8 data plus parity) or 2) int bits = (value & 127) >> 5; System.out.println("Data bits set to " + (8 - bits)); switch (bits) { case 0: DATA_BITS = 0x0FF; break; case 1: DATA_BITS = 0x07F; break; case 2: DATA_BITS = 0x03F; break; case 3: DATA_BITS = 0x01F; break; } } break; } if (newValue > -1) { e.setNewValue(newValue); } } catch (IOException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); } } @Override public void tick() { if (RECV_IRQ_ENABLED && newInputAvailable) { newInputAvailable = false; triggerIRQ(); } } public boolean inputAvailable() throws IOException { if (isConnected() && clientSocket != null && socketInput != null) { // return socketInput.available() > 0; return socketInput.ready(); } else { return false; } } private int getInputByte() throws IOException { if (inputAvailable()) { int in = socketInput.read() & DATA_BITS; if (RECV_STRIP_LF && in == 10 && lastInputByte == 13) { in = socketInput.read() & DATA_BITS; } lastInputByte = in; } return lastInputByte; } long lastSuccessfulWrite = -1L; private void sendOutputByte(int i) throws IOException { if (clientSocket != null && clientSocket.isConnected()) { try { clientSocket.getOutputStream().write(i & DATA_BITS); if (TRANS_ADD_LF && (i & DATA_BITS) == 13) { clientSocket.getOutputStream().write(10); } clientSocket.getOutputStream().flush(); lastSuccessfulWrite = System.currentTimeMillis(); } catch (IOException e) { lastSuccessfulWrite = -1L; hangUp(); } } else { lastSuccessfulWrite = -1L; } } private void setCTS(boolean b) throws InterruptedException { PORT_CONNECTED = b; if (b == false) { reset(); } } private boolean getCTS() throws InterruptedException { return PORT_CONNECTED; } private void triggerIRQ() { IRQ_TRIGGERED = true; computer.getCpu().generateInterrupt(); } public void hangUp() { lastInputByte = 0; lastSuccessfulWrite = -1L; if (clientSocket != null && clientSocket.isConnected()) { try { clientSocket.shutdownInput(); clientSocket.shutdownOutput(); clientSocket.close(); } catch (IOException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); } } clientSocket = null; } /** * Detach from server socket port and ensure that the card's resources are * no longer in use * * @return */ @Override public boolean suspend() { synchronized (this) { if (socket != null) { try { socket.close(); } catch (IOException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); } } hangUp(); if (listenThread != null && listenThread.isAlive()) { try { listenThread.interrupt(); listenThread.join(100); } catch (InterruptedException ex) { Logger.getLogger(CardSSC.class.getName()).log(Level.SEVERE, null, ex); } } listenThread = null; socket = null; return super.suspend(); } } @Override public void resume() { synchronized (this) { if (!isRunning()) { super.resume(); RECV_IRQ_ENABLED = false; TRANS_IRQ_ENABLED = false; IRQ_TRIGGERED = false; //socket.setReuseAddress(true); listenThread = new Thread(this::socketMonitor); listenThread.setDaemon(false); listenThread.setName("SSC port listener, slot" + getSlot()); listenThread.start(); } } } @ConfigurableField(category = "Advanced", name = "Liveness check interval", description = "How often the connection is polled for signs of life (in milliseconds)") public int livenessCheck = 10000; public boolean isConnected() { if (clientSocket == null || !clientSocket.isConnected()) { return false; } if (lastSuccessfulWrite == -1 || System.currentTimeMillis() > (lastSuccessfulWrite + livenessCheck)) { try { sendOutputByte(0); return true; } catch (IOException e) { return false; } } else { return true; } } @Override protected void handleFirmwareAccess(int register, TYPE type, int value, RAMEvent e) { // Do nothing -- the card rom does everything } @Override protected void handleC8FirmwareAccess(int register, TYPE type, int value, RAMEvent e) { // There is no special c8 rom behavior for this card } }