Closeable interface.
15 | *
16 | * @param close
17 | * Connection or stream to close.
18 | */
19 | public static void close(final Closeable close) {
20 | if (close != null) {
21 | try {
22 | close.close();
23 | } catch (final IOException e) {
24 | // do nothing
25 | }
26 | }
27 | }
28 |
29 | private Closer() {
30 | // static singleton
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/sr201-common/src/main/java/li/cryx/sr201/connection/IpAddressValidator.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.connection;
2 |
3 | import java.net.InetAddress;
4 | import java.net.UnknownHostException;
5 |
6 | /**
7 | * A class to validate IP addresses that were entered as string.
8 | *
9 | * @author cryxli
10 | */
11 | public class IpAddressValidator {
12 |
13 | /**
14 | * Validate the given IP address.
15 | *
16 | * @param ip
17 | * String representation of an IP address.
18 | * @return true, if the address is valid.
19 | */
20 | public boolean isValid(final String ip) {
21 | try {
22 | // let java do the validation against RFC 2732
23 | InetAddress.getByName(ip);
24 | return true;
25 | } catch (final UnknownHostException e) {
26 | return false;
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/sr201-common/pom.xml:
--------------------------------------------------------------------------------
1 | 10 | * Note about design: The implementations are kept low level and therefore 11 | * operate only on bytes. The same functionality as implemented in this class 12 | * could be achieved using the connection and wrapping the stream into 13 | * readers/writers. 14 | *
15 | * 16 | * @author cryxli 17 | */ 18 | abstract class AbstractSr201Connection implements Sr201Connection { 19 | 20 | @Override 21 | public String getStates() throws ConnectionException { 22 | // delegate to byte version of this method 23 | return toString(getStateBytes()); 24 | } 25 | 26 | @Override 27 | public String send(final String data) throws ConnectionException { 28 | // delegate to byte version of this method 29 | final byte[] sendBytes = data.getBytes(StandardCharsets.US_ASCII); 30 | final byte[] receivedBytes = send(sendBytes); 31 | return toString(receivedBytes); 32 | } 33 | 34 | /** 35 | * Turn the given byte array into a string. 36 | * 37 | * @param data 38 | * @return String containing the same bytes using US_ASCII 39 | */ 40 | private String toString(final byte[] data) { 41 | if (data != null) { 42 | return new String(data, StandardCharsets.US_ASCII); 43 | } else { 44 | return null; 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/ConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import li.cryx.sr201.Settings; 4 | 5 | /** 6 | * Factory class to create a {@link Sr201Connection} fitting for the given 7 | * configuration. 8 | * 9 | * @author cryxli 10 | */ 11 | public class ConnectionFactory { 12 | 13 | /** The connection configuration */ 14 | private final Settings settings; 15 | 16 | /** Create a new factory with the given config */ 17 | public ConnectionFactory(final Settings settings) { 18 | this.settings = settings; 19 | } 20 | 21 | /** 22 | * Create a new connection for the config. 23 | * 24 | * @return Instance of a {@link Sr201Connection} implementation. 25 | * @throws ConnectionException 26 | * is thrown, if the IP address is not valid. 27 | */ 28 | public Sr201Connection getConnection() throws ConnectionException { 29 | if (!settings.isValid()) { 30 | // validation failed 31 | throw new ConnectionException("msg.conn.factory.ip.invalid"); 32 | 33 | } else if (settings.isTcp()) { 34 | // TCP case 35 | return new Sr201TcpConnection(settings.getIp()); 36 | 37 | } else { 38 | // UDP case 39 | return new Sr201UdpConnection(settings.getIp()); 40 | } 41 | } 42 | 43 | public Sr201Connection getTcpConnection() throws ConnectionException { 44 | if (!settings.isValid()) { 45 | // validation failed 46 | throw new ConnectionException("msg.conn.factory.ip.invalid"); 47 | } else { 48 | // always return a TCP connection 49 | return new Sr201TcpConnection(settings.getIp()); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /sr201-common/src/test/java/li/cryx/sr201/connection/AbstractSocketProvider.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import java.io.IOException; 4 | import java.net.DatagramSocket; 5 | import java.net.Socket; 6 | import java.net.SocketException; 7 | import java.net.UnknownHostException; 8 | 9 | import li.cryx.sr201.connection.SocketFactory.SocketProvider; 10 | 11 | /** 12 | * special socket provider that lets a unittest replace the sockets on the fly. 13 | * 14 | * @author cryxli 15 | */ 16 | public class AbstractSocketProvider implements SocketProvider { 17 | 18 | private Socket socket; 19 | 20 | private DatagramSocket datagramSocket; 21 | 22 | /** Last host */ 23 | private String host; 24 | 25 | /** Last port */ 26 | private int port; 27 | 28 | public DatagramSocket getDatagramSocket() { 29 | return datagramSocket; 30 | } 31 | 32 | public String getLastHost() { 33 | return host; 34 | } 35 | 36 | public int getLastPort() { 37 | return port; 38 | } 39 | 40 | public Socket getSocket() { 41 | return socket; 42 | } 43 | 44 | @Override 45 | public DatagramSocket newDatagramSocket() throws SocketException { 46 | return datagramSocket; 47 | } 48 | 49 | @Override 50 | public Socket newSocket(final String host, final int port) throws UnknownHostException, IOException { 51 | this.host = host; 52 | this.port = port; 53 | return socket; 54 | } 55 | 56 | public void setDatagramSocket(final DatagramSocket datagramSocket) { 57 | this.datagramSocket = datagramSocket; 58 | } 59 | 60 | public void setSocket(final Socket socket) { 61 | this.socket = socket; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/State.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import javax.swing.Icon; 4 | 5 | import li.cryx.sr201.util.IconSupport; 6 | import li.cryx.sr201.util.Icons; 7 | 8 | /** 9 | * This enum represents the state of a relay. 10 | * 11 | * @author cryxli 12 | */ 13 | public enum State { 14 | 15 | /** Indicator that the state of a relay is not known. */ 16 | UNKNOWN('.', '.', Icons.UNKNOWN.resource()), 17 | /** Relay is off or released */ 18 | OFF('0', '2', Icons.OFF.resource()), 19 | /** Relay is on or pulled */ 20 | ON('1', '1', Icons.ON.resource()); 21 | 22 | public static State valueOfReceived(final byte b) { 23 | for (State s : values()) { 24 | if (s.receive == b) { 25 | return s; 26 | } 27 | } 28 | return UNKNOWN; 29 | } 30 | 31 | public static State valueOfSend(final byte b) { 32 | for (State s : values()) { 33 | if (s.send == b) { 34 | return s; 35 | } 36 | } 37 | return UNKNOWN; 38 | } 39 | 40 | private final byte receive; 41 | 42 | private final byte send; 43 | 44 | private final Icon icon; 45 | 46 | private State(final char receive, final char send, final String iconResource) { 47 | this.receive = (byte) receive; 48 | this.send = (byte) send; 49 | icon = IconSupport.getIcon(iconResource); 50 | } 51 | 52 | public Icon icon() { 53 | return icon; 54 | } 55 | 56 | /** Get the byte that is received and corresponds to this state. */ 57 | public byte receive() { 58 | return receive; 59 | } 60 | 61 | /** Get the byte that must be sent to switch to the current state. */ 62 | public byte send() { 63 | return send; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/client/DialogFactory.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client; 2 | 3 | import java.awt.Component; 4 | import java.util.ResourceBundle; 5 | 6 | import javax.swing.JOptionPane; 7 | 8 | public class DialogFactory { 9 | 10 | private static String appTitleKey = "app.title"; 11 | 12 | private final ResourceBundle resourceBundle; 13 | 14 | private final Component parent; 15 | 16 | public DialogFactory(final ResourceBundle resourceBundle) { 17 | this(resourceBundle, null); 18 | } 19 | 20 | public DialogFactory(final ResourceBundle resourceBundle, final Component parent) { 21 | this.resourceBundle = resourceBundle; 22 | this.parent = parent; 23 | } 24 | 25 | public void changeAppTitle(final String langKey) { 26 | appTitleKey = langKey; 27 | } 28 | 29 | public void error(final String msg) { 30 | JOptionPane.showMessageDialog(parent, msg, resourceBundle.getString(appTitleKey), JOptionPane.ERROR_MESSAGE); 31 | } 32 | 33 | public void errorTranslate(final String msgKey) { 34 | error(resourceBundle.getString(msgKey)); 35 | } 36 | 37 | public void info(final String msg) { 38 | JOptionPane.showMessageDialog(parent, msg, resourceBundle.getString(appTitleKey), 39 | JOptionPane.INFORMATION_MESSAGE); 40 | } 41 | 42 | public void infoTranslate(final String msgKey) { 43 | info(resourceBundle.getString(msgKey)); 44 | } 45 | 46 | public void warn(final String msg) { 47 | JOptionPane.showMessageDialog(parent, msg, resourceBundle.getString(appTitleKey), JOptionPane.WARNING_MESSAGE); 48 | } 49 | 50 | public void warnTranslate(final String msgKey) { 51 | warn(resourceBundle.getString(msgKey)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /sr201-common/src/test/java/li/cryx/sr201/connection/StateTest.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * Ensure additional functionality of enum {@link State}. 8 | * 9 | * @author cryxli 10 | */ 11 | public class StateTest { 12 | 13 | @Test 14 | public void testOffState() { 15 | final byte rec = '0'; 16 | final byte send = '2'; 17 | 18 | Assert.assertEquals(rec, State.OFF.receive()); 19 | Assert.assertEquals(send, State.OFF.send()); 20 | 21 | Assert.assertEquals(State.OFF, State.valueOfReceived(rec)); 22 | Assert.assertEquals(State.OFF, State.valueOfSend(send)); 23 | } 24 | 25 | @Test 26 | public void testOnState() { 27 | final byte rec = '1'; 28 | final byte send = '1'; 29 | 30 | Assert.assertEquals(rec, State.ON.receive()); 31 | Assert.assertEquals(send, State.ON.send()); 32 | 33 | Assert.assertEquals(State.ON, State.valueOfReceived(rec)); 34 | Assert.assertEquals(State.ON, State.valueOfSend(send)); 35 | } 36 | 37 | @Test 38 | public void testUnknownState() { 39 | final byte rec = '.'; 40 | final byte send = '.'; 41 | 42 | Assert.assertEquals(rec, State.UNKNOWN.receive()); 43 | Assert.assertEquals(send, State.UNKNOWN.send()); 44 | 45 | Assert.assertEquals(State.UNKNOWN, State.valueOfReceived(rec)); 46 | Assert.assertEquals(State.UNKNOWN, State.valueOfSend(send)); 47 | } 48 | 49 | @Test 50 | public void testValueOfReceived() { 51 | final byte b = 'X'; 52 | Assert.assertEquals(State.UNKNOWN, State.valueOfReceived(b)); 53 | } 54 | 55 | @Test 56 | public void testValueOfSend() { 57 | final byte b = 'A'; 58 | Assert.assertEquals(State.UNKNOWN, State.valueOfSend(b)); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /sr201-client/src/main/java/li/cryx/sr201/client/StateButton.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client; 2 | 3 | import java.awt.Dimension; 4 | import java.text.MessageFormat; 5 | import java.util.ResourceBundle; 6 | 7 | import javax.swing.JButton; 8 | 9 | import li.cryx.sr201.connection.Channel; 10 | import li.cryx.sr201.connection.State; 11 | 12 | public class StateButton extends JButton { 13 | 14 | private static final long serialVersionUID = 2915542922568617023L; 15 | 16 | private final ResourceBundle msg; 17 | 18 | private final Channel channel; 19 | 20 | private State state; 21 | 22 | public StateButton(final ResourceBundle msg, final Channel channel) { 23 | this(msg, channel, State.UNKNOWN); 24 | } 25 | 26 | public StateButton(final ResourceBundle msg, final Channel channel, final State state) { 27 | this.msg = msg; 28 | this.channel = channel; 29 | setState(state); 30 | } 31 | 32 | public Channel getChannel() { 33 | return channel; 34 | } 35 | 36 | // make square buttons 37 | @Override 38 | public Dimension getPreferredSize() { 39 | Dimension d = super.getPreferredSize(); 40 | int s = (int) (d.getWidth() < d.getHeight() ? d.getHeight() : d.getWidth()); 41 | return new Dimension(s, s); 42 | } 43 | 44 | public State getState() { 45 | return state; 46 | } 47 | 48 | public void setState(final State state) { 49 | this.state = state; 50 | 51 | setIcon(state.icon()); 52 | 53 | String tooltip = msg.getString("view.toggle.but.tooltip"); 54 | String channelStr = msg.getString("view.toggle.but.tooltip." + channel.name()); 55 | String stateStr = msg.getString("view.toggle.but.tooltip." + state.name()); 56 | setToolTipText(MessageFormat.format(tooltip, channelStr, stateStr)); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /sr201-server/pom.xml: -------------------------------------------------------------------------------- 1 |ResourceBundle factory on how to load
16 | * translation from XML Properties files.
17 | *
18 | * @author cryxli
19 | */
20 | public class XMLResourceBundleControl extends Control {
21 |
22 | @Override
23 | public ListDatagramPacket is an arbitrary java class that it created
12 | * within the method we want to set, we cannot compare two instances directly.
13 | * For this case Mockito offers a custom method to compare complex objects using
14 | * the ArgumentMatcher.
15 | *
16 | *
17 | * This matcher is configured with the expected values that were used to create
18 | * the DatagramPacket and it will compare them against the values
19 | * of the received packet.
20 | *
DatagramPacket sent to a
34 | * DatagramSocket.
35 | *
36 | * @param data
37 | * Expected sent data. US_ASCII is used to convert the string to
38 | * bytes.
39 | * @param ipAdr
40 | * IP address or host name of the target address.
41 | * @param port
42 | * Target port.
43 | * @throws UnknownHostException
44 | * is thrown, if the ipAdr cannot be resolved to an
45 | * actual IP address by Java.
46 | */
47 | public DatagramMatcher(final String data, final String ipAdr, final int port) throws UnknownHostException {
48 | this.data = data;
49 | this.ipAdr = InetAddress.getByName(ipAdr);
50 | this.port = port;
51 | }
52 |
53 | @Override
54 | public boolean matches(final Object argument) {
55 | final DatagramPacket p = (DatagramPacket) argument;
56 | return //
57 | // compare IP addresses
58 | p.getAddress().equals(ipAdr) //
59 | // compare ports
60 | && p.getPort() == port //
61 | // and data length
62 | && p.getLength() == data.length() //
63 | // compare data
64 | && data.equals(new String(p.getData(), StandardCharsets.US_ASCII));
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/sr201-common/src/test/java/li/cryx/sr201/connection/Sr201UdpConnectionTest.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.connection;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramSocket;
5 |
6 | import org.junit.AfterClass;
7 | import org.junit.Assert;
8 | import org.junit.Before;
9 | import org.junit.BeforeClass;
10 | import org.junit.Test;
11 | import org.mockito.Matchers;
12 | import org.mockito.Mockito;
13 |
14 | /**
15 | * Verify datagram interaction of {@link Sr201UdpConnection}.
16 | *
17 | * @author cryxli
18 | */
19 | public class Sr201UdpConnectionTest {
20 |
21 | private static AbstractSocketProvider socketProvider;
22 |
23 | @BeforeClass
24 | public static void replaceSockets() {
25 | // intersect socket
26 | socketProvider = new AbstractSocketProvider();
27 | SocketFactory.changeSocketProvider(socketProvider);
28 | }
29 |
30 | @AfterClass
31 | public static void resetSockets() {
32 | // reset socket factory
33 | SocketFactory.useDefaultSockets();
34 | }
35 |
36 | /** The connection under test */
37 | private Sr201UdpConnection conn;
38 |
39 | @Before
40 | public void createConnection() {
41 | // create new mock
42 | socketProvider.setDatagramSocket(Mockito.mock(DatagramSocket.class));
43 | // create new connection
44 | conn = new Sr201UdpConnection("192.168.1.100");
45 | }
46 |
47 | @Test
48 | public void testMultipleSend() throws IOException {
49 | // test
50 | Assert.assertEquals("00000000", conn.send("2X"));
51 | Assert.assertEquals("01000000", conn.send("12"));
52 | Assert.assertEquals("00000000", conn.send("22"));
53 | // verify
54 | Mockito.verify(socketProvider.getDatagramSocket())
55 | .send(Matchers.argThat(new DatagramMatcher("2X", "192.168.1.100", 6723)));
56 | Mockito.verify(socketProvider.getDatagramSocket())
57 | .send(Matchers.argThat(new DatagramMatcher("12", "192.168.1.100", 6723)));
58 | Mockito.verify(socketProvider.getDatagramSocket())
59 | .send(Matchers.argThat(new DatagramMatcher("22", "192.168.1.100", 6723)));
60 | }
61 |
62 | @Test
63 | public void testSend() throws IOException {
64 | // test
65 | Assert.assertEquals(".0......", conn.send("22"));
66 | // verify
67 | Mockito.verify(socketProvider.getDatagramSocket())
68 | .send(Matchers.argThat(new DatagramMatcher("22", "192.168.1.100", 6723)));
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/sr201-config-client/pom.xml:
--------------------------------------------------------------------------------
1 | Properties files.
14 | *
15 | * @author cryxli
16 | */
17 | public class PropertiesSupport {
18 |
19 | private static final Logger LOG = LoggerFactory.getLogger(PropertiesSupport.class);
20 |
21 | /**
22 | * Load an XML properties file from a file.
23 | *
24 | * @param file
25 | * Location on disk.
26 | * @return The loaded properties file, or, null in case of an
27 | * error.
28 | */
29 | public static Properties loadFromXml(final File file) {
30 | FileInputStream fis = null;
31 | try {
32 | fis = new FileInputStream(file);
33 | return loadFromXml(fis);
34 | } catch (IOException e) {
35 | LOG.error("Failed to load properties: " + file, e);
36 | return null;
37 | } finally {
38 | Closer.close(fis);
39 | }
40 | }
41 |
42 | /**
43 | * Load an XML properties file from an input stream.
44 | *
45 | * @param in
46 | * Data as stream.
47 | * @return The loaded properties file, or, null in case of an
48 | * error.
49 | */
50 | public static Properties loadFromXml(final InputStream in) {
51 | try {
52 | final Properties prop = new Properties();
53 | prop.loadFromXML(in);
54 | return prop;
55 | } catch (IOException e) {
56 | LOG.error("Failed to load properties from stream", e);
57 | return null;
58 | } finally {
59 | Closer.close(in);
60 | }
61 | }
62 |
63 | /**
64 | * Load an XML properties file from a classpath resource.
65 | *
66 | * @param resource
67 | * Location of resource.
68 | * @return The loaded properties file, or, null in case of an
69 | * error.
70 | */
71 | public static Properties loadFromXmlResource(final String resource) {
72 | try {
73 | final Properties prop = new Properties();
74 | prop.loadFromXML(PropertiesSupport.class.getResourceAsStream(resource));
75 | return prop;
76 | } catch (IOException e) {
77 | LOG.error("Failed to load properties from resource: " + resource, e);
78 | return null;
79 | }
80 | }
81 |
82 | private PropertiesSupport() {
83 | // static singleton
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/sr201-common/src/main/java/li/cryx/sr201/connection/SocketFactory.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.connection;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramSocket;
5 | import java.net.Socket;
6 | import java.net.SocketException;
7 | import java.net.UnknownHostException;
8 |
9 | /**
10 | * Since there is no easy way to intercept the low level communication, this
11 | * factory allows to replace socket implementation completely.
12 | *
13 | * @author cryxli
14 | */
15 | public class SocketFactory {
16 |
17 | /**
18 | * Definition of a provider for socket implementations.
19 | *
20 | * @author cryxli
21 | */
22 | public interface SocketProvider {
23 |
24 | /** Create a new instance of a UDP socket. */
25 | DatagramSocket newDatagramSocket() throws SocketException;
26 |
27 | /** Create a new instance of a TCP socket. */
28 | Socket newSocket(String host, int port) throws UnknownHostException, IOException;
29 |
30 | }
31 |
32 | /**
33 | * Default implementation that just returns Java's default sockets.
34 | */
35 | private static final SocketProvider DEFAULT_SOCKET_PROVIDER = new SocketProvider() {
36 |
37 | @Override
38 | public DatagramSocket newDatagramSocket() throws SocketException {
39 | return new DatagramSocket();
40 | }
41 |
42 | @Override
43 | public Socket newSocket(final String host, final int port) throws UnknownHostException, IOException {
44 | return new Socket(host, port);
45 | }
46 |
47 | };
48 |
49 | /**
50 | * Current socket provider. Defaults to Java's implementation from
51 | * java.net package.
52 | */
53 | private static SocketProvider provider = DEFAULT_SOCKET_PROVIDER;
54 |
55 | /** Replace socket factory with the provided one. */
56 | public static void changeSocketProvider(final SocketProvider socketProvider) {
57 | if (socketProvider != null) {
58 | provider = socketProvider;
59 | }
60 | }
61 |
62 | /** Get a new instance of a UDP DatagramSocket. */
63 | public static DatagramSocket newDatagramSocket() throws SocketException {
64 | return provider.newDatagramSocket();
65 | }
66 |
67 | /** Get a new instance of a TCP Socket. */
68 | public static Socket newSocket(final String host, final int port) throws UnknownHostException, IOException {
69 | return provider.newSocket(host, port);
70 | }
71 |
72 | /** Revert to using Java sockets. */
73 | public static void useDefaultSockets() {
74 | provider = DEFAULT_SOCKET_PROVIDER;
75 | }
76 |
77 | private SocketFactory() {
78 | // static stingleton
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/sr201-php-cloud-service/index.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | null.
33 | */
34 | public static ImageIcon getIcon(final Icons icon) {
35 | return getIcon(icon.resource());
36 | }
37 |
38 | /**
39 | * Get the icon from the indicated resource.
40 | *
41 | * @param resource
42 | * Classpath to the icon.
43 | * @return Loaded image as icon, or, null.
44 | */
45 | public static ImageIcon getIcon(final String resource) {
46 | Image img = getImage(resource);
47 | if (img != null) {
48 | return new ImageIcon(img);
49 | } else {
50 | return null;
51 | }
52 | }
53 |
54 | /**
55 | * Get the image as defined by the given enum.
56 | *
57 | * @param resource
58 | * Classpath to the image.
59 | * @return Loaded image, or, null.
60 | */
61 | public static BufferedImage getImage(final Icons icon) {
62 | return getImage(icon.resource());
63 | }
64 |
65 | /**
66 | * Get the image as defined by the given enum.
67 | *
68 | * @param icon
69 | * Enum representing the requested image.
70 | * @return Loaded image, or, null.
71 | */
72 | public static BufferedImage getImage(final String resource) {
73 | if (cache.get(resource) == null) {
74 | try {
75 | final BufferedImage img = ImageIO.read(IconSupport.class.getResourceAsStream(resource));
76 | cache.put(resource, img);
77 | } catch (IOException e) {
78 | LOG.warn("Cannot load image from resource: " + resource, e);
79 | }
80 | }
81 | return cache.get(resource);
82 | }
83 |
84 | private IconSupport() {
85 | // static singleton
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/sr201-common/src/test/java/li/cryx/sr201/config/Sr201CommandTest.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.config;
2 |
3 | import org.junit.Assert;
4 | import org.junit.Test;
5 |
6 | /**
7 | * Ensure additional functionality of enum {@link Sr201Command}.
8 | *
9 | * @author cryxli
10 | */
11 | public class Sr201CommandTest {
12 |
13 | @Test
14 | public void Command1() {
15 | Assert.assertEquals("#11111;", Sr201Command.QUERY_STATE.cmd(1111));
16 | Assert.assertEquals("#11111;", Sr201Command.QUERY_STATE.cmd(1111, "."));
17 | }
18 |
19 | @Test
20 | public void Command2() {
21 | Assert.assertEquals("#21111,{};", Sr201Command.SET_IP.cmd(1111));
22 | Assert.assertEquals("#21111,.;", Sr201Command.SET_IP.cmd(1111, "."));
23 | }
24 |
25 | @Test
26 | public void Command3() {
27 | Assert.assertEquals("#31111,{};", Sr201Command.SET_SUBNET.cmd(1111));
28 | Assert.assertEquals("#31111,.;", Sr201Command.SET_SUBNET.cmd(1111, "."));
29 | }
30 |
31 | @Test
32 | public void Command4() {
33 | Assert.assertEquals("#41111,{};", Sr201Command.SET_GATEWAY.cmd(1111));
34 | Assert.assertEquals("#41111,.;", Sr201Command.SET_GATEWAY.cmd(1111, "."));
35 | }
36 |
37 | @Test
38 | public void Command6off() {
39 | Assert.assertEquals("#61111,0;", Sr201Command.STATE_TEMPORARY.cmd(1111));
40 | Assert.assertEquals("#61111,0;", Sr201Command.STATE_TEMPORARY.cmd(1111, "."));
41 | }
42 |
43 | @Test
44 | public void Command6on() {
45 | Assert.assertEquals("#61111,1;", Sr201Command.STATE_PERSISTENT.cmd(1111));
46 | Assert.assertEquals("#61111,1;", Sr201Command.STATE_PERSISTENT.cmd(1111, "."));
47 | }
48 |
49 | @Test
50 | public void Command7() {
51 | Assert.assertEquals("#71111;", Sr201Command.RESTART.cmd(1111));
52 | Assert.assertEquals("#71111;", Sr201Command.RESTART.cmd(1111, "."));
53 | }
54 |
55 | @Test
56 | public void Command8() {
57 | Assert.assertEquals("#81111,{};", Sr201Command.SET_DNS.cmd(1111));
58 | Assert.assertEquals("#81111,.;", Sr201Command.SET_DNS.cmd(1111, "."));
59 | }
60 |
61 | @Test
62 | public void Command9() {
63 | Assert.assertEquals("#91111,{};", Sr201Command.SET_HOST.cmd(1111));
64 | Assert.assertEquals("#91111,.;", Sr201Command.SET_HOST.cmd(1111, "."));
65 | }
66 |
67 | @Test
68 | public void CommandAoff() {
69 | Assert.assertEquals("#A1111,0;", Sr201Command.CLOUD_DISABLE.cmd(1111));
70 | Assert.assertEquals("#A1111,0;", Sr201Command.CLOUD_DISABLE.cmd(1111, "."));
71 | }
72 |
73 | @Test
74 | public void CommandAon() {
75 | Assert.assertEquals("#A1111,1;", Sr201Command.CLOUD_ENABLE.cmd(1111));
76 | Assert.assertEquals("#A1111,1;", Sr201Command.CLOUD_ENABLE.cmd(1111, "."));
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/sr201-common/src/main/java/li/cryx/sr201/connection/ConnectionException.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.connection;
2 |
3 | import java.text.MessageFormat;
4 | import java.util.ResourceBundle;
5 |
6 | /**
7 | * Custom exception class that can be thrown in the context of a
8 | * {@link Sr201Connection}.
9 | *
10 | *
11 | * The {@link #getMessage()} method will return a language not the error message
12 | * itself. You can use the {@link #translate(ResourceBundle)} method to turn it
13 | * into an error text by supplying an appropriate ResourceBundle.
14 | *
15 | * The implementation should not throw IllegalStateExceptions
16 | * when the connection has already been closed.
17 | *
0 or 1, or, 48 or
36 | * 49 in decimal values.
37 | * @throws ConnectionException
38 | * is thrown when the command could not be sent or the answer
39 | * was not received correctly.
40 | */
41 | byte[] getStateBytes() throws ConnectionException;
42 |
43 | /**
44 | * Query the relay for its state. Only supported by TCP. UDP may emulate
45 | * this by returning the last state.
46 | *
47 | * @return 8 character string. Each character represents the state of a
48 | * relay as 0s or 1s.
49 | * @throws ConnectionException
50 | * is thrown when the command could not be sent or the answer
51 | * was not received correctly.
52 | */
53 | String getStates() throws ConnectionException;
54 |
55 | /**
56 | * Get the state of the connection.
57 | *
58 | * @return true means that the connection to the relay is open;
59 | * false that there is no connection.
60 | */
61 | boolean isConnected();
62 |
63 | /**
64 | * Send the given command to the relay and return its answer.
65 | *
66 | * @param data
67 | * Arbitrary command sequence understood by the relay.
68 | * @return 8 byte array representing the state of each relay as characters
69 | * 0 or 1, or, 48 or
70 | * 49 in decimal values.
71 | * @throws ConnectionException
72 | * is thrown when the command could not be sent or the answer
73 | * was not received correctly.
74 | */
75 | byte[] send(byte[] data) throws ConnectionException;
76 |
77 | /**
78 | * Send the given command to the relay and return its answer.
79 | *
80 | * @param data
81 | * Arbitrary command sequence understood by the relay. This
82 | * command will be turned into bytes using US_ASCII before
83 | * sending.
84 | * @return 8 character string. Each character represents the state of a
85 | * relay as 0s or 1s.
86 | * @throws ConnectionException
87 | * is thrown when the command could not be sent or the answer
88 | * was not received correctly.
89 | */
90 | String send(String data) throws ConnectionException;
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/sr201-php-cloud-service/form.css:
--------------------------------------------------------------------------------
1 | /* Better mobile view */
2 | @media screen and (max-width: 1000px) {
3 | body {
4 | zoom: 180%;
5 | }
6 | }
7 |
8 | /* Source: https://www.sanwebe.com/2014/08/css-html-forms-designs */
9 |
10 | .form-style-5{
11 | max-width: 500px;
12 | padding: 10px 20px;
13 | background: #f4f7f8;
14 | margin: 10px auto;
15 | padding: 20px;
16 | background: #f4f7f8;
17 | border-radius: 8px;
18 | font-family: Georgia, "Times New Roman", Times, serif;
19 | }
20 | .form-style-5 fieldset{
21 | border: none;
22 | }
23 | .form-style-5 legend {
24 | font-size: 1.4em;
25 | margin-bottom: 10px;
26 | }
27 | .form-style-5 label {
28 | display: block;
29 | margin-bottom: 8px;
30 | }
31 | .form-style-5 input[type="text"],
32 | .form-style-5 input[type="password"],
33 | .form-style-5 input[type="date"],
34 | .form-style-5 input[type="datetime"],
35 | .form-style-5 input[type="email"],
36 | .form-style-5 input[type="number"],
37 | .form-style-5 input[type="search"],
38 | .form-style-5 input[type="time"],
39 | .form-style-5 input[type="url"],
40 | .form-style-5 textarea,
41 | .form-style-5 select {
42 | font-family: Georgia, "Times New Roman", Times, serif;
43 | background: rgba(255,255,255,.1);
44 | border: none;
45 | border-radius: 4px;
46 | font-size: 16px;
47 | margin: 0;
48 | outline: 0;
49 | padding: 7px;
50 | width: 100%;
51 | box-sizing: border-box;
52 | -webkit-box-sizing: border-box;
53 | -moz-box-sizing: border-box;
54 | background-color: #e8eeef;
55 | color:#8a97a0;
56 | -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.03) inset;
57 | box-shadow: 0 1px 0 rgba(0,0,0,0.03) inset;
58 | margin-bottom: 30px;
59 |
60 | }
61 | .form-style-5 input[type="text"]:focus,
62 | .form-style-5 input[type="password"]:focus,
63 | .form-style-5 input[type="date"]:focus,
64 | .form-style-5 input[type="datetime"]:focus,
65 | .form-style-5 input[type="email"]:focus,
66 | .form-style-5 input[type="number"]:focus,
67 | .form-style-5 input[type="search"]:focus,
68 | .form-style-5 input[type="time"]:focus,
69 | .form-style-5 input[type="url"]:focus,
70 | .form-style-5 textarea:focus,
71 | .form-style-5 select:focus{
72 | background: #d2d9dd;
73 | }
74 | .form-style-5 select{
75 | -webkit-appearance: menulist-button;
76 | height:35px;
77 | }
78 | .form-style-5 .number {
79 | background: #1abc9c;
80 | color: #fff;
81 | height: 30px;
82 | width: 30px;
83 | display: inline-block;
84 | font-size: 0.8em;
85 | margin-right: 4px;
86 | line-height: 30px;
87 | text-align: center;
88 | text-shadow: 0 1px 0 rgba(255,255,255,0.2);
89 | border-radius: 15px 15px 15px 0px;
90 | }
91 |
92 | .form-style-5 input[type="submit"],
93 | .form-style-5 input[type="button"]
94 | {
95 | position: relative;
96 | display: block;
97 | padding: 19px 39px 18px 39px;
98 | color: #FFF;
99 | margin: 0 auto;
100 | background: #1abc9c;
101 | font-size: 18px;
102 | text-align: center;
103 | font-style: normal;
104 | width: 100%;
105 | border: 1px solid #16a085;
106 | border-width: 1px 1px 3px;
107 | margin-bottom: 10px;
108 | }
109 | .form-style-5 input[type="submit"]:hover,
110 | .form-style-5 input[type="button"]:hover
111 | {
112 | background: #109177;
113 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Client Application for SR-201 Ethernet Relay Board
2 |
3 | Resently I ordered a little board 50mm x 70mm with two relays. They can be switched by sending commands over TCP or UDP. The only problem with it is, that the code examples and instruction manual are entierly written in Chinese. Therefore, I created this repo to keep track of my findings regarding the SR-201-2.
4 |
5 | ## Models
6 |
7 | The same idea, switching relays over ethernet, resulted in at least four different models of the SR-201:
8 |
9 | * SR-201-1CH - Cased, single relay
10 | * SR-201-2 - Plain board, two relays (mine)
11 | * SR-201-RTC - Cased, four relays
12 | * SR-201-E8B - Plain board, eight relays
13 |
14 | They all seem to work with the same chip and software. Although, e.g., the SR-201-2 only has two relays, it also has an extension port with another 6 pins which can be switched, too.
15 |
16 | ## Protocols and Ports
17 |
18 | The board supports the protocols ARP, ICMP, IP, TCP, UDP. Or short, everything needed to allow TCP and UDP connections.
19 |
20 | When connected over TCP (port **6722**), the board will only accept 6 connections at a time. To prevent starving, it will close TCP connection after they have been idle for 15 seconds.
21 |
22 | Since UDP (port **6723**) is not an end-to-end connection, there are no restrictions. But it is noteworthy that the board will execute UDP commands, but it will never answer. Therefore querying the state of the relays has to be done over TCP.
23 |
24 | The board also listens to the TCP port **5111**. Over this connection the board can be configured. E.g., its static IP address can be changed.
25 |
26 | ## Factory Defaults
27 |
28 | * Static IP address : 192.168.1.100
29 | * Subnet mask : 255.255.255.0
30 | * Default Gateway : 192.168.1.1
31 | * Persistent relay state when power is lost : off
32 | * Cloud service password : 000000
33 | * DNS Server : 192.168.1.1
34 | * Cloud service : connect.tutuuu.com
35 | * Cloud service enabled: false
36 |
37 | ## Example Code
38 |
39 | This repo contains the following modules:
40 |
41 | * sr201-config-client - Client to read and change the config of the board.
42 | * sr201-client - Simple client with 8 toggle buttons to change the state of the relays.
43 | * sr201-server - REST interface to change the state of the relays.
44 | * sr201-php-cloud-service - Example implementation of a cloud service back-end in PHP provided by [hobpet](https://github.com/hobpet) following the findings of [anakhaema](https://github.com/anakhaema).
45 |
46 | Maven will create an executable JAR in each of the modules target directories.
47 |
48 | ## Scripts
49 |
50 | In addition to my Java code examples that are clearly intended as a replacement for the default VB and Delphi programs, I added a scripts directory that contains simpler more pragmatic approaches to the SR-201 communication scheme.
51 |
52 | * perl-config-script - A PERL script to manipulate the board's configuration by Christian DEGUEST.
53 | * python-config-script - A python script to manipulate the board's configuration.
54 |
55 | Many thanks to anyone who contributed to this knowledge base!
56 |
57 | ## Own Scripts
58 |
59 | If you want to quickly setup your SR-201 without even starting a script or anything else, just check the protocol [Config commands](https://github.com/cryxli/sr201/wiki/Config-commands) and e.g. send a command via netcat:
60 |
61 | printf "#11111;" | nc [yourip] 5111
62 |
63 | Note: It is crucial to use printf here, as newlines are seen as errors. It drove me crazy to find out about this one.
64 |
--------------------------------------------------------------------------------
/sr201-common/src/main/java/li/cryx/sr201/connection/Sr201TcpConnection.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.connection;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.io.OutputStream;
6 | import java.net.Socket;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.Arrays;
9 |
10 | import li.cryx.sr201.util.Closer;
11 |
12 | /**
13 | * TCP implementation of a {@link Sr201Connection}.
14 | *
15 | * 16 | * Only when connected over TCP the board will answer on any command with its 17 | * current state. But it only supports 6 continuous connections. Therefore, a 18 | * connection is closed after 15 seconds after the last command was received. 19 | * The manufacturer suggests to frequently send state queries to keep the 20 | * connection alive. 21 | *
22 | * 23 | *
24 | * Since only over TCP the board answers, only over TCP the command to query the
25 | * relay states is implemented. Sending the two ASCII character
26 | * "00" which is the same as 48, 48 in
27 | * decimal, will make the board to answer with its current state without
28 | * changing anything.
29 | *
32 | * The board will always answer with exactly 8 bytes after a successful command.
33 | * Each byte indicating the state of a relay. Off or released state is
34 | * represented by the decimal value 48 which is when turned into
35 | * US_ASCII the value of the 0 character. On or pulled state is
36 | * 49 decimal or 1 as a character.
37 | *
6722. */
44 | private static final int PORT = 6722;
45 |
46 | /** IP address of the relay. */
47 | private final String ip;
48 |
49 | /** Connection to the relay. */
50 | private Socket socket;
51 |
52 | /** Stream to send data */
53 | private OutputStream out;
54 |
55 | /** Stream to receive data */
56 | private InputStream in;
57 |
58 | public Sr201TcpConnection(final String ip) {
59 | this.ip = ip;
60 | }
61 |
62 | @Override
63 | public void close() {
64 | if (isConnected()) {
65 | Closer.close(socket);
66 | socket = null;
67 | out = null;
68 | in = null;
69 | }
70 | }
71 |
72 | @Override
73 | public void connect() throws ConnectionException {
74 | try {
75 | socket = SocketFactory.newSocket(ip, PORT);
76 | out = socket.getOutputStream();
77 | in = socket.getInputStream();
78 | } catch (final IOException e) {
79 | close();
80 | throw new ConnectionException("msg.tcp.cannot.connect", e, ip, PORT);
81 | }
82 | }
83 |
84 | @Override
85 | protected void finalize() throws Throwable {
86 | close();
87 | }
88 |
89 | @Override
90 | public byte[] getStateBytes() throws ConnectionException {
91 | return send("00".getBytes(StandardCharsets.US_ASCII));
92 | }
93 |
94 | @Override
95 | public boolean isConnected() {
96 | return socket != null;
97 | }
98 |
99 | @Override
100 | public byte[] send(final byte[] data) throws ConnectionException {
101 | if (!isConnected()) {
102 | // we have not yet connected
103 | connect();
104 | }
105 |
106 | try {
107 | out.write(data);
108 | out.flush();
109 | } catch (final IOException e) {
110 | throw new ConnectionException("msg.tcp.cannot.send", e);
111 | }
112 |
113 | try {
114 | final byte[] buf = new byte[1024];
115 | final int len = in.read(buf);
116 | if (len >= 0) {
117 | return Arrays.copyOf(buf, len);
118 | } else {
119 | // detect disconnect by peer
120 | close();
121 | throw new DisconnectedException();
122 | }
123 | } catch (final IOException e) {
124 | throw new ConnectionException("msg.tcp.cannot.receive", e);
125 | }
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/sr201-common/src/main/java/li/cryx/sr201/config/Sr201Command.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.config;
2 |
3 | import java.util.Random;
4 |
5 | /**
6 | * This enum contains the command sequences that can be sent to the SR-201 board
7 | * over TCP on port 5111. One command usually changes one parameter. Therefore,
8 | * commands sent in succession share a common sequence number.
9 | *
10 | * 11 | * // generate a random 4 digit number 12 | * int run = 1000 + new Random().nextInt(9000); // 9876 13 | * // set (Cloud) password to 123456 14 | * String setPw = Sr201Command.SET_PW.cmd(run, "123456"); // #B9876,123456; 15 | * // and restart the board within the same run 16 | * String restart = Sr201Command.RESTART.cmd(run); // #79876; 17 | *18 | * 19 | *
20 | * The board always answers with a string starting with > and
21 | * ending with ;. If the board could execute the command a
22 | * >OK; is returned. An >ERR;, if it does not
23 | * know the command.
24 | *
33 | * The board answers with a comma separated list of its settings. 34 | *
35 | * 36 | *37 | * >192.168.1.100,255.255.255.0,192.168.1.1,,0,435,F449007D02E2EB000000,192.168.1.1,connect.tutuuu.com,0; 38 | *39 | */ 40 | QUERY_STATE("#1{};"), 41 | 42 | /** 43 | * Set the board's IP address. Expects a IPv4 as a string (192.168.1.100) as 44 | * an argument. 45 | */ 46 | SET_IP("#2{},{};"), 47 | 48 | /** 49 | * Set the subnet mask. Expects a IPv4 mask as a string (255.255.255.0) as 50 | * an argument. 51 | */ 52 | SET_SUBNET("#3{},{};"), 53 | 54 | /** 55 | * Set the default gateway used to resolve the cloud service. Expects a IPv4 56 | * as a string (192.168.1.1) as an argument. 57 | */ 58 | SET_GATEWAY("#4{},{};"), 59 | 60 | // unknown command #5 61 | 62 | /** 63 | * Enable persistent relay states when board is powered off and on again. 64 | */ 65 | STATE_PERSISTENT("#6{},1;"), 66 | 67 | /** 68 | * Disable persistent relay states when board is powered off and on again. 69 | */ 70 | STATE_TEMPORARY("#6{},0;"), 71 | 72 | /** Restart the board. Make changes take effect. */ 73 | RESTART("#7{};"), 74 | 75 | /** 76 | * Set the DNS server used to resolve the cloud service. Expects a IPv4 as a 77 | * string (192.168.1.1) as an argument. 78 | */ 79 | SET_DNS("#8{},{};"), 80 | 81 | /** 82 | * Set the cloud server host. Expects a host name as an argument. 83 | */ 84 | SET_HOST("#9{},{};"), 85 | 86 | /** Disable cloud service. */ 87 | CLOUD_DISABLE("#A{},0;"), 88 | 89 | /** Enable cloud service. */ 90 | CLOUD_ENABLE("#A{},1;"), 91 | 92 | /** 93 | * Set the password of the cloud service. Expects a 6 character long 94 | * password as an argument. 95 | */ 96 | SET_PW("#B{},{};"); 97 | 98 | /** Config commands are sent to TCP port 5111. */ 99 | public static final int CONFIG_PORT = 5111; 100 | 101 | private final String cmd; 102 | 103 | private Sr201Command(final String cmd) { 104 | this.cmd = cmd; 105 | } 106 | 107 | /** Get command with random sequence. */ 108 | public String cmd() { 109 | return cmd(1000 + new Random().nextInt(9000)); 110 | } 111 | 112 | /** Get command with given sequence. */ 113 | public String cmd(final int run) { 114 | if (run >= 1000 && run <= 9999) { 115 | return cmd.replaceFirst("\\{\\}", String.valueOf(run)); 116 | } else { 117 | return cmd(); 118 | } 119 | } 120 | 121 | /** Get command with given sequence and data. */ 122 | public String cmd(final int run, final String data) { 123 | return cmd(run).replaceFirst("\\{\\}", data); 124 | } 125 | 126 | /** Get command with random sequence and given data. */ 127 | public String cmd(final String data) { 128 | return cmd(0, data); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/HighLevelConnection.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import java.io.Closeable; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import li.cryx.sr201.Settings; 8 | 9 | /** 10 | * This class offers a higher degree of abstraction to the TCP or UDP 11 | * implementation of the {@link Sr201Connection}. 12 | * 13 | * @author cryxli 14 | */ 15 | public class HighLevelConnection implements Closeable { 16 | 17 | /** Underlying connection. */ 18 | private final Sr201Connection conn; 19 | 20 | /** 21 | * Create new instance by wrapping the connection defined in the given 22 | * settings. 23 | * 24 | * @param settings 25 | * Connection settings. 26 | */ 27 | public HighLevelConnection(final Settings settings) { 28 | conn = new ConnectionFactory(settings).getConnection(); 29 | } 30 | 31 | /** 32 | * Turn a 8 byte answer into a map of channels and states. 33 | * 34 | * @param states 35 | * 8 byte array representing the state of each relay as 36 | * characters
0 or 1, or,
37 | * 48 or 49 in decimal values.
38 | * @return Map representing the same relay states.
39 | */
40 | private Maptrue means connected, false is not
90 | * connected.
91 | * @see Sr201Connection#isConnected()
92 | */
93 | public boolean isConnected() {
94 | return conn.isConnected();
95 | }
96 |
97 | /**
98 | * Send a state change command to a channel.
99 | *
100 | * @param channel
101 | * Indicates which relay should switch. Also supports special
102 | * {@link Channel#ALL} to switch all relays at once.
103 | * @param state
104 | * Into which state the relay should switch.
105 | * {@link State#UNKNOWN} is not supported and will therefore not
106 | * issue a command.
107 | * @return Map of channels and associated states.
108 | * @throws ConnectionException
109 | * is thrown if the command could not be executed correctly.
110 | */
111 | public Map17 | * With UDP there is not open connection to the board. Therefore, opening this 18 | * connection does not tell whether the board is available. TCP would run into a 19 | * timeout, if the board did not answer. 20 | *
21 | * 22 | *
23 | * Over UDP the board does not answer with its state. This implementation tries
24 | * to keep track of the sent commands ands determines the relay state from them.
25 | * Not all commands are understood. In addition to the usual 0 and
26 | * 1 answer characters . is used to express
27 | * uncertainty.
28 | *
6723. */
35 | private static final int PORT = 6723;
36 |
37 | /** IP address of the relay. */
38 | private final String ip;
39 |
40 | /** Connection to the relay. */
41 | private DatagramSocket socket;
42 |
43 | /** Validated IP address */
44 | private InetAddress ipAddress;
45 |
46 | /** Keep track of state changes */
47 | private byte[] states = "........".getBytes(StandardCharsets.US_ASCII);
48 |
49 | public Sr201UdpConnection(final String ip) {
50 | this.ip = ip;
51 | }
52 |
53 | @Override
54 | public void close() {
55 | if (isConnected()) {
56 | Closer.close(socket);
57 | socket = null;
58 | ipAddress = null;
59 | }
60 | }
61 |
62 | @Override
63 | public void connect() throws ConnectionException {
64 | // open socket
65 | try {
66 | socket = SocketFactory.newDatagramSocket();
67 | } catch (final SocketException e) {
68 | close();
69 | throw new ConnectionException("msg.udp.cannot.connect", e);
70 | }
71 | // validate IP address
72 | try {
73 | ipAddress = InetAddress.getByName(ip);
74 | } catch (final UnknownHostException e) {
75 | close();
76 | throw new ConnectionException("msg.udp.cannot.resolve.ip", e, ip);
77 | }
78 | // reset internal state
79 | updateStates(null);
80 | }
81 |
82 | @Override
83 | protected void finalize() {
84 | close();
85 | }
86 |
87 | @Override
88 | public byte[] getStateBytes() throws ConnectionException {
89 | return states;
90 | }
91 |
92 | @Override
93 | public boolean isConnected() {
94 | return socket != null;
95 | }
96 |
97 | @Override
98 | public byte[] send(final byte[] data) throws ConnectionException {
99 | // ensure connection
100 | if (!isConnected()) {
101 | connect();
102 | }
103 |
104 | final DatagramPacket sendPacket = new DatagramPacket(data, data.length, ipAddress, PORT);
105 | try {
106 | socket.send(sendPacket);
107 | } catch (final IOException e) {
108 | throw new ConnectionException("msg.udp.cannot.send", e);
109 | }
110 |
111 | // relay does not answer through UDP
112 | updateStates(data);
113 |
114 | return states;
115 | }
116 |
117 | /**
118 | * Since the board does not answer with its state, we keep track of the
119 | * changes we ordered it to make.
120 | *
121 | * @param data
122 | * Data bytes of latest command.
123 | */
124 | private void updateStates(final byte[] data) {
125 | if (data == null || data.length != 2) {
126 | // unknown command
127 | states = "........".getBytes(StandardCharsets.US_ASCII);
128 | return;
129 | }
130 |
131 | // EBNF:
132 | // COMMAND := STATE CHANNEL.
133 | // STATE := ON_STATE | OFF_STATE.
134 | // CHANNEL := CH1 | CH2 | CH3 | CH4 | CH5 | CH6 | CH7 | CH8 | ALL.
135 | // ON_STATE := <49 dec, or, "1" in ASCII>.
136 | // OFF_STATE := <50 dec, or, "2" in ASCII>.
137 | // CH1 := <49 dec, or, "1" in ASCII>.
138 | // CH2 := <50 dec, or, "2" in ASCII>.
139 | // etc.
140 | // ALL := <88 dec, or "X" in ASCII>.
141 |
142 | // any of the above commands can be executed for a certain amount of
143 | // seconds only (ASCII only):
144 | // COMMAND := STATE CHANNEL ":" SECONDS.
145 | // SECONDS := DIGIT DIGIT.
146 | // DIGIT := "0" | "1" | ... | "9".
147 | // but it is difficult to calculate a "resulting" state from it
148 |
149 | // There are two other "extended" commands:
150 | // COMMAND := STATE CHANNEL "*".
151 | // COMMAND := STATE CHANNEL "K".
152 | // which I do not understand
153 |
154 | final int state = data[0];
155 | final int channel = data[1];
156 |
157 | if (channel >= 49 && channel <= 56) {
158 | // channel 1 to 8
159 | if (state == 49) {
160 | states[channel - 49] = 49;
161 | } else if (state == 50) {
162 | states[channel - 49] = 48;
163 | }
164 | } else if (channel == 88) {
165 | // channel X = all relays at once
166 | if (state == 49) {
167 | states = "11111111".getBytes(StandardCharsets.US_ASCII);
168 | } else if (state == 50) {
169 | states = "00000000".getBytes(StandardCharsets.US_ASCII);
170 | }
171 | } else {
172 | // unknown command, unknown resulting state
173 | states = "........".getBytes(StandardCharsets.US_ASCII);
174 | }
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/sr201-common/src/test/java/li/cryx/sr201/TestConfigProtocol.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStreamReader;
5 | import java.io.OutputStreamWriter;
6 | import java.net.Socket;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.Random;
9 |
10 | import org.junit.After;
11 | import org.junit.Before;
12 | import org.junit.Ignore;
13 | import org.junit.Test;
14 |
15 | import li.cryx.sr201.config.BoardState;
16 | import li.cryx.sr201.config.Sr201Command;
17 | import li.cryx.sr201.util.Closer;
18 |
19 | /**
20 | * This test will try to connect to the SR-201 board and disable cloud service
21 | * and persistent relay state.
22 | *
23 | * @author cryxli
24 | */
25 | @Ignore
26 | public class TestConfigProtocol {
27 |
28 | /** IP address of board */
29 | private static final String IP_ADDRESS = "192.168.0.201";
30 |
31 | /** Default gateway */
32 | private static final String DNS_SERVER = "192.168.1.1";
33 |
34 | /** Password of cloud service */
35 | private static final String CLOUD_PASSWORD = "000000";
36 |
37 | private Socket socket;
38 |
39 | private OutputStreamWriter osw;
40 |
41 | private InputStreamReader isr;
42 |
43 | private int run;
44 |
45 | @Before
46 | public void connect() throws IOException {
47 | // connect to board
48 | socket = new Socket(IP_ADDRESS, Sr201Command.CONFIG_PORT);
49 | osw = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII);
50 | isr = new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII);
51 |
52 | // prepare a random sequence for the commands
53 | run = 1000 + new Random().nextInt(9000);
54 | }
55 |
56 | /**
57 | * Test to disable cloud service. No error handling.
58 | */
59 | @Test
60 | public void disableCloudTest() throws IOException {
61 | // get current config of board
62 | sendCommand(Sr201Command.QUERY_STATE.cmd(run));
63 | final BoardState state = BoardState.parseState(readAnswer());
64 | System.out.println(state);
65 |
66 | // only disable cloud service, if it is on
67 | if (state.isCloudServiceEnabled()) {
68 | // it is possible to send multiple commands one after another
69 | sendCommand(Sr201Command.CLOUD_DISABLE.cmd(run));
70 | if (!receiveOk()) {
71 | System.out.println("failed to disable Cloud service");
72 | return;
73 | }
74 | sendCommand(Sr201Command.SET_DNS.cmd(run, DNS_SERVER));
75 | if (!receiveOk()) {
76 | System.out.println("failed to set DNS server");
77 | return;
78 | }
79 | sendCommand(Sr201Command.SET_PW.cmd(run, CLOUD_PASSWORD));
80 | if (!receiveOk()) {
81 | System.out.println("failed to set password");
82 | return;
83 | }
84 | sendCommand(Sr201Command.RESTART.cmd(run));
85 | if (!receiveOk()) {
86 | System.out.println("failed to restart board");
87 | return;
88 | }
89 | }
90 | }
91 |
92 | /**
93 | * Test to disable persistent states. No error handling.
94 | */
95 | @Test
96 | public void disablePersistentTest() throws IOException {
97 | // get current config of board
98 | sendCommand(Sr201Command.QUERY_STATE.cmd(run));
99 | final BoardState state = BoardState.parseState(readAnswer());
100 | System.out.println(state);
101 |
102 | // only disable persistent states, if they are on
103 | if (state.isPersistent()) {
104 | sendCommand(Sr201Command.STATE_TEMPORARY.cmd(run));
105 | if (!receiveOk()) {
106 | System.out.println("failed to reset persistent state");
107 | return;
108 | }
109 | sendCommand(Sr201Command.RESTART.cmd(run));
110 | if (!receiveOk()) {
111 | System.out.println("failed to restart board");
112 | return;
113 | }
114 | }
115 | }
116 |
117 | @After
118 | public void disconnect() {
119 | Closer.close(socket);
120 | }
121 |
122 | /**
123 | * Read answer of board.
124 | *
125 | * @return String version of the byte[] answer after converting using
126 | * US_ASCII.
127 | * @throws IOException
128 | * is thrown, if an error occurred reading from the socket.
129 | */
130 | private String readAnswer() throws IOException {
131 | // read from socket
132 | final char[] chars = new char[1024];
133 | final int len = isr.read(chars);
134 | // turn into a string
135 | final String s = String.copyValueOf(chars, 0, len);
136 | // log received answer
137 | System.out.println("<<< " + s);
138 | // done
139 | return s;
140 | }
141 |
142 | /**
143 | * Read and check answer from board.
144 | *
145 | * @return true, if the board answered with an OK message,
146 | * false otherwise.
147 | * @throws IOException
148 | * is thrown, if an error occurred reading from the socket.
149 | */
150 | private boolean receiveOk() throws IOException {
151 | // read answer
152 | final String s = readAnswer();
153 | // expect it to be an OK answer
154 | return ">OK;".equals(s);
155 | }
156 |
157 | /**
158 | * Send the given command to the board.
159 | *
160 | * @param cmd
161 | * String version of a command for the board. Will be turned into
162 | * bytes using US_ASCII.
163 | * @throws IOException
164 | * is thrown, if an error occurred writing to the socket.
165 | */
166 | private void sendCommand(final String cmd) throws IOException {
167 | // log command
168 | System.out.println(">>> " + cmd);
169 | // send command
170 | osw.write(cmd);
171 | osw.flush();
172 | }
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/sr201-client/src/main/java/li/cryx/sr201/client/SettingsPanel.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.client;
2 |
3 | import java.awt.BorderLayout;
4 | import java.awt.Color;
5 | import java.awt.FlowLayout;
6 | import java.awt.event.ActionEvent;
7 | import java.awt.event.ActionListener;
8 | import java.util.ResourceBundle;
9 |
10 | import javax.swing.BorderFactory;
11 | import javax.swing.ButtonGroup;
12 | import javax.swing.JButton;
13 | import javax.swing.JLabel;
14 | import javax.swing.JPanel;
15 | import javax.swing.JRadioButton;
16 | import javax.swing.JTextField;
17 | import javax.swing.border.Border;
18 | import javax.swing.event.ChangeEvent;
19 | import javax.swing.event.ChangeListener;
20 |
21 | import org.slf4j.Logger;
22 | import org.slf4j.LoggerFactory;
23 |
24 | import com.jgoodies.forms.layout.CellConstraints;
25 | import com.jgoodies.forms.layout.FormLayout;
26 |
27 | import li.cryx.sr201.Settings;
28 | import li.cryx.sr201.connection.ConnectionException;
29 | import li.cryx.sr201.connection.ConnectionFactory;
30 | import li.cryx.sr201.connection.Sr201Connection;
31 | import li.cryx.sr201.util.Closer;
32 | import li.cryx.sr201.util.IconSupport;
33 | import li.cryx.sr201.util.Icons;
34 |
35 | public class SettingsPanel extends JPanel {
36 |
37 | private static final long serialVersionUID = -4290740433164662420L;
38 |
39 | private static final Logger LOG = LoggerFactory.getLogger(SettingsPanel.class);
40 |
41 | private JLabel lbIp;
42 |
43 | private JTextField txIp;
44 |
45 | private JLabel lbPort;
46 |
47 | private JTextField txPort;
48 |
49 | private JLabel lbProtocol;
50 |
51 | private JRadioButton rbTcp;
52 |
53 | private JRadioButton rbUdp;
54 |
55 | private JButton butTest;
56 |
57 | private JButton butAccept;
58 |
59 | private JButton butExit;
60 |
61 | /** Border of valid values. */
62 | private Border normal;
63 |
64 | /** Border to indicate invalid values. */
65 | private final Border error = BorderFactory.createLineBorder(Color.RED, 2);
66 |
67 | private final ResourceBundle msg;
68 |
69 | private JButton butSettings;
70 |
71 | public SettingsPanel(final ResourceBundle msg) {
72 | this.msg = msg;
73 | init();
74 | }
75 |
76 | public Settings applySettings(final Settings settings) {
77 | settings.setIp(getTxIp().getText());
78 | if (settings.isValid()) {
79 | getTxIp().setBorder(normal);
80 | } else {
81 | getTxIp().setBorder(error);
82 | }
83 |
84 | settings.setTcp(getRbTcp().isSelected());
85 |
86 | return settings;
87 | }
88 |
89 | public JButton getButAccept() {
90 | if (butAccept == null) {
91 | butAccept = new JButton(msg.getString("view.prop.but.accept.label"));
92 | butAccept.setIcon(IconSupport.getIcon(Icons.CONNECT));
93 | }
94 | return butAccept;
95 | }
96 |
97 | public JButton getButExit() {
98 | if (butExit == null) {
99 | butExit = new JButton(msg.getString("view.prop.but.exit.label"));
100 | butExit.setIcon(IconSupport.getIcon(Icons.EXIT));
101 | }
102 | return butExit;
103 | }
104 |
105 | public JButton getButSettings() {
106 | if (butSettings == null) {
107 | butSettings = new JButton(msg.getString(""));
108 | butSettings.setIcon(IconSupport.getIcon(Icons.SETTINGS));
109 | }
110 | return butSettings;
111 | }
112 |
113 | public JButton getButTest() {
114 | if (butTest == null) {
115 | butTest = new JButton(msg.getString("view.prop.but.test.label"));
116 | butTest.setIcon(IconSupport.getIcon(Icons.TEST));
117 |
118 | butTest.addActionListener(new ActionListener() {
119 | @Override
120 | public void actionPerformed(final ActionEvent evt) {
121 | final DialogFactory dialog = new DialogFactory(msg);
122 |
123 | final Settings tempSettings = applySettings(new Settings());
124 | if (!tempSettings.isValid()) {
125 | dialog.warnTranslate("msg.conn.factory.ip.invalid");
126 | } else {
127 | final Sr201Connection conn = new ConnectionFactory(tempSettings).getConnection();
128 | try {
129 | conn.connect();
130 | dialog.infoTranslate("msg.conn.factory.conn.ok");
131 | } catch (ConnectionException e) {
132 | LOG.warn("No connection to relay", e);
133 | dialog.warnTranslate("msg.conn.factory.cannot.connect");
134 | } finally {
135 | Closer.close(conn);
136 | }
137 | }
138 | }
139 | });
140 | }
141 | return butTest;
142 | }
143 |
144 | private JLabel getLbIp() {
145 | if (lbIp == null) {
146 | lbIp = new JLabel(msg.getString("view.prop.ip.label"));
147 | }
148 | return lbIp;
149 | }
150 |
151 | private JLabel getLbPort() {
152 | if (lbPort == null) {
153 | lbPort = new JLabel(msg.getString("view.prop.port.label"));
154 | }
155 | return lbPort;
156 | }
157 |
158 | private JLabel getLbProtocol() {
159 | if (lbProtocol == null) {
160 | lbProtocol = new JLabel(msg.getString("view.prop.protocol.label"));
161 | }
162 | return lbProtocol;
163 | }
164 |
165 | protected JRadioButton getRbTcp() {
166 | if (rbTcp == null) {
167 | rbTcp = new JRadioButton(msg.getString("view.prop.tcp.label"));
168 |
169 | // change port depending on selected protocol
170 | rbTcp.addChangeListener(new ChangeListener() {
171 | @Override
172 | public void stateChanged(final ChangeEvent evt) {
173 | if (getRbTcp().isSelected()) {
174 | getTxPort().setText("6722");
175 | } else {
176 | getTxPort().setText("6723");
177 | }
178 | }
179 | });
180 | }
181 | return rbTcp;
182 | }
183 |
184 | protected JRadioButton getRbUcp() {
185 | if (rbUdp == null) {
186 | rbUdp = new JRadioButton(msg.getString("view.prop.ucp.label"));
187 | }
188 | return rbUdp;
189 | }
190 |
191 | protected JTextField getTxIp() {
192 | if (txIp == null) {
193 | txIp = new JTextField();
194 | // remember "normal" border
195 | normal = txIp.getBorder();
196 | }
197 | return txIp;
198 | }
199 |
200 | protected JTextField getTxPort() {
201 | if (txPort == null) {
202 | txPort = new JTextField();
203 | txPort.setEditable(false);
204 | }
205 | return txPort;
206 | }
207 |
208 | private void init() {
209 | setLayout(new BorderLayout());
210 | setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
211 |
212 | final JPanel main = new JPanel(new FormLayout("p,3dlu,f:p:g", "p,4dlu,p,4dlu,p,p"));
213 | add(main, BorderLayout.NORTH);
214 |
215 | final CellConstraints cc = new CellConstraints();
216 |
217 | int row = 1;
218 | main.add(getLbIp(), cc.xy(1, row));
219 | main.add(getTxIp(), cc.xy(3, row));
220 | row += 2;
221 | main.add(getLbPort(), cc.xy(1, row));
222 | main.add(getTxPort(), cc.xy(3, row));
223 | row += 2;
224 | main.add(getLbProtocol(), cc.xy(1, row));
225 | main.add(getRbTcp(), cc.xy(3, row));
226 | row++;
227 | main.add(getRbUcp(), cc.xy(3, row));
228 |
229 | final ButtonGroup bg = new ButtonGroup();
230 | bg.add(getRbTcp());
231 | bg.add(getRbUcp());
232 |
233 | final JPanel butPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
234 | add(butPanel, BorderLayout.SOUTH);
235 | butPanel.add(getButTest());
236 | butPanel.add(getButAccept());
237 | butPanel.add(getButExit());
238 | }
239 |
240 | public void setFromSettings(final Settings settings) {
241 | getTxIp().setText(settings.getIp());
242 | getRbTcp().setSelected(settings.isTcp());
243 | getRbUcp().setSelected(!settings.isTcp());
244 | }
245 |
246 | }
--------------------------------------------------------------------------------
/sr201-common/src/main/resources/log4j.dtd:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
65 |
69 |
70 |
71 |
74 |
75 |
76 |
79 |
80 |
81 |
82 |
83 |
84 |
87 |
88 |
89 |
90 |
91 |
94 |
95 |
96 |
100 |
101 |
102 |
103 |
104 |
108 |
109 |
110 |
111 |
115 |
116 |
117 |
118 |
119 |
120 |
125 |
126 |
127 |
128 |
129 |
133 |
134 |
135 |
136 |
138 |
139 |
140 |
142 |
143 |
144 |
147 |
148 |
149 |
150 |
154 |
155 |
156 |
159 |
160 |
161 |
164 |
165 |
166 |
170 |
171 |
172 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
193 |
194 |
195 |
196 |
198 |
199 |
200 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
220 |
221 |
222 |
223 |
224 |
228 |
--------------------------------------------------------------------------------
/sr201-common/src/test/java/li/cryx/sr201/connection/Sr201TcpConnectionTest.java:
--------------------------------------------------------------------------------
1 | package li.cryx.sr201.connection;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.ByteArrayOutputStream;
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.net.Socket;
8 | import java.nio.charset.StandardCharsets;
9 |
10 | import org.junit.AfterClass;
11 | import org.junit.Assert;
12 | import org.junit.Before;
13 | import org.junit.BeforeClass;
14 | import org.junit.Test;
15 | import org.mockito.Matchers;
16 | import org.mockito.Mockito;
17 | import org.mockito.invocation.InvocationOnMock;
18 | import org.mockito.stubbing.Answer;
19 |
20 | /**
21 | * Verify basic functionality of {@link Sr201TcpConnection}.
22 | *
23 | * @author cryxli
24 | */
25 | public class Sr201TcpConnectionTest {
26 |
27 | private static AbstractSocketProvider socketProvider;
28 |
29 | @BeforeClass
30 | public static void replaceSockets() {
31 | // intersect socket
32 | socketProvider = new AbstractSocketProvider();
33 | SocketFactory.changeSocketProvider(socketProvider);
34 | }
35 |
36 | @AfterClass
37 | public static void resetSockets() {
38 | // reset socket factory
39 | SocketFactory.useDefaultSockets();
40 | }
41 |
42 | /** The connection under test */
43 | private Sr201TcpConnection conn;
44 |
45 | private void assertBytes(final String expected, final byte[] actual) {
46 | Assert.assertEquals(expected, new String(actual, StandardCharsets.US_ASCII));
47 | }
48 |
49 | @Before
50 | public void createConnection() {
51 | // create new mock
52 | socketProvider.setSocket(Mockito.mock(Socket.class));
53 | // create new connection
54 | conn = new Sr201TcpConnection("192.168.1.100");
55 | }
56 |
57 | /**
58 | * Verify that the connection does clear it internal state. This is only
59 | * visible as the {@link Sr201Connection#isConnected()} value.
60 | */
61 | @Test
62 | public void testClose() throws IOException {
63 | // test - never connected
64 | Assert.assertFalse(conn.isConnected());
65 | conn.close();
66 | Assert.assertFalse(conn.isConnected());
67 | // verify
68 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close();
69 |
70 | // test - connect first, then disconnect
71 | conn.connect();
72 | Assert.assertTrue(conn.isConnected());
73 | conn.close();
74 | Assert.assertFalse(conn.isConnected());
75 | // verify
76 | Mockito.verify(socketProvider.getSocket()).close();
77 | }
78 |
79 | /**
80 | * Verify that the connection prepares itself properly.
81 | */
82 | @Test
83 | public void testConnect() throws IOException {
84 | // test
85 | Assert.assertFalse(conn.isConnected());
86 | conn.connect();
87 | Assert.assertTrue(conn.isConnected());
88 |
89 | // verify
90 | Assert.assertEquals("192.168.1.100", socketProvider.getLastHost());
91 | Assert.assertEquals(6722, socketProvider.getLastPort());
92 | Mockito.verify(socketProvider.getSocket()).getInputStream();
93 | Mockito.verify(socketProvider.getSocket()).getOutputStream();
94 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close();
95 | }
96 |
97 | @Test
98 | public void testGetStateBytes() throws IOException {
99 | // prepare
100 | final ByteArrayInputStream is = new ByteArrayInputStream("000000".getBytes(StandardCharsets.US_ASCII));
101 | Mockito.when(socketProvider.getSocket().getInputStream()).thenReturn(is);
102 | final ByteArrayOutputStream os = new ByteArrayOutputStream();
103 | Mockito.when(socketProvider.getSocket().getOutputStream()).thenReturn(os);
104 | // test
105 | Assert.assertFalse(conn.isConnected());
106 | assertBytes("000000", conn.getStateBytes());
107 | Assert.assertTrue(conn.isConnected());
108 | // verify
109 | assertBytes("00", os.toByteArray());
110 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close();
111 | }
112 |
113 | @Test
114 | public void testGetStates() throws IOException {
115 | // prepare
116 | final ByteArrayInputStream is = new ByteArrayInputStream("000000".getBytes(StandardCharsets.US_ASCII));
117 | Mockito.when(socketProvider.getSocket().getInputStream()).thenReturn(is);
118 | final ByteArrayOutputStream os = new ByteArrayOutputStream();
119 | Mockito.when(socketProvider.getSocket().getOutputStream()).thenReturn(os);
120 | // test
121 | Assert.assertFalse(conn.isConnected());
122 | Assert.assertEquals("000000", conn.getStates());
123 | Assert.assertTrue(conn.isConnected());
124 | // verify
125 | assertBytes("00", os.toByteArray());
126 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close();
127 | }
128 |
129 | @Test
130 | public void testMultipleSends() throws IOException {
131 | // prepare - have input stream deliver two different streams
132 | final InputStream is = Mockito.mock(InputStream.class);
133 | Mockito.when(is.read(Matchers.any(byte[].class))).then(new Answer37 | * Whenever possible user inputs are validated before they are sent to the 38 | * board. Mainly IP addresses must comply to the RFC 1918. 39 | *
40 | * 41 | * @author cryxli 42 | */ 43 | public class RemoteConfigWindow extends JFrame { 44 | 45 | private static final long serialVersionUID = 965921233014060445L; 46 | 47 | private static final Logger LOG = LoggerFactory.getLogger(RemoteConfigWindow.class); 48 | 49 | /** Start config tool as standalone application */ 50 | public static void main(final String[] args) { 51 | new RemoteConfigWindow(); 52 | } 53 | 54 | /** Loaded translations */ 55 | private final ResourceBundle msg; 56 | 57 | /** Current static IP of the board. */ 58 | private String targetIp; 59 | 60 | /** Current settings received from the board. */ 61 | private BoardState config; 62 | 63 | private InfoPanel infoPanel; 64 | 65 | private IpAddressPanel ipPanel; 66 | 67 | private PersistPanel persistPanel; 68 | 69 | private CloudPannel cloudPanel; 70 | 71 | private JButton butSend; 72 | 73 | public RemoteConfigWindow() { 74 | // load translation 75 | msg = ResourceBundle.getBundle("i18n/lang", new XMLResourceBundleControl()); 76 | // load settings 77 | targetIp = SettingsFactory.loadSettings().getIp(); 78 | 79 | // prepare GUI 80 | init(); 81 | } 82 | 83 | private JButton getButSend() { 84 | if (butSend == null) { 85 | butSend = new JButton(msg.getString("view.config.send.button")); 86 | butSend.setIcon(IconSupport.getIcon(Icons.TEST)); 87 | } 88 | return butSend; 89 | } 90 | 91 | private void init() { 92 | setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 93 | setTitle(msg.getString("view.config.caption")); 94 | new DialogFactory(msg).changeAppTitle("view.config.caption"); 95 | 96 | final JPanel main = new JPanel(new FormLayout("f:p:g", "p,4dlu,p,4dlu,p,4dlu,p,4dlu,p,4dlu,p")); 97 | getContentPane().add(main); 98 | main.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 99 | final CellConstraints cc = new CellConstraints(); 100 | 101 | int row = 1; 102 | final ConnectionPanel connPanel = new ConnectionPanel(msg); 103 | main.add(connPanel, cc.xy(1, row)); 104 | connPanel.getTxIp().setText(targetIp); 105 | connPanel.getButConnect().addActionListener(new ActionListener() { 106 | @Override 107 | public void actionPerformed(final ActionEvent evt) { 108 | onConnect(connPanel.getTxIp().getText()); 109 | } 110 | }); 111 | row += 2; 112 | infoPanel = new InfoPanel(msg); 113 | main.add(infoPanel, cc.xy(1, row)); 114 | row += 2; 115 | ipPanel = new IpAddressPanel(msg); 116 | main.add(ipPanel, cc.xy(1, row)); 117 | row += 2; 118 | persistPanel = new PersistPanel(msg); 119 | main.add(persistPanel, cc.xy(1, row)); 120 | row += 2; 121 | cloudPanel = new CloudPannel(msg); 122 | main.add(cloudPanel, cc.xy(1, row)); 123 | // option buttons 124 | row += 2; 125 | final JPanel butPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); 126 | main.add(butPanel, cc.xy(1, row)); 127 | butPanel.add(getButSend()); 128 | getButSend().addActionListener(new ActionListener() { 129 | @Override 130 | public void actionPerformed(final ActionEvent evt) { 131 | onSend(); 132 | } 133 | }); 134 | 135 | // disable subpanels 136 | setSubpanelsEnabled(false); 137 | 138 | // center on screen 139 | pack(); 140 | final Dimension dim = getSize(); 141 | if (dim.width < 400) { 142 | dim.width = 400; 143 | } 144 | setSize(dim); 145 | setLocationRelativeTo(null); 146 | setVisible(true); 147 | } 148 | 149 | private void onConnect(final String ip) { 150 | final DialogFactory dialogFactory = new DialogFactory(msg); 151 | 152 | // validate current IP 153 | if (!new IpAddressValidator().isValid(ip)) { 154 | dialogFactory.warnTranslate("msg.conf.invalid.ip"); 155 | return; 156 | } 157 | targetIp = ip; 158 | 159 | // connect to current IP and read settings 160 | try { 161 | setSubpanelsEnabled(false); 162 | config = new ConfigConnectionBuilder(targetIp).readConfig(); 163 | } catch (final ConnectionException e) { 164 | LOG.error("Cannot read config", e); 165 | config = null; 166 | dialogFactory.error(e.translate(msg)); 167 | return; 168 | } 169 | 170 | // display current settings 171 | infoPanel.getTxSerial().setText(config.getSerialNumber()); 172 | infoPanel.getTxVersion().setText(config.getVersion()); 173 | ipPanel.getTxIp().setText(config.getIpAddress()); 174 | ipPanel.getTxSubnet().setText(config.getSubnetMask()); 175 | ipPanel.getTxGateway().setText(config.getGateway()); 176 | persistPanel.getChPersist().setSelected(config.isPersistent()); 177 | cloudPanel.getChService().setSelected(config.isCloudServiceEnabled()); 178 | cloudPanel.getTxService().setText(config.getCloudService()); 179 | cloudPanel.getTxPassword().setText(config.getPassword()); 180 | cloudPanel.getTxDns().setText(config.getDnsServer()); 181 | 182 | // enable lower part of window 183 | setSubpanelsEnabled(true); 184 | } 185 | 186 | private void onSend() { 187 | final ConfigConnectionBuilder builder = new ConfigConnectionBuilder(targetIp); 188 | final DialogFactory dialogFactory = new DialogFactory(msg); 189 | final IpAddressValidator validIp = new IpAddressValidator(); 190 | 191 | if (!config.getIpAddress().equals(ipPanel.getTxIp().getText())) { 192 | // static IP changed 193 | if (validIp.isValid(ipPanel.getTxIp().getText())) { 194 | builder.ip(ipPanel.getTxIp().getText()); 195 | } else { 196 | // not valid 197 | dialogFactory.errorTranslate("msg.conf.valid.ip"); 198 | return; 199 | } 200 | } 201 | if (!config.getSubnetMask().equals(ipPanel.getTxSubnet().getText())) { 202 | // subnet mask changed 203 | if (validIp.isValid(ipPanel.getTxSubnet().getText())) { 204 | builder.subnet(ipPanel.getTxSubnet().getText()); 205 | } else { 206 | // not valid 207 | dialogFactory.errorTranslate("msg.conf.valid.subnet"); 208 | return; 209 | } 210 | } 211 | if (!config.getGateway().equals(ipPanel.getTxGateway().getText())) { 212 | // default gateway changed 213 | if (validIp.isValid(ipPanel.getTxGateway().getText())) { 214 | builder.gateway(ipPanel.getTxGateway().getText()); 215 | } else { 216 | // not valid 217 | dialogFactory.errorTranslate("msg.conf.valid.gateway"); 218 | return; 219 | } 220 | } 221 | if (config.isPersistent() != persistPanel.getChPersist().isSelected()) { 222 | // persist relay state flag changed 223 | builder.persistRelayState(persistPanel.getChPersist().isSelected()); 224 | } 225 | if (config.isCloudServiceEnabled() != cloudPanel.getChService().isSelected()) { 226 | // enable cloud service flag changed 227 | builder.enableCloudService(cloudPanel.getChService().isSelected()); 228 | } 229 | if (!config.getCloudService().equals(cloudPanel.getTxService().getText())) { 230 | // cloud service host changed 231 | builder.cloudHost(cloudPanel.getTxService().getText()); 232 | } 233 | if (!config.getPassword().equals(cloudPanel.getTxPassword().getText())) { 234 | // cloud service password changed 235 | builder.password(cloudPanel.getTxPassword().getText()); 236 | } 237 | if (!config.getDnsServer().equals(cloudPanel.getTxDns().getText())) { 238 | // DNS server changed 239 | if (validIp.isValid(cloudPanel.getTxDns().getText())) { 240 | builder.dns(cloudPanel.getTxDns().getText()); 241 | } else { 242 | // not valid 243 | dialogFactory.errorTranslate("msg.conf.valid.dns"); 244 | return; 245 | } 246 | } 247 | 248 | if (!builder.hasPendingCommands()) { 249 | // no changes detected 250 | dialogFactory.infoTranslate("msg.conf.nothing"); 251 | return; 252 | } 253 | 254 | // send commands 255 | try { 256 | builder.send(); 257 | dialogFactory.infoTranslate("msg.conf.success"); 258 | } catch (final ConnectionException e) { 259 | LOG.error("Cannot send config", e); 260 | dialogFactory.error(e.translate(msg)); 261 | } 262 | 263 | // reset GUI, force user to reload config 264 | config = null; 265 | setSubpanelsEnabled(false); 266 | } 267 | 268 | private void setSubpanelsEnabled(final boolean enabled) { 269 | // enable/disable lower part of window 270 | infoPanel.setEnabled(enabled); 271 | ipPanel.setEnabled(enabled); 272 | persistPanel.setEnabled(enabled); 273 | cloudPanel.setEnabled(enabled); 274 | getButSend().setEnabled(enabled); 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/config/ConfigConnectionBuilder.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.config; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStreamReader; 5 | import java.io.OutputStreamWriter; 6 | import java.io.Reader; 7 | import java.io.Writer; 8 | import java.net.Socket; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Collections; 11 | import java.util.Comparator; 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.Random; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import li.cryx.sr201.connection.ConnectionException; 20 | import li.cryx.sr201.connection.DisconnectedException; 21 | import li.cryx.sr201.connection.SocketFactory; 22 | import li.cryx.sr201.util.Closer; 23 | 24 | /** 25 | * This class abstracts handling of the config commands. 26 | *27 | * The methods usually support chaining. This will execute multiple commands at 28 | * once, once {@link #send()} is called. 29 | *
30 | *31 | * The {@link #readConfig()} method will query the internal config of the board 32 | * immediately and wipe all pending commands. 33 | *
34 | * 35 | * @author cryxli 36 | */ 37 | public class ConfigConnectionBuilder { 38 | 39 | /** Internal data structure to have commands and their error combined */ 40 | private static class Command { 41 | /** Resolved command string. */ 42 | public String cmd; 43 | /** Language key to be displayed in case of an error */ 44 | public String error; 45 | 46 | public Command(final String cmd, final String error) { 47 | this.cmd = cmd; 48 | this.error = error; 49 | } 50 | } 51 | 52 | /** Comparator to sort {@link Command}s. */ 53 | private static class CommandComparator implements Comparator109 | * This command is staged and only takes effect, with the next 110 | * {@link #send()} method. 111 | *
112 | * 113 | * @param cloudHost 114 | * New host. 115 | */ 116 | public ConfigConnectionBuilder cloudHost(final String cloudHost) { 117 | cmds.add(new Command(Sr201Command.SET_HOST.cmd(run, cloudHost), "msg.conf.err.host")); 118 | return this; 119 | } 120 | 121 | /** 122 | * Open connection to board using the given IP address. 123 | * 124 | * @throws ConnectionException 125 | * is thrown whenever an IOException is detected. In that case 126 | * the connection is closed again. 127 | */ 128 | private void connect() throws ConnectionException { 129 | try { 130 | socket = SocketFactory.newSocket(ip, Sr201Command.CONFIG_PORT); 131 | in = new InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII); 132 | out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.US_ASCII); 133 | } catch (final IOException e) { 134 | close(); 135 | throw new ConnectionException("msg.conf.cannot.connect"); 136 | } 137 | } 138 | 139 | /** 140 | * Change the board's DNS server. 141 | *142 | * This command is staged and only takes effect, with the next 143 | * {@link #send()} method. 144 | *
145 | * 146 | * @param dns 147 | * New DNS server. 148 | */ 149 | public ConfigConnectionBuilder dns(final String dns) { 150 | cmds.add(new Command(Sr201Command.SET_DNS.cmd(run, dns), "msg.conf.err.dns")); 151 | return this; 152 | } 153 | 154 | /** 155 | * Set whether the board should connect to the cloud service. 156 | *157 | * This command is staged and only takes effect, with the next 158 | * {@link #send()} method. 159 | *
160 | * 161 | * @param enabled 162 | *true will enable the service, false
163 | * will disable it.
164 | */
165 | public ConfigConnectionBuilder enableCloudService(final boolean enabled) {
166 | if (enabled) {
167 | cmds.add(new Command(Sr201Command.CLOUD_ENABLE.cmd(run), "msg.conf.err.cloud"));
168 | } else {
169 | cmds.add(new Command(Sr201Command.CLOUD_DISABLE.cmd(run), "msg.conf.err.cloud"));
170 | }
171 | return this;
172 | }
173 |
174 | /**
175 | * Change the default gateway.
176 | * 177 | * This command is staged and only takes effect, with the next 178 | * {@link #send()} method. 179 | *
180 | * 181 | * @param ip 182 | * New default gateway. 183 | */ 184 | public ConfigConnectionBuilder gateway(final String gateway) { 185 | cmds.add(new Command(Sr201Command.SET_GATEWAY.cmd(run, gateway), "msg.conf.err.gateway")); 186 | return this; 187 | } 188 | 189 | /** 190 | * Indicator whether are are pending commands. 191 | * 192 | * @returntrue, if there is at least one command,
193 | * false otherwise.
194 | */
195 | public boolean hasPendingCommands() {
196 | return cmds.size() > 0;
197 | }
198 |
199 | /**
200 | * Change the board's IP address.
201 | * 202 | * This command is staged and only takes effect, with the next 203 | * {@link #send()} method. 204 | *
205 | * 206 | * @param ip 207 | * New IP address. 208 | */ 209 | public ConfigConnectionBuilder ip(final String ip) { 210 | cmds.add(new Command(Sr201Command.SET_IP.cmd(run, ip), "msg.conf.err.ip")); 211 | return this; 212 | } 213 | 214 | /** Create a new random sequence number. */ 215 | private void newRun() { 216 | run = 1000 + new Random().nextInt(9000); 217 | } 218 | 219 | /** 220 | * Change the cloud service password. 221 | *222 | * This command is staged and only takes effect, with the next 223 | * {@link #send()} method. 224 | *
225 | * 226 | * @param password 227 | * New password. 228 | */ 229 | public ConfigConnectionBuilder password(final String password) { 230 | cmds.add(new Command(Sr201Command.SET_PW.cmd(run, password), "msg.conf.err.password")); 231 | return this; 232 | } 233 | 234 | /** 235 | * Set whether the board should persist the relay states when power is lost, 236 | * and restore it after power is back. 237 | *238 | * This command is staged and only takes effect, with the next 239 | * {@link #send()} method. 240 | *
241 | * 242 | * @param enabled 243 | *true will enable the feature, false
244 | * will disable it.
245 | */
246 | public ConfigConnectionBuilder persistRelayState(final boolean persistent) {
247 | if (persistent) {
248 | cmds.add(new Command(Sr201Command.STATE_PERSISTENT.cmd(run), "msg.conf.err.persist"));
249 | } else {
250 | cmds.add(new Command(Sr201Command.STATE_TEMPORARY.cmd(run), "msg.conf.err.persist"));
251 | }
252 | return this;
253 | }
254 |
255 | /**
256 | * Ask board for its current config.
257 | *
258 | * @return Parsed config.
259 | * @throws ConnectionException
260 | * is thrown when a communication error occurs.
261 | */
262 | public BoardState readConfig() throws ConnectionException {
263 | connect();
264 | final String configString = send(Sr201Command.QUERY_STATE.cmd());
265 | close();
266 | return BoardState.parseState(configString);
267 | }
268 |
269 | /**
270 | * Send all pending commands.
271 | *
272 | * @throws ConnectionException
273 | * Sending is aborted, if any communication error is detected.
274 | * It is also aborted, if the board does not accept a command.
275 | */
276 | public void send() throws ConnectionException {
277 | if (cmds.size() == 0) {
278 | // nothing to do
279 | return;
280 | }
281 | // send commands in order
282 | Collections.sort(cmds, new CommandComparator());
283 | // add the restart/finished command
284 | cmds.add(new Command(Sr201Command.RESTART.cmd(run), "msg.conf.err.restart"));
285 |
286 | connect();
287 | // handshake by reading state
288 | send(Sr201Command.QUERY_STATE.cmd(run));
289 | // then execute commands
290 | for (final Command command : cmds) {
291 | // one command at a time
292 | final String answer = send(command.cmd);
293 | // check answer
294 | if (!">OK;".equals(answer)) {
295 | // board rejects command, abort
296 | if (command.error != null) {
297 | throw new ConnectionException(command.error);
298 | } else {
299 | break;
300 | }
301 | }
302 | }
303 | close();
304 | }
305 |
306 | /**
307 | * Send a command and wait for the answer from the board.
308 | *
309 | * @param cmd
310 | * A config command.
311 | * @return Received answer from board.
312 | * @throws ConnectionException
313 | * is thrown when a communication error occurs.
314 | */
315 | private String send(final String cmd) throws ConnectionException {
316 | try {
317 | LOG.info(">>> " + cmd);
318 | out.write(cmd);
319 | out.flush();
320 | } catch (final IOException e) {
321 | throw new ConnectionException("msg.conf.cannot.send", e);
322 | }
323 |
324 | try {
325 | final char[] buf = new char[1024];
326 | final int len = in.read(buf);
327 | if (len >= 0) {
328 | final String s = String.copyValueOf(buf, 0, len);
329 | LOG.info("<<< " + s);
330 | return s;
331 | } else {
332 | throw new DisconnectedException();
333 | }
334 | } catch (final IOException e) {
335 | throw new ConnectionException("msg.conf.cannot.receive", e);
336 | }
337 | }
338 |
339 | /**
340 | * Change the board's subnet mask.
341 | * 342 | * This command is staged and only takes effect, with the next 343 | * {@link #send()} method. 344 | *
345 | * 346 | * @param ip 347 | * New subnet mask. 348 | */ 349 | public ConfigConnectionBuilder subnet(final String subnet) { 350 | cmds.add(new Command(Sr201Command.SET_SUBNET.cmd(run, subnet), "msg.conf.err.subnet")); 351 | return this; 352 | } 353 | 354 | } 355 | -------------------------------------------------------------------------------- /scripts/perl-config-script/ConfigSR-201.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # Auteur : Christian DEGUEST 4 | # 5 | # Date : 19/08/2016 6 | # 7 | # But : Modifier la configuration du module SR-201 8 | # 9 | # Remerciements : Urs P. Stettler (https://github.com/cryxli) 10 | # Donc un grand merci a Urs pour son travail. 11 | # Globalement ce fichier n'est qu'une remise en forme de tout son travail 12 | # pour une utilisation en perl. 13 | # Total respect. 14 | # 15 | # 16 | # Protocole de configuration 17 | # 18 | # Port de connexion : 5111 19 | # 20 | # Dans ce qui suit les XXXX correspondent à un numéro de séquence arbitraire 21 | # Les commandes doivent etre envoyees sans CR ou CR/LF en fin de ligne. 22 | # 23 | # Toutes les commandes commencent par le caractere diese (#) et se finissent 24 | # par le caractere point virgule (;). 25 | # 26 | # Toutes les reponses commencent par le caractere > et se finissent par 27 | # le caractere point virgule (;). 28 | # 29 | # Hormis la commande #1, le module repond : 30 | # - Si la commande est correcte: 31 | # >OK; 32 | # 33 | # - Si la commande est incorrecte: 34 | # >ERR; 35 | # 36 | # Les modifications ne sont prises en compte qu'apres l'envoi d'une commande 37 | # #7XXXX; 38 | # 39 | # Pour le fun, vous pouvez utiliser l'utilitaire nc pour dialoguer avec le 40 | # module. 41 | # Attention, pas de CR ni de CR/LF a la fin d'une commande (donc pas de touche 42 | #