Source code for json_link.test.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 ..json_link import json_link
from ..json_link import raw_xchg

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

#-------------------
# runSimpleMaster
#-------------------
[docs]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 #-------------------
[docs]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 #-------------------
[docs]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 #-------------------
[docs]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 #-------------------
[docs]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 #-------------------
[docs]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()