/*
* #%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.CloseEvent;
import tuwien.auto.calimero.DataUnitBuilder;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.Settings;
import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.exception.KNXIllegalArgumentException;
import tuwien.auto.calimero.knxnetip.KNXnetIPConnection;
import tuwien.auto.calimero.link.KNXNetworkMonitor;
import tuwien.auto.calimero.link.KNXNetworkMonitorFT12;
import tuwien.auto.calimero.link.KNXNetworkMonitorIP;
import tuwien.auto.calimero.link.event.LinkListener;
import tuwien.auto.calimero.link.event.MonitorFrameEvent;
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.RawFrame;
import tuwien.auto.calimero.link.medium.RawFrameBase;
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;
/**
* A tool for Calimero allowing monitoring of KNX network messages.
* <p>
* NetworkMonitor is a console based tool implementation allowing a user to track KNX
* network messages in a KNX network. It allows monitoring access using a KNXnet/IP
* connection or FT1.2 connection. It shows the necessary interaction with the Calimero
* API for this particular task. To start monitoring invoke the <code>main</code>-method
* of this class. Note that by default the network monitor will run with 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 KNXNetworkMonitor}, which offers monitoring access to a KNX network. All
* monitoring output, as well as occurring problems are written to <code>System.out
* </code>.
* <p>
* To quit a running monitor in the console, use a user interrupt for termination (
* <code>^C</code> for example).
*
* @author B. Malinowsky
*/
public class NetworkMonitor
{
private static final String tool = "NetworkMonitor";
private static final String version = "0.2";
private static final String sep = System.getProperty("line.separator");
private final Map options;
private KNXNetworkMonitor m;
private LogWriter w;
private final class MonitorListener implements LinkListener
{
private MonitorListener()
{}
public void indication(FrameEvent e)
{
final StringBuffer sb = new StringBuffer();
sb.append(e.getFrame().toString());
// since we specified decoding of raw frames in createMonitor(), we
// can get the decoded raw frame here
// but note, that on decoding error null is returned
final RawFrame raw = ((MonitorFrameEvent) e).getRawFrame();
if (raw != null) {
sb.append(": ").append(raw.toString());
if (raw instanceof RawFrameBase) {
final RawFrameBase f = (RawFrameBase) raw;
sb.append(": ").append(
DataUnitBuilder.decode(f.getTPDU(), f.getDestination()));
}
}
System.out.println(sb);
}
public void linkClosed(CloseEvent e)
{}
}
/**
* Creates a new NetworkMonitor 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, can be <code>null</code>
* @throws KNXException on instantiation problems
* @throws KNXIllegalArgumentException on unknown/invalid options
*/
public NetworkMonitor(String[] args, LogWriter w) throws KNXException
{
this.w = w;
try {
// read the command line options
options = new HashMap();
if (!parseOptions(args, options))
throw new KNXException("only show usage/version information, abort "
+ tool);
}
catch (final RuntimeException e) {
throw new KNXIllegalArgumentException(e.getMessage());
}
}
/**
* Entry point for running the NetworkMonitor.
* <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 network
* monitoring:
* <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>-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>
*
* @param args command line options for network monitoring
*/
public static void main(String[] args)
{
try {
final NetworkMonitor m = new NetworkMonitor(args, null);
// supply a log writer for System.out (console)
m.w = new ConsoleWriter(m.options.containsKey("verbose"));
m.run(m.new MonitorListener());
}
catch (final Throwable t) {
if (t.getMessage() != null)
System.out.println(t.getMessage());
else
System.out.println(t.getClass().getName());
}
}
/**
* Runs the network monitor.
* <p>
* This method returns when the network monitor is closed.
*
* @param l a link listener for monitor events
* @throws KNXException on problems on creating monitor or during monitoring
*/
public void run(LinkListener l) throws KNXException
{
createMonitor(l);
final Thread sh = registerShutdownHandler();
// TODO actually, this waiting block is just necessary if we're in console mode
// to keep the current thread alive and for clean up
// when invoked externally by a user, an immediate return could save one
// additional thread (with the requirement to call quit for cleanup)
try {
// just wait for the network monitor to quit
synchronized (this) {
while (m.isOpen())
try {
wait();
}
catch (final InterruptedException e) {}
}
}
finally {
Runtime.getRuntime().removeShutdownHook(sh);
}
}
/**
* Quits the network monitor, if running.
* <p>
*/
public void quit()
{
if (m != null && m.isOpen()) {
m.close();
synchronized (this) {
notifyAll();
}
}
}
/**
* Creates a new network monitor using the supplied options.
* <p>
*
* @throws KNXException on problems on monitor creation
*/
private void createMonitor(LinkListener l) throws KNXException
{
final KNXMediumSettings medium = (KNXMediumSettings) options.get("medium");
if (options.containsKey("serial")) {
final String p = (String) options.get("serial");
try {
m = new KNXNetworkMonitorFT12(Integer.parseInt(p), medium);
}
catch (final NumberFormatException e) {
m = new KNXNetworkMonitorFT12(p, medium);
}
}
else {
// create local and remote socket address for monitor 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());
// create the monitor link, based on the KNXnet/IP protocol
// specify whether network address translation shall be used,
// and tell the physical medium of the KNX network
m = new KNXNetworkMonitorIP(local, host, options.containsKey("nat"), medium);
}
// add the log writer for monitor log events
LogManager.getManager().addWriter(m.getName(), w);
// on console we want to have all possible information, so enable
// decoding of a received raw frame by the monitor link
m.setDecodeRawFrames(true);
// listen to monitor link events
m.addMonitorListener(l);
// we always need a link closed notification (even with user supplied listener)
m.addMonitorListener(new LinkListener() {
public void indication(FrameEvent e)
{}
public void linkClosed(CloseEvent e)
{
System.out.println("network monitor exit, " + e.getReason());
synchronized (NetworkMonitor.this) {
NetworkMonitor.this.notify();
}
}
});
}
private Thread registerShutdownHandler()
{
final class ShutdownHandler extends Thread
{
ShutdownHandler()
{}
public void run()
{
System.out.println("shutdown");
quit();
}
}
final ShutdownHandler sh = new ShutdownHandler();
Runtime.getRuntime().addShutdownHook(sh);
return sh;
}
/**
* Reads all options in the specified array, and puts relevant options into the
* supplied options map.
* <p>
* On options not relevant for doing network monitoring (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 monitoring, <code>false</code> otherwise or if the options were
* handled by this method
*/
private static boolean parseOptions(String[] args, Map options)
{
if (args.length == 0) {
System.out.println("A tool for monitoring a KNX network");
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, "-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 (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);
}
return true;
}
private static void showUsage()
{
final StringBuffer sb = new StringBuffer();
sb.append("usage: ").append(tool).append(" [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(")").append(sep);
sb.append(" -nat -n enable Network Address Translation").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);
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 static final class ConsoleWriter extends LogStreamWriter
{
ConsoleWriter(boolean verbose)
{
super(verbose ? LogLevel.TRACE : LogLevel.WARN, System.out, true);
}
public void close()
{}
}
}