### Navigation

- index
- modules |
- JSON Link 0.9.2a documentation »
- Module code »

# Source code for json_link.bin.json_link_tester

    #!/usr/local/bin/python3

    """
    json_test:
    A simple program for test the JSON link protocol over TCP or UDP exchange.  It
    can act as either the Master (aka client) or the Slave (aka server)

    As a SLAVE (Default) it:
        1) Listens on a specified IP port for UDP or TCP packets
        2) Displays any packets received
        3) Waits a specified delay (to simulate processing or waiting for the slave
           values to stabilize.
        4) Replies with a modified packet. In this packet, the clock string is
           updated, a random y0 value and a value of y1=2*x1 (zero if no x1) are
           added and x0; any x0 or x1 values sent are removed; and any other fields
           are returned intact.
        5) Resumes listening

    As a MASTER it:
        1) Sending a dummy JSON data packet to a remote host via UDP or TCP
        2) Waiting for a JSON data reply (with a timeout)
        3) Repeats these steps at at a specified (DELAY) rate

    Classes:
      NONE

    Implemented Methods:
      main()              #the main program

    Notes:

    * Should work with Python 2.6+ and 3.x. Tested with 2.7.2 and 3.3
    * Currently only supports IPv4
    * The SLAVE will handle connections from any number of masters, BUT since the
      code is only single threaded each processing (including delay) are
      blocking. Hence, the SLAVE may miss packets if it is busy processing
      (or waiting to send) a different reply.

    @author: Bryan Palmintier, NREL 2013
    """
    # Enable use of Python 3.0 print statement in older (2.x) versions, ignored in
    # newer (3.x) versions
    # Must occur at top of file
    from __future__ import print_function

    __author__ = "Bryan Palmintier"
    __copyright__ = "Copyright (c) 2013 Bryan Palmintier"
    __license__ = """
    @copyright: Copyright (c) 2013, Bryan Palmintier
    @license: BSD 3-clause --
    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:
     1) Redistributions of source code must retain the above copyright notice, this
        list of conditions and the following disclaimer.
     2) Redistributions in binary form must reproduce the above copyright notice,
        this list of conditions and the following disclaimer in the documentation
        and/or other materials provided with the distribution.
     3) Neither the name of the National Renewable Energy Lab nor the names of its
        contributors may be used to endorse or promote products derived from this
        software without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
    ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
    ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."""

    ## ===== History ====
    #  [Current]             version     date       time     who     Comments
    #                        -------  ----------    ----- ---------- -------------
    __version__, __date__ = "0.8.2a","2013-04-29"  #20:10   BryanP   Expanded/Refined timing options: run_timestep vs delay vs timeout
    # [Older, in reverse order]
    # version      date       time     who     Comments
    # -------   ----------    ----- ---------- ---------------------------------------
    #  0.8.1a   2013-04-28    22:20   BryanP   Rename gld_tcp_udp to raw_xchg & methods to Xchg* and Errors to RawXchg*Error
    #  0.8.0a   2013-04-27    02:00   BryanP   RENAMED json_link_test. Use json_link for FULL HANDSHAKING. Added --run_count
    #  0.7.1a   2013-04-25    16:23   BryanP   Updated for gld_upd_tcp v 0.3.0a (Exceptions Link* -> Xchg*)
    #  0.7.0a   2013-04-25    03:33   BryanP   Extracted low-level request/response header to raw_xchg.py
    #  0.6.1a   2013-04-24    00:43   BryanP   BUG FIX: UDP socket close, abstract out setupSlaveSocket()
    #   0.6a    2013-04-23    22:43   BryanP   Added GridLAB request/response header support
    #  0.5.5a   2013-04-09    15:23   BryanP   Corrected python3.x and windows support, added num_var option
    #  0.5.4a   2013-04-09    11:23   BryanP   Switched to master/slave terminology. Default now slave
    #  0.5.3a   2013-04-09    00:23   BryanP   Client auto-repeat
    #  0.5.2a   2013-04-08    12:32   BryanP   Added server delay & client timeout
    #  0.5.1a   2013-04-08    22:00   BryanP   Implemented (basic, fixed) JSON exchange
    #   0.4a    2013-04-08    17:30   BryanP   Merged client and server (and shared) into single file
    #   0.3a    2013-04-08            BryanP   Command Line options using JSON_test_shared
    #   0.2a    2013-04-08    12:32   BryanP   Added Metadata Header
    #   0.1a    2012-04-05            BryanP   Initial Code, partially adapted from http://docs.python.org/3.3/library/socket.html#example

    # Standard Library imports
    import optparse  # Use older optparse for pre-2.7/3.2 compatibility. argparse preferred for newer projects
    import json
    import datetime as dt
    import random
    import time
    import timeit

    # Local imports
    from .. import json_link
    from ..xchg import raw_xchg

    # module variables
    TIMEOUT_REDUCE = 0.001     #sec to allow for actual python processing

    #-------------------
    # runSimpleMaster
    #-------------------
    def runSimpleMaster(opt):

        # Initialize underlying socket.
        # Note: if opt.raw is used, use raw_xchg to print packet data
        link = raw_xchg.MasterXchg(opt.host_addr, opt.port, opt.sock_type,
                                      opt_verbose=opt.raw,
                                      opt_header=(not opt.no_head),
                                      timeout=opt.timeout)

        link.setupXchg()
        end_time = timeit.default_timer()

        while True:
            end_time += opt.delay

            if link.isReady():
                # -- Build message and send it
                data = buildMessage(opt)
                msg_out = json.dumps(data)
                link.send(msg_out)
                if not opt.raw:
                    printIO('TX: ', data)

                # And wait for a response
                try:
                    reply = link.receive()
                except raw_xchg.RawXchgTimeoutError:
                    reply = None

                if reply is not None:
                    reply = json.loads(reply)
                    if not opt.raw:
                        printIO('RX: ', reply)

            # Add blank line between exchanges
            print()

            # Setup link here for more consistant tx interval, since not dependent on setup delays
            link.setupXchg()

            # Wait for repeat timer, but yield each loop to prevent excess CPU
            while timeit.default_timer() < end_time:
                time.sleep(opt.delay / 1000)

    #-------------------
    # runSimpleSlave
    #-------------------
    def runSimpleSlave(opt):
        # Initialize underlying socket.
        # Note: if opt.raw is used, use raw_xchg to print packet data
        link = raw_xchg.SlaveXchg(opt.host_addr, opt.port, opt.sock_type,
                                     opt_verbose=opt.raw,
                                     opt_header=(not opt.no_head),
                                     timeout=opt.timeout)

        while True:
            link.setupXchg()

            # --Wait for connection
            try:
                data = link.receive()
            except raw_xchg.RawXchgTimeoutError:
                continue

            if data is None:
                continue

            # Process incoming
            if opt.echo:
                msg_out = data
            else:
                data = json.loads(data)
            if not opt.raw:
                printIO('RX: ', data)

            # Wait delay period
            if opt.delay > 0:
                time.sleep(opt.delay)

            # Build Reply
            reply = buildReply(opt, data)
            msg_out = json.dumps(reply)

            # and send it out
            link.send(msg_out)
            if not opt.raw:
                printIO('TX: ', reply)
            print()

    #-------------------
    # buildMessage
    #-------------------
    def buildMessage(opt):
        data = dict()
        data['clock'] = dt.datetime.now().isoformat(' ')
        for n in range(0, opt.num_var):
            data['x%d' % n] = random.random()
        return data

    #-------------------
    # buildReply
    #-------------------
    def buildReply(opt, data):

        if type(data) is dict:
            # Build reply
            if 'clock' in data:
                data['clock'] = dt.datetime.now().isoformat(' ')

            for n in range(0, opt.num_var):
                # Use random numbers for first half of y data
                if n < opt.num_var / 2:
                    data['y%d' % n] = random.random()
                else:
                # For second half, double corresponding x, or return 0
                    if 'x%d' % n in data:
                        data['y%d' % n] = 2 * data['x%d' % n]
                    else:
                        data['y%d' % n] = 0

            # For all cases remove all x followed by only digits
            data_keys = list(data.keys())  # Note cache full key list b/c otherwise python complains when the dict changes size
            for k in data_keys:
                if k.startswith('x') and k.lstrip('x').isdigit():
                    data.pop(k)

        else:  # Non-dictionary value for data
            print('ERROR: Expected a dictionary object in JSON. Returning Empty JSON reply')
            data = dict()
        return data

    #-------------------
    # printIO
    #-------------------
    def printIO(prefix, data, suffix=''):
        print(prefix, end='')
        if type(data) is dict:
            if 'clock' in data:
                print("clock='%s' " % (data['clock']), end='')
            for k in sorted(data.keys()):
                if k != 'clock':
                    print("%s=%.3f " % (k, data[k]), end='')
        else:
            print(data, end='')
        print(suffix)

    #-------------------
    # _handleCommandLine
    #-------------------
    def _handleCommandLine():
        # Note: this function uses the now depreciated optparse module for backward compatibility

        # Define some string constants
        usage = "usage: %prog [options]"
        ver_str = "JSON Link over Raw UDP/TCP Exchange tester -- v%s (%s)" % (__version__, __date__)

        # Initialize command line parser
        p = optparse.OptionParser(usage, version=ver_str)

        # -- Add shared options
        p.add_option('--doc', action='store_true', default=False,
                     help="Display extended help documentation")
        p.add_option("-t", "--tcp", action="store_const", const=raw_xchg.TCP, dest="sock_type",
                    help="Use TCP sockets")
        p.add_option("-u", "--udp", action="store_const", const=raw_xchg.UDP, dest="sock_type",
                    help="Use UDP sockets (default)")
        p.add_option("-a", "--host_addr", default='localhost',
                    help="Host name/IP address (Default: localhost) for SLAVE (aka SERVER) on non-windows use All for all interfaces")
        p.add_option("-p", "--port", default=50007, type="int",
                    help="Port Number")
        p.add_option("--buf_size", default=4906, type="int",
                    help="Network buffer size in bytes")
        p.add_option("-r", "--run_timestep", type="float",
                    help="Run timestep in Sec. Duration of each sub-run in the internal process loop. MASTER: sets to the transmit interval. SLAVE: time to wait before running using the last received value (default=0.5)")
        p.add_option("-d", "--delay", type="float", default=0.1,
                    help="Minimum delay after rx before tx. Used to simulate a minimum internal synchronous process running time (default=0.1sec)")
        p.add_option("-T", "--timeout", type="float",
                    help="Timeout for low-level data exchange in seconds. (default=step-delay-%f)"%(TIMEOUT_REDUCE))
        p.add_option('-s', '--slave', action="store_true", dest="slave",
                     help="Run as SLAVE (aka server) by listening for data & replying (Default)")
        p.add_option('-m', '--master', action="store_false", dest="slave",
                     help="Run as MASTER (aka client) by initiating data transfer & waiting for reply")
        p.add_option('--raw', action="store_true", default=False,
                     help="Print Raw packet contents including header (Default=False, print cleaner output)")
        p.add_option('-H', '--no_head', action="store_true", default=False,
                     help="Suppress request/response header and send only JSON contents (Default=False, use full header)")
        p.add_option('-c', '--run_count', type="int", default= -1,
                     help="Number of run iterations before quitting (default=-1, Unlimited)")

        p.add_option('--simple', action="store_true", default=False,
                     help="Exchange simple, bare JSON data packets without using GridLAB-D JSON link protocol handshaking (Default=False, use handshaking)")
        p.add_option('-e', '--echo', action="store_true", default=False,
                     help="!SIMPLE ONLY! SLAVE (aka SERVER) only: Echo (do not process) received packets")
        p.add_option('-n', '--num_var', type="int", default=2,
                     help="!SIMPLE ONLY! Number of numeric variable to include in JSON packet")

        # Setup multi-option defaults... otherwise uses add_option default=...
        p.set_defaults(sock_type=raw_xchg.UDP, slave=True)

        # Actually parse command line
        (opt, args) = p.parse_args()

        # Implement our own help function so we can include the Doc string
        if opt.doc:
            print()
            print(ver_str)
            p.print_help()  # Show default optparse generated help info
            print(__doc__)  # Tack on our doc string, the first string in this file
            exit()

        # Update values based on mode
        if opt.slave:
            if opt.run_timestep is None:
                opt.run_timestep = 0.5
            if opt.host_addr == 'All':
                opt.host_addr = ''
        else:  # Master
            if opt.run_timestep is None:
                opt.run_timestep = 1.0
        if opt.timeout is None:
            opt.timeout = opt.run_timestep-opt.delay-TIMEOUT_REDUCE

        # --Display status information
        print("\nJSON Link packet test (v%s) "%(__version__), end='')
        if opt.slave:
            print("SLAVE (aka SERVER) mode (waits for data)")
        else:
            print("MASTER (aka CLIENT) mode (initiates transfer)")

        print("    Run timestep = %.3fsec" % opt.run_timestep)
        print("    (minimum) Reply Delay = %.3fsec" % opt.delay)
        print("    (raw exchange) Timeout = %.3fsec" % opt.timeout)

        if opt.host_addr == '':
            host_txt = "AnyInterface"
        else:
            host_txt = opt.host_addr

        if opt.sock_type == raw_xchg.TCP:
            print("    Using TCP packets on %s:%d" % (host_txt, opt.port))
        else:
            print("    Using UDP packets on %s:%d" % (host_txt, opt.port))

        if opt.slave and opt.echo:
            print("    ECHO mode on (no JSON processing for reply)")

        if opt.raw:
            print("    RAW mode on: displaying full packet with header")

        if opt.no_head:
            print("    NO_HEADer mode: Using raw JSON without reply/response header")

        # Add blank line
        print()

        # Return a parse object, accessible using opt.OPTION
        return (opt, args)

    #-------------------
    # Run main when called stand-alone
    #-------------------
    def main():
        # -- Get Command Line options
        (opt, _args) = _handleCommandLine()

        if opt.simple:
            if opt.slave:
                runSimpleSlave(opt)
            else:
                runSimpleMaster(opt)
        else:
            keyword_args = {'host_addr':opt.host_addr,
                            'port':opt.port,
                            'protocol':opt.sock_type,
                            'opt_verbose':opt.raw,
                            'buf_size':opt.buf_size,
                            'opt_header':(not opt.no_head),
                            'run_max':opt.run_count,
                            'run_timestep':opt.run_timestep,
                            'run_delay':opt.delay,
                            'timeout':opt.timeout
                            }
            if opt.slave:
                state_machine = json_link.SlaveLink(**keyword_args)

            else:
                state_machine = json_link.MasterLink(**keyword_args)

            state_machine.go()

    if __name__ == '__main__':
        main()

### Navigation

- index
- modules |
- JSON Link 0.9.2a documentation »
- Module code »