#!/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
#-------------------
[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()