/* * #%L * OW2 Chameleon - Fuchsia Framework * %% * Copyright (C) 2009 - 2014 OW2 Chameleon * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ /* Calimero - A library for KNX network access Copyright (C) 2006-2008 W. Kastner This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package tuwien.auto.calimero.tools; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.Settings; import tuwien.auto.calimero.datapoint.Datapoint; import tuwien.auto.calimero.datapoint.StateDP; import tuwien.auto.calimero.exception.KNXException; import tuwien.auto.calimero.exception.KNXFormatException; import tuwien.auto.calimero.exception.KNXIllegalArgumentException; import tuwien.auto.calimero.knxnetip.KNXnetIPConnection; import tuwien.auto.calimero.link.KNXNetworkLink; import tuwien.auto.calimero.link.KNXNetworkLinkFT12; import tuwien.auto.calimero.link.KNXNetworkLinkIP; import tuwien.auto.calimero.link.medium.KNXMediumSettings; import tuwien.auto.calimero.link.medium.PLSettings; import tuwien.auto.calimero.link.medium.RFSettings; import tuwien.auto.calimero.link.medium.TPSettings; import tuwien.auto.calimero.log.LogLevel; import tuwien.auto.calimero.log.LogManager; import tuwien.auto.calimero.log.LogStreamWriter; import tuwien.auto.calimero.log.LogWriter; import tuwien.auto.calimero.process.ProcessCommunicator; import tuwien.auto.calimero.process.ProcessCommunicatorImpl; import tuwien.auto.calimero.process.ProcessListener; /** * A tool for Calimero allowing simple process communication. * <p> * ProcComm is a console based tool implementation allowing a user to read or write group * values in a KNX network. It supports network access using a KNXnet/IP connection or * FT1.2 connection. It shows the necessary interaction with the Calimero API for this * particular task. To read or write one value, invoke the <code>main</code>-method of * this class. Note that by default the communication will use common settings, if not * specified otherwise using command line options. Since these settings might be system * dependent (for example the local host) and not always predictable, a user may want to * specify particular settings using available option flags. * <p> * The main part of this tool implementation interacts with the type * {@link ProcessCommunicator}, which offers high level access for reading and writing * process values. It also shows creation of a {@link KNXNetworkLink}, which is supplied * to the process communicator, serving as the link to the KNX network. All read * responses, as well as occurring problems are written to <code>System.out * </code>. * * @author B. Malinowsky */ public class ProcComm { private static final String tool = "ProcComm"; private static final String version = "0.2"; private static final String sep = System.getProperty("line.separator"); /** * The used process communicator. */ protected ProcessCommunicator pc; // specifies parameters to use for the network link private final Map options; private LogWriter w; private ShutdownHandler sh; /** * Creates a new ProcComm instance using the supplied options. * <p> * See {@link #main(String[])} for a list of options. * * @param args list with options * @param w a log writer, might be <code>null</code> * @throws KNXException */ public ProcComm(String[] args, LogWriter w) throws KNXException { this.w = w; // read the command line options and run the process communicator options = new HashMap(); if (!parseOptions(args, options)) throw new KNXException("only show usage/version information, " + "abort " + tool); } /** * Entry point for running ProcComm. * <p> * An IP host or port identifier has to be supplied, specifying the endpoint for the * KNX network access.<br> * To show the usage message of this tool on the console, supply the command line * option -help (or -h).<br> * Command line options are treated case sensitive. Available options for the * communication connection: * <ul> * <li><code>-help -h</code> show help message</li> * <li><code>-version</code> show tool/library version and exit</li> * <li><code>-verbose -v</code> enable verbose status output</li> * <li><code>-localhost</code> <i>id</i>  local IP/host name</li> * <li><code>-localport</code> <i>number</i>  local UDP port (default system * assigned)</li> * <li><code>-port -p</code> <i>number</i>  UDP port on host (default 3671)</li> * <li><code>-nat -n</code> enable Network Address Translation</li> * <li><code>-routing</code> use KNXnet/IP routing</li> * <li><code>-serial -s</code> use FT1.2 serial communication</li> * <li><code>-medium -m</code> <i>id</i>  KNX medium [tp0|tp1|p110|p132|rf] * (defaults to tp1)</li> * </ul> * Available commands for process communication: * <ul> * <li><code>read</code> <i>DPT  KNX-address</i>  read from group * address, using DPT value format</li> * <li><code>write</code> <i>DPT  value  KNX-address</i>  write * to group address, using DPT value format</li> * </ul> * For the more common datapoint types (DPTs) the following name aliases can be used * instead of the general DPT number string: * <ul> * <li><code>switch</code> for DPT 1.001</li> * <li><code>bool</code> for DPT 1.002</li> * <li><code>string</code> for DPT 16.001</li> * <li><code>float</code> for DPT 9.002</li> * <li><code>ucount</code> for DPT 5.010</li> * <li><code>angle</code> for DPT 5.003</li> * </ul> * * @param args command line options for process communication */ public static void main(String[] args) { try { final ProcComm pc = new ProcComm(args, null); // use a log writer for the console (System.out), setting the user // specified log level, if any pc.w = new ConsoleWriter(pc.options.containsKey("verbose")); if (pc.options.containsKey("read") == pc.options.containsKey("write")) throw new IllegalArgumentException("do either read or write"); try { pc.run(null); pc.readWrite(); } finally { pc.quit(); } } catch (final Throwable t) { if (t.getMessage() != null) System.out.println(t.getMessage()); } } /** * Runs the process communicator. * <p> * This method immediately returns when the process communicator is running. Call * {@link #quit()} to quit process communication. * * @param l a process event listener, can be <code>null</code> * @throws KNXException on problems creating network link or communication */ public void run(ProcessListener l) throws KNXException { // create the network link to the KNX network final KNXNetworkLink lnk = createLink(); LogManager.getManager().addWriter(lnk.getName(), w); // create process communicator with the established link pc = new ProcessCommunicatorImpl(lnk); if (l != null) pc.addProcessListener(l); registerShutdownHandler(); // user might specify a response timeout for KNX message // answers from the KNX network if (options.containsKey("timeout")) pc.setResponseTimeout(((Integer) options.get("timeout")).intValue()); } /** * Quits process communication. * <p> * Detaches the network link from the process communicator and closes the link. */ public void quit() { if (pc != null) { final KNXNetworkLink lnk = pc.detach(); if (lnk != null) lnk.close(); Runtime.getRuntime().removeShutdownHook(sh); } } /** * Creates the KNX network link to access the network specified in * <code>options</code>. * <p> * * @return the KNX network link * @throws KNXException on problems on link creation */ private KNXNetworkLink createLink() throws KNXException { final KNXMediumSettings medium = (KNXMediumSettings) options.get("medium"); if (options.containsKey("serial")) { // create FT1.2 network link final String p = (String) options.get("serial"); try { return new KNXNetworkLinkFT12(Integer.parseInt(p), medium); } catch (final NumberFormatException e) { return new KNXNetworkLinkFT12(p, medium); } } // create local and remote socket address for network link final InetSocketAddress local = createLocalSocket((InetAddress) options.get("localhost"), (Integer) options.get("localport")); final InetSocketAddress host = new InetSocketAddress((InetAddress) options.get("host"), ((Integer) options.get("port")).intValue()); final int mode = options.containsKey("routing") ? KNXNetworkLinkIP.ROUTER : KNXNetworkLinkIP.TUNNEL; return new KNXNetworkLinkIP(mode, local, host, options.containsKey("nat"), medium); } /** * Gets the datapoint type identifier from the <code>options</code>, and maps alias * names of common datapoint types to its datapoint type ID. * <p> * The option map must contain a "dpt" key with value. * * @return datapoint type identifier */ private String getDPT() { final String dpt = (String) options.get("dpt"); if (dpt.equals("switch")) return "1.001"; if (dpt.equals("bool")) return "1.002"; if (dpt.equals("string")) return "16.001"; if (dpt.equals("float")) return "9.002"; if (dpt.equals("ucount")) return "5.010"; if (dpt.equals("angle")) return "5.003"; return dpt; } private void readWrite() throws KNXException { // check if we are doing a read or write operation final boolean read = options.containsKey("read"); final GroupAddress main = (GroupAddress) options.get(read ? "read" : "write"); // encapsulate information into a datapoint // this is a convenient way to let the process communicator // handle the DPT stuff, so an already formatted string will be returned final Datapoint dp = new StateDP(main, "", 0, getDPT()); if (read) System.out.println("read value: " + pc.read(dp)); else { // note, a write to a non existing datapoint might finish successfully, // too.. no check for existence or read back of a written value is done pc.write(dp, (String) options.get("value")); System.out.println("write successful"); } } /** * Reads all options in the specified array, and puts relevant options into the * supplied options map. * <p> * On options not relevant for doing process communication (like <code>help</code>), * this method will take appropriate action (like showing usage information). On * occurrence of such an option, other options will be ignored. On unknown options, an * IllegalArgumentException is thrown. * * @param args array with command line options * @param options map to store options, optionally with its associated value * @return <code>true</code> if the supplied provide enough information to continue * with communication, <code>false</code> otherwise or if the options were * handled by this method */ private static boolean parseOptions(String[] args, Map options) throws KNXFormatException { if (args.length == 0) { System.out.println("A tool for KNX process communication"); showVersion(); System.out.println("type -help for help message"); return false; } // add defaults options.put("port", new Integer(KNXnetIPConnection.IP_PORT)); options.put("medium", TPSettings.TP1); int i = 0; for (; i < args.length; i++) { final String arg = args[i]; if (isOption(arg, "-help", "-h")) { showUsage(); return false; } if (isOption(arg, "-version", null)) { showVersion(); return false; } if (isOption(arg, "-verbose", "-v")) options.put("verbose", null); else if (isOption(arg, "read", null)) { if (i + 2 >= args.length) break; options.put("dpt", args[++i]); options.put("read", new GroupAddress(args[++i])); } else if (isOption(arg, "write", null)) { if (i + 3 >= args.length) break; options.put("dpt", args[++i]); options.put("value", args[++i]); options.put("write", new GroupAddress(args[++i])); } else if (isOption(arg, "-localhost", null)) parseHost(args[++i], true, options); else if (isOption(arg, "-localport", null)) options.put("localport", Integer.decode(args[++i])); else if (isOption(arg, "-port", "-p")) options.put("port", Integer.decode(args[++i])); else if (isOption(arg, "-nat", "-n")) options.put("nat", null); else if (isOption(arg, "-serial", "-s")) options.put("serial", null); else if (isOption(arg, "-medium", "-m")) options.put("medium", getMedium(args[++i])); else if (isOption(arg, "-timeout", "-t")) options.put("timeout", Integer.decode(args[++i])); else if (isOption(arg, "-routing", null)) options.put("routing", null); else if (options.containsKey("serial")) // add port number/identifier to serial option options.put("serial", arg); else if (!options.containsKey("host")) parseHost(arg, false, options); else throw new IllegalArgumentException("unknown option " + arg); } if (options.containsKey("host") == options.containsKey("serial")) throw new IllegalArgumentException("no host or serial port specified"); return true; } private static void showUsage() { final StringBuffer sb = new StringBuffer(); sb.append("usage: ").append(tool + " [options] <host|port>").append(sep); sb.append("options:").append(sep); sb.append(" -help -h show this help message").append(sep); sb.append(" -version show tool/library version and exit").append( sep); sb.append(" -verbose -v enable verbose status output").append(sep); sb.append(" -localhost <id> local IP/host name").append(sep); sb.append(" -localport <number> local UDP port (default system assigned)") .append(sep); sb.append(" -port -p <number> UDP port on <host> (default ").append( KNXnetIPConnection.IP_PORT + ")").append(sep); sb.append(" -nat -n enable Network Address Translation").append( sep); sb.append(" -routing use KNX net/IP routing").append(sep); sb.append(" -serial -s use FT1.2 serial communication").append(sep); sb.append(" -medium -m <id> KNX medium [tp0|tp1|p110|p132|rf] " + "(default tp1)").append(sep); sb.append("Available commands for process communication:").append(sep); sb.append(" read <DPT> <KNX address> read from group address") .append(sep); sb.append(" write <DPT> <value> <KNX address> write to group address") .append(sep); sb.append("Additionally recognized name aliases for DPT numbers:").append(sep); sb.append(" switch (1.001), bool (1.002), string (16.001)").append(sep) .append(" float (9.002), ucount (5.010), angle (5.003)"); System.out.println(sb); } // // utility methods // private static void showVersion() { System.out.println(tool + " version " + version + " using " + Settings.getLibraryHeader(false)); } /** * Creates a medium settings type for the supplied medium identifier. * <p> * * @param id a medium identifier from command line option * @return medium settings object * @throws KNXIllegalArgumentException on unknown medium identifier */ private static KNXMediumSettings getMedium(String id) { if (id.equals("tp0")) return TPSettings.TP0; else if (id.equals("tp1")) return TPSettings.TP1; else if (id.equals("p110")) return new PLSettings(false); else if (id.equals("p132")) return new PLSettings(true); else if (id.equals("rf")) return new RFSettings(null); else throw new KNXIllegalArgumentException("unknown medium"); } private static void parseHost(String host, boolean local, Map options) { try { options.put(local ? "localhost" : "host", InetAddress.getByName(host)); } catch (final UnknownHostException e) { throw new IllegalArgumentException("failed to read host " + host); } } private static InetSocketAddress createLocalSocket(InetAddress host, Integer port) { final int p = port != null ? port.intValue() : 0; try { return host != null ? new InetSocketAddress(host, p) : p != 0 ? new InetSocketAddress(InetAddress.getLocalHost(), p) : null; } catch (final UnknownHostException e) { throw new IllegalArgumentException("failed to create local host " + e.getMessage()); } } private static boolean isOption(String arg, String longOpt, String shortOpt) { return arg.equals(longOpt) || shortOpt != null && arg.equals(shortOpt); } private final class ShutdownHandler extends Thread { ShutdownHandler() {} public void run() { quit(); } } private void registerShutdownHandler() { Runtime.getRuntime().addShutdownHook(sh = new ShutdownHandler()); } // a log writer writing log events to System.out (i.e console in most cases) private static final class ConsoleWriter extends LogStreamWriter { ConsoleWriter(boolean verbose) { super(verbose ? LogLevel.INFO : LogLevel.ERROR, System.out, true); } public void close() {} } }