Merge pull request #781 from ejoerns/pull-req/cooja-serialsocket

[Cooja] Configurable serial socket plugin
This commit is contained in:
Fredrik Österlind 2014-09-24 12:49:15 +02:00
commit 8df1c98a85
5 changed files with 1227 additions and 531 deletions

View file

@ -1,2 +1,2 @@
org.contikios.cooja.Cooja.PLUGINS = + SerialSocketClient SerialSocketServer org.contikios.cooja.Cooja.PLUGINS = + org.contikios.cooja.serialsocket.SerialSocketClient org.contikios.cooja.serialsocket.SerialSocketServer
org.contikios.cooja.Cooja.JARFILES = + serial_socket.jar org.contikios.cooja.Cooja.JARFILES = + serial_socket.jar

View file

@ -1,243 +0,0 @@
/*
* Copyright (c) 2010, Swedish Institute of Computer Science.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MotePlugin;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.interfaces.SerialPort;
/**
* Socket to simulated serial port forwarder. Client version.
*
* @author Fredrik Osterlind
*/
@ClassDescription("Serial Socket (CLIENT)")
@PluginType(PluginType.MOTE_PLUGIN)
public class SerialSocketClient extends VisPlugin implements MotePlugin {
private static final long serialVersionUID = 1L;
private static Logger logger = Logger.getLogger(SerialSocketClient.class);
private final static int LABEL_WIDTH = 100;
private final static int LABEL_HEIGHT = 15;
public final static String SERVER_HOST = "localhost";
public final static int SERVER_PORT = 1234;
private SerialPort serialPort;
private Observer serialDataObserver;
private JLabel statusLabel, inLabel, outLabel;
private int inBytes = 0, outBytes = 0;
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
private Mote mote;
public SerialSocketClient(Mote mote, Simulation simulation, final Cooja gui) {
super("Serial Socket (CLIENT) (" + mote + ")", gui, false);
this.mote = mote;
/* GUI components */
if (Cooja.isVisualized()) {
Box northBox = Box.createHorizontalBox();
northBox.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
statusLabel = configureLabel(northBox, "", "");
Box mainBox = Box.createHorizontalBox();
mainBox.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
inLabel = configureLabel(mainBox, "socket -> mote:", "0 bytes");
outLabel = configureLabel(mainBox, "mote -> socket", "0 bytes");
getContentPane().add(BorderLayout.NORTH, northBox);
getContentPane().add(BorderLayout.CENTER, mainBox);
pack();
}
/* Mote serial port */
serialPort = (SerialPort) mote.getInterfaces().getLog();
if (serialPort == null) {
throw new RuntimeException("No mote serial port");
}
try {
logger.info("Connecting: " + SERVER_HOST + ":" + SERVER_PORT);
socket = new Socket(SERVER_HOST, SERVER_PORT);
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
out.flush();
startSocketReadThread(in);
} catch (Exception e) {
throw (RuntimeException) new RuntimeException(
"Connection error: " + e.getMessage()).initCause(e);
}
/* Observe serial port for outgoing data */
serialPort.addSerialDataObserver(serialDataObserver = new Observer() {
public void update(Observable obs, Object obj) {
try {
if (out == null) {
return;
}
out.write(serialPort.getLastSerialData());
out.flush();
outBytes++;
if (Cooja.isVisualized()) {
outLabel.setText(outBytes + " bytes");
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
private void startSocketReadThread(final DataInputStream in) {
/* Forward data: virtual port -> mote */
Thread incomingDataThread = new Thread(new Runnable() {
public void run() {
int numRead = 0;
byte[] data = new byte[1024];
logger.info("Forwarder: socket -> serial port");
while (true) {
numRead = -1;
try {
numRead = in.read(data);
} catch (IOException e) {
e.printStackTrace();
return;
}
if (numRead >= 0) {
for (int i=0; i < numRead; i++) {
serialPort.writeByte(data[i]);
}
inBytes += numRead;
if (Cooja.isVisualized()) {
inLabel.setText(inBytes + " bytes");
}
} else {
logger.warn("Incoming data thread shut down");
cleanup();
break;
}
}
}
});
incomingDataThread.start();
}
private JLabel configureLabel(JComponent pane, String desc, String value) {
JPanel smallPane = new JPanel(new BorderLayout());
JLabel label = new JLabel(desc);
label.setPreferredSize(new Dimension(LABEL_WIDTH,LABEL_HEIGHT));
smallPane.add(BorderLayout.WEST, label);
label = new JLabel(value);
label.setPreferredSize(new Dimension(LABEL_WIDTH,LABEL_HEIGHT));
smallPane.add(BorderLayout.CENTER, label);
pane.add(smallPane);
return label;
}
public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
return true;
}
public Collection<Element> getConfigXML() {
return null;
}
private void cleanup() {
serialPort.deleteSerialDataObserver(serialDataObserver);
try {
if (socket != null) {
socket.close();
socket = null;
}
} catch (IOException e1) {
}
try {
if (in != null) {
in.close();
in = null;
}
} catch (IOException e) {
}
try {
if (out != null) {
out.close();
out = null;
}
} catch (IOException e) {
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
SerialSocketClient.this.setTitle(SerialSocketClient.this.getTitle() + " *DISCONNECTED*");
statusLabel.setText("Disconnected from server");
}
});
}
public void closePlugin() {
cleanup();
}
public Mote getMote() {
return mote;
}
}

View file

@ -1,287 +0,0 @@
/*
* Copyright (c) 2010, Swedish Institute of Computer Science.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MotePlugin;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.interfaces.SerialPort;
/**
* Socket to simulated serial port forwarder. Server version.
*
* @author Fredrik Osterlind
*/
@ClassDescription("Serial Socket (SERVER)")
@PluginType(PluginType.MOTE_PLUGIN)
public class SerialSocketServer extends VisPlugin implements MotePlugin {
private static final long serialVersionUID = 1L;
private static Logger logger = Logger.getLogger(SerialSocketServer.class);
private final static int LABEL_WIDTH = 100;
private final static int LABEL_HEIGHT = 15;
public final int LISTEN_PORT;
private SerialPort serialPort;
private Observer serialDataObserver;
private JLabel statusLabel, inLabel, outLabel;
private int inBytes = 0, outBytes = 0;
private ServerSocket server;
private Socket client;
private DataInputStream in;
private DataOutputStream out;
private Mote mote;
public SerialSocketServer(Mote mote, Simulation simulation, final Cooja gui) {
super("Serial Socket (SERVER) (" + mote + ")", gui, false);
this.mote = mote;
updateTimer.start();
LISTEN_PORT = 60000 + mote.getID();
/* GUI components */
if (Cooja.isVisualized()) {
Box northBox = Box.createHorizontalBox();
northBox.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
statusLabel = configureLabel(northBox, "", "");
Box mainBox = Box.createHorizontalBox();
mainBox.setBorder(BorderFactory.createEmptyBorder(0, 5, 5, 5));
inLabel = configureLabel(mainBox, "socket -> mote:", "0 bytes");
outLabel = configureLabel(mainBox, "mote -> socket", "0 bytes");
getContentPane().add(BorderLayout.NORTH, northBox);
getContentPane().add(BorderLayout.CENTER, mainBox);
pack();
}
/* Mote serial port */
serialPort = (SerialPort) mote.getInterfaces().getLog();
if (serialPort == null) {
throw new RuntimeException("No mote serial port");
}
try {
logger.info("Listening on port: " + LISTEN_PORT);
if (Cooja.isVisualized()) {
statusLabel.setText("Listening on port: " + LISTEN_PORT);
}
server = new ServerSocket(LISTEN_PORT);
new Thread() {
public void run() {
while (server != null) {
try {
client = server.accept();
in = new DataInputStream(client.getInputStream());
out = new DataOutputStream(client.getOutputStream());
out.flush();
startSocketReadThread(in);
if (Cooja.isVisualized()) {
statusLabel.setText("Client connected: " + client.getInetAddress());
}
} catch (IOException e) {
logger.fatal("Listening thread shut down: " + e.getMessage());
server = null;
cleanupClient();
break;
}
}
}
}.start();
} catch (Exception e) {
throw (RuntimeException) new RuntimeException(
"Connection error: " + e.getMessage()).initCause(e);
}
/* Observe serial port for outgoing data */
serialPort.addSerialDataObserver(serialDataObserver = new Observer() {
public void update(Observable obs, Object obj) {
try {
if (out == null) {
/*logger.debug("out is null");*/
return;
}
out.write(serialPort.getLastSerialData());
out.flush();
outBytes++;
} catch (IOException e) {
cleanupClient();
}
}
});
}
private void startSocketReadThread(final DataInputStream in) {
/* Forward data: virtual port -> mote */
Thread incomingDataThread = new Thread(new Runnable() {
public void run() {
int numRead = 0;
byte[] data = new byte[1024];
logger.info("Forwarder: socket -> serial port");
while (true) {
numRead = -1;
try {
numRead = in.read(data);
} catch (IOException e) {
numRead = -1;
}
if (numRead >= 0) {
for (int i=0; i < numRead; i++) {
serialPort.writeByte(data[i]);
}
inBytes += numRead;
} else {
cleanupClient();
break;
}
}
}
});
incomingDataThread.start();
}
private JLabel configureLabel(JComponent pane, String desc, String value) {
JPanel smallPane = new JPanel(new BorderLayout());
JLabel label = new JLabel(desc);
label.setPreferredSize(new Dimension(LABEL_WIDTH,LABEL_HEIGHT));
smallPane.add(BorderLayout.WEST, label);
label = new JLabel(value);
label.setPreferredSize(new Dimension(LABEL_WIDTH,LABEL_HEIGHT));
smallPane.add(BorderLayout.CENTER, label);
pane.add(smallPane);
return label;
}
public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
return true;
}
public Collection<Element> getConfigXML() {
return null;
}
private void cleanupClient() {
try {
if (client != null) {
client.close();
client = null;
}
} catch (IOException e1) {
}
try {
if (in != null) {
in.close();
in = null;
}
} catch (IOException e) {
}
try {
if (out != null) {
out.close();
out = null;
}
} catch (IOException e) {
}
if (Cooja.isVisualized()) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
statusLabel.setText("Listening on port: " + LISTEN_PORT);
}
});
}
}
private boolean closed = false;
public void closePlugin() {
closed = true;
cleanupClient();
serialPort.deleteSerialDataObserver(serialDataObserver);
try {
server.close();
} catch (IOException e) {
}
}
public Mote getMote() {
return mote;
}
private static final int UPDATE_INTERVAL = 150;
private Timer updateTimer = new Timer(UPDATE_INTERVAL, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (closed) {
updateTimer.stop();
return;
}
inLabel.setText(inBytes + " bytes");
outLabel.setText(outBytes + " bytes");
}
});
}

View file

@ -0,0 +1,569 @@
package org.contikios.cooja.serialsocket;
/*
* Copyright (c) 2014, TU Braunschweig.
* Copyright (c) 2010, Swedish Institute of Computer Science.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.EtchedBorder;
import javax.swing.text.NumberFormatter;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MotePlugin;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.interfaces.SerialPort;
/**
* Socket to simulated serial port forwarder. Client version.
*
* @author Fredrik Osterlind
* @author Enrico Jorns
*/
@ClassDescription("Serial Socket (CLIENT)")
@PluginType(PluginType.MOTE_PLUGIN)
public class SerialSocketClient extends VisPlugin implements MotePlugin {
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(SerialSocketClient.class);
private static final String SERVER_DEFAULT_HOST = "localhost";
private static final int SERVER_DEFAULT_PORT = 1234;
private final static int STATUSBAR_WIDTH = 350;
private static final Color ST_COLOR_UNCONNECTED = Color.DARK_GRAY;
private static final Color ST_COLOR_CONNECTED = new Color(0, 161, 83);
private static final Color ST_COLOR_FAILED = Color.RED;
private SerialPort serialPort;
private Observer serialDataObserver;
private JLabel socketToMoteLabel;
private JLabel moteToSocketLabel;
private JLabel socketStatusLabel;
private JTextField serverHostField;
private JFormattedTextField serverPortField;
private JButton serverSelectButton;
private int inBytes = 0, outBytes = 0;
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
private final Mote mote;
private final Simulation simulation;
public SerialSocketClient(Mote mote, Simulation simulation, final Cooja gui) {
super("Serial Socket (CLIENT) (" + mote + ")", gui, false);
this.mote = mote;
this.simulation = simulation;
/* GUI components */
if (Cooja.isVisualized()) {
setResizable(false);
setLayout(new BorderLayout());
// --- Server setup
GridBagConstraints c = new GridBagConstraints();
JPanel serverSelectPanel = new JPanel(new GridBagLayout());
serverSelectPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
JLabel label = new JLabel("Host:");
c.gridx = 0;
c.gridy = 0;
c.gridx++;
serverSelectPanel.add(label, c);
serverHostField = new JTextField(SERVER_DEFAULT_HOST);
serverHostField.setColumns(10);
c.gridx++;
c.weightx = 1.0;
serverSelectPanel.add(serverHostField, c);
label = new JLabel("Port:");
c.gridx++;
c.weightx = 0.0;
serverSelectPanel.add(label, c);
NumberFormat nf = NumberFormat.getIntegerInstance();
nf.setGroupingUsed(false);
serverPortField = new JFormattedTextField(new NumberFormatter(nf));
serverPortField.setColumns(5);
serverPortField.setText(String.valueOf(SERVER_DEFAULT_PORT));
c.gridx++;
serverSelectPanel.add(serverPortField, c);
serverSelectButton = new JButton("Connect") { // Button for label toggeling
private final String altString = "Disconnect";
@Override
public Dimension getPreferredSize() {
String origText = getText();
Dimension origDim = super.getPreferredSize();
setText(altString);
Dimension altDim = super.getPreferredSize();
setText(origText);
return new Dimension(Math.max(origDim.width, altDim.width), origDim.height);
}
};
c.gridx++;
c.weightx = 0.1;
c.anchor = GridBagConstraints.EAST;
serverSelectPanel.add(serverSelectButton, c);
c.gridx = 0;
c.gridy++;
c.gridwidth = GridBagConstraints.REMAINDER;
c.fill = GridBagConstraints.HORIZONTAL;
serverSelectPanel.add(new JSeparator(JSeparator.HORIZONTAL), c);
add(BorderLayout.NORTH, serverSelectPanel);
// --- Incoming / outgoing info
JPanel connectionInfoPanel = new JPanel(new GridLayout(0, 2));
connectionInfoPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
c = new GridBagConstraints();
label = new JLabel("socket -> mote: ");
label.setHorizontalAlignment(JLabel.RIGHT);
c.gridx = 0;
c.gridy = 0;
c.anchor = GridBagConstraints.EAST;
connectionInfoPanel.add(label);
socketToMoteLabel = new JLabel("0 bytes");
c.gridx++;
c.anchor = GridBagConstraints.WEST;
connectionInfoPanel.add(socketToMoteLabel);
label = new JLabel("mote -> socket: ");
label.setHorizontalAlignment(JLabel.RIGHT);
c.gridx = 0;
c.gridy++;
c.anchor = GridBagConstraints.EAST;
connectionInfoPanel.add(label);
moteToSocketLabel = new JLabel("0 bytes");
c.gridx++;
c.anchor = GridBagConstraints.WEST;
connectionInfoPanel.add(moteToSocketLabel);
add(BorderLayout.CENTER, connectionInfoPanel);
// --- Status bar
JPanel statusBarPanel = new JPanel(new BorderLayout()) {
@Override
public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
return new Dimension(STATUSBAR_WIDTH, d.height);
}
};
statusBarPanel.setLayout(new BoxLayout(statusBarPanel, BoxLayout.LINE_AXIS));
statusBarPanel.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
label = new JLabel("Status: ");
statusBarPanel.add(label);
socketStatusLabel = new JLabel("Disconnected");
socketStatusLabel.setForeground(Color.DARK_GRAY);
statusBarPanel.add(socketStatusLabel);
add(BorderLayout.SOUTH, statusBarPanel);
/* Mote serial port */
serialPort = (SerialPort) mote.getInterfaces().getLog();
if (serialPort == null) {
throw new RuntimeException("No mote serial port");
}
serverSelectButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("Connect")) {
try {
serverPortField.commitEdit();
startClient(serverHostField.getText(), ((Long) serverPortField.getValue()).intValue());
} catch (ParseException ex) {
logger.error(ex);
}
} else {
// close socket
cleanup();
}
}
});
/* Observe serial port for outgoing data and write to socket */
serialPort.addSerialDataObserver(serialDataObserver = new Observer() {
@Override
public void update(Observable obs, Object obj) {
try {
if (out == null) {
return;
}
out.write(serialPort.getLastSerialData());
out.flush();
outBytes++;
if (Cooja.isVisualized()) {
moteToSocketLabel.setText(outBytes + " bytes");
}
} catch (IOException ex) {
logger.error(ex.getMessage());
socketStatusLabel.setText("failed");
socketStatusLabel.setForeground(ST_COLOR_FAILED);
}
}
});
}
if (Cooja.isVisualized()) {
addClientListener(new ClientListener() {
@Override
public void onError(final String msg) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
socketStatusLabel.setForeground(ST_COLOR_FAILED);
socketStatusLabel.setText(msg);
}
});
}
@Override
public void onConnected() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
socketStatusLabel.setText("Connected");
socketStatusLabel.setForeground(ST_COLOR_CONNECTED);
serverHostField.setEnabled(false);
serverPortField.setEnabled(false);
serverSelectButton.setText("Disconnect");
}
});
}
@Override
public void onDisconnected() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
socketStatusLabel.setForeground(ST_COLOR_UNCONNECTED);
socketStatusLabel.setText("Disconnected");
serverHostField.setEnabled(true);
serverPortField.setEnabled(true);
serverSelectButton.setText("Connect");
}
});
}
});
}
pack();
}
private List<ClientListener> listeners = new LinkedList<>();
public interface ClientListener {
void onError(String msg);
void onConnected();
void onDisconnected();
}
public void addClientListener(ClientListener listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
private void notifyClientError(String msg) {
for (ClientListener l : listeners) {
l.onError(msg);
}
}
private void notifyClientConnected() {
for (ClientListener l : listeners) {
l.onConnected();
}
}
private void notifyClientDisconnected() {
for (ClientListener l : listeners) {
l.onDisconnected();
}
}
public void startClient(String host, int port) {
if (socket == null) {
// connect to serer
try {
logger.info("Connecting: " + host + ":" + port);
socket = new Socket(host, port);
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
out.flush();
startSocketReadThread(in);
notifyClientConnected();
} catch (IOException ex) {
logger.error(ex.getMessage());
notifyClientError(ex.getMessage());
}
} else {
// disconnect from server
try {
logger.info("Closing connection to serer...");
socket.close();
notifyClientDisconnected();
} catch (IOException ex) {
logger.error(ex);
notifyClientError(ex.getMessage());
} finally {
socket = null;
}
}
}
private void startSocketReadThread(final DataInputStream in) {
/* Forward data: virtual port -> mote */
Thread incomingDataThread = new Thread(new Runnable() {
@Override
public void run() {
int numRead = 0;
byte[] data = new byte[1024];
logger.info("Start forwarding: socket -> serial port");
while (numRead >= 0) {
try {
numRead = in.read(data);
} catch (IOException e) {
logger.info(e.getMessage());
numRead = -1;
continue;
}
if (numRead >= 0) {
final int finalNumRead = numRead;
final byte[] finalData = data;
/* We are not on the simulation thread */
simulation.invokeSimulationThread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < finalNumRead; i++) {
serialPort.writeByte(finalData[i]);
}
}
});
inBytes += numRead;
if (Cooja.isVisualized()) {
socketToMoteLabel.setText(inBytes + " bytes");
}
}
}
logger.info("Incoming data thread shut down");
cleanup();
notifyClientDisconnected();
}
});
incomingDataThread.start();
}
@Override
public Collection<Element> getConfigXML() {
List<Element> config = new ArrayList<>();
Element element;
// XXX isVisualized guards?
element = new Element("host");
if (socket == null || !socket.isBound()) {
element.setText(serverHostField.getText());
} else {
element.setText(socket.getInetAddress().getHostName());
}
config.add(element);
element = new Element("port");
if (socket == null || !socket.isBound()) {
try {
serverPortField.commitEdit();
element.setText(String.valueOf((Long) serverPortField.getValue()));
} catch (ParseException ex) {
logger.error(ex.getMessage());
serverPortField.setText("null");
}
} else {
element.setText(String.valueOf(socket.getPort()));
}
config.add(element);
element = new Element("bound");
if (socket == null) {
element.setText(String.valueOf(false));
} else {
element.setText(String.valueOf(socket.isBound()));
}
config.add(element);
return config;
}
@Override
public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
String host = null;
Integer port = null;
boolean bound = false;
for (Element element : configXML) {
switch (element.getName()) {
case "host":
host = element.getText();
break;
case "port":
port = Integer.parseInt(element.getText());
break;
case "bound":
bound = Boolean.parseBoolean(element.getText());
break;
default:
logger.warn("Unknwon config element: " + element.getName());
break;
}
}
// XXX binding might fail if server not configured yet
if (Cooja.isVisualized()) {
if (host != null) {
serverHostField.setText(host);
}
if (port != null) {
serverPortField.setText(String.valueOf(port));
}
if (bound) {
serverSelectButton.doClick();
}
} else {
// if bound and all set up, start client
if (host != null && port != null) {
startClient(host, port);
} else {
logger.error("Client not started due to incomplete configuration");
}
}
return true;
}
private void cleanup() {
serialPort.deleteSerialDataObserver(serialDataObserver);
try {
if (socket != null) {
socket.close();
socket = null;
}
} catch (IOException e1) {
logger.warn(e1.getMessage());
}
try {
if (in != null) {
in.close();
in = null;
}
} catch (IOException e) {
logger.warn(e.getMessage());
}
try {
if (out != null) {
out.close();
out = null;
}
} catch (IOException e) {
logger.warn(e.getMessage());
}
}
@Override
public void closePlugin() {
cleanup();
}
@Override
public Mote getMote() {
return mote;
}
}

View file

@ -0,0 +1,657 @@
package org.contikios.cooja.serialsocket;
/*
* Copyright (c) 2014, TU Braunschweig.
* Copyright (c) 2010, Swedish Institute of Computer Science.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.border.EtchedBorder;
import javax.swing.text.NumberFormatter;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.Mote;
import org.contikios.cooja.MotePlugin;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.interfaces.SerialPort;
/**
* Socket to simulated serial port forwarder. Server version.
*
* @author Fredrik Osterlind
* @author Enrico Jorns
*/
@ClassDescription("Serial Socket (SERVER)")
@PluginType(PluginType.MOTE_PLUGIN)
public class SerialSocketServer extends VisPlugin implements MotePlugin {
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(SerialSocketServer.class);
private final static int STATUSBAR_WIDTH = 350;
private static final Color COLOR_NEUTRAL = Color.DARK_GRAY;
private static final Color COLOR_POSITIVE = new Color(0, 161, 83);
private static final Color COLOR_NEGATIVE = Color.RED;
private final int SERVER_DEFAULT_PORT;
private final SerialPort serialPort;
private Observer serialDataObserver;
private JLabel socketToMoteLabel;
private JLabel moteToSocketLabel;
private JLabel socketStatusLabel;
private JFormattedTextField listenPortField;
private JButton serverStartButton;
private int inBytes = 0, outBytes = 0;
private ServerSocket serverSocket;
private Socket clientSocket;
private Mote mote;
private Simulation simulation;
public SerialSocketServer(Mote mote, Simulation simulation, final Cooja gui) {
super("Serial Socket (SERVER) (" + mote + ")", gui, false);
this.mote = mote;
this.simulation = simulation;
updateTimer.start();
SERVER_DEFAULT_PORT = 60000 + mote.getID();
/* GUI components */
if (Cooja.isVisualized()) {
setResizable(false);
setLayout(new BorderLayout());
// --- Server Port setup
GridBagConstraints c = new GridBagConstraints();
JPanel socketPanel = new JPanel(new GridBagLayout());
socketPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
JLabel label = new JLabel("Listen port: ");
c.gridx = 0;
c.gridy = 0;
c.weightx = 0.1;
c.anchor = GridBagConstraints.EAST;
socketPanel.add(label, c);
NumberFormat nf = NumberFormat.getIntegerInstance();
nf.setGroupingUsed(false);
listenPortField = new JFormattedTextField(new NumberFormatter(nf));
listenPortField.setColumns(5);
listenPortField.setText(String.valueOf(SERVER_DEFAULT_PORT));
c.gridx++;
c.weightx = 0.0;
socketPanel.add(listenPortField, c);
serverStartButton = new JButton("Start") { // Button for label toggeling
private final String altString = "Stop";
@Override
public Dimension getPreferredSize() {
String origText = getText();
Dimension origDim = super.getPreferredSize();
setText(altString);
Dimension altDim = super.getPreferredSize();
setText(origText);
return new Dimension(Math.max(origDim.width, altDim.width), origDim.height);
}
};
c.gridx++;
c.weightx = 0.1;
c.anchor = GridBagConstraints.EAST;
socketPanel.add(serverStartButton, c);
c.gridx = 0;
c.gridy++;
c.gridwidth = GridBagConstraints.REMAINDER;
c.fill = GridBagConstraints.HORIZONTAL;
socketPanel.add(new JSeparator(JSeparator.HORIZONTAL), c);
add(BorderLayout.NORTH, socketPanel);
// --- Incoming / outgoing info
JPanel connectionInfoPanel = new JPanel(new GridLayout(0, 2));
connectionInfoPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
c = new GridBagConstraints();
label = new JLabel("socket -> mote: ");
label.setHorizontalAlignment(JLabel.RIGHT);
c.gridx = 0;
c.gridy = 0;
c.anchor = GridBagConstraints.EAST;
connectionInfoPanel.add(label);
socketToMoteLabel = new JLabel("0 bytes");
c.gridx++;
c.anchor = GridBagConstraints.WEST;
connectionInfoPanel.add(socketToMoteLabel);
label = new JLabel("mote -> socket: ");
label.setHorizontalAlignment(JLabel.RIGHT);
c.gridx = 0;
c.gridy++;
c.anchor = GridBagConstraints.EAST;
connectionInfoPanel.add(label);
moteToSocketLabel = new JLabel("0 bytes");
c.gridx++;
c.anchor = GridBagConstraints.WEST;
connectionInfoPanel.add(moteToSocketLabel);
add(BorderLayout.CENTER, connectionInfoPanel);
// --- Status bar
JPanel statusBarPanel = new JPanel(new BorderLayout()) {
@Override
public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
return new Dimension(STATUSBAR_WIDTH, d.height);
}
};
statusBarPanel.setLayout(new BoxLayout(statusBarPanel, BoxLayout.LINE_AXIS));
statusBarPanel.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
label = new JLabel("Status: ");
statusBarPanel.add(label);
socketStatusLabel = new JLabel("Idle");
socketStatusLabel.setForeground(Color.DARK_GRAY);
statusBarPanel.add(socketStatusLabel);
add(BorderLayout.SOUTH, statusBarPanel);
serverStartButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals("Start")) {
try {
listenPortField.commitEdit();
} catch (ParseException ex) {
java.util.logging.Logger.getLogger(SerialSocketClient.class.getName()).log(Level.SEVERE, null, ex);
}
startServer(((Long) listenPortField.getValue()).intValue());
} else {
stopServer();
}
}
});
pack();
}
/* Mote serial port */
serialPort = (SerialPort) mote.getInterfaces().getLog();
if (serialPort == null) {
throw new RuntimeException("No mote serial port");
}
if (Cooja.isVisualized()) {
// gui updates for server status updates
addServerListener(new ServerListener() {
@Override
public void onServerStarted(final int port) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
System.out.println("onServerStarted");
socketStatusLabel.setForeground(COLOR_NEUTRAL);
socketStatusLabel.setText("Listening on port " + String.valueOf(port));
listenPortField.setEnabled(false);
serverStartButton.setText("Stop");
}
});
}
@Override
public void onClientConnected(final Socket client) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
socketStatusLabel.setForeground(COLOR_POSITIVE);
socketStatusLabel.setText("Client "
+ client.getInetAddress() + ":" + client.getPort()
+ " connected.");
}
});
}
@Override
public void onClientDisconnected() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// XXX check why needed
if (serverSocket != null) {
socketStatusLabel.setForeground(COLOR_NEUTRAL);
socketStatusLabel.setText("Listening on port " + String.valueOf(serverSocket.getLocalPort()));
}
}
});
}
@Override
public void onServerStopped() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
listenPortField.setEnabled(true);
serverStartButton.setText("Start");
socketStatusLabel.setForeground(COLOR_NEUTRAL);
socketStatusLabel.setText("Idle");
}
});
}
@Override
public void onServerError(final String msg) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
socketStatusLabel.setForeground(COLOR_NEGATIVE);
socketStatusLabel.setText(msg);
}
});
}
});
}
}
private List<ServerListener> listeners = new LinkedList<>();
public interface ServerListener {
void onServerStarted(int port);
void onClientConnected(Socket client);
void onClientDisconnected();
void onServerStopped();
void onServerError(String msg);
}
private void addServerListener(ServerListener listener) {
listeners.add(listener);
}
public void notifyServerStarted(int port) {
for (ServerListener listener : listeners) {
listener.onServerStarted(port);
}
}
public void notifyClientConnected(Socket client) {
for (ServerListener listener : listeners) {
listener.onClientConnected(client);
}
}
public void notifyClientDisconnected() {
for (ServerListener listener : listeners) {
listener.onClientDisconnected();
}
}
public void notifyServerStopped() {
for (ServerListener listener : listeners) {
listener.onServerStopped();
}
}
public void notifyServerError(String msg) {
for (ServerListener listener : listeners) {
listener.onServerError(msg);
}
}
/**
* Start server ..
* @param port
*/
public void startServer(int port) {
try {
serverSocket = new ServerSocket(port);
logger.info("Listening on port: " + port);
notifyServerStarted(port);
} catch (IOException ex) {
logger.error(ex.getMessage());
notifyServerError(ex.getMessage());
return;
}
new Thread() {
private Thread incomingDataHandler;
@Override
public void run() {
while (!serverSocket.isClosed()) {
try {
// wait for next client
Socket candidateSocket = serverSocket.accept();
// reject connection if already one client connected
if (clientSocket != null && !clientSocket.isClosed()) {
logger.info("Refused connection of client " + candidateSocket.getInetAddress());
candidateSocket.close();
continue;
}
clientSocket = candidateSocket;
/* Start handler for data input from socket */
incomingDataHandler = new Thread(new IncomingDataHandler());
incomingDataHandler.start();
/* Observe serial port for outgoing data */
serialDataObserver = new SerialDataObserver();
serialPort.addSerialDataObserver(serialDataObserver);
inBytes = outBytes = 0;
logger.info("Client connected: " + clientSocket.getInetAddress());
notifyClientConnected(clientSocket);
} catch (IOException e) {
logger.info("Listening thread shut down: " + e.getMessage());
try {
serverSocket.close();
} catch (IOException ex) {
logger.error(ex);
}
}
}
cleanupClient();
if (incomingDataHandler != null) {
// Wait for reader thread to terminate
try {
incomingDataHandler.join(500);
} catch (InterruptedException ex) {
logger.warn(ex);
}
}
notifyServerStopped();
}
}.start();
}
/**
* Stops server by closing server listen socket.
*/
public void stopServer() {
try {
serverSocket.close();
} catch (IOException ex) {
logger.error(ex);
}
}
/* Forward data: virtual port -> mote */
private class IncomingDataHandler implements Runnable {
DataInputStream in;
@Override
public void run() {
int numRead = 0;
byte[] data = new byte[1024];
try {
in = new DataInputStream(clientSocket.getInputStream());
} catch (IOException ex) {
logger.error(ex);
return;
}
logger.info("Forwarder: socket -> serial port");
while (numRead >= 0) {
final int finalNumRead = numRead;
final byte[] finalData = data;
/* We are not on the simulation thread */
simulation.invokeSimulationThread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < finalNumRead; i++) {
serialPort.writeByte(finalData[i]);
}
inBytes += finalNumRead;
}
});
try {
numRead = in.read(data);
} catch (IOException e) {
logger.info(e.getMessage());
numRead = -1;
}
}
logger.info("End of Stream");
cleanupClient();
}
}
private class SerialDataObserver implements Observer {
DataOutputStream out;
public SerialDataObserver() {
try {
out = new DataOutputStream(clientSocket.getOutputStream());
} catch (IOException ex) {
logger.error(ex);
out = null;
}
}
@Override
public void update(Observable obs, Object obj) {
try {
if (out == null) {
/*logger.debug("out is null");*/
return;
}
out.write(serialPort.getLastSerialData());
out.flush();
outBytes++;
} catch (IOException ex) {
logger.error(ex);
cleanupClient();
}
}
}
@Override
public Collection<Element> getConfigXML() {
List<Element> config = new ArrayList<>();
Element element;
// XXX isVisualized guards?
element = new Element("port");
if (serverSocket == null || !serverSocket.isBound()) {
try {
listenPortField.commitEdit();
element.setText(String.valueOf((Long) listenPortField.getValue()));
} catch (ParseException ex) {
logger.error(ex.getMessage());
listenPortField.setText("null");
}
} else {
element.setText(String.valueOf(serverSocket.getLocalPort()));
}
config.add(element);
element = new Element("bound");
if (serverSocket == null) {
element.setText(String.valueOf(false));
} else {
element.setText(String.valueOf(!serverSocket.isClosed()));
}
config.add(element);
return config;
}
@Override
public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
Integer port = null;
boolean bound = false;
for (Element element : configXML) {
switch (element.getName()) {
case "port":
port = Integer.parseInt(element.getText());
break;
case "bound":
bound = Boolean.parseBoolean(element.getText());
break;
default:
logger.warn("Unknwon config element: " + element.getName());
break;
}
}
if (Cooja.isVisualized()) {
if (port != null) {
listenPortField.setText(String.valueOf(port));
}
if (bound) {
serverStartButton.doClick();
}
} else {
// if bound and all set up, start client
if (port != null) {
startServer(port);
} else {
logger.error("Server not started due to incomplete configuration");
}
}
return true;
}
private void cleanupClient() {
try {
if (clientSocket != null) {
clientSocket.close();
clientSocket = null;
}
} catch (IOException e1) {
logger.error(e1.getMessage());
}
serialPort.deleteSerialDataObserver(serialDataObserver);
notifyClientDisconnected();
}
private boolean closed = false;
@Override
public void closePlugin() {
closed = true;
cleanupClient();
try {
if (serverSocket != null) {
serverSocket.close();
}
} catch (IOException ex) {
logger.error(ex);
}
}
@Override
public Mote getMote() {
return mote;
}
private static final int UPDATE_INTERVAL = 150;
private Timer updateTimer = new Timer(UPDATE_INTERVAL, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (closed) {
updateTimer.stop();
return;
}
socketToMoteLabel.setText(inBytes + " bytes");
moteToSocketLabel.setText(outBytes + " bytes");
}
});
}