├── .gitignore ├── sr201-php-cloud-service ├── logs │ ├── clients.log │ ├── devices.log │ └── .htaccess ├── devices │ └── .htaccess ├── SyncServiceImpl.svc │ ├── .htaccess │ └── ReportStatus.php ├── index.php ├── form.css ├── device.php ├── form2.css ├── index2.php └── README.md ├── sr201-common ├── src │ ├── main │ │ ├── resources │ │ │ ├── icon │ │ │ │ ├── wrench-16.png │ │ │ │ ├── cancel-1-32.png │ │ │ │ ├── question-32.png │ │ │ │ ├── checked-1-16.png │ │ │ │ ├── checked-2-16.png │ │ │ │ ├── checked-2-32.png │ │ │ │ ├── power-button-16.png │ │ │ │ └── README │ │ │ ├── config │ │ │ │ └── default.xml │ │ │ ├── log4j.xml │ │ │ ├── i18n │ │ │ │ └── lang.xml │ │ │ └── log4j.dtd │ │ └── java │ │ │ └── li │ │ │ └── cryx │ │ │ └── sr201 │ │ │ ├── connection │ │ │ ├── DisconnectedException.java │ │ │ ├── Channel.java │ │ │ ├── IpAddressValidator.java │ │ │ ├── AbstractSr201Connection.java │ │ │ ├── ConnectionFactory.java │ │ │ ├── State.java │ │ │ ├── SocketFactory.java │ │ │ ├── ConnectionException.java │ │ │ ├── Sr201Connection.java │ │ │ ├── Sr201TcpConnection.java │ │ │ ├── HighLevelConnection.java │ │ │ └── Sr201UdpConnection.java │ │ │ ├── SettingsFactory.java │ │ │ ├── util │ │ │ ├── Closer.java │ │ │ ├── Icons.java │ │ │ ├── PropertiesSupport.java │ │ │ └── IconSupport.java │ │ │ ├── i18n │ │ │ ├── XMLResourceBundle.java │ │ │ └── XMLResourceBundleControl.java │ │ │ ├── client │ │ │ └── DialogFactory.java │ │ │ ├── Settings.java │ │ │ └── config │ │ │ ├── Sr201Command.java │ │ │ ├── BoardState.java │ │ │ └── ConfigConnectionBuilder.java │ └── test │ │ └── java │ │ └── li │ │ └── cryx │ │ └── sr201 │ │ ├── connection │ │ ├── AbstractSocketProvider.java │ │ ├── StateTest.java │ │ ├── DatagramMatcher.java │ │ ├── Sr201UdpConnectionTest.java │ │ └── Sr201TcpConnectionTest.java │ │ ├── config │ │ ├── BoardStateTest.java │ │ └── Sr201CommandTest.java │ │ └── TestConfigProtocol.java └── pom.xml ├── scripts └── perl-config-script │ ├── README.md │ └── ConfigSR-201.pl ├── sr201-server ├── README ├── src │ └── main │ │ ├── java │ │ └── li │ │ │ └── cryx │ │ │ └── sr201 │ │ │ └── server │ │ │ ├── controller │ │ │ ├── JsonSchema.java │ │ │ └── Sr201Controller.java │ │ │ └── ServerStarter.java │ │ └── resources │ │ └── GenericAnswer.json └── pom.xml ├── sr201-client ├── README ├── src │ └── main │ │ └── java │ │ └── li │ │ └── cryx │ │ └── sr201 │ │ └── client │ │ ├── StateButton.java │ │ ├── MainWindow.java │ │ ├── TogglePanel.java │ │ └── SettingsPanel.java └── pom.xml ├── sr201-config-client ├── README ├── src │ └── main │ │ └── java │ │ └── li │ │ └── cryx │ │ └── sr201 │ │ └── client │ │ └── conf │ │ ├── PersistPanel.java │ │ ├── ConnectionPanel.java │ │ ├── InfoPanel.java │ │ ├── IpAddressPanel.java │ │ ├── CloudPannel.java │ │ └── RemoteConfigWindow.java └── pom.xml ├── launcher └── sr201 clean install.launch ├── LICENSE ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | target 5 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/logs/clients.log: -------------------------------------------------------------------------------- 1 | Client access log comes here 2 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/logs/devices.log: -------------------------------------------------------------------------------- 1 | Device access log comes here 2 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/devices/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule .* / [R] 3 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/logs/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule .* / [R] 3 | -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/wrench-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/wrench-16.png -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/cancel-1-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/cancel-1-32.png -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/question-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/question-32.png -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/checked-1-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/checked-1-16.png -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/checked-2-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/checked-2-16.png -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/checked-2-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/checked-2-32.png -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/power-button-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryxli/sr201/HEAD/sr201-common/src/main/resources/icon/power-button-16.png -------------------------------------------------------------------------------- /sr201-php-cloud-service/SyncServiceImpl.svc/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | Options -Indexes 3 | 4 | RewriteCond %{REQUEST_FILENAME} !-d 5 | RewriteCond %{REQUEST_FILENAME}\.php -f 6 | RewriteRule ^(.+)$ $1.php [L] -------------------------------------------------------------------------------- /sr201-common/src/main/resources/icon/README: -------------------------------------------------------------------------------- 1 | The icons in the swing clients are from the Multimedia Element Set by Hadrien 2 | 3 | License: CC 3.0 BY 4 | Source.: http://www.flaticon.com/packs/multimedia-element-set 5 | -------------------------------------------------------------------------------- /sr201-common/src/main/resources/config/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default settings 5 | 192.168.1.100 6 | TCP 7 | 8082 8 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/DisconnectedException.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | public class DisconnectedException extends ConnectionException { 4 | 5 | private static final long serialVersionUID = -6902261289003066861L; 6 | 7 | public DisconnectedException() { 8 | super("msg.tcp.disconnected"); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /scripts/perl-config-script/README.md: -------------------------------------------------------------------------------- 1 | # ConfigSR-201.pl 2 | 3 | Implementation of the config-client as a PERL command line tool by Christian 4 | DEGUEST. 5 | 6 | In addition to the PERL implementation the script contains a French translation 7 | of the config wiki pages. 8 | 9 | The script may be especially interesting for Linux users as PERL is much more 10 | common on that platform than Java. 11 | -------------------------------------------------------------------------------- /sr201-common/src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sr201-server/README: -------------------------------------------------------------------------------- 1 | sr201-server 2 | ------------ 3 | 4 | This module illustrates how to create a REST interface on top of the normal TCP 5 | connection. 6 | 7 | I use Spring Boot to keep the code clean. Of course, Spring boot is complete 8 | overkill. But it produces a stand-alone web server for free. 9 | 10 | 11 | Usage 12 | ----- 13 | 14 | Simply start the produces *.jar in the target directory: 15 | 16 | > java -jar sr201-server-.jar 17 | 18 | It will read the Settings in your user home sr201.xml and listen to the "server.port" 19 | property specified therein. By default this is port 8082. 20 | -------------------------------------------------------------------------------- /sr201-client/README: -------------------------------------------------------------------------------- 1 | sr201-client 2 | ------------ 3 | 4 | This module implements a simple swing client that lets you switch all 8 relays. 5 | 6 | <> 7 | 8 | Usage 9 | ----- 10 | 11 | Start the JAR in the target directory that contains all the dependencies: 12 | 13 | > java -jar sr201-client--jar-with-dependencies.jar 14 | 15 | At first you are presented with the config dialog that lets you specify the boards IP 16 | address and over which protocol you want to connect to it. 17 | 18 | Then a dialog with eight toggle buttons is shown. The icons of the buttons represent the 19 | states of the relays. Clicking a button toggles the respective relay state. 20 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/SettingsFactory.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201; 2 | 3 | import java.io.File; 4 | import java.util.Properties; 5 | 6 | import li.cryx.sr201.util.PropertiesSupport; 7 | 8 | public class SettingsFactory { 9 | 10 | public static Settings loadSettings() { 11 | final File file = new File(System.getProperty("user.home"), "sr201.xml"); 12 | if (file.isFile()) { 13 | final Properties prop = PropertiesSupport.loadFromXml(file); 14 | return prop != null ? new Settings(prop) : new Settings(); 15 | } else { 16 | return new Settings(); 17 | } 18 | } 19 | 20 | private SettingsFactory() { 21 | // singleton 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /sr201-server/src/main/java/li/cryx/sr201/server/controller/JsonSchema.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.server.controller; 2 | 3 | import org.springframework.core.io.ClassPathResource; 4 | import org.springframework.core.io.Resource; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping("/api/schema") 11 | public class JsonSchema { 12 | 13 | @RequestMapping(value = "answer.json", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) 14 | public Resource getAnswerSchema() { 15 | return new ClassPathResource("GenericAnswer.json"); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/Channel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | public enum Channel { 4 | /** Relay 1 */ 5 | CH1('1'), 6 | /** Relay 2 */ 7 | CH2('2'), 8 | /** Relay 3 */ 9 | CH3('3'), 10 | /** Relay 4 */ 11 | CH4('4'), 12 | /** Relay 5 */ 13 | CH5('5'), 14 | /** Relay 6 */ 15 | CH6('6'), 16 | /** Relay 7 */ 17 | CH7('7'), 18 | /** Relay 8 */ 19 | CH8('8'), 20 | 21 | /** 22 | * Special constant indicating "all relays". Used to switch all relays at 23 | * once. 24 | */ 25 | ALL('X'); 26 | 27 | private final byte key; 28 | 29 | private Channel(final char ch) { 30 | key = (byte) ch; 31 | } 32 | 33 | /** Get byte representing this channel. */ 34 | public byte key() { 35 | return key; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/util/Closer.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.util; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 7 | * A helper class to silently close connections, streams and alike. 8 | * 9 | * @author cryxli 10 | */ 11 | public class Closer { 12 | 13 | /** 14 | * Close the given implementation of the 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 | 3 | 4.0.0 4 | 5 | li.cryx.sr201 6 | sr201 7 | 0.0.1-SNAPSHOT 8 | 9 | sr201-common 10 | 11 | 12 | 13 | 14 | org.slf4j 15 | slf4j-api 16 | 17 | 18 | 19 | 20 | junit 21 | junit 22 | 23 | 24 | org.mockito 25 | mockito-core 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sr201-config-client/README: -------------------------------------------------------------------------------- 1 | sr201-config-client 2 | ------------------- 3 | 4 | This module implements a swing GUI tool that lets you change the settings of the 5 | relay board. 6 | 7 | You can change the IP address and subnet mask of the board. The ports are fixed. You can 8 | also change the settings for the cloud service. 9 | 10 | 11 | Usage 12 | ----- 13 | 14 | Start the JAR in the target directory that contains all the dependencies: 15 | 16 | > java -jar sr201-config-client--jar-with-dependencies.jar 17 | 18 | At first everything but the current IP address field is grayed out. You have to connect 19 | once and therefore read the current configuration from the board to activate the other 20 | field. 21 | 22 | Once you changed everything you want, all settings are sent at once and the connection is 23 | closed again. Therefore, the GUI will return to grayed out state and you have to reconnect 24 | to activate it again. 25 | -------------------------------------------------------------------------------- /launcher/sr201 clean install.launch: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/util/Icons.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.util; 2 | 3 | /** 4 | * This class centralizes the definition of icons and images. 5 | * 6 | * @author cryxli 7 | */ 8 | public enum Icons { 9 | 10 | /** Icon for "test connection" button. */ 11 | TEST("/icon/checked-2-16.png"), 12 | /** Icon for "connect" button. */ 13 | CONNECT("/icon/checked-1-16.png"), 14 | /** Icon for "exit application" button. */ 15 | EXIT("/icon/power-button-16.png"), 16 | /** Icon for "show settings" button. */ 17 | SETTINGS("/icon/wrench-16.png"), 18 | 19 | /** Icon for "unknown relay state" toggle button */ 20 | UNKNOWN("/icon/question-32.png"), 21 | /** Icon for "relay on state" toggle button */ 22 | ON("/icon/checked-2-32.png"), 23 | /** Icon for "relay off state" toggle button */ 24 | OFF("/icon/cancel-1-32.png"); 25 | 26 | private final String resource; 27 | 28 | private Icons(final String resource) { 29 | this.resource = resource; 30 | } 31 | 32 | /** Get the classpath to the image. */ 33 | public String resource() { 34 | return resource; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/i18n/XMLResourceBundle.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.i18n; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.Enumeration; 6 | import java.util.Properties; 7 | import java.util.ResourceBundle; 8 | 9 | /** 10 | * This is a container to keep a loaded language file in memory when it was read 11 | * from XML. 12 | * 13 | * @author cryxli 14 | */ 15 | public class XMLResourceBundle extends ResourceBundle { 16 | 17 | /** List of translated keys */ 18 | private final Properties props; 19 | 20 | /** 21 | * Create a new instance and load the translations from the given input 22 | * stream. 23 | */ 24 | XMLResourceBundle(final InputStream stream) throws IOException { 25 | props = new Properties(); 26 | props.loadFromXML(stream); 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | @Override 31 | public Enumeration getKeys() { 32 | return (Enumeration) props.propertyNames(); 33 | } 34 | 35 | @Override 36 | protected Object handleGetObject(final String key) { 37 | return props.getProperty(key); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Urs P. Stettler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sr201-config-client/src/main/java/li/cryx/sr201/client/conf/PersistPanel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client.conf; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import javax.swing.BorderFactory; 6 | import javax.swing.JCheckBox; 7 | import javax.swing.JPanel; 8 | 9 | import com.jgoodies.forms.layout.CellConstraints; 10 | import com.jgoodies.forms.layout.FormLayout; 11 | 12 | public class PersistPanel extends JPanel { 13 | 14 | private static final long serialVersionUID = 2090270720647409113L; 15 | 16 | private final ResourceBundle msg; 17 | 18 | private JCheckBox chPersist; 19 | 20 | public PersistPanel(final ResourceBundle msg) { 21 | this.msg = msg; 22 | 23 | setBorder(BorderFactory.createTitledBorder(msg.getString("view.conf.persist.caption"))); 24 | setLayout(new FormLayout("4dlu,p,4dlu,", "p,4dlu")); 25 | final CellConstraints cc = new CellConstraints(); 26 | 27 | final int row = 1; 28 | add(getChPersist(), cc.xy(2, row)); 29 | } 30 | 31 | public JCheckBox getChPersist() { 32 | if (chPersist == null) { 33 | chPersist = new JCheckBox(msg.getString("view.conf.persist.enable.label")); 34 | } 35 | return chPersist; 36 | } 37 | 38 | @Override 39 | public void setEnabled(final boolean enabled) { 40 | super.setEnabled(enabled); 41 | 42 | chPersist.setEnabled(enabled); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /sr201-server/src/main/resources/GenericAnswer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | 4 | "type": "object", 5 | "properties": { 6 | "nok": { 7 | "description": "Optional error message.", 8 | "type": "string" 9 | }, 10 | "states": { 11 | "description": "Each relay is repesented with its current state.", 12 | "type": "object", 13 | "properties": { 14 | "1": { "$ref": "#/definitions/state" }, 15 | "2": { "$ref": "#/definitions/state" }, 16 | "3": { "$ref": "#/definitions/state" }, 17 | "4": { "$ref": "#/definitions/state" }, 18 | "5": { "$ref": "#/definitions/state" }, 19 | "6": { "$ref": "#/definitions/state" }, 20 | "7": { "$ref": "#/definitions/state" }, 21 | "8": { "$ref": "#/definitions/state" } 22 | }, 23 | "additionalProperties": false 24 | } 25 | }, 26 | "required": [ "states" ], 27 | "additionalProperties": false, 28 | 29 | "definitions": { 30 | "state": { 31 | "description": "State of a relay can be: 1=on/pulled, 0=off/released, 2=unknown", 32 | "type": "number", 33 | "multipleOf" : 1, 34 | "minimum": 0, 35 | "maximum": 2 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/AbstractSr201Connection.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | /** 6 | * Methods common to TCP and UDP implementation of {@link Sr201Connection} are 7 | * implemented in this common super class. 8 | * 9 | *

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 | 3 | 4.0.0 4 | 5 | li.cryx.sr201 6 | sr201 7 | 0.0.1-SNAPSHOT 8 | 9 | sr201-server 10 | 11 | 12 | 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-dependencies 17 | 1.4.0.RELEASE 18 | pom 19 | import 20 | 21 | 22 | 23 | 24 | 25 | 26 | ${project.groupId} 27 | sr201-common 28 | ${project.version} 29 | 30 | 31 | 32 | 33 | org.slf4j 34 | slf4j-api 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-web 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | repackage 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /sr201-config-client/src/main/java/li/cryx/sr201/client/conf/ConnectionPanel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client.conf; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import javax.swing.BorderFactory; 6 | import javax.swing.JButton; 7 | import javax.swing.JLabel; 8 | import javax.swing.JPanel; 9 | import javax.swing.JTextField; 10 | 11 | import com.jgoodies.forms.layout.CellConstraints; 12 | import com.jgoodies.forms.layout.FormLayout; 13 | 14 | import li.cryx.sr201.util.IconSupport; 15 | import li.cryx.sr201.util.Icons; 16 | 17 | public class ConnectionPanel extends JPanel { 18 | 19 | private static final long serialVersionUID = -8641903408959280465L; 20 | 21 | private final ResourceBundle msg; 22 | 23 | private JLabel lbIp; 24 | 25 | private JTextField txIp; 26 | 27 | private JButton butConnect; 28 | 29 | public ConnectionPanel(final ResourceBundle msg) { 30 | this.msg = msg; 31 | 32 | setBorder(BorderFactory.createTitledBorder(msg.getString("view.conf.conn.caption"))); 33 | setLayout(new FormLayout("4dlu,p,2dlu,f:p:g,6dlu,p,4dlu,", "p,4dlu")); 34 | 35 | final CellConstraints cc = new CellConstraints(); 36 | final int row = 1; 37 | add(getLbIp(), cc.xy(2, row)); 38 | add(getTxIp(), cc.xy(4, row)); 39 | add(getButConnect(), cc.xy(6, row)); 40 | } 41 | 42 | public JButton getButConnect() { 43 | if (butConnect == null) { 44 | butConnect = new JButton(msg.getString("view.conf.conn.connect.button")); 45 | butConnect.setIcon(IconSupport.getIcon(Icons.CONNECT)); 46 | } 47 | return butConnect; 48 | } 49 | 50 | private JLabel getLbIp() { 51 | if (lbIp == null) { 52 | lbIp = new JLabel(msg.getString("view.conf.conn.ip.label")); 53 | } 54 | return lbIp; 55 | } 56 | 57 | public JTextField getTxIp() { 58 | if (txIp == null) { 59 | txIp = new JTextField(); 60 | } 61 | return txIp; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /sr201-server/src/main/java/li/cryx/sr201/server/ServerStarter.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.server; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; 6 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.stereotype.Component; 9 | 10 | import li.cryx.sr201.Settings; 11 | import li.cryx.sr201.SettingsFactory; 12 | import li.cryx.sr201.connection.HighLevelConnection; 13 | 14 | @SpringBootApplication 15 | public class ServerStarter { 16 | 17 | /** Config spring boot to start servlet container on a specific port. */ 18 | @Component 19 | public class CustomizationBean implements EmbeddedServletContainerCustomizer { 20 | 21 | @Override 22 | public void customize(final ConfigurableEmbeddedServletContainer container) { 23 | // apply port 24 | container.setPort(settings.getServerPort()); 25 | } 26 | 27 | } 28 | 29 | /** Keep user settings in memory */ 30 | private static Settings settings; 31 | 32 | /** Start server */ 33 | public static void main(final String[] args) { 34 | // load settings 35 | settings = SettingsFactory.loadSettings(); 36 | 37 | // start server 38 | SpringApplication.run(ServerStarter.class, args); 39 | } 40 | 41 | /** Define a connection to the relay board. */ 42 | @Bean 43 | public HighLevelConnection getConnection() { 44 | // temporarily change the settings to force TCP connection 45 | final boolean oldValue = settings.isTcp(); 46 | settings.setTcp(true); 47 | final HighLevelConnection conn = new HighLevelConnection(settings); 48 | // reset settings 49 | settings.setTcp(oldValue); 50 | // return connection 51 | return conn; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /sr201-common/src/test/java/li/cryx/sr201/config/BoardStateTest.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.config; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | /** 7 | * Ensure that {@link BoardState} does parse the config of the board correctly. 8 | * 9 | * @author cryxli 10 | */ 11 | public class BoardStateTest { 12 | 13 | @Test 14 | public void testParseState() { 15 | BoardState parsed = BoardState.parseState(">1,2,3,4,5,6,77777777777777//////,8,9,10;"); 16 | Assert.assertEquals("1", parsed.getIpAddress()); 17 | Assert.assertEquals("2", parsed.getSubnetMask()); 18 | Assert.assertEquals("3", parsed.getGateway()); 19 | Assert.assertFalse(parsed.isPersistent()); 20 | Assert.assertEquals("1.0.6", parsed.getVersion()); 21 | Assert.assertEquals("77777777777777", parsed.getSerialNumber()); 22 | Assert.assertEquals("//////", parsed.getPassword()); 23 | Assert.assertEquals("8", parsed.getDnsServer()); 24 | Assert.assertEquals("9", parsed.getCloudService()); 25 | Assert.assertFalse(parsed.isCloudServiceEnabled()); 26 | 27 | parsed = BoardState.parseState(">a,a,a,a,1,a,aaaaaaaaaaaaaabbbbbb,a,a,a;"); 28 | Assert.assertTrue(parsed.isPersistent()); 29 | Assert.assertFalse(parsed.isCloudServiceEnabled()); 30 | 31 | parsed = BoardState.parseState(">a,a,a,a,a,a,aaaaaaaaaaaaaabbbbbb,a,a,1;"); 32 | Assert.assertFalse(parsed.isPersistent()); 33 | Assert.assertTrue(parsed.isCloudServiceEnabled()); 34 | } 35 | 36 | @Test(expected = IllegalArgumentException.class) 37 | public void testParseStateEndsWith() { 38 | BoardState.parseState(">foobar"); 39 | } 40 | 41 | @Test(expected = IllegalArgumentException.class) 42 | public void testParseStateNotNull() { 43 | BoardState.parseState(null); 44 | } 45 | 46 | @Test(expected = StringIndexOutOfBoundsException.class) 47 | public void testParseStateSerialNumberTooShort() { 48 | BoardState.parseState(">1,2,3,4,5,6,7,8,9,10;"); 49 | } 50 | 51 | @Test(expected = IllegalArgumentException.class) 52 | public void testParseStateStartsWith() { 53 | BoardState.parseState("foobar;"); 54 | } 55 | 56 | @Test(expected = IllegalArgumentException.class) 57 | public void testParseStateTooShort() { 58 | BoardState.parseState(">;"); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /sr201-config-client/src/main/java/li/cryx/sr201/client/conf/InfoPanel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client.conf; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import javax.swing.BorderFactory; 6 | import javax.swing.JLabel; 7 | import javax.swing.JPanel; 8 | import javax.swing.JTextField; 9 | 10 | import com.jgoodies.forms.layout.CellConstraints; 11 | import com.jgoodies.forms.layout.FormLayout; 12 | 13 | public class InfoPanel extends JPanel { 14 | 15 | private static final long serialVersionUID = -3120902001715562919L; 16 | 17 | private final ResourceBundle msg; 18 | 19 | private JLabel lbSerial; 20 | 21 | private JTextField txSerial; 22 | 23 | private JLabel lbVersion; 24 | 25 | private JTextField txVersion; 26 | 27 | public InfoPanel(final ResourceBundle msg) { 28 | this.msg = msg; 29 | 30 | setBorder(BorderFactory.createTitledBorder(msg.getString("view.conf.info.caption"))); 31 | setLayout(new FormLayout("4dlu,p,2dlu,f:p:g,4dlu,", "p,4dlu,p,4dlu")); 32 | final CellConstraints cc = new CellConstraints(); 33 | 34 | int row = 1; 35 | add(getLbSerial(), cc.xy(2, row)); 36 | add(getTxSerial(), cc.xy(4, row)); 37 | row += 2; 38 | add(getLbVersion(), cc.xy(2, row)); 39 | add(getTxVersion(), cc.xy(4, row)); 40 | } 41 | 42 | private JLabel getLbSerial() { 43 | if (lbSerial == null) { 44 | lbSerial = new JLabel(msg.getString("view.conf.info.serial.label")); 45 | } 46 | return lbSerial; 47 | } 48 | 49 | private JLabel getLbVersion() { 50 | if (lbVersion == null) { 51 | lbVersion = new JLabel(msg.getString("view.conf.info.version.label")); 52 | } 53 | return lbVersion; 54 | } 55 | 56 | public JTextField getTxSerial() { 57 | if (txSerial == null) { 58 | txSerial = new JTextField(); 59 | txSerial.setEditable(false); 60 | } 61 | return txSerial; 62 | } 63 | 64 | public JTextField getTxVersion() { 65 | if (txVersion == null) { 66 | txVersion = new JTextField(); 67 | txVersion.setEditable(false); 68 | } 69 | return txVersion; 70 | } 71 | 72 | @Override 73 | public void setEnabled(final boolean enabled) { 74 | super.setEnabled(enabled); 75 | 76 | getLbSerial().setEnabled(enabled); 77 | getLbVersion().setEnabled(enabled); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/i18n/XMLResourceBundleControl.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.i18n; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.net.URL; 7 | import java.net.URLConnection; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Locale; 11 | import java.util.ResourceBundle; 12 | import java.util.ResourceBundle.Control; 13 | 14 | /** 15 | * This class instructs the 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 List getFormats(final String baseName) { 24 | if (baseName == null) { 25 | throw new NullPointerException(); 26 | } 27 | // only support XML 28 | return Arrays.asList("xml"); 29 | } 30 | 31 | @Override 32 | public ResourceBundle newBundle(final String baseName, final Locale locale, final String format, 33 | final ClassLoader loader, final boolean reload) 34 | throws IllegalAccessException, InstantiationException, IOException { 35 | if (baseName == null || locale == null || format == null || loader == null) { 36 | throw new NullPointerException(); 37 | } 38 | ResourceBundle bundle = null; 39 | if ("xml".equals(format)) { 40 | final String bundleName = toBundleName(baseName, locale); 41 | final String resourceName = toResourceName(bundleName, format); 42 | InputStream stream = null; 43 | if (reload) { 44 | final URL url = loader.getResource(resourceName); 45 | if (url != null) { 46 | final URLConnection connection = url.openConnection(); 47 | if (connection != null) { 48 | // Disable caches to get fresh data for reloading. 49 | connection.setUseCaches(false); 50 | stream = connection.getInputStream(); 51 | } 52 | } 53 | } else { 54 | stream = loader.getResourceAsStream(resourceName); 55 | } 56 | if (stream != null) { 57 | final BufferedInputStream bis = new BufferedInputStream(stream); 58 | bundle = new XMLResourceBundle(bis); 59 | bis.close(); 60 | } 61 | } 62 | return bundle; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /sr201-client/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | li.cryx.sr201 6 | sr201 7 | 0.0.1-SNAPSHOT 8 | 9 | sr201-client 10 | 11 | 12 | 13 | ${project.groupId} 14 | sr201-common 15 | ${project.version} 16 | 17 | 18 | 19 | 20 | org.slf4j 21 | slf4j-api 22 | 23 | 24 | org.slf4j 25 | jcl-over-slf4j 26 | 27 | 28 | org.slf4j 29 | slf4j-log4j12 30 | 31 | 32 | log4j 33 | log4j 34 | 35 | 36 | 37 | 38 | com.jgoodies 39 | jgoodies-forms 40 | 41 | 42 | 43 | 44 | junit 45 | junit 46 | 47 | 48 | org.mockito 49 | mockito-core 50 | 51 | 52 | 53 | 54 | 55 | 56 | maven-assembly-plugin 57 | 58 | 59 | 60 | li.cryx.sr201.client.MainWindow 61 | 62 | 63 | 64 | jar-with-dependencies 65 | 66 | 67 | 68 | 69 | make-assembly 70 | package 71 | 72 | single 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /sr201-common/src/test/java/li/cryx/sr201/connection/DatagramMatcher.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import java.net.DatagramPacket; 4 | import java.net.InetAddress; 5 | import java.net.UnknownHostException; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | import org.mockito.ArgumentMatcher; 9 | 10 | /** 11 | * Since DatagramPacket 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 | *

21 | * 22 | * @author cryxli 23 | */ 24 | public class DatagramMatcher extends ArgumentMatcher { 25 | 26 | private final String data; 27 | 28 | private final InetAddress ipAdr; 29 | 30 | private final int port; 31 | 32 | /** 33 | * Create a matcher for a 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 | 3 | 4.0.0 4 | 5 | li.cryx.sr201 6 | sr201 7 | 0.0.1-SNAPSHOT 8 | 9 | sr201-config-client 10 | 11 | 12 | 13 | 14 | ${project.groupId} 15 | sr201-common 16 | ${project.version} 17 | 18 | 19 | 20 | 21 | org.slf4j 22 | slf4j-api 23 | 24 | 25 | org.slf4j 26 | jcl-over-slf4j 27 | 28 | 29 | org.slf4j 30 | slf4j-log4j12 31 | 32 | 33 | log4j 34 | log4j 35 | 36 | 37 | 38 | 39 | com.jgoodies 40 | jgoodies-forms 41 | 42 | 43 | 44 | 45 | junit 46 | junit 47 | 48 | 49 | org.mockito 50 | mockito-core 51 | 52 | 53 | 54 | 55 | 56 | 57 | maven-assembly-plugin 58 | 59 | 60 | 61 | li.cryx.sr201.client.conf.RemoteConfigWindow 62 | 63 | 64 | 65 | jar-with-dependencies 66 | 67 | 68 | 69 | 70 | make-assembly 71 | package 72 | 73 | single 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/util/PropertiesSupport.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.util; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Properties; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | /** 13 | * Class that provides common operations on 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 |
4 | sr201 Contol 5 | 6 |
7 | 8 | 9 |
10 |
11 |
12 | 1 Device Info 13 | 14 | 15 | 16 | 17 |
18 |
19 | 2 Device Action 20 | 21 | 32 | 33 | 37 | 38 | 46 |
47 | 48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/util/IconSupport.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.util; 2 | 3 | import java.awt.Image; 4 | import java.awt.image.BufferedImage; 5 | import java.io.IOException; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import javax.imageio.ImageIO; 10 | import javax.swing.ImageIcon; 11 | 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | /** 16 | * This class serves as a factory an in-memory cache for images and icons. 17 | * 18 | * @author cryxli 19 | */ 20 | public class IconSupport { 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(IconSupport.class); 23 | 24 | /** Only load images once, then serve them from this cache. */ 25 | private static final Map cache = new HashMap(); 26 | 27 | /** 28 | * Get the icon as defined by the given enum. 29 | * 30 | * @param icon 31 | * Enum representing the requested icon. 32 | * @return Loaded image as icon, or, 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 | * 16 | * @author cryxli 17 | */ 18 | public class ConnectionException extends RuntimeException { 19 | 20 | private static final long serialVersionUID = 7616505850922142113L; 21 | 22 | /** Optional arguments for the translated message */ 23 | private Object[] arguments = null; 24 | 25 | /** 26 | * Create a new exception. 27 | * 28 | * @param langKey 29 | * Language key into the translations. 30 | */ 31 | public ConnectionException(final String langKey) { 32 | super(langKey); 33 | } 34 | 35 | /** 36 | * Create a new exception. 37 | * 38 | * @param langKey 39 | * Language key into the translations. 40 | * @param arguments 41 | * Optional arguments for the translated message. 42 | */ 43 | public ConnectionException(final String langKey, final Object... arguments) { 44 | super(langKey); 45 | this.arguments = arguments; 46 | } 47 | 48 | /** 49 | * Create a new exception. 50 | * 51 | * @param langKey 52 | * Language key into the translations. 53 | * @param cause 54 | * Child exception causing this one. 55 | */ 56 | public ConnectionException(final String langKey, final Throwable cause) { 57 | super(langKey, cause); 58 | } 59 | 60 | /** 61 | * Create a new exception. 62 | * 63 | * @param langKey 64 | * Language key into the translations. 65 | * @param cause 66 | * Child exception causing this one. 67 | * @param arguments 68 | * Optional arguments for the translated message. 69 | */ 70 | public ConnectionException(final String langKey, final Throwable cause, final Object... arguments) { 71 | super(langKey, cause); 72 | this.arguments = arguments; 73 | } 74 | 75 | /** 76 | * Get the translated error message. 77 | * 78 | * @param msg 79 | * ResourceBundle capable of translating the language keys. 80 | * @return Translated message, arguments are already replaced. 81 | * @see MessageFormat 82 | */ 83 | public String translate(final ResourceBundle msg) { 84 | if (arguments != null) { 85 | return MessageFormat.format(msg.getString(getMessage()), arguments); 86 | } else { 87 | return msg.getString(getMessage()); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/SyncServiceImpl.svc/ReportStatus.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sr201-config-client/src/main/java/li/cryx/sr201/client/conf/IpAddressPanel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client.conf; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import javax.swing.BorderFactory; 6 | import javax.swing.JLabel; 7 | import javax.swing.JPanel; 8 | import javax.swing.JTextField; 9 | 10 | import com.jgoodies.forms.layout.CellConstraints; 11 | import com.jgoodies.forms.layout.FormLayout; 12 | 13 | public class IpAddressPanel extends JPanel { 14 | 15 | private static final long serialVersionUID = -280529740503393758L; 16 | 17 | private final ResourceBundle msg; 18 | 19 | private JLabel lbIp; 20 | 21 | private JTextField txIp; 22 | 23 | private JLabel lbSubnet; 24 | 25 | private JTextField txSubnet; 26 | 27 | private JLabel lbGateway; 28 | 29 | private JTextField txGateway; 30 | 31 | public IpAddressPanel(final ResourceBundle msg) { 32 | this.msg = msg; 33 | 34 | setBorder(BorderFactory.createTitledBorder(msg.getString("view.conf.ip.caption"))); 35 | setLayout(new FormLayout("4dlu,p,2dlu,f:p:g,4dlu,", "p,4dlu,p,4dlu,p,4dlu")); 36 | final CellConstraints cc = new CellConstraints(); 37 | 38 | int row = 1; 39 | add(getLbIp(), cc.xy(2, row)); 40 | add(getTxIp(), cc.xy(4, row)); 41 | row += 2; 42 | add(getLbSubnet(), cc.xy(2, row)); 43 | add(getTxSubnet(), cc.xy(4, row)); 44 | row += 2; 45 | add(getLbGateway(), cc.xy(2, row)); 46 | add(getTxGateway(), cc.xy(4, row)); 47 | } 48 | 49 | private JLabel getLbGateway() { 50 | if (lbGateway == null) { 51 | lbGateway = new JLabel(msg.getString("view.conf.ip.gateway.label")); 52 | } 53 | return lbGateway; 54 | } 55 | 56 | private JLabel getLbIp() { 57 | if (lbIp == null) { 58 | lbIp = new JLabel(msg.getString("view.conf.ip.ip.label")); 59 | } 60 | return lbIp; 61 | } 62 | 63 | private JLabel getLbSubnet() { 64 | if (lbSubnet == null) { 65 | lbSubnet = new JLabel(msg.getString("view.conf.ip.subnet.label")); 66 | } 67 | return lbSubnet; 68 | } 69 | 70 | public JTextField getTxGateway() { 71 | if (txGateway == null) { 72 | txGateway = new JTextField(); 73 | } 74 | return txGateway; 75 | } 76 | 77 | public JTextField getTxIp() { 78 | if (txIp == null) { 79 | txIp = new JTextField(); 80 | } 81 | return txIp; 82 | } 83 | 84 | public JTextField getTxSubnet() { 85 | if (txSubnet == null) { 86 | txSubnet = new JTextField(); 87 | } 88 | return txSubnet; 89 | } 90 | 91 | @Override 92 | public void setEnabled(final boolean enabled) { 93 | super.setEnabled(enabled); 94 | 95 | getLbIp().setEnabled(enabled); 96 | getTxIp().setEnabled(enabled); 97 | getLbSubnet().setEnabled(enabled); 98 | getTxSubnet().setEnabled(enabled); 99 | getLbGateway().setEnabled(enabled); 100 | getTxGateway().setEnabled(enabled); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | li.cryx.sr201 5 | sr201 6 | 0.0.1-SNAPSHOT 7 | pom 8 | 9 | 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-api 15 | ${slf4j.version} 16 | 17 | 18 | org.slf4j 19 | jcl-over-slf4j 20 | ${slf4j.version} 21 | 22 | 23 | org.slf4j 24 | slf4j-log4j12 25 | ${slf4j.version} 26 | 27 | 28 | log4j 29 | log4j 30 | ${log4j.version} 31 | 32 | 33 | 34 | 35 | com.jgoodies 36 | jgoodies-forms 37 | ${jgoodies-forms.version} 38 | 39 | 40 | 41 | 42 | junit 43 | junit 44 | ${junit.version} 45 | test 46 | 47 | 48 | org.mockito 49 | mockito-core 50 | ${mockito.version} 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-compiler-plugin 62 | 2.3.2 63 | 64 | ${jdk} 65 | ${jdk} 66 | ${project.build.sourceEncoding} 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-assembly-plugin 72 | 2.2-beta-5 73 | 74 | 75 | 76 | 77 | 78 | 79 | 1.7 80 | UTF-8 81 | 82 | 1.9.0 83 | 4.12 84 | 1.7.21 85 | 1.2.17 86 | 1.10.19 87 | 88 | 89 | 90 | sr201-config-client 91 | sr201-client 92 | sr201-common 93 | sr201-server 94 | 95 | 96 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/Settings.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201; 2 | 3 | import java.util.Properties; 4 | 5 | import li.cryx.sr201.connection.IpAddressValidator; 6 | import li.cryx.sr201.util.PropertiesSupport; 7 | 8 | /** 9 | * Data structure to hold relay end-point settings. Also adds additional methods 10 | * to read and write settings from/to properties files. 11 | * 12 | * @author cryxli 13 | */ 14 | public class Settings { 15 | 16 | /** Property key for IP setting */ 17 | private static final String KEY_IP = "conn.ip"; 18 | 19 | /** Property key for protocol setting */ 20 | private static final String KEY_TCP = "conn.protocol"; 21 | 22 | /** Property key for local server port. */ 23 | private static final String KEY_SERVER_PORT = "server.port"; 24 | 25 | /** Indicator that IP is valid */ 26 | private boolean valid = false; 27 | 28 | /** IP address of relay */ 29 | private String ip; 30 | 31 | /** Use TCP or UDP protocol */ 32 | private boolean tcp; 33 | 34 | /** Local server port. */ 35 | private int serverPort; 36 | 37 | /** Create settings with default values */ 38 | public Settings() { 39 | this(new Properties()); 40 | } 41 | 42 | /** 43 | * Create settings from given Properties file. Will fall back to default 44 | * settings, if a property is missing. 45 | */ 46 | public Settings(final Properties prop) { 47 | final Properties defaultProp = PropertiesSupport.loadFromXmlResource("/config/default.xml"); 48 | 49 | if (prop.getProperty(KEY_IP) != null) { 50 | setIp(prop.getProperty(KEY_IP)); 51 | } else { 52 | setIp(defaultProp.getProperty(KEY_IP)); 53 | } 54 | 55 | if (prop.getProperty(KEY_TCP) != null) { 56 | setTcp("TCP".equalsIgnoreCase(prop.getProperty(KEY_TCP))); 57 | } else { 58 | setTcp("TCP".equalsIgnoreCase(defaultProp.getProperty(KEY_TCP))); 59 | } 60 | 61 | try { 62 | setServerPort(Integer.parseInt(prop.getProperty(KEY_SERVER_PORT))); 63 | } catch (final NumberFormatException e) { 64 | setServerPort(Integer.parseInt(defaultProp.getProperty(KEY_SERVER_PORT))); 65 | } 66 | } 67 | 68 | public Properties exportProperties() { 69 | return exportProperties(new Properties()); 70 | } 71 | 72 | public Properties exportProperties(final Properties prop) { 73 | prop.setProperty(KEY_IP, getIp()); 74 | prop.setProperty(KEY_TCP, isTcp() ? "TCP" : "UDP"); 75 | return prop; 76 | } 77 | 78 | public String getIp() { 79 | return ip; 80 | } 81 | 82 | public int getServerPort() { 83 | return serverPort; 84 | } 85 | 86 | public boolean isTcp() { 87 | return tcp; 88 | } 89 | 90 | public boolean isValid() { 91 | return valid; 92 | } 93 | 94 | public void setIp(final String ip) { 95 | this.ip = ip; 96 | valid = new IpAddressValidator().isValid(ip); 97 | } 98 | 99 | public void setServerPort(final int serverPort) { 100 | this.serverPort = serverPort; 101 | } 102 | 103 | public void setTcp(final boolean tcp) { 104 | this.tcp = tcp; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /sr201-config-client/src/main/java/li/cryx/sr201/client/conf/CloudPannel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client.conf; 2 | 3 | import java.util.ResourceBundle; 4 | 5 | import javax.swing.BorderFactory; 6 | import javax.swing.JCheckBox; 7 | import javax.swing.JLabel; 8 | import javax.swing.JPanel; 9 | import javax.swing.JTextField; 10 | 11 | import com.jgoodies.forms.layout.CellConstraints; 12 | import com.jgoodies.forms.layout.FormLayout; 13 | 14 | public class CloudPannel extends JPanel { 15 | 16 | private static final long serialVersionUID = 4046052815511264897L; 17 | 18 | private final ResourceBundle msg; 19 | 20 | private JCheckBox chService; 21 | 22 | private JLabel lbService; 23 | 24 | private JTextField txService; 25 | 26 | private JLabel lbPassword; 27 | 28 | private JTextField txPassword; 29 | 30 | private JLabel lbDns; 31 | 32 | private JTextField txDns; 33 | 34 | public CloudPannel(final ResourceBundle msg) { 35 | this.msg = msg; 36 | 37 | setBorder(BorderFactory.createTitledBorder(msg.getString("view.conf.cloud.caption"))); 38 | setLayout(new FormLayout("4dlu,p,2dlu,f:p:g,4dlu,", "p,4dlu,p,4dlu,p,4dlu,p,4dlu")); 39 | final CellConstraints cc = new CellConstraints(); 40 | 41 | int row = 1; 42 | add(getChService(), cc.xyw(2, row, 3)); 43 | row += 2; 44 | add(getLbService(), cc.xy(2, row)); 45 | add(getTxService(), cc.xy(4, row)); 46 | row += 2; 47 | add(getLbPassword(), cc.xy(2, row)); 48 | add(getTxPassword(), cc.xy(4, row)); 49 | row += 2; 50 | add(getLbDns(), cc.xy(2, row)); 51 | add(getTxDns(), cc.xy(4, row)); 52 | } 53 | 54 | public JCheckBox getChService() { 55 | if (chService == null) { 56 | chService = new JCheckBox(msg.getString("view.conf.cloud.enabled.label")); 57 | } 58 | return chService; 59 | } 60 | 61 | private JLabel getLbDns() { 62 | if (lbDns == null) { 63 | lbDns = new JLabel(msg.getString("view.conf.cloud.dns.label")); 64 | } 65 | return lbDns; 66 | } 67 | 68 | private JLabel getLbPassword() { 69 | if (lbPassword == null) { 70 | lbPassword = new JLabel(msg.getString("view.conf.cloud.password.label")); 71 | } 72 | return lbPassword; 73 | } 74 | 75 | private JLabel getLbService() { 76 | if (lbService == null) { 77 | lbService = new JLabel(msg.getString("view.conf.cloud.service.label")); 78 | } 79 | return lbService; 80 | } 81 | 82 | public JTextField getTxDns() { 83 | if (txDns == null) { 84 | txDns = new JTextField(); 85 | } 86 | return txDns; 87 | } 88 | 89 | public JTextField getTxPassword() { 90 | if (txPassword == null) { 91 | txPassword = new JTextField(); 92 | } 93 | return txPassword; 94 | } 95 | 96 | public JTextField getTxService() { 97 | if (txService == null) { 98 | txService = new JTextField(); 99 | } 100 | return txService; 101 | } 102 | 103 | @Override 104 | public void setEnabled(final boolean enabled) { 105 | super.setEnabled(enabled); 106 | 107 | getChService().setEnabled(enabled); 108 | getLbService().setEnabled(enabled); 109 | getTxService().setEnabled(enabled); 110 | getLbPassword().setEnabled(enabled); 111 | getTxPassword().setEnabled(enabled); 112 | getLbDns().setEnabled(enabled); 113 | getTxDns().setEnabled(enabled); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/Sr201Connection.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import java.io.Closeable; 4 | 5 | /** 6 | * Low level (TCP or UDP) communication channel to the relay. 7 | * 8 | * @author cryxli 9 | */ 10 | public interface Sr201Connection extends Closeable { 11 | 12 | /** 13 | * Close the connection, if not already close. 14 | *

15 | * The implementation should not throw IllegalStateExceptions 16 | * when the connection has already been closed. 17 | *

18 | */ 19 | @Override 20 | void close(); 21 | 22 | /** 23 | * Open connection to the relay. 24 | * 25 | * @throws ConnectionException 26 | * is thrown when the connection could not be established. 27 | */ 28 | void connect() throws ConnectionException; 29 | 30 | /** 31 | * Query the relay for its state. Only supported by TCP. UDP may emulate 32 | * this by returning the last state. 33 | * 34 | * @return 8 byte array representing the state of each relay as characters 35 | * 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 | *

30 | * 31 | *

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 | *

38 | * 39 | * @author cryxli 40 | */ 41 | class Sr201TcpConnection extends AbstractSr201Connection { 42 | 43 | /** TCP Port cannot be changed. Fixed to 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 | *

25 | * 26 | * @author cryxli 27 | */ 28 | public enum Sr201Command { 29 | 30 | /** 31 | * Ask the board for its current settings. 32 | *

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 Map bytesToMap(final byte[] states) { 41 | final Map result = new HashMap(8); 42 | result.put(Channel.CH1, State.valueOfReceived(states[0])); 43 | result.put(Channel.CH2, State.valueOfReceived(states[1])); 44 | result.put(Channel.CH3, State.valueOfReceived(states[2])); 45 | result.put(Channel.CH4, State.valueOfReceived(states[3])); 46 | result.put(Channel.CH5, State.valueOfReceived(states[4])); 47 | result.put(Channel.CH6, State.valueOfReceived(states[5])); 48 | result.put(Channel.CH7, State.valueOfReceived(states[6])); 49 | result.put(Channel.CH8, State.valueOfReceived(states[7])); 50 | return result; 51 | } 52 | 53 | /** 54 | * Close the underlying connection. 55 | * 56 | * @see Sr201Connection#close() 57 | */ 58 | @Override 59 | public void close() { 60 | conn.close(); 61 | } 62 | 63 | /** 64 | * Open the wrapped connection. 65 | * 66 | * @throws ConnectionException 67 | * is thrown if the connection cannot be established. 68 | * @see Sr201Connection#connect() 69 | */ 70 | public void connect() throws ConnectionException { 71 | conn.connect(); 72 | } 73 | 74 | /** 75 | * Query the relays for their current states. 76 | * 77 | * @return Map of channels and associated states. 78 | * @throws ConnectionException 79 | * is thrown if the query command could not be executed 80 | * correctly. 81 | */ 82 | public Map getStates() throws ConnectionException { 83 | return bytesToMap(conn.getStateBytes()); 84 | } 85 | 86 | /** 87 | * Get the state of the connection. 88 | * 89 | * @return true 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 Map send(final Channel channel, final State state) throws ConnectionException { 112 | if (channel == null || state == null || state == State.UNKNOWN) { 113 | // nothing to send 114 | return null; 115 | } 116 | return bytesToMap(conn.send(new byte[] { state.send(), channel.key() })); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/device.php: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 |
37 | sr201 Contol 38 | 39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 |
48 | 1 Action Result 49 | 50 |

51 | 2 Device Status 52 | 53 | "> 54 |
55 |
56 | 3 Device Action 57 | 58 | 69 | 70 | 74 | 75 | 83 |
84 | 85 |
86 |
87 | 88 | -------------------------------------------------------------------------------- /sr201-server/src/main/java/li/cryx/sr201/server/controller/Sr201Controller.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.server.controller; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Map.Entry; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import li.cryx.sr201.connection.Channel; 16 | import li.cryx.sr201.connection.ConnectionException; 17 | import li.cryx.sr201.connection.HighLevelConnection; 18 | import li.cryx.sr201.connection.State; 19 | 20 | @RestController 21 | @RequestMapping("/api") 22 | public class Sr201Controller { 23 | 24 | private static final Logger LOG = LoggerFactory.getLogger(Sr201Controller.class); 25 | 26 | private static Map unknownStates() { 27 | final Map states = new HashMap<>(); 28 | for (final Channel c : Channel.values()) { 29 | if (c != Channel.ALL) { 30 | states.put(c, State.UNKNOWN); 31 | } 32 | } 33 | return states; 34 | } 35 | 36 | /** Connection through which to execute commands on the relay board. */ 37 | @Autowired 38 | private HighLevelConnection conn; 39 | 40 | /** 41 | * Send a command to the relay board. 42 | * 43 | * @param channel 44 | * Channel (relay) indicator. Can be "1" to "8", or "all" to 45 | * switch all relays at once. 46 | * @param state 47 | * State indicator. Can be "on" or "1", anything else will be 48 | * interpreted as "off". 49 | * @return New states of all relays. 50 | */ 51 | @RequestMapping("/channel/{channel}/state/{state}") 52 | public @ResponseBody Map changeRelay( // 53 | @PathVariable("channel") final String channel, // 54 | @PathVariable("state") final String state // 55 | ) { 56 | // resolve channel 57 | Channel relay = null; 58 | if ("all".equalsIgnoreCase(channel) || "x".equalsIgnoreCase(channel)) { 59 | relay = Channel.ALL; 60 | } else { 61 | try { 62 | relay = Channel.valueOf("CH" + channel); 63 | } catch (final IllegalArgumentException e) { 64 | relay = null; 65 | LOG.warn("Unknown channel: " + channel); 66 | return nok("Unknown channel"); 67 | } 68 | } 69 | 70 | // resolve state 71 | State newState; 72 | if ("on".equalsIgnoreCase(state) || "1".equals(state)) { 73 | newState = State.ON; 74 | } else { 75 | newState = State.OFF; 76 | } 77 | 78 | final Map response = new HashMap<>(); 79 | // send command 80 | Map states; 81 | try { 82 | states = conn.send(relay, newState); 83 | } catch (final ConnectionException e) { 84 | states = unknownStates(); 85 | LOG.error("Could not send command", e); 86 | response.put("nok", "Could not send command"); 87 | } 88 | 89 | response.put("states", statesToMap(states)); 90 | return response; 91 | } 92 | 93 | /** 94 | * Get the current state of the relays. 95 | * 96 | * @return States of all relays. 97 | */ 98 | @RequestMapping("/channel/{channel}") 99 | public @ResponseBody Map getRelayStates() { 100 | Map states; 101 | try { 102 | states = conn.getStates(); 103 | } catch (final ConnectionException e) { 104 | states = unknownStates(); 105 | LOG.error("Could not query relay", e); 106 | } 107 | 108 | final Map response = new HashMap<>(); 109 | response.put("states", statesToMap(states)); 110 | return response; 111 | } 112 | 113 | private Map nok(final String msg) { 114 | final Map response = new HashMap<>(); 115 | response.put("nok", msg); 116 | return response; 117 | } 118 | 119 | private Map statesToMap(final Map stateEnums) { 120 | final Map states = new HashMap<>(); 121 | for (final Entry e : stateEnums.entrySet()) { 122 | int state; 123 | if (e.getValue() == State.ON) { 124 | state = 1; 125 | } else if (e.getValue() == State.OFF) { 126 | state = 0; 127 | } else { 128 | state = 2; 129 | } 130 | states.put(String.valueOf(e.getKey().key() - '0'), state); 131 | } 132 | return states; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/form2.css: -------------------------------------------------------------------------------- 1 | /* Better mobile view */ 2 | @media screen and (max-width: 1000px) { 3 | body { 4 | zoom: 180%; 5 | } 6 | } 7 | /* Source: https://www.sanwebe.com/2014/08/css-html-forms-designs */ 8 | 9 | .form-style-5{ 10 | max-width: 500px; 11 | padding: 10px 20px; 12 | background: #f4f7f8; 13 | margin: 10px auto; 14 | padding:20px; 15 | background: #f4f7f8; 16 | border-radius: 8px; 17 | font-family: Georgia, "Times New Roman", Times, serif; 18 | } 19 | .form-style-5 fieldset{ 20 | border: none; 21 | } 22 | .form-style-5 legend { 23 | font-size: 1.4em; 24 | margin-bottom: 10px; 25 | } 26 | .form-style-5 label { 27 | display: block; 28 | margin-bottom: 8px; 29 | } 30 | .form-style-5 input[type="text"], 31 | .form-style-5 input[type="password"], 32 | .form-style-5 input[type="date"], 33 | .form-style-5 input[type="datetime"], 34 | .form-style-5 input[type="email"], 35 | .form-style-5 input[type="number"], 36 | .form-style-5 input[type="search"], 37 | .form-style-5 input[type="time"], 38 | .form-style-5 input[type="url"], 39 | .form-style-5 textarea, 40 | .form-style-5 select { 41 | font-family: Georgia, "Times New Roman", Times, serif; 42 | background: rgba(255,255,255,.1); 43 | border: none; 44 | border-radius: 4px; 45 | font-size: 16px; 46 | margin: 0; 47 | outline: 0; 48 | padding: 7px; 49 | width: 100%; 50 | box-sizing: border-box; 51 | -webkit-box-sizing: border-box; 52 | -moz-box-sizing: border-box; 53 | background-color: #e8eeef; 54 | color:#8a97a0; 55 | -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.03) inset; 56 | box-shadow: 0 1px 0 rgba(0,0,0,0.03) inset; 57 | margin-bottom: 30px; 58 | 59 | } 60 | .form-style-5 input[type="text"]:focus, 61 | .form-style-5 input[type="password"]:focus, 62 | .form-style-5 input[type="date"]:focus, 63 | .form-style-5 input[type="datetime"]:focus, 64 | .form-style-5 input[type="email"]:focus, 65 | .form-style-5 input[type="number"]:focus, 66 | .form-style-5 input[type="search"]:focus, 67 | .form-style-5 input[type="time"]:focus, 68 | .form-style-5 input[type="url"]:focus, 69 | .form-style-5 textarea:focus, 70 | .form-style-5 select:focus{ 71 | background: #d2d9dd; 72 | } 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: red; 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: orange; 113 | } 114 | */ 115 | 116 | 117 | 118 | 119 | .button 120 | { 121 | position: relative; 122 | display: block; 123 | padding: 19px 39px 18px 39px; 124 | color: #fff; 125 | margin: 10px auto; 126 | background-color: #1abc9c; 127 | font-size: 24px; 128 | cursor: pointer; 129 | text-align: center; 130 | font-style: normal; 131 | width: 100%; 132 | border: 1px solid #16a085; 133 | border-width: 1px 1px 3px; 134 | border-radius: 15px; 135 | margin-botton: 30px; 136 | box-shadow: 0 9px #999; 137 | } 138 | 139 | .button:hover 140 | { 141 | background-color: green; 142 | opacity: 0.5; 143 | } 144 | 145 | .button:active 146 | { 147 | background-color: green; 148 | box-shadow: 0 5px #666; 149 | transform: translateY(9px); 150 | } 151 | 152 | .button:focus 153 | { 154 | background-color: green; 155 | } 156 | 157 | .toggle 158 | { 159 | background-color: green; 160 | } 161 | 162 | .toggle:hover 163 | { 164 | background-color: #1abc9c; 165 | opacity: 0.5; 166 | } 167 | 168 | .center 169 | { 170 | margin: auto; 171 | text-align: center; 172 | width: 100%; 173 | } 174 | img 175 | { 176 | display: block; 177 | margin-left: auto; 178 | margin-right: auto; 179 | } 180 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/index2.php: -------------------------------------------------------------------------------- 1 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | SR201 CONTROL 80 | 81 | 82 |

SR201 CONTROL

83 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 |
92 | 93 | 94 | // enter your mjpeg camera feed here 95 | 96 | Exit-Cam 97 | 98 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/config/BoardState.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.config; 2 | 3 | /** 4 | * Data structure to represent the current configuration of a SR-201 board. 5 | * 6 | * @author cryxli 7 | */ 8 | public class BoardState { 9 | 10 | public static BoardState parseState(String s) throws IllegalArgumentException { 11 | if (s == null) { 12 | throw new IllegalArgumentException("No config sequence given. String must not be null."); 13 | } else if (!s.startsWith(">")) { 14 | throw new IllegalArgumentException("Config sequence does not start with an >"); 15 | } else if (!s.endsWith(";")) { 16 | throw new IllegalArgumentException("Config sequence does not end with an ;"); 17 | } 18 | 19 | s = s.substring(1, s.length() - 1); 20 | final String[] ss = s.split(","); 21 | if (ss.length != 10) { 22 | // unknown answer 23 | throw new IllegalArgumentException("Unknown number of parameters. Expected 10 is " + ss.length); 24 | } 25 | 26 | final BoardState state = new BoardState(); 27 | state.ipAddress = ss[0]; 28 | state.subnetMask = ss[1]; 29 | state.gateway = ss[2]; 30 | // unknown "empty" 31 | state.persistent = "1".equals(ss[4]); 32 | state.version = "1.0." + ss[5]; 33 | state.serialNumber = ss[6].substring(0, 14); 34 | state.password = ss[6].substring(14); 35 | state.dnsServer = ss[7]; 36 | state.cloudService = ss[8]; 37 | state.cloudServiceEnabled = "1".equals(ss[9]); 38 | return state; 39 | } 40 | 41 | private String ipAddress; 42 | 43 | private String subnetMask; 44 | 45 | private String gateway; 46 | 47 | private boolean persistent; 48 | 49 | private String version; 50 | 51 | private String serialNumber; 52 | 53 | private String password; 54 | 55 | private String dnsServer; 56 | 57 | private String cloudService; 58 | 59 | private boolean cloudServiceEnabled; 60 | 61 | private BoardState() { 62 | } 63 | 64 | public String getCloudService() { 65 | return cloudService; 66 | } 67 | 68 | public String getDnsServer() { 69 | return dnsServer; 70 | } 71 | 72 | public String getGateway() { 73 | return gateway; 74 | } 75 | 76 | public String getIpAddress() { 77 | return ipAddress; 78 | } 79 | 80 | public String getPassword() { 81 | return password; 82 | } 83 | 84 | public String getSerialNumber() { 85 | return serialNumber; 86 | } 87 | 88 | public String getSubnetMask() { 89 | return subnetMask; 90 | } 91 | 92 | public String getVersion() { 93 | return version; 94 | } 95 | 96 | public boolean isCloudServiceEnabled() { 97 | return cloudServiceEnabled; 98 | } 99 | 100 | public boolean isPersistent() { 101 | return persistent; 102 | } 103 | 104 | public void setCloudService(final String cloudService) { 105 | this.cloudService = cloudService; 106 | } 107 | 108 | public void setCloudServiceEnabled(final boolean cloudServiceEnabled) { 109 | this.cloudServiceEnabled = cloudServiceEnabled; 110 | } 111 | 112 | public void setDnsServer(final String dnsServer) { 113 | this.dnsServer = dnsServer; 114 | } 115 | 116 | public void setGateway(final String gateway) { 117 | this.gateway = gateway; 118 | } 119 | 120 | public void setIpAddress(final String ipAddress) { 121 | this.ipAddress = ipAddress; 122 | } 123 | 124 | public void setPassword(final String password) { 125 | this.password = password; 126 | } 127 | 128 | public void setPersistent(final boolean persistent) { 129 | this.persistent = persistent; 130 | } 131 | 132 | public void setSerialNumber(final String serialNumber) { 133 | this.serialNumber = serialNumber; 134 | } 135 | 136 | public void setSubnetMask(final String subnetMask) { 137 | this.subnetMask = subnetMask; 138 | } 139 | 140 | public void setVersion(final String version) { 141 | this.version = version; 142 | } 143 | 144 | @Override 145 | public String toString() { 146 | final StringBuffer buf = new StringBuffer(); 147 | 148 | buf.append("IP address ..........: ").append(ipAddress).append('\n'); 149 | buf.append("Subnet mask .........: ").append(subnetMask).append('\n'); 150 | buf.append("Gateway .............: ").append(gateway).append('\n'); 151 | buf.append("Persistent state ....: ").append(persistent).append('\n'); 152 | buf.append("Version .............: ").append(version).append('\n'); 153 | buf.append("Serial number .......: ").append(serialNumber).append('\n'); 154 | buf.append("Password ............: ").append(password).append('\n'); 155 | buf.append("DNS Server ..........: ").append(dnsServer).append('\n'); 156 | buf.append("Cloud service .......: ").append(cloudService).append('\n'); 157 | buf.append("Cloud service enabled: ").append(cloudServiceEnabled).append('\n'); 158 | 159 | return buf.toString(); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /sr201-client/src/main/java/li/cryx/sr201/client/MainWindow.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.Dimension; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | import java.awt.event.WindowAdapter; 8 | import java.awt.event.WindowEvent; 9 | import java.io.File; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | import java.util.ResourceBundle; 13 | 14 | import javax.swing.JFrame; 15 | import javax.swing.JPanel; 16 | import javax.swing.WindowConstants; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import li.cryx.sr201.Settings; 22 | import li.cryx.sr201.SettingsFactory; 23 | import li.cryx.sr201.i18n.XMLResourceBundleControl; 24 | import li.cryx.sr201.util.Closer; 25 | 26 | public class MainWindow extends JFrame { 27 | 28 | private static final long serialVersionUID = 8263665017528562277L; 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class); 31 | 32 | public static void main(final String[] args) { 33 | new MainWindow(); 34 | } 35 | 36 | /** Translations */ 37 | private final ResourceBundle msg; 38 | 39 | /** User settings */ 40 | private final Settings settings; 41 | 42 | /** Main panel */ 43 | private JPanel panel; 44 | 45 | /** Current detail panel */ 46 | private JPanel currentPanel; 47 | 48 | public MainWindow() { 49 | // load translation 50 | msg = ResourceBundle.getBundle("i18n/lang", new XMLResourceBundleControl()); 51 | // load settings 52 | settings = SettingsFactory.loadSettings(); 53 | 54 | // prepare GUI 55 | init(); 56 | } 57 | 58 | private void changeMainPanel(final JPanel newPanel) { 59 | if (currentPanel != null) { 60 | panel.remove(currentPanel); 61 | } 62 | panel.add(newPanel, BorderLayout.CENTER); 63 | currentPanel = newPanel; 64 | 65 | validate(); 66 | repaint(); 67 | } 68 | 69 | private void init() { 70 | // set application title 71 | setTitle(msg.getString("app.title")); 72 | 73 | // handle window closing event manually 74 | setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 75 | addWindowListener(new WindowAdapter() { 76 | @Override 77 | public void windowClosing(final WindowEvent evt) { 78 | MainWindow.this.onExit(); 79 | } 80 | }); 81 | 82 | panel = new JPanel(new BorderLayout()); 83 | getContentPane().add(panel); 84 | 85 | showSettingsPanel(); 86 | 87 | // ensure a minimal window size 88 | pack(); 89 | final Dimension dim = getSize(); 90 | if (dim.width < 390) { 91 | dim.width = 390; 92 | } 93 | dim.height = (int) (dim.width / 1.618); 94 | setSize(dim); 95 | // center window on screen 96 | setLocationRelativeTo(null); 97 | setVisible(true); 98 | } 99 | 100 | private void onAcceptSettings() { 101 | // save settings 102 | final File file = new File(System.getProperty("user.home"), "sr201.xml"); 103 | FileOutputStream fos = null; 104 | try { 105 | fos = new FileOutputStream(file); 106 | settings.exportProperties().storeToXML(fos, null); 107 | } catch (final IOException e) { 108 | LOG.error("Cannot save settings", e); 109 | new DialogFactory(msg, this).errorTranslate("msg.cannot.save.settings"); 110 | } finally { 111 | Closer.close(fos); 112 | } 113 | 114 | // show relay panel 115 | showTogglePanel(); 116 | } 117 | 118 | private void onExit() { 119 | // shutdown everyting 120 | if (currentPanel instanceof TogglePanel) { 121 | ((TogglePanel) currentPanel).close(); 122 | } 123 | 124 | // kill window and AWT thread 125 | dispose(); 126 | } 127 | 128 | private void showSettingsPanel() { 129 | final SettingsPanel settingsPanel = new SettingsPanel(msg); 130 | changeMainPanel(settingsPanel); 131 | settingsPanel.setFromSettings(settings); 132 | 133 | settingsPanel.getButExit().addActionListener(new ActionListener() { 134 | @Override 135 | public void actionPerformed(final ActionEvent evt) { 136 | MainWindow.this.onExit(); 137 | } 138 | }); 139 | 140 | settingsPanel.getButAccept().addActionListener(new ActionListener() { 141 | @Override 142 | public void actionPerformed(final ActionEvent evt) { 143 | settingsPanel.applySettings(settings); 144 | MainWindow.this.onAcceptSettings(); 145 | } 146 | }); 147 | } 148 | 149 | private void showTogglePanel() { 150 | final TogglePanel togglePanel = new TogglePanel(msg); 151 | changeMainPanel(togglePanel); 152 | togglePanel.setFromSettings(settings); 153 | 154 | togglePanel.getButExit().addActionListener(new ActionListener() { 155 | @Override 156 | public void actionPerformed(final ActionEvent evt) { 157 | MainWindow.this.onExit(); 158 | } 159 | }); 160 | 161 | togglePanel.getButSettings().addActionListener(new ActionListener() { 162 | @Override 163 | public void actionPerformed(final ActionEvent e) { 164 | togglePanel.close(); 165 | showSettingsPanel(); 166 | } 167 | }); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /sr201-php-cloud-service/README.md: -------------------------------------------------------------------------------- 1 | # Simple PHP "Cloud Service" Application for SR-201 Ethernet Relay Board 2 | ## Introduction 3 | The device supports Cloud Service connectivity. Using this, users can remote control the relays without direct connectivity to the device itself. This is achieved by having the device calling out the Cloud Service to get the instructions and pass the relay statuses. 4 | 5 | ## Technical Background 6 | Basically, the device regularly sends POST requests to a Web Service, to the configured web server (default *connect.tutuuu.com*). The device only posts a single string having the *Content-Type: application/json*. 7 | 8 | ``` 9 | POST /SyncServiceImpl.svc/ReportStatus HTTP/1.1 10 | User-Agent: SR-201W/M96Y 11 | Content-Type: application/json 12 | Host: sr201.000webhostapp.com 13 | Content-Length: 30 14 | 15 | "F0123456789ABCXXXXXX00000000" 16 | ``` 17 | The first 13 characters after 'F' is the device serial (all hex), followed by the 6-digit password (numerical, plain text) and then the 8-digit current relay state. 18 | 19 | There is then a 60 second TTL for the response, which should be of the form: 20 | 21 | ``` 22 | HTTP/1.1 200 OK 23 | Date: Tue, 25 Jul 2017 16:56:15 GMT 24 | Content-Type: application/json; charset=utf-8 25 | Content-Length: 3 26 | Connection: keep-alive 27 | Server: awex 28 | X-Xss-Protection: 1; mode=block 29 | X-Content-Type-Options: nosniff 30 | X-Request-ID: 4e7cae65837705fad795cccdc0dbec9f 31 | 32 | "A" 33 | ``` 34 | This is the constant-state response - simply indicating that both endpoints are live and ready. 35 | (As with standard HTTP, all lines must terminate with the windows style newline "\r\n") 36 | 37 | To change the relay states, the final line must change from "A" to any of the standard port commands, until the change is acknowledged in the subsequent "pings". 38 | Eg. "A11" to switch relay 1 on, etc. 39 | 40 | The device cannot manage chunked http response, so the *Content-length:* header parameter must be set. 41 | 42 | According my experience if *"A"* is received, the device will send the next request in 1 second. In case there was an error it goes up to 10 seconds. 43 | 44 | ## Sample Implementation 45 | The sample php implementation is providing a simple php page to process the POST request from the device. After some checking all it does is that it stores the current status of the relays into a file named *MD5(DeviceSerial+Password)_sta*. At the same time it reads the *MD5(DeviceSerial+Password)_cmd* file and passes back the content of the file. The file content is rewritten to the default *"A"* so the next request will just get the 'ping' response. 46 | 47 | There is a simple user interface created where the users can enter the Device Serial and the Password to identify the device (Actually the username and password entered are used to define the filename that is used for the communication between the "GUI" and the "Cloud Service".) The Serial and the passwords are stored locally in cookies. 48 | 49 | Then the channel can be selected (Channel 1 is the default) with the Action (Pull or Release) and optionally the timeout can be defined (Jog is the default as I'm using this service to open my gate :) ). 50 | 51 | The pages constructs the command text and saves it to the *MD5(DeviceSerial+Password)_cmd* file. You can just query the status without sending any instruction to the device. 52 | 53 | Make sure to use *.htaccess* files properly to provide some level of safety for the app. The one in the *SyncServiceImpl.svc* is required to process the *ReportStatus* "Web Service" request as *ReportStatus.php*. 54 | 55 | Alternatively index2.php can be used with form2.css instead of index.php and form.css. To impliment, edit index2.php with your serial number and password. ip web cameras, camera1 is an http mjpeg camera while camera2 is a snapshot http jpeg camera that updates the image. If using apache2, ensure permissions are www-data and apache2.conf AllowOverride All is set under . Check relay is communicating with server with tail -f /var/log/apache2/access.log. 56 | ``` 57 | "POST /SyncServiceImpl.svc/ReportStatus HTTP/1.1" 200 157 "-" "SR-201W/M96Y" 58 | ``` 59 | 60 | ## Test Cloud Service 61 | The sample code is hosted on a free hosting site and accessible at the following URL: 62 | 63 | 64 | To configure your device to use this sample service, all you need to do is to send the following strings to the config TCP port (5111) to set the server: 65 | ``` 66 | #91111,sr201.000webhostapp.com; 67 | ``` 68 | then enable the cloud service: 69 | ``` 70 | #A1111,1; 71 | ``` 72 | and save the changes: 73 | ``` 74 | #71111; 75 | ``` 76 | 77 | See for more details. 78 | 79 | ## Disclaimer 80 | 81 | Both the sample php code and the hosted service is for demonstration purposes only, provided as it is without any support whatsoever. Use the service on your own risk. 82 | The service logs the IP address of both the device and the client requests, but does not store the password, only the hash. Please check the source code for more details. 83 | 84 | Thanks and credit goes to **anakhaema** for the initial post. 85 | -------------------------------------------------------------------------------- /sr201-common/src/main/resources/i18n/lang.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | English translation of "LAN Ethernet 2 Channel Relay Board 5 | Delay Switch TCP/UDP Controller Module" demo application. Also acting 6 | as the defaul language. 7 | SR201 Demo 8 | Settings 9 | IPv4 10 | Port 11 | Protocol 12 | TCP 13 | UCP 14 | Test 15 | Connect 16 | Exit 17 | 18 | Error 19 | Could not save settings. Your changes will be lost. 20 | IP address is not valid. 21 | 22 | Could not connect to {0}:{1} 23 | Could not send data 24 | Could not read data 25 | Connection was reset by peer. 26 | 27 | Could not open UDP socket. 28 | Could not resolve address {0}. 29 | Could not send data 30 | 31 | Connection established. 32 | Cannot connect to relay. 33 | 34 | {0}: {1} 35 | Channel 1 36 | Channel 2 37 | Channel 3 38 | Channel 4 39 | Channel 5 40 | Channel 6 41 | Channel 7 42 | Channel 8 43 | state unknown 44 | relay on 45 | relay off 46 | Settings 47 | 48 | SR-201 Config Tool 49 | Send 50 | Target board 51 | Current IP address 52 | Connect 53 | Info 54 | Serial Number 55 | Version 56 | IP Address 57 | Static IP 58 | Subnet Mask 59 | Default Gateway 60 | Relay State 61 | Persist state when power lost 62 | Cloud Service 63 | Enable cloud service 64 | Service host 65 | Service password 66 | DNS Server 67 | 68 | You entered an invalid IP address. 69 | Cannot connect to config port. 70 | Could not send command to board. 71 | Could not read answer of board. 72 | Could not set new IP address. 73 | Could not set new subnet mask. 74 | Could not set new default gateway. 75 | Could not set persistent relay state. 76 | Could not change cloud service state. 77 | Could not change cloud service host. 78 | Could not change cloud service password. 79 | Could not change DNS server. 80 | Could not restart board. 81 | Static IP is not valid. 82 | Subnet mask is not valid. 83 | Default gateway is not valid. 84 | DNS server is not valid. 85 | No changes to send. 86 | Config sent to board. 87 | 88 | -------------------------------------------------------------------------------- /sr201-client/src/main/java/li/cryx/sr201/client/TogglePanel.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.FlowLayout; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Map.Entry; 10 | import java.util.ResourceBundle; 11 | 12 | import javax.swing.BorderFactory; 13 | import javax.swing.JButton; 14 | import javax.swing.JPanel; 15 | import javax.swing.SwingUtilities; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import li.cryx.sr201.Settings; 21 | import li.cryx.sr201.connection.Channel; 22 | import li.cryx.sr201.connection.ConnectionException; 23 | import li.cryx.sr201.connection.HighLevelConnection; 24 | import li.cryx.sr201.connection.State; 25 | import li.cryx.sr201.util.IconSupport; 26 | import li.cryx.sr201.util.Icons; 27 | 28 | public class TogglePanel extends JPanel { 29 | 30 | private static final long serialVersionUID = -6330786353916074194L; 31 | 32 | private static final Logger LOG = LoggerFactory.getLogger(TogglePanel.class); 33 | 34 | private final ResourceBundle msg; 35 | 36 | private HighLevelConnection conn; 37 | 38 | private final Map butStates = new HashMap(8); 39 | 40 | private ActionListener buttonListener; 41 | 42 | private JButton butExit; 43 | 44 | private JButton butSettings; 45 | 46 | public TogglePanel(final ResourceBundle msg) { 47 | this.msg = msg; 48 | init(); 49 | } 50 | 51 | public void close() { 52 | conn.close(); 53 | } 54 | 55 | private StateButton createStateButton(final Channel channel) { 56 | final StateButton but = new StateButton(msg, channel); 57 | but.addActionListener(buttonListener); 58 | butStates.put(channel, but); 59 | return but; 60 | } 61 | 62 | private void disableButtons() { 63 | for (final JButton but : butStates.values()) { 64 | but.setEnabled(false); 65 | } 66 | } 67 | 68 | private void enableButtons() { 69 | for (final JButton but : butStates.values()) { 70 | but.setEnabled(true); 71 | } 72 | } 73 | 74 | public JButton getButExit() { 75 | if (butExit == null) { 76 | butExit = new JButton(msg.getString("view.prop.but.exit.label")); 77 | butExit.setIcon(IconSupport.getIcon(Icons.EXIT)); 78 | } 79 | return butExit; 80 | } 81 | 82 | public JButton getButSettings() { 83 | if (butSettings == null) { 84 | butSettings = new JButton(msg.getString("view.toggle.but.settings.label")); 85 | butSettings.setIcon(IconSupport.getIcon(Icons.SETTINGS)); 86 | } 87 | return butSettings; 88 | } 89 | 90 | private void init() { 91 | setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 92 | setLayout(new BorderLayout()); 93 | 94 | // create only one listener for all toggle buttons 95 | buttonListener = new ActionListener() { 96 | @Override 97 | public void actionPerformed(final ActionEvent evt) { 98 | final StateButton but = (StateButton) evt.getSource(); 99 | disableButtons(); 100 | 101 | SwingUtilities.invokeLater(new Runnable() { 102 | @Override 103 | public void run() { 104 | final Map states; 105 | try { 106 | if (but.getState() == State.ON) { 107 | states = conn.send(but.getChannel(), State.OFF); 108 | } else { 109 | states = conn.send(but.getChannel(), State.ON); 110 | } 111 | updateStates(states); 112 | } catch (final ConnectionException e) { 113 | LOG.error("Cannot send", e); 114 | new DialogFactory(msg).warn(e.translate(msg)); 115 | } 116 | enableButtons(); 117 | } 118 | }); 119 | } 120 | }; 121 | 122 | // create panel with 8 toggle buttons 123 | final JPanel statePanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); 124 | add(statePanel, BorderLayout.CENTER); 125 | statePanel.add(createStateButton(Channel.CH1)); 126 | statePanel.add(createStateButton(Channel.CH2)); 127 | statePanel.add(createStateButton(Channel.CH3)); 128 | statePanel.add(createStateButton(Channel.CH4)); 129 | statePanel.add(createStateButton(Channel.CH5)); 130 | statePanel.add(createStateButton(Channel.CH6)); 131 | statePanel.add(createStateButton(Channel.CH7)); 132 | statePanel.add(createStateButton(Channel.CH8)); 133 | 134 | // option buttons 135 | final JPanel butPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); 136 | add(butPanel, BorderLayout.SOUTH); 137 | butPanel.add(getButSettings()); 138 | butPanel.add(getButExit()); 139 | } 140 | 141 | public void setFromSettings(final Settings settings) { 142 | disableButtons(); 143 | SwingUtilities.invokeLater(new Runnable() { 144 | @Override 145 | public void run() { 146 | try { 147 | conn = new HighLevelConnection(settings); 148 | final Map states = conn.getStates(); 149 | updateStates(states); 150 | enableButtons(); 151 | } catch (final ConnectionException e) { 152 | LOG.error("Cannot connect", e); 153 | new DialogFactory(msg).error(e.translate(msg)); 154 | } 155 | } 156 | }); 157 | } 158 | 159 | private void updateStates(final Map states) { 160 | for (final Entry e : states.entrySet()) { 161 | final StateButton but = butStates.get(e.getKey()); 162 | but.setState(e.getValue()); 163 | } 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /sr201-common/src/main/java/li/cryx/sr201/connection/Sr201UdpConnection.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.connection; 2 | 3 | import java.io.IOException; 4 | import java.net.DatagramPacket; 5 | import java.net.DatagramSocket; 6 | import java.net.InetAddress; 7 | import java.net.SocketException; 8 | import java.net.UnknownHostException; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import li.cryx.sr201.util.Closer; 12 | 13 | /** 14 | * UDP implementation of a {@link Sr201Connection}. 15 | * 16 | *

17 | * 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 | *

29 | * 30 | * @author cryxli 31 | */ 32 | public class Sr201UdpConnection extends AbstractSr201Connection { 33 | 34 | /** UDP Port cannot be changed. Fixed to 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 Answer() { 134 | int counter = 0; 135 | 136 | @Override 137 | public Integer answer(final InvocationOnMock invocation) throws Throwable { 138 | final byte[] buffer = invocation.getArgumentAt(0, byte[].class); 139 | buffer[0] = (byte) (48 + counter % 2); 140 | buffer[1] = 48; 141 | buffer[2] = 48; 142 | buffer[3] = 48; 143 | buffer[4] = 48; 144 | buffer[5] = 48; 145 | counter++; 146 | return 6; 147 | } 148 | }); 149 | Mockito.when(socketProvider.getSocket().getInputStream()).thenReturn(is); 150 | // prepare - ByteArrayOutputStream can be reset 151 | final ByteArrayOutputStream os = new ByteArrayOutputStream(); 152 | Mockito.when(socketProvider.getSocket().getOutputStream()).thenReturn(os); 153 | 154 | // test - first part 155 | Assert.assertFalse(conn.isConnected()); 156 | Assert.assertEquals("000000", conn.getStates()); 157 | // verify - first part 158 | Mockito.verify(is).read(Matchers.any(byte[].class)); 159 | assertBytes("00", os.toByteArray()); 160 | os.reset(); 161 | // test - second part 162 | Assert.assertTrue(conn.isConnected()); 163 | Assert.assertEquals("100000", conn.send("11")); 164 | Assert.assertTrue(conn.isConnected()); 165 | // verify - second part 166 | assertBytes("11", os.toByteArray()); 167 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close(); 168 | Mockito.verify(is, Mockito.times(2)).read(Matchers.any(byte[].class)); 169 | } 170 | 171 | @Test 172 | public void testSendBytes() throws IOException { 173 | // prepare 174 | final ByteArrayInputStream is = new ByteArrayInputStream("111111".getBytes(StandardCharsets.US_ASCII)); 175 | Mockito.when(socketProvider.getSocket().getInputStream()).thenReturn(is); 176 | final ByteArrayOutputStream os = new ByteArrayOutputStream(); 177 | Mockito.when(socketProvider.getSocket().getOutputStream()).thenReturn(os); 178 | // test 179 | Assert.assertFalse(conn.isConnected()); 180 | assertBytes("111111", conn.send("1X:10".getBytes(StandardCharsets.US_ASCII))); 181 | Assert.assertTrue(conn.isConnected()); 182 | // verify 183 | assertBytes("1X:10", os.toByteArray()); 184 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close(); 185 | } 186 | 187 | @Test 188 | public void testSendString() throws IOException { 189 | // prepare 190 | final ByteArrayInputStream is = new ByteArrayInputStream("111111".getBytes(StandardCharsets.US_ASCII)); 191 | Mockito.when(socketProvider.getSocket().getInputStream()).thenReturn(is); 192 | final ByteArrayOutputStream os = new ByteArrayOutputStream(); 193 | Mockito.when(socketProvider.getSocket().getOutputStream()).thenReturn(os); 194 | // test 195 | Assert.assertFalse(conn.isConnected()); 196 | Assert.assertEquals("111111", conn.send("1X:10")); 197 | Assert.assertTrue(conn.isConnected()); 198 | // verify 199 | assertBytes("1X:10", os.toByteArray()); 200 | Mockito.verify(socketProvider.getSocket(), Mockito.never()).close(); 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /sr201-config-client/src/main/java/li/cryx/sr201/client/conf/RemoteConfigWindow.java: -------------------------------------------------------------------------------- 1 | package li.cryx.sr201.client.conf; 2 | 3 | import java.awt.Dimension; 4 | import java.awt.FlowLayout; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | import java.util.ResourceBundle; 8 | 9 | import javax.swing.BorderFactory; 10 | import javax.swing.JButton; 11 | import javax.swing.JFrame; 12 | import javax.swing.JPanel; 13 | import javax.swing.WindowConstants; 14 | 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | import com.jgoodies.forms.layout.CellConstraints; 19 | import com.jgoodies.forms.layout.FormLayout; 20 | 21 | import li.cryx.sr201.SettingsFactory; 22 | import li.cryx.sr201.client.DialogFactory; 23 | import li.cryx.sr201.config.BoardState; 24 | import li.cryx.sr201.config.ConfigConnectionBuilder; 25 | import li.cryx.sr201.connection.ConnectionException; 26 | import li.cryx.sr201.connection.IpAddressValidator; 27 | import li.cryx.sr201.i18n.XMLResourceBundleControl; 28 | import li.cryx.sr201.util.IconSupport; 29 | import li.cryx.sr201.util.Icons; 30 | 31 | /** 32 | * This class starts the config tool for the SR-201. It can read and display the 33 | * current state of a board and attempt to change the settings according to user 34 | * input. 35 | * 36 | *

37 | * 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 Comparator { 54 | @Override 55 | public int compare(final Command c1, final Command c2) { 56 | return c1.cmd.compareTo(c2.cmd); 57 | } 58 | } 59 | 60 | private static final Logger LOG = LoggerFactory.getLogger(ConfigConnectionBuilder.class); 61 | 62 | /** Current IP address of the board. */ 63 | private final String ip; 64 | 65 | /** Sequence for the current commands. */ 66 | private int run; 67 | 68 | /** Connection to the board. */ 69 | private Socket socket; 70 | 71 | /** Receiving data */ 72 | private Reader in; 73 | 74 | /** Sending data. */ 75 | private Writer out; 76 | 77 | /** 78 | * List of pending commands. Will be executed once {@link #send()} is 79 | * called. 80 | */ 81 | private final List cmds = new LinkedList(); 82 | 83 | /** 84 | * Create a new builder that will send commands to the given IP address. 85 | * 86 | * @param ip 87 | * Current IP address of board. 88 | */ 89 | public ConfigConnectionBuilder(final String ip) { 90 | this.ip = ip; 91 | newRun(); 92 | } 93 | 94 | /** Close connection, release resources and reset internal state. */ 95 | private void close() { 96 | if (socket != null) { 97 | Closer.close(socket); 98 | in = null; 99 | out = null; 100 | // just in case the user recycles this instance 101 | cmds.clear(); 102 | newRun(); 103 | } 104 | } 105 | 106 | /** 107 | * Change the Cloud service host. 108 | *

109 | * 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 | * @return true, 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 | # ), mais envoi de la commande par CTRL-D (fin de flux). 43 | # 44 | # Remarque : le module vous deconnecte au bout de 15s sans activite. 45 | # 46 | # exemple : 47 | # $ nc 192.168.0.200 5111 48 | # #11111; (puis CTRL-D) 49 | # 50 | # renvoie: 51 | # >192.168.0.200,255.255.255.0,192.168.0.1,,0,435,F44900BD02CA27000000,192.168.0.1,connect.tutuuu.com,0; 52 | # 53 | # 54 | # #1XXXX; 55 | # ------- 56 | # lit la configuraion du module 57 | # Ask the board for its current settings. 58 | # 59 | # Le module renvoit les informations au sous forme d'une chaine de caracteres. 60 | # - La chaine commence par un caractere >. 61 | # - Les parametres sont separes par des virgules (,). 62 | # - la chaine se finit par un point virgule (;) 63 | # 64 | # The board answers with a comma separated list of its settings. 65 | # 66 | # exemple: 67 | # #11111; 68 | # renvoit 69 | # >192.168.1.100,255.255.255.0,192.168.1.1,,0,435,F449007D02E2EB000000,192.168.1.1,connect.tutuuu.com,0; 70 | # 71 | # Interpretation: 72 | # 1er parametre : 192.168.1.100 => adresse IP 73 | # 2eme parametre : 255.255.255.0 => masque IP 74 | # 3eme parametre : 192.168.1.1 => passerelle IP 75 | # 4eme parametre : => parametre inconnu 76 | # 5eme parametre : 0 => persistence de l'etat des relais lors d'un redemarrage 77 | # 6eme paramatre : 435 => version 78 | # 7eme parametre : F449007D02E2EB000000 => numero de serie (14c) + passwd 79 | # 8eme parametre : 192.168.1.1 => DNS 80 | # 9eme parametre : connect.tutuuu.com => serverur Cloud (????) 81 | # 10eme parametre : Etat du mode Cloud (????) 82 | # 83 | # Remarque: 84 | # Toujours envoyer une commande #1XXXX avant de modifier les parametres a l'aide 85 | # d'une autre commande (#2XXXX,aaa.bbb.ccc.ddd; par exemple). 86 | # Dans le cas contraire le module repond >ERR; a toutes vodes demandes de 87 | # modification. 88 | # 89 | # 90 | # #2XXXX,aaa.bbb.ccc.ddd; 91 | # ----------------------- 92 | # definit l'adresse IP du module. Attend une adresse IPV4 au format texte. 93 | # Set the board's IP address. Expects a IPv4 as a string (192.168.1.100) as 94 | # argument. 95 | # 96 | # exemple : #21111,192.168.0.200; 97 | # 98 | # 99 | # #3XXXX,aaa.bbb.ccc.ddd; 100 | # ----------------------- 101 | # definit le masque IP du module. Attend un masque IPV4 au format texte. 102 | # Set the subnet mask. Expects a IPv4 mask as a string (255.255.255.0) as 103 | # an argument. 104 | # 105 | # exemple : #31111,255.255.255.0; 106 | # 107 | # 108 | # #4XXXX,aaa.bbb.ccc.ddd; 109 | # ----------------------- 110 | # definit la passerelle IP. Attend une adresse IPV4 au format texte. 111 | # Set the default gateway used to resolve the cloud service. Expects a IPv4 112 | # as a string (192.168.1.1) as an argument. 113 | # 114 | # exemple : #41111,192.168.0.1; 115 | # 116 | # 117 | # #5XXXX,a; 118 | # --------- 119 | # commande inconnue 120 | # unknown command 121 | # 122 | # 123 | # #6XXXX,a; 124 | # --------- 125 | # valide la perstitence de l'etat des relais lors d'un redemarrage 126 | # Enable persistent relay states when board is powered off and on again. 127 | # 128 | # exemple : #61111,1; 129 | # 130 | # 131 | # #7XXXX; 132 | # ------- 133 | # Redemarre le module et donc prend en compte les modifications faites. 134 | # Restart the board. Make changes take effect. 135 | # 136 | # exemple : #71111; 137 | # 138 | # 139 | # #8XXXX,aaa.bbb.ccc.ddd; 140 | # ----------------------- 141 | # definit le DNS. Utile pour utiliser le Cloud. Attend une adresse IPV4 au 142 | # format texte. 143 | # Set the DNS server used to resolve the cloud service. Expects a IPv4 as a 144 | # string (192.168.1.1) as an argument. 145 | # 146 | # exemple : #81111,192.168.0.1; 147 | # 148 | # 149 | # #9XXXX,abcdefg...xyz; 150 | # -------------------- 151 | # definit le serveur Cloud (???). Attend un nom d'host. 152 | # Set the cloud server host. Expects a host name as an argument. 153 | # 154 | # exemple : #91111,connect.tutuuu.com; 155 | # 156 | # 157 | # #AXXXX,a; 158 | # --------- 159 | # valide l'utilisation du cloud (???) 160 | # Enable or disable the cloud service. 161 | # 162 | # exemple : #A1111,0; 163 | # 164 | # 165 | # #BXXXX,abcdef; 166 | # -------------- 167 | # definit le mot de passe pour l'utilisation du cloud (???). Attend un MdP sur 168 | # 6 caracteres. 169 | # Set the password of the cloud service. Expects a 6 character long password 170 | # as an argument. 171 | # 172 | # exemple : #B1111,0123456; 173 | # 174 | # 175 | 176 | use Net::Telnet; 177 | use Term::UI; 178 | use Term::ReadLine; 179 | 180 | my $Session = new Net::Telnet(Port => "5111"); 181 | 182 | my $Term = Term::ReadLine->new('prompt'); 183 | 184 | my @Choix = ( 185 | "Lire la configuration", 186 | "Modifier la configuration", 187 | "Quitter"); 188 | 189 | my $Saisie; 190 | my $Reponse; 191 | my $AdresseIP; 192 | my $MasqueIP; 193 | my $Passerelle; 194 | my $Persistenec; 195 | my $DNS; 196 | 197 | do { 198 | $Saisie = $Term->get_reply( 199 | prompt => 'Choisir un nombre : ', 200 | choices => \@Choix, 201 | default => $Choix[2], 202 | ); 203 | 204 | if($Saisie eq $Choix[0]) { 205 | $Session->open($ARGV[0]); 206 | 207 | # Lecture des parametres actuelles 208 | my $Commande = "#11111;"; 209 | $Session->put($Commande); 210 | $Reponse = $Session->get(); 211 | 212 | $Session->close(); 213 | 214 | # $Commande = "#41111,192.168.0.1;"; 215 | # $Session->put($Commande); 216 | # $Reponse = $Session->get(); 217 | # print $ Reponse; 218 | if($Reponse eq ">ERR;") { 219 | print("Erreur\n"); 220 | } else { 221 | @Parametres = split(/,/,$Reponse); 222 | 223 | $AdresseIP = substr($Parametres[0],1,15);; 224 | $MasqueIP = $Parametres[1]; 225 | $Passerelle = $Parametres[2]; 226 | $Persistence = $Parametres[4]; 227 | $DNS = $Parametres[7]; 228 | 229 | print "\n"; 230 | print "Parametres:\n"; 231 | print "Adresse Ip : ".$AdresseIP."\n"; 232 | print "Masque Ip : ".$MasqueIP."\n"; 233 | print "Passerelle : ".$Passerelle."\n"; 234 | print "Persistence : ".$Persistence."\n"; 235 | print "DNS : ".$DNS."\n"; 236 | print "\n"; 237 | } 238 | } elsif ($Saisie eq $Choix[1]) { 239 | $Session->open($ARGV[0]); 240 | 241 | # Lecture des parametres actuelles 242 | my $Commande = "#11111;"; 243 | $Session->put($Commande); 244 | $Reponse = $Session->get(); 245 | 246 | $Session->close(); 247 | 248 | # $Commande = "#41111,192.168.0.1;"; 249 | # $Session->put($Commande); 250 | # $Reponse = $Session->get(); 251 | # print $ Reponse; 252 | if($Reponse eq ">ERR;") { 253 | print("Erreur de lecture des parametres\n"); 254 | } else { 255 | @Parametres = split(/,/,$Reponse); 256 | 257 | $AdresseIP = substr($Parametres[0],1,15);; 258 | $MasqueIP = $Parametres[1]; 259 | $Passerelle = $Parametres[2]; 260 | $Persistence = $Parametres[4]; 261 | $DNS = $Parametres[7]; 262 | 263 | print "\n"; 264 | print "Parametres:\n"; 265 | $AdresseIP = $Term->get_reply( 266 | prompt => "Adresse Ip", 267 | default => $AdresseIP); 268 | $MasqueIP = $Term->get_reply( 269 | prompt => "Masque Ip", 270 | default => $MasqueIP); 271 | $Passerelle = $Term->get_reply( 272 | prompt => "Passerelle", 273 | default => $Passerelle); 274 | $Persistence = $Term->get_reply( 275 | prompt => "Persistence", 276 | default => $Persistence); 277 | $DNS = $Term->get_reply( 278 | prompt => "DNS", 279 | default => $DNS); 280 | print "\n"; 281 | 282 | my $OuiNon = $Term->ask_yn( 283 | prompt => "Ecrire ces valeurs et redemarrer le module?", 284 | default => 'n', 285 | ); 286 | if($OuiNon) { 287 | $Session->open($ARGV[0]); 288 | 289 | # Lecture des parametres actuelles (obligatoire au cause du timout 290 | # de 15 secondes 291 | my $Commande = "#11111;"; 292 | $Session->put($Commande); 293 | $Reponse = $Session->get(); 294 | 295 | if($Reponse eq ">ERR;") { 296 | print("Erreur de lecture des parametres\n"); 297 | } 298 | 299 | # adresse IP 300 | $Commande = "#21111,".$AdresseIP.";"; 301 | $Session->put($Commande); 302 | $Reponse = $Session->get(); 303 | 304 | if($Reponse eq ">ERR;") { 305 | print("Erreur d'ecriture de l'adresse IP\n"); 306 | } 307 | 308 | # Masque IP 309 | $Commande = "#31111,".$MasqueIP.";"; 310 | $Session->put($Commande); 311 | $Reponse = $Session->get(); 312 | 313 | if($Reponse eq ">ERR;") { 314 | print("Erreur d'ecriture du masque IP\n"); 315 | } 316 | 317 | # Passerelle 318 | $Commande = "#41111,".$Passerelle.";"; 319 | $Session->put($Commande); 320 | $Reponse = $Session->get(); 321 | 322 | if($Reponse eq ">ERR;") { 323 | print("Erreur d'ecriture de la passerelle\n"); 324 | } 325 | # Persistence 326 | $Commande = "#61111,".$Persistence.";"; 327 | $Session->put($Commande); 328 | $Reponse = $Session->get(); 329 | 330 | if($Reponse eq ">ERR;") { 331 | print("Erreur d'ecriture de la persitence\n"); 332 | } 333 | 334 | # DNS 335 | $Commande = "#81111,".$DNS.";"; 336 | $Session->put($Commande); 337 | $Reponse = $Session->get(); 338 | 339 | if($Reponse eq ">ERR;") { 340 | print("Erreur d'ecriture du DNS\n"); 341 | } 342 | 343 | 344 | $Commande = "#71111;"; 345 | $Session->put($Commande); 346 | $Reponse = $Session->get(); 347 | 348 | if($Reponse eq ">ERR;") { 349 | print("Erreur de prise en compte des parametres"); 350 | } 351 | $Session->close(); 352 | } 353 | } 354 | } 355 | } 356 | while($Saisie ne $Choix[2]); 357 | 358 | 359 | --------------------------------------------------------------------------------