diff --git a/src/edu/nps/moves/dis7/utilities/DisChannel.java b/src/edu/nps/moves/dis7/utilities/DisChannel.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9b0b99fe87cdd0c7e9d68f46a2557cef16fce45
--- /dev/null
+++ b/src/edu/nps/moves/dis7/utilities/DisChannel.java
@@ -0,0 +1,380 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package edu.nps.moves.dis7.utilities;
+
+import edu.nps.moves.dis7.enumerations.VariableRecordType;
+import edu.nps.moves.dis7.pdus.CommentPdu;
+import edu.nps.moves.dis7.pdus.EntityID;
+import edu.nps.moves.dis7.pdus.Pdu;
+import edu.nps.moves.dis7.utilities.DisThreadedNetworkInterface;
+import edu.nps.moves.dis7.utilities.DisTime;
+import edu.nps.moves.dis7.utilities.PduFactory;
+import edu.nps.moves.dis7.utilities.stream.PduRecorder;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+// import jdk.internal.vm.annotation.IntrinsicCandidate;
+
+/**
+ * DisChannel integrates multiple utility capabilities to handle most  networking and entity-management tasks.
+ * Provides a simplified interface wrapping DisThreadedNetworkInterface, PduRecorder and SimulationManager
+ * for programs connecting to OpenDis7 communications.
+ * TODO future work will confirm that multiple different DisChannel connections can be used simultaneously by a parent program.
+ * @author brutzman
+ */
+public class DisChannel 
+{
+    private String        descriptor = this.getClass().getSimpleName();
+    /**
+     * Output prefix to help with logging by identifying this class.
+     */
+    // might have different DisChannel objects created on different channels, so TRACE_PREFIX is non-static
+    private              String TRACE_PREFIX            = "[" + descriptor + "] "; 
+    private static       String thisHostName            = "localhost";
+    private static final String NETWORK_ADDRESS_DEFAULT = "239.1.2.3";
+    private static final int       NETWORK_PORT_DEFAULT = 3000;
+    private static final String  DEFAULT_PDULOG_OUTPUT_DIRECTORY = "./pduLog";
+    
+    protected boolean verboseComments = true;
+    String networkAddress = NETWORK_ADDRESS_DEFAULT;
+    int       networkPort = NETWORK_PORT_DEFAULT;
+    static DisTime.TimestampStyle timestampStyle = DisTime.TimestampStyle.IEEE_ABSOLUTE;
+    
+    /** Creates DIS Protocol Data Unit (PDU) classes for simulation entities */
+    private static PduFactory                       pduFactory;
+    
+    // class variables
+    private DisThreadedNetworkInterface             disNetworkInterface;
+            DisThreadedNetworkInterface.PduListener pduListener;
+            Pdu                                     receivedPdu;
+    private PduRecorder                             pduRecorder;
+          
+    /* VariableRecordType enumerations have potential use with CommentPdu logs */
+    /* TODO contrast to EntityType */
+    public final VariableRecordType     descriptionCommentType = VariableRecordType.DESCRIPTION;
+    public final VariableRecordType       narrativeCommentType = VariableRecordType.COMPLETE_EVENT_REPORT;
+    public final VariableRecordType          statusCommentType = VariableRecordType.APPLICATION_STATUS;
+    public final VariableRecordType currentTimeStepCommentType = VariableRecordType.APPLICATION_TIMESTEP;
+    
+    /** SimulationManager class handles DIS joining, announcing and leaving tasks.
+     * It is instantiated here as an object */
+    SimulationManager        simulationManager = new SimulationManager();
+
+    /** Base constructor */
+    public DisChannel()
+    {
+        // base constructor is not invoked automatically by other constructors
+        // https://stackoverflow.com/questions/581873/best-way-to-handle-multiple-constructors-in-java
+        
+        initialize();
+    }
+    /** Constructor with new descriptor
+     * @param newDescriptor descriptor for this instance */
+    public DisChannel(String newDescriptor)
+    {
+        descriptor = newDescriptor;
+        initialize();
+    }
+    /** Initialize this class */
+    private void initialize()
+    {
+        DisTime.setTimestampStyle(timestampStyle); // DISTime is a singleton shared class
+        pduFactory          = new PduFactory(timestampStyle);
+        
+        try
+        {
+            thisHostName = InetAddress.getLocalHost().getHostName();
+            printlnTRACE("thisHostName=" + thisHostName);
+        }
+        catch (UnknownHostException uhe)
+        {
+            printlnTRACE(thisHostName + " is not connected to network: " + uhe.getMessage());
+        }
+    }
+    /** add entity using SimulationManager
+     * @param newEntity new entity to add for announcement by SimulationManager */
+    public void addEntity(EntityID newEntity)
+    {
+        // TODO send simulation management PDUs
+        simulationManager.addEntity(newEntity);
+    }
+    
+    /** Join DIS channel using SimulationManager */
+    public void join()
+    {
+        // TODO simulation management PDUs for startup, planning to design special class support 
+//        simulationManager.addEntity();
+        simulationManager.setDescriptor(descriptor);
+        simulationManager.addHost(getThisHostName());
+        simulationManager.setDisThreadedNetworkInterface(disNetworkInterface);
+        
+        simulationManager.simulationJoin();
+        simulationManager.simulationStart();
+        // TODO consider boolean response indicating if join was successful
+    }
+    /** Leave DIS channel using SimulationManager */
+    public void leave()
+    {
+        // TODO send simulation management PDUs
+        simulationManager.simulationStop();
+        simulationManager.simulationLeave();
+        // TODO consider boolean response indicating if leave was successful
+    }
+    
+    /**
+     * get current networkAddress as a string
+     * @return the networkAddress
+     */
+    public String getNetworkAddress() {
+        return networkAddress;
+    }
+
+    /**
+     * set current networkAddress using a string
+     * @param newNetworkAddress the networkAddress to set
+     */
+    public final void setNetworkAddress(String newNetworkAddress) {
+        this.networkAddress = newNetworkAddress;
+    }
+
+    /**
+     * get current networkPort
+     * @return the networkPort
+     */
+    public int getNetworkPort() {
+        return networkPort;
+    }
+
+    /**
+     * set current networkPort
+     * @param newNetworkPort the networkPort to set
+     */
+    public final void setNetworkPort(int newNetworkPort) {
+        this.networkPort = newNetworkPort;
+    }
+
+    /**
+     * Get timestampStyle used by PduFactory
+     * @return current timestampStyle
+     */
+    public DisTime.TimestampStyle getTimestampStyle() {
+        return timestampStyle;
+    }
+
+    /**
+     * Set timestampStyle used by PduFactory
+     * @param newTimestampStyle the timestampStyle to set
+     */
+    public void setTimestampStyle(DisTime.TimestampStyle newTimestampStyle) {
+        timestampStyle = newTimestampStyle;
+        DisTime.setTimestampStyle(newTimestampStyle);
+    }
+
+    /**
+     * Initialize network interface, choosing best available network interface
+     */
+    public void setUpNetworkInterface() 
+    {
+        if (disNetworkInterface != null)
+        {
+            printlnTRACE("*** Warning: setUpNetworkInterface() has already created disNetworkInterface, second invocation ignored");
+            return;
+        }            
+        disNetworkInterface = new DisThreadedNetworkInterface(getNetworkAddress(), getNetworkPort());
+        getDisNetworkInterface().setDescriptor(descriptor);
+        printlnTRACE("Network confirmation:" + " address=" + getDisNetworkInterface().getAddress() + //  disNetworkInterface.getMulticastGroup() +
+                                                  " port=" + getDisNetworkInterface().getPort()); // + disNetworkInterface.getDisPort());
+        pduListener = new DisThreadedNetworkInterface.PduListener() {
+            /** Callback handler for listener */
+            @Override
+            public void incomingPdu(Pdu newPdu) {
+                receivedPdu = newPdu;
+            }
+        };
+        getDisNetworkInterface().addListener(pduListener);
+        String pduLogOutputDirectory = DEFAULT_PDULOG_OUTPUT_DIRECTORY;
+        printlnTRACE("Beginning pdu save to directory " + pduLogOutputDirectory);
+        pduRecorder = new PduRecorder(pduLogOutputDirectory, getNetworkAddress(), getNetworkPort()); // assumes save
+        pduRecorder.setEncodingPduLog(PduRecorder.ENCODING_PLAINTEXT);
+        pduRecorder.setVerbose(true); // either sending, receiving or both
+        pduRecorder.start(); // begin running
+    }
+
+    /** All done, release network resources */
+    public void tearDownNetworkInterface() {
+        getPduRecorder().stop(); // handles disNetworkInterface.close(), tears down threads and sockets
+    }
+
+    /** 
+     * Send a single Protocol Data Unit (PDU) of any type
+     * @param pdu the pdu to send
+     */
+    protected void sendSinglePdu(Pdu pdu)
+    {
+        if (getDisNetworkInterface() == null)
+            setUpNetworkInterface(); // ensure connected
+        try
+        {
+            getDisNetworkInterface().send(pdu);
+            Thread.sleep(100); // TODO consider refactoring the wait logic and moving externally
+        } 
+        catch (InterruptedException ex)
+        {
+            System.err.println(this.getClass().getSimpleName() + " Error sending PDU: " + ex.getLocalizedMessage());
+            System.exit(1);
+        }
+    }
+    /**
+     * Send Comment PDU
+     * @see <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html">Passing Information to a Method or a Constructor</a> Arbitrary Number of Arguments
+     * @param commentType    enumeration value describing purpose of the narrative comment
+     * @param comments       String array of narrative comments
+     */
+    public void sendCommentPdu(VariableRecordType commentType,
+                                     // vararg... variable-length set of String comments can optionally follow
+                                        String... comments)
+    {
+        if ((comments != null) && (comments.length > 0))
+        {
+            ArrayList<String> newCommentsList = new ArrayList<>();
+            for (String comment : comments)
+            {
+                if (!comment.isEmpty())
+                {
+                    newCommentsList.add(comment); // OK found something to send
+                }
+            }
+            if (!newCommentsList.isEmpty())
+            {
+                if (getDisNetworkInterface() == null)
+                    setUpNetworkInterface(); // ensure connected
+            
+                if (commentType == null)
+                    commentType = VariableRecordType.OTHER; // fallback value; TODO consider pushing into pduFactory
+                // now build the commentPdu from these string inputs, thus constructing a narrative entry
+                @SuppressWarnings("CollectionsToArray")
+                CommentPdu commentPdu = getPduFactory().makeCommentPdu(commentType, newCommentsList.toArray(new String[0])); // comments);
+                sendSinglePdu(commentPdu);
+                if (isVerboseComments())
+                {
+                    printlnTRACE("*** [CommentPdu narrative sent: " + commentType.name() + "] " + newCommentsList.toString());
+                    System.out.flush();
+                }
+            }
+        }
+    }
+
+    /**
+     * test for verboseComments mode
+     * @return whether verboseComments mode is enabled
+     */
+    public boolean isVerboseComments() {
+        return verboseComments;
+    }
+
+    /**
+     * set verboseComments mode
+     * @param newVerboseComments whether verboseComments mode is enabled
+     */
+    public void setVerboseComments(boolean newVerboseComments) {
+        this.verboseComments = newVerboseComments;
+    }
+
+    /**
+     * @return the TRACE_PREFIX
+     */
+    public String getTRACE_PREFIX() {
+        return TRACE_PREFIX;
+    }
+
+    /**
+     * @param newTRACE_PREFIX the TRACE_PREFIX to set
+     */
+    public final void setTRACE_PREFIX(String newTRACE_PREFIX) {
+        if  (newTRACE_PREFIX == null)
+             newTRACE_PREFIX = "";
+        if  (newTRACE_PREFIX.isBlank())
+             TRACE_PREFIX = "[" + DisThreadedNetworkInterface.class.getSimpleName()                    + "] ";
+        else if (newTRACE_PREFIX.contains(this.getClass().getSimpleName()))
+             TRACE_PREFIX = "[" + newTRACE_PREFIX + "] ";
+        else TRACE_PREFIX = "[" + this.getClass().getSimpleName() + " " + newTRACE_PREFIX + "] ";
+    }
+
+    /**
+     * Print message with TRACE_PREFIX prepended
+     * @param message String to print
+     */
+    public void printTRACE(String message) {
+        System.out.print(TRACE_PREFIX + message);
+    }
+    /**
+     * Print message with TRACE_PREFIX prepended
+     * @param message String to print
+     */
+    public void printlnTRACE(String message) {
+        System.out.println(TRACE_PREFIX + message);
+    }
+
+    /**
+     * @return the pduFactory, simplifying program imports and configuration
+     */
+    public PduFactory getPduFactory() {
+        if (pduFactory == null)
+            pduFactory = new PduFactory(timestampStyle);
+        return pduFactory;
+    }
+
+    /**
+     * @return the disNetworkInterface
+     */
+    public DisThreadedNetworkInterface getDisNetworkInterface() {
+        return disNetworkInterface;
+    }
+
+    /**
+     * @return the thisHostName
+     */
+    public static String getThisHostName() {
+        return thisHostName;
+    }
+
+    /**
+     * @param aThisHostName the thisHostName to set
+     */
+    public static void setThisHostName(String aThisHostName) {
+        thisHostName = aThisHostName;
+    }
+
+    /**
+     * @return the pduRecorder
+     */
+    public PduRecorder getPduRecorder() {
+        return pduRecorder;
+    }
+
+    /**
+     * Get simple descriptor (such as parent class name) for this network interface, used in trace statements
+     * @return simple descriptor name
+     */
+    public String getDescriptor() {
+        return descriptor;
+    }
+
+    /**
+     * Set new simple descriptor (such as parent class name) for this network interface, used in trace statements
+     * @param newDescriptor simple descriptor name for this interface
+     */
+    public void setDescriptor(String newDescriptor) {
+        // might have different DisChannel objects created on different channels, so descriptor is non-static
+        if (newDescriptor == null)
+            newDescriptor = "";
+        this.descriptor = newDescriptor;
+        setTRACE_PREFIX(descriptor);
+        if (disNetworkInterface != null)
+            disNetworkInterface.setDescriptor(descriptor);
+        if (simulationManager != null)
+            simulationManager.setDescriptor(descriptor);
+    }
+}
diff --git a/src/edu/nps/moves/dis7/utilities/SimulationManager.java b/src/edu/nps/moves/dis7/utilities/SimulationManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4fef60b18fc8dc85790d5ae675f1d8d670c38cc
--- /dev/null
+++ b/src/edu/nps/moves/dis7/utilities/SimulationManager.java
@@ -0,0 +1,703 @@
+/*
+Copyright (c) 1995-2022 held by the author(s).  All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * 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.
+    * Neither the names of the Naval Postgraduate School (NPS)
+      Modeling Virtual Environments and Simulation (MOVES) Institute
+      https://www.nps.edu and https://www.nps.edu/web/moves
+      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 OWNER 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.
+*/
+// TODO move into opendis7 distribution tree
+
+package edu.nps.moves.dis7.utilities;
+
+import edu.nps.moves.dis7.pdus.CreateEntityPdu;
+import edu.nps.moves.dis7.pdus.EntityID;
+import edu.nps.moves.dis7.pdus.RemoveEntityPdu;
+import edu.nps.moves.dis7.utilities.DisThreadedNetworkInterface;
+import edu.nps.moves.dis7.utilities.DisTime;
+import java.util.ArrayList;
+
+/**
+ * Manage overall Simulation Management (SIMAN) choreography for a DIS channel participant.
+ * TODO once operation is working satisfactorily, this class will be moved into the opendis7-java distribution utilities.
+ * @see <a href="https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/-/blob/master/specifications/README.md" target="_blank">Networked Graphics MV3500, Specification Documents, IEEE and SISO</a>
+ * @see <a href="https://ieeexplore.ieee.org/document/6387564" target="_blank">1278.1-2012. IEEE Standard for Distributed Interactive Simulation (DIS) - Application Protocols</a> 5.6.3 The simulation management computer
+ * @see <a href="https://ieeexplore.ieee.org/document/587529" target="_blank">1278.3-1996. IEEE Recommended Practice for Distributed Interactive Simulation - Exercise Management and Feedback</a>
+ * @see IEEE 1278.1 DIS Application Protocols, 4.6.3.a.5 Timestamp, General Requirements, page 43
+ * @see IEEE 1278.1 DIS Application Protocols, 4.6.3.c.1 Timestamp, Relative Timestamps, page 44
+ * @see IEEE 1278.1 DIS Application Protocols, 5.6 Simulation management, page 89
+ * @author brutzman
+ */
+public class SimulationManager 
+{
+    private        DisThreadedNetworkInterface disThreadedNetworkInterface;
+    private static ArrayList<RecordType>      entityRecordList = new ArrayList<>();
+    private static ArrayList<RecordType>        hostRecordList = new ArrayList<>();
+    private static ArrayList<RecordType> applicationRecordList = new ArrayList<>();
+    private        String                           descriptor = new String();
+    private static int                                  hostID = 0;
+    
+    private String TRACE_PREFIX = "[" + (SimulationManager.class.getSimpleName()) + "] ";
+    
+    /**
+     * Object constructor with descriptor
+     * @param newDescriptor simple descriptor name for this class
+     */
+    public SimulationManager (String newDescriptor)
+    {
+        if  (newDescriptor != null)
+             descriptor = newDescriptor.trim();
+        else descriptor = "";
+    }
+    /**
+     * Object constructor
+     */
+    public SimulationManager ()
+    {
+        this("");
+    }
+    
+    /**
+     * Start the simulation according to specifications
+     */
+    public void simulationStart()
+    {
+        // TODO
+    }
+    /**
+     * Pause the simulation according to specifications
+     */
+    public void simulationPause()
+    {
+        // TODO
+    }
+    /**
+     * Resume the simulation according to specifications
+     */
+    public void simulationResume()
+    {
+        // TODO
+    }
+    /**
+     * Stop the simulation according to specifications
+     */
+    public void simulationStop()
+    {
+        // TODO
+    }
+    /**
+     * An entity can Join the simulation according to specifications
+     */
+    public void simulationJoin()
+    {
+        CreateEntityPdu createEntityPdu = new CreateEntityPdu();
+        createEntityPdu.setExerciseID(123); // TODO
+//      createEntityPdu.setPduStatus(); // TODO
+        
+        if (hasDisThreadedNetworkInterface())
+        {
+            for (RecordType entity : entityRecordList)
+            {
+                // TODO set record parameters
+                createEntityPdu.setExerciseID(entity.getId());
+                createEntityPdu.setTimestamp(DisTime.getCurrentDisTimestamp());
+                disThreadedNetworkInterface.send(createEntityPdu);
+            }
+        }
+        else
+        {
+            System.err.println(TRACE_PREFIX + "addEntity() unable to send CreateEntityPdu since no disThreadedNetworkInterface found");
+            //  TODO consider queue for unsent entities
+        }
+    }
+    /**
+     * An entity can Leave the simulation according to specifications
+     */
+    public void simulationLeave()
+    {
+        // TODO
+    }
+    
+    /**
+     * Simple simulation record type
+     */
+    public class RecordType
+    {
+        private int    id          = -1;
+        private String name        = new String();
+        private String alias       = new String();
+        private String description = new String();
+        private String reference   = new String();
+        private boolean isHostType  = false;
+    
+        /**
+         * Constructor for new record
+         * @param id   identifying number
+         * @param name common name
+         * @param description longer description
+         * @param reference   formal reference for this record, if any
+         */
+        public RecordType (int id, String name, String description, String reference)
+        {
+            this.id          = id;
+            this.name        = name;
+            this.description = description;
+            this.reference   = reference;
+            // TODO create alias: if IP address then check for hostname, and vice versa
+        }
+        /**
+         * Utility constructor for new record, description and reference remain blank
+         * @param id   identifying number
+         * @param name common name
+         */
+        public RecordType (int id, String name)
+        {
+            this.id          = id;
+            this.name        = name;
+            this.description = "";
+            this.reference   = "";
+            // TODO create alias: if IP address then check for hostname, and vice versa
+        }
+        /**
+         * Utility constructor for new record, description and reference remain blank
+         * @param id   identifying number
+         * @param name common name
+         * @param isHostType whether or not this record is for a host
+         */
+        public RecordType (int id, String name, boolean isHostType)
+        {
+            this.id          = id;
+            this.name        = name;
+            this.description = "";
+            this.reference   = "";
+            this.isHostType  = isHostType;
+            // TODO create alias: if IP address then check for hostname, and vice versa
+        }
+        
+        /**
+         * Simple representation of record
+         * @return id,name,"description"
+         */
+        @Override
+        public String toString()
+        {
+            return "id" + "," + name + ",\"" + description + "\"";
+        }
+
+        /**
+         * get record id
+         * @return the id
+         */
+        public int getId() {
+            return id;
+        }
+
+        /**
+         * set record id
+         * @param newID the id to set
+         * @return same object to permit progressive setters
+         */
+        public RecordType setId(int newID) {
+            this.id = newID;
+            return this;
+        }
+
+        /**
+         * get record name
+         * @return the name
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * set record name
+         * @param newName the name to set
+         * @return same object to permit progressive setters
+         */
+        public RecordType setName(String newName) {
+            this.name = newName;
+            return this;
+        }
+
+        /**
+         * get record description
+         * @return the description
+         */
+        public String getDescription() {
+            return description;
+        }
+
+        /**
+         * set record description
+         * @param newDescription the description to set
+         * @return same object to permit progressive setters
+         */
+        public RecordType setDescription(String newDescription) {
+            this.description = newDescription;
+            return this;
+        }
+
+        /**
+         * get record reference
+         * @return the reference
+         */
+        public String getReference() {
+            return reference;
+        }
+
+        /**
+         * set record reference
+         * @param newReference the reference to set
+         * @return same object to permit progressive setters
+         */
+        public RecordType setReference(String newReference) {
+            this.reference = newReference;
+            return this;
+        }
+
+        /**
+         * get record alias name
+         * @return the alias
+         */
+        public String getAlias() {
+            return alias;
+        }
+
+        /**
+         * set record alias name
+         * @param alias the alias to set
+         * @return same object to permit progressive setters
+         */
+        public RecordType setAlias(String alias) {
+            this.alias = alias;
+            return this;
+        }
+
+        /**
+         * Does record represent a network address
+         * @return whether record is a network address
+         */
+        public boolean isNetworkAddress() {
+            return isHostType;
+        }
+
+        /**
+         * Set whether record represents a network address
+         * @param isAddress the isAddress to set
+         */
+        public void setNetworkAddress(boolean isAddress) {
+            this.isHostType = isAddress;
+        }
+    }
+
+    /**
+     * Get a single entityRecord from list
+     * @param index which record to retrieve
+     * @return the record matching this index
+     */
+    public RecordType getEntityRecordByIndex(int index) 
+    {
+        if (entityRecordList.isEmpty())
+        {
+            System.err.println ("*** getEntityRecordByIndex list is empty, unable to get index=" + index);
+            return null;
+        }
+        else if (entityRecordList.size() <= index)
+        {
+            System.err.println ("*** getEntityRecordByIndex list has size=" + entityRecordList.size() + ", unable to get index=" + index);
+            return null;
+        }
+        else if (index < 0)
+        {
+            System.err.println ("*** getEntityRecordByIndex cannot retrieve illegal index=" + index);
+            return null;
+        }
+        else return entityRecordList.get(index);
+    }
+
+    /**
+     * Get a single hostRecord from list
+     * @param index which record to retrieve
+     * @return the record matching this index
+     */
+    public RecordType getHostRecordByIndex(int index) 
+    {
+        if (hostRecordList.isEmpty())
+        {
+            System.err.println ("*** getHostRecordByIndex list is empty, unable to get index=" + index);
+            return null;
+        }
+        else if (hostRecordList.size() <= index)
+        {
+            System.err.println ("*** getHostRecordByIndex list has size=" + hostRecordList.size() + ", unable to get index=" + index);
+            return null;
+        }
+        else if (index < 0)
+        {
+            System.err.println ("*** getHostRecordByIndex cannot retrieve illegal index=" + index);
+            return null;
+        }
+        else return hostRecordList.get(index);
+    }
+
+    /**
+     * Get a single applicationRecord from list
+     * @param index which record to retrieve
+     * @return the record matching this index
+     */
+    public RecordType getApplicationRecordByIndex(int index) 
+    {
+        if (applicationRecordList.isEmpty())
+        {
+            System.err.println ("*** getApplicationRecordByIndex list is empty, unable to get index=" + index);
+            return null;
+        }
+        else if (applicationRecordList.size() <= index)
+        {
+            System.err.println ("*** getApplicationRecordByIndex list has size=" + applicationRecordList.size() + ", unable to get index=" + index);
+            return null;
+        }
+        else if (index < 0)
+        {
+            System.err.println ("*** getApplicationRecordByIndex cannot retrieve illegal index=" + index);
+            return null;
+        }
+        else return applicationRecordList.get(index);
+    }
+
+    /**
+     * Get a single entityRecord from list matching ID
+     * @param valueOfInterest id for record to retrieve
+     * @return the record matching this ID
+     */
+    public RecordType getEntityRecordByID(int valueOfInterest) 
+    {
+        for (RecordType entity : entityRecordList)
+        {
+            if (entity.getId() == valueOfInterest)
+                return entity;
+        }
+        System.err.println ("*** getEntityRecordByID cannot find id=" + valueOfInterest);
+        return null;
+    }
+    /**
+     * Get a single hostRecord from list matching ID
+     * @param valueOfInterest id for record to retrieve
+     * @return the record matching this ID
+     */
+    public RecordType getHostRecordByID(int valueOfInterest) 
+    {
+        for (RecordType host : hostRecordList)
+        {
+            if (host.getId() == valueOfInterest)
+                return host;
+        }
+        System.err.println ("*** getHostRecordByID cannot find id=" + valueOfInterest);
+        return null;
+    }
+    /**
+     * Get a single applicationRecord from list matching ID
+     * @param valueOfInterest id for record to retrieve
+     * @return the record matching this ID
+     */
+    public RecordType getApplicationRecordByID(int valueOfInterest) 
+    {
+        for (RecordType application : applicationRecordList)
+        {
+            if (application.getId() == valueOfInterest)
+                return application;
+        }
+        System.err.println ("*** getApplicationRecordByID cannot find id=" + valueOfInterest);
+        return null;
+    }
+
+    /**
+     * Provide entire entityRecordList
+     * @return the entityRecordList
+     */
+    public ArrayList<RecordType> getEntityRecordList() {
+        return entityRecordList;
+    }
+
+    /**
+     * Provide entire hostRecordList
+     * @return the hostRecordList
+     */
+    public ArrayList<RecordType> getHostRecordList() {
+        return hostRecordList;
+    }
+
+    /**
+     * Provide entire applicationRecordList
+     * @return the applicationRecordList
+     */
+    public ArrayList<RecordType> getApplicationRecordList() {
+        return applicationRecordList;
+    }
+
+    /**
+     * Provide access to current disThreadedNetworkInterface
+     * @return the disThreadedNetworkInterface
+     */
+    protected DisThreadedNetworkInterface getDisThreadedNetworkInterface() {
+        return disThreadedNetworkInterface;
+    }
+    /**
+     * Set the disThreadedNetworkInterface singleton to match other classes
+     * @param disThreadedNetworkInterface the disThreadedNetworkInterface to set
+     * @return same object to permit progressive setters
+     */
+    public SimulationManager setDisThreadedNetworkInterface(DisThreadedNetworkInterface disThreadedNetworkInterface) {
+        this.disThreadedNetworkInterface = disThreadedNetworkInterface;
+        return this;
+    }
+    /**
+     * Check for disThreadedNetworkInterface
+     * @return whether singleton disThreadedNetworkInterface has been instantiated
+     */
+    protected boolean hasDisThreadedNetworkInterface() 
+    {
+        return (this.disThreadedNetworkInterface != null);
+    }
+    /**
+     * Create disThreadedNetworkInterface
+     */
+    protected void createDisThreadedNetworkInterface() 
+    {
+        this.disThreadedNetworkInterface = new DisThreadedNetworkInterface(descriptor);
+    }
+    /**
+     * Constructor for disThreadedNetworkInterface with descriptor, 
+     * using default multicast address and port
+     * @param newDescriptor simple descriptor name for this interface
+     */
+    protected void createDisThreadedNetworkInterface(String newDescriptor) 
+    {
+        this.disThreadedNetworkInterface = new DisThreadedNetworkInterface(newDescriptor);
+    }
+    /**
+     * Constructor for disThreadedNetworkInterface using specified multicast address and port 
+     * @param address the multicast group or unicast address to utilize
+     * @param port the multicast port to utilize
+     */
+    protected void createDisThreadedNetworkInterface(String address, int port) 
+    {
+        this.disThreadedNetworkInterface = new DisThreadedNetworkInterface(address, port, descriptor);
+    }
+    /**
+     * Constructor for disThreadedNetworkInterface using specified multicast address and port, plus descriptor.
+     * @param address the multicast group or unicast address to utilize
+     * @param port the multicast port to utilize
+     * @param newDescriptor simple descriptor name for this interface
+     */
+    protected void createDisThreadedNetworkInterface(String address, int port, String newDescriptor) 
+    {
+        this.disThreadedNetworkInterface = new DisThreadedNetworkInterface(address, port, newDescriptor);
+    }
+    /**
+     * Get simple descriptor (such as parent class name) for this SimulationManager, used in trace statements
+     * @return simple descriptor name
+     */
+    public String getDescriptor() 
+    {
+        return descriptor;
+    }
+    /**
+     * Set new simple descriptor (such as parent class name) for this SimulationManager, used in trace statements
+     * @param newDescriptor simple descriptor name for this interface
+     * @return same object to permit progressive setters */
+    public SimulationManager setDescriptor(String newDescriptor) 
+    {
+        if (newDescriptor != null)
+            this.descriptor = newDescriptor.trim();
+        TRACE_PREFIX = "[" + DisThreadedNetworkInterface.class.getSimpleName() + " " + descriptor + "] ";
+        return this;
+    }
+    /**
+     * Reset descriptor 
+     * @return same object to permit progressive setters */
+    public SimulationManager clearDescriptor()
+    {
+        setDescriptor("");
+        return this;
+    }
+    /**
+     * clear all lists
+     * @return same object to permit progressive setters */
+    public SimulationManager clearAll() 
+    {
+        entityRecordList.clear();
+        hostRecordList.clear();
+        applicationRecordList.clear();
+        clearDescriptor();
+        return this;
+    }
+    /**
+     * Add entity to simulation list, if this is first occurrence
+     * @param newEntityID new entity to add
+     * @return same object to permit progressive setters */
+    public SimulationManager addEntity(EntityID newEntityID)
+    {
+        RecordType newEntity = new RecordType(newEntityID.getEntityID(), // short
+                                              "TODOname",
+                                              "TODO description",
+                                              "TODO reference");
+        entityRecordList.add(newEntity);
+        return this;
+    }
+    /**
+     * Add entity to simulation list and announce using CreateEntityPdu
+     * @param newEntity new entity to add
+     * @return same object to permit progressive setters */
+    public SimulationManager addEntity(RecordType newEntity)
+    {
+        if (!entityRecordList.contains(newEntity))
+        {
+            // TODO check record type
+            entityRecordList.add(newEntity);
+            if (hasDisThreadedNetworkInterface())
+            {
+                CreateEntityPdu createEntityPdu = new CreateEntityPdu();
+                // TODO set record parameters
+                getDisThreadedNetworkInterface().send(createEntityPdu);
+            }
+            else
+            {
+                System.err.println(TRACE_PREFIX + "addEntity() unable to send CreateEntityPdu since no disThreadedNetworkInterface found");
+                //  TODO consider queue for unsent entities
+            }
+        }
+        return this;
+    }
+    /**
+     * Remove entity from simulation list, if found
+     * @param oldEntity old entity to remove
+     * @return same object to permit progressive setters */
+    public SimulationManager removeEntity(RecordType oldEntity)
+    {
+        if (!entityRecordList.contains(oldEntity))
+        {
+            // TODO check record type
+            entityRecordList.remove(oldEntity);
+            if (hasDisThreadedNetworkInterface())
+            {
+                RemoveEntityPdu removeEntityPdu = new RemoveEntityPdu();
+                // TODO set record parameters
+                getDisThreadedNetworkInterface().send(removeEntityPdu);
+            }
+            else
+            {
+                System.err.println(TRACE_PREFIX + "removeEntity() unable to send RemoveEntityPdu since no disThreadedNetworkInterface found");
+                //  TODO consider queue for unsent entities
+            }
+        }
+        return this;
+    }
+    /**
+     * Add host to simulation list, if this is first occurrence
+     * @param newHost new host to add
+     * @return same object to permit progressive setters */
+    public SimulationManager addHost(String newHost)
+    {
+        boolean  nameFound = false;
+        boolean aliasFound = false;
+        for (RecordType nextRecord : hostRecordList)
+        {
+            if ( nextRecord.name.equalsIgnoreCase(newHost.trim()))
+                 nameFound = true;
+            if (nextRecord.alias.equalsIgnoreCase(newHost.trim()))
+                aliasFound = true;
+            if ((nameFound || aliasFound) && !nextRecord.isHostType)
+                 nextRecord.isHostType = true; // make sure
+        }
+        if (!nameFound && !aliasFound)
+        {
+            RecordType newRecord = new RecordType(hostID, newHost, true);
+            // TODO set alias to IP number
+            hostRecordList.add(newRecord);
+            hostID++;
+            // no PDU sent
+        }
+        return this;
+    }
+    /**
+     * Remove host from simulation list, if found
+     * @param oldHost old host to remove
+     * @return same object to permit progressive setters */
+    public SimulationManager removeHost(String oldHost)
+    {
+        boolean  nameFound = false;
+        boolean aliasFound = false;
+        for (RecordType nextRecord : hostRecordList)
+        {
+            if ( nextRecord.name.equalsIgnoreCase(oldHost.trim()))
+                 nameFound = true;
+            if (nextRecord.alias.equalsIgnoreCase(oldHost.trim()))
+                aliasFound = true;
+            if ((nameFound || aliasFound) && !nextRecord.isHostType)
+                 nextRecord.isHostType = true; // make sure
+            
+            if (nameFound || aliasFound)
+            {
+                hostRecordList.remove(nextRecord);
+                // no PDU sent
+                break;
+            }
+        }
+        return this;
+    }
+    
+    /** Self test to check basic operation, invoked by main() */
+    public void selfTest()
+    {
+        createDisThreadedNetworkInterface();
+        
+        // TODO
+        
+        disThreadedNetworkInterface.close(); // tears down threads and sockets
+    }
+    
+    /**
+     * Main method for testing.
+     * @see <a href="https://docs.oracle.com/javase/tutorial/getStarted/application/index.html">Java Tutorials: A Closer Look at the "Hello World!" Application</a>
+     * @param args [address, port, descriptor] command-line arguments are an array of optional String parameters that are passed from execution environment during invocation
+     */
+    public static void main(String[] args)
+    {
+        System.out.println("*** SimulationManager main() self test started...");
+          
+        SimulationManager simulationManager = new SimulationManager("main() self test");
+        
+        simulationManager.setDescriptor("main() self test");
+        
+        simulationManager.selfTest();
+        
+        System.out.println("*** SimulationManager main() self test complete.");
+    }
+}
diff --git a/src/edu/nps/moves/dis7/utilities/SimulationManagerLog.txt b/src/edu/nps/moves/dis7/utilities/SimulationManagerLog.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b75df43da0992c0188cbd98d508028708bfc63d2
--- /dev/null
+++ b/src/edu/nps/moves/dis7/utilities/SimulationManagerLog.txt
@@ -0,0 +1,21 @@
+ant -f C:\\x-nps-gitlab\\NetworkedGraphicsMV3500\\examples -Dnb.internal.action.name=run.single -Djavac.includes=OpenDis7Examples/SimulationManager.java -Drun.class=OpenDis7Examples.SimulationManager run-single
+init:
+Deleting: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\build\built-jar.properties
+deps-jar:
+Updating property file: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\build\built-jar.properties
+Compiling 1 source file to C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\build\classes
+compile-single:
+run-single:
+*** SimulationManager main() self test started...
+[DisThreadedNetworkInterface] using network interface PANGP Virtual Ethernet Adapter
+[DisThreadedNetworkInterface main() self test] datagramSocket.joinGroup  address=239.1.2.3 port=3000 isConnected()=false createDatagramSocket() complete.
+[DisThreadedNetworkInterface main() self test] createThreads() receiveThread.isAlive()=true
+[DisThreadedNetworkInterface main() self test] createThreads() sendingThread.isAlive()=true
+*** setKillSentinelAndInterrupts() killed=true sendingThread.isInterrupted()=true receiveThread.isInterrupted()=true
+[DisThreadedNetworkInterface main() self test] close(): pdus2send.size()=0 baos.size()=0 dos.size()=0
+[DisThreadedNetworkInterface main() self test] datagramSocket.leaveGroup address=239.1.2.3 port=3000 isClosed()=true close() complete.
+*** killThread() status: sendingThread.isAlive()=false sendingThread.isInterrupted()=true
+*** killThread() status: receiveThread.isAlive()=false receiveThread.isInterrupted()=true
+*** Thread close status: sendingThread.isAlive()=false receiveThread.isAlive()=false
+*** SimulationManager main() self test complete.
+BUILD SUCCESSFUL (total time: 2 seconds)