001/*----------------------------------------------------------------------------*/
002/* Copyright (c) FIRST 2017-2018. All Rights Reserved.                        */
003/* Open Source Software - may be modified and shared by FRC teams. The code   */
004/* must be accompanied by the FIRST BSD license file in the root directory of */
005/* the project.                                                               */
006/*----------------------------------------------------------------------------*/
007
008package edu.wpi.first.networktables;
009
010import java.util.HashMap;
011import java.util.Map;
012import java.util.concurrent.ConcurrentHashMap;
013import java.util.concurrent.ConcurrentMap;
014import java.util.concurrent.TimeUnit;
015import java.util.concurrent.locks.Condition;
016import java.util.concurrent.locks.ReentrantLock;
017import java.util.function.Consumer;
018
019/**
020 * NetworkTables Instance.
021 *
022 * Instances are completely independent from each other.  Table operations on
023 * one instance will not be visible to other instances unless the instances are
024 * connected via the network.  The main limitation on instances is that you
025 * cannot have two servers on the same network port.  The main utility of
026 * instances is for unit testing, but they can also enable one program to
027 * connect to two different NetworkTables networks.
028 *
029 * The global "default" instance (as returned by {@link #getDefault()}) is
030 * always available, and is intended for the common case when there is only
031 * a single NetworkTables instance being used in the program.
032 *
033 * Additional instances can be created with the {@link #create()} function.
034 * A reference must be kept to the NetworkTableInstance returned by this
035 * function to keep it from being garbage collected.
036 */
037public final class NetworkTableInstance {
038  /**
039   * Client/server mode flag values (as returned by {@link #getNetworkMode()}).
040   * This is a bitmask.
041   */
042  public static final int kNetModeNone = 0x00;
043  public static final int kNetModeServer = 0x01;
044  public static final int kNetModeClient = 0x02;
045  public static final int kNetModeStarting = 0x04;
046  public static final int kNetModeFailure = 0x08;
047
048  /**
049   * The default port that network tables operates on.
050   */
051  public static final int kDefaultPort = 1735;
052
053  /**
054   * Construct from native handle.
055   * @param handle Native handle
056   */
057  private NetworkTableInstance(int handle) {
058    m_owned = false;
059    m_handle = handle;
060  }
061
062  /**
063   * Destroys the instance (if created by {@link #create()}).
064   */
065  public synchronized void free() {
066    if (m_owned && m_handle != 0) {
067      NetworkTablesJNI.destroyInstance(m_handle);
068    }
069  }
070
071  /**
072   * Determines if the native handle is valid.
073   * @return True if the native handle is valid, false otherwise.
074   */
075  public boolean isValid() {
076    return m_handle != 0;
077  }
078
079  /* The default instance. */
080  private static NetworkTableInstance s_defaultInstance;
081
082  /**
083   * Get global default instance.
084   * @return Global default instance
085   */
086  public synchronized static NetworkTableInstance getDefault() {
087    if (s_defaultInstance == null) {
088      s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
089    }
090    return s_defaultInstance;
091  }
092
093  /**
094   * Create an instance.
095   * Note: A reference to the returned instance must be retained to ensure the
096   * instance is not garbage collected.
097   * @return Newly created instance
098   */
099  public static NetworkTableInstance create() {
100    NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
101    inst.m_owned = true;
102    return inst;
103  }
104
105  /**
106   * Gets the native handle for the entry.
107   * @return Native handle
108   */
109  public int getHandle() {
110    return m_handle;
111  }
112
113  /**
114   * Gets the entry for a key.
115   * @param name Key
116   * @return Network table entry.
117   */
118  public NetworkTableEntry getEntry(String name) {
119    return new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
120  }
121
122  /**
123   * Get entries starting with the given prefix.
124   * The results are optionally filtered by string prefix and entry type to
125   * only return a subset of all entries.
126   *
127   * @param prefix entry name required prefix; only entries whose name
128   * starts with this string are returned
129   * @param types bitmask of types; 0 is treated as a "don't care"
130   * @return Array of entries.
131   */
132  public NetworkTableEntry[] getEntries(String prefix, int types) {
133    int[] handles = NetworkTablesJNI.getEntries(m_handle, prefix, types);
134    NetworkTableEntry[] entries = new NetworkTableEntry[handles.length];
135    for (int i = 0; i < handles.length; i++) {
136      entries[i] = new NetworkTableEntry(this, handles[i]);
137    }
138    return entries;
139  }
140
141  /**
142   * Get information about entries starting with the given prefix.
143   * The results are optionally filtered by string prefix and entry type to
144   * only return a subset of all entries.
145   *
146   * @param prefix entry name required prefix; only entries whose name
147   * starts with this string are returned
148   * @param types bitmask of types; 0 is treated as a "don't care"
149   * @return Array of entry information.
150   */
151  public EntryInfo[] getEntryInfo(String prefix, int types) {
152    return NetworkTablesJNI.getEntryInfo(this, m_handle, prefix, types);
153  }
154
155  /* Cache of created tables. */
156  private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();
157
158  /**
159   * Gets the table with the specified key.
160   *
161   * @param key the key name
162   * @return The network table
163   */
164  public NetworkTable getTable(String key) {
165    // prepend leading / if not present
166    String theKey;
167    if (key.isEmpty() || key.equals("/")) {
168      theKey = "";
169    } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
170      theKey = key;
171    } else {
172      theKey = NetworkTable.PATH_SEPARATOR + key;
173    }
174
175    // cache created tables
176    NetworkTable table = m_tables.get(theKey);
177    if (table == null) {
178      table = new NetworkTable(this, theKey);
179      NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
180      if (oldTable != null) {
181        table = oldTable;
182      }
183    }
184    return table;
185  }
186
187  /**
188   * Deletes ALL keys in ALL subtables (except persistent values).
189   * Use with caution!
190   */
191  public void deleteAllEntries() {
192    NetworkTablesJNI.deleteAllEntries(m_handle);
193  }
194
195  /*
196   * Callback Creation Functions
197   */
198
199  private static class EntryConsumer<T> {
200    final NetworkTableEntry entry;
201    final Consumer<T> consumer;
202
203    EntryConsumer(NetworkTableEntry entry, Consumer<T> consumer) {
204      this.entry = entry;
205      this.consumer = consumer;
206    }
207  }
208
209  private final ReentrantLock m_entryListenerLock = new ReentrantLock();
210  private final Map<Integer, EntryConsumer<EntryNotification>> m_entryListeners = new HashMap<>();
211  private Thread m_entryListenerThread;
212  private int m_entryListenerPoller;
213  private boolean m_entryListenerWaitQueue;
214  private final Condition m_entryListenerWaitQueueCond = m_entryListenerLock.newCondition();
215
216  private void startEntryListenerThread() {
217    m_entryListenerThread = new Thread(() -> {
218      boolean wasInterrupted = false;
219      while (!Thread.interrupted()) {
220        EntryNotification[] events;
221        try {
222          events = NetworkTablesJNI.pollEntryListener(this, m_entryListenerPoller);
223        } catch (InterruptedException ex) {
224          m_entryListenerLock.lock();
225          try {
226            if (m_entryListenerWaitQueue) {
227              m_entryListenerWaitQueue = false;
228              m_entryListenerWaitQueueCond.signalAll();
229              continue;
230            }
231          } finally {
232            m_entryListenerLock.unlock();
233          }
234          Thread.currentThread().interrupt();
235          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
236          break;
237        }
238        for (EntryNotification event : events) {
239          EntryConsumer<EntryNotification> listener;
240          m_entryListenerLock.lock();
241          try {
242            listener = m_entryListeners.get(event.listener);
243          } finally {
244            m_entryListenerLock.unlock();
245          }
246          if (listener != null) {
247            event.entryObject = listener.entry;
248            try {
249              listener.consumer.accept(event);
250            } catch (Throwable throwable) {
251              System.err.println("Unhandled exception during entry listener callback: " + throwable.toString());
252              throwable.printStackTrace();
253            }
254          }
255        }
256      }
257      m_entryListenerLock.lock();
258      try {
259        if (!wasInterrupted) {
260          NetworkTablesJNI.destroyEntryListenerPoller(m_entryListenerPoller);
261        }
262        m_entryListenerPoller = 0;
263      } finally {
264        m_entryListenerLock.unlock();
265      }
266    }, "NTEntryListener");
267    m_entryListenerThread.setDaemon(true);
268    m_entryListenerThread.start();
269  }
270
271  /**
272   * Add a listener for all entries starting with a certain prefix.
273   *
274   * @param prefix            UTF-8 string prefix
275   * @param listener          listener to add
276   * @param flags             {@link EntryListenerFlags} bitmask
277   * @return Listener handle
278   */
279  public int addEntryListener(String prefix, Consumer<EntryNotification> listener, int flags) {
280    m_entryListenerLock.lock();
281    try {
282      if (m_entryListenerPoller == 0) {
283        m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
284        startEntryListenerThread();
285      }
286      int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, prefix, flags);
287      m_entryListeners.put(handle, new EntryConsumer<>(null, listener));
288      return handle;
289    } finally {
290      m_entryListenerLock.unlock();
291    }
292  }
293
294  /**
295   * Add a listener for a particular entry.
296   *
297   * @param entry             the entry
298   * @param listener          listener to add
299   * @param flags             {@link EntryListenerFlags} bitmask
300   * @return Listener handle
301   */
302  public int addEntryListener(NetworkTableEntry entry, Consumer<EntryNotification> listener, int flags) {
303    if (!equals(entry.getInstance())) {
304      throw new IllegalArgumentException("entry does not belong to this instance");
305    }
306    m_entryListenerLock.lock();
307    try {
308      if (m_entryListenerPoller == 0) {
309        m_entryListenerPoller = NetworkTablesJNI.createEntryListenerPoller(m_handle);
310        startEntryListenerThread();
311      }
312      int handle = NetworkTablesJNI.addPolledEntryListener(m_entryListenerPoller, entry.getHandle(), flags);
313      m_entryListeners.put(handle, new EntryConsumer<>(entry, listener));
314      return handle;
315    } finally {
316      m_entryListenerLock.unlock();
317    }
318  }
319
320  /**
321   * Remove an entry listener.
322   * @param listener Listener handle to remove
323   */
324  public void removeEntryListener(int listener) {
325    NetworkTablesJNI.removeEntryListener(listener);
326  }
327
328  /**
329   * Wait for the entry listener queue to be empty.  This is primarily useful
330   * for deterministic testing.  This blocks until either the entry listener
331   * queue is empty (e.g. there are no more events that need to be passed along
332   * to callbacks or poll queues) or the timeout expires.
333   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
334   *                  or a negative value to block indefinitely
335   * @return False if timed out, otherwise true.
336  */
337  public boolean waitForEntryListenerQueue(double timeout) {
338    if (!NetworkTablesJNI.waitForEntryListenerQueue(m_handle, timeout)) {
339      return false;
340    }
341    m_entryListenerLock.lock();
342    try {
343      if (m_entryListenerPoller != 0) {
344        m_entryListenerWaitQueue = true;
345        NetworkTablesJNI.cancelPollEntryListener(m_entryListenerPoller);
346        while (m_entryListenerWaitQueue) {
347          try {
348            if (timeout < 0) {
349              m_entryListenerWaitQueueCond.await();
350            } else {
351              return m_entryListenerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
352            }
353          } catch (InterruptedException ex) {
354            Thread.currentThread().interrupt();
355            return true;
356          }
357        }
358      }
359    } finally {
360      m_entryListenerLock.unlock();
361    }
362    return true;
363  }
364
365  private final ReentrantLock m_connectionListenerLock = new ReentrantLock();
366  private final Map<Integer, Consumer<ConnectionNotification>> m_connectionListeners = new HashMap<>();
367  private Thread m_connectionListenerThread;
368  private int m_connectionListenerPoller;
369  private boolean m_connectionListenerWaitQueue;
370  private final Condition m_connectionListenerWaitQueueCond = m_connectionListenerLock.newCondition();
371
372  private void startConnectionListenerThread() {
373    m_connectionListenerThread = new Thread(() -> {
374      boolean wasInterrupted = false;
375      while (!Thread.interrupted()) {
376        ConnectionNotification[] events;
377        try {
378          events = NetworkTablesJNI.pollConnectionListener(this, m_connectionListenerPoller);
379        } catch (InterruptedException ex) {
380          m_connectionListenerLock.lock();
381          try {
382            if (m_connectionListenerWaitQueue) {
383              m_connectionListenerWaitQueue = false;
384              m_connectionListenerWaitQueueCond.signalAll();
385              continue;
386            }
387          } finally {
388            m_connectionListenerLock.unlock();
389          }
390          Thread.currentThread().interrupt();
391          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
392          break;
393        }
394        for (ConnectionNotification event : events) {
395          Consumer<ConnectionNotification> listener;
396          m_connectionListenerLock.lock();
397          try {
398            listener = m_connectionListeners.get(event.listener);
399          } finally {
400            m_connectionListenerLock.unlock();
401          }
402          if (listener != null) {
403            try {
404              listener.accept(event);
405            } catch (Throwable throwable) {
406              System.err.println("Unhandled exception during connection listener callback: " + throwable.toString());
407              throwable.printStackTrace();
408            }
409          }
410        }
411      }
412      m_connectionListenerLock.lock();
413      try {
414        if (!wasInterrupted) {
415          NetworkTablesJNI.destroyConnectionListenerPoller(m_connectionListenerPoller);
416        }
417        m_connectionListenerPoller = 0;
418      } finally {
419        m_connectionListenerLock.unlock();
420      }
421    }, "NTConnectionListener");
422    m_connectionListenerThread.setDaemon(true);
423    m_connectionListenerThread.start();
424  }
425
426  /**
427   * Add a connection listener.
428   *
429   * @param listener Listener to add
430   * @param immediateNotify Notify listener of all existing connections
431   * @return Listener handle
432   */
433  public int addConnectionListener(Consumer<ConnectionNotification> listener, boolean immediateNotify) {
434    m_connectionListenerLock.lock();
435    try {
436      if (m_connectionListenerPoller == 0) {
437        m_connectionListenerPoller = NetworkTablesJNI.createConnectionListenerPoller(m_handle);
438        startConnectionListenerThread();
439      }
440      int handle = NetworkTablesJNI.addPolledConnectionListener(m_connectionListenerPoller, immediateNotify);
441      m_connectionListeners.put(handle, listener);
442      return handle;
443    } finally {
444      m_connectionListenerLock.unlock();
445    }
446  }
447
448  /**
449   * Remove a connection listener.
450   * @param listener Listener handle to remove
451   */
452  public void removeConnectionListener(int listener) {
453    m_connectionListenerLock.lock();
454    try {
455      m_connectionListeners.remove(listener);
456    } finally {
457      m_connectionListenerLock.unlock();
458    }
459    NetworkTablesJNI.removeConnectionListener(listener);
460  }
461
462  /**
463   * Wait for the connection listener queue to be empty.  This is primarily useful
464   * for deterministic testing.  This blocks until either the connection listener
465   * queue is empty (e.g. there are no more events that need to be passed along
466   * to callbacks or poll queues) or the timeout expires.
467   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
468   *                  or a negative value to block indefinitely
469   * @return False if timed out, otherwise true.
470   */
471  public boolean waitForConnectionListenerQueue(double timeout) {
472    if (!NetworkTablesJNI.waitForConnectionListenerQueue(m_handle, timeout)) {
473      return false;
474    }
475    m_connectionListenerLock.lock();
476    try {
477      if (m_connectionListenerPoller != 0) {
478        m_connectionListenerWaitQueue = true;
479        NetworkTablesJNI.cancelPollConnectionListener(m_connectionListenerPoller);
480        while (m_connectionListenerWaitQueue) {
481          try {
482            if (timeout < 0) {
483              m_connectionListenerWaitQueueCond.await();
484            } else {
485              return m_connectionListenerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
486            }
487          } catch (InterruptedException ex) {
488            Thread.currentThread().interrupt();
489            return true;
490          }
491        }
492      }
493    } finally {
494      m_connectionListenerLock.unlock();
495    }
496    return true;
497  }
498
499  /*
500   * Remote Procedure Call Functions
501   */
502
503  private final ReentrantLock m_rpcCallLock = new ReentrantLock();
504  private final Map<Integer, EntryConsumer<RpcAnswer>> m_rpcCalls = new HashMap<>();
505  private Thread m_rpcCallThread;
506  private int m_rpcCallPoller;
507  private boolean m_rpcCallWaitQueue;
508  private final Condition m_rpcCallWaitQueueCond = m_rpcCallLock.newCondition();
509
510  private void startRpcCallThread() {
511    m_rpcCallThread = new Thread(() -> {
512      boolean wasInterrupted = false;
513      while (!Thread.interrupted()) {
514        RpcAnswer[] events;
515        try {
516          events = NetworkTablesJNI.pollRpc(this, m_rpcCallPoller);
517        } catch (InterruptedException ex) {
518          m_rpcCallLock.lock();
519          try {
520            if (m_rpcCallWaitQueue) {
521              m_rpcCallWaitQueue = false;
522              m_rpcCallWaitQueueCond.signalAll();
523              continue;
524            }
525          } finally {
526            m_rpcCallLock.unlock();
527          }
528          Thread.currentThread().interrupt();
529          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
530          break;
531        }
532        for (RpcAnswer event : events) {
533          EntryConsumer<RpcAnswer> listener;
534          m_rpcCallLock.lock();
535          try {
536            listener = m_rpcCalls.get(event.entry);
537          } finally {
538            m_rpcCallLock.unlock();
539          }
540          if (listener != null) {
541            event.entryObject = listener.entry;
542            try {
543              listener.consumer.accept(event);
544            } catch (Throwable throwable) {
545              System.err.println("Unhandled exception during RPC callback: " + throwable.toString());
546              throwable.printStackTrace();
547            }
548          }
549        }
550      }
551      m_rpcCallLock.lock();
552      try {
553        if (!wasInterrupted) {
554          NetworkTablesJNI.destroyRpcCallPoller(m_rpcCallPoller);
555        }
556        m_rpcCallPoller = 0;
557      } finally {
558        m_rpcCallLock.unlock();
559      }
560    }, "NTRpcCall");
561    m_rpcCallThread.setDaemon(true);
562    m_rpcCallThread.start();
563  }
564
565  private static final byte[] rev0def = new byte[] {0};
566
567  /**
568   * Create a callback-based RPC entry point.  Only valid to use on the server.
569   * The callback function will be called when the RPC is called.
570   * This function creates RPC version 0 definitions (raw data in and out).
571   * @param entry     the entry
572   * @param callback  callback function
573   */
574  public void createRpc(NetworkTableEntry entry, Consumer<RpcAnswer> callback) {
575    m_rpcCallLock.lock();
576    try {
577      if (m_rpcCallPoller == 0) {
578        m_rpcCallPoller = NetworkTablesJNI.createRpcCallPoller(m_handle);
579        startRpcCallThread();
580      }
581      NetworkTablesJNI.createPolledRpc(entry.getHandle(), rev0def, m_rpcCallPoller);
582      m_rpcCalls.put(entry.getHandle(), new EntryConsumer<>(entry, callback));
583    } finally {
584      m_rpcCallLock.unlock();
585    }
586  }
587
588  /**
589   * Wait for the incoming RPC call queue to be empty.  This is primarily useful
590   * for deterministic testing.  This blocks until either the RPC call
591   * queue is empty (e.g. there are no more events that need to be passed along
592   * to callbacks or poll queues) or the timeout expires.
593   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
594   *                  or a negative value to block indefinitely
595   * @return False if timed out, otherwise true.
596   */
597  public boolean waitForRpcCallQueue(double timeout) {
598    if (!NetworkTablesJNI.waitForRpcCallQueue(m_handle, timeout)) {
599      return false;
600    }
601    m_rpcCallLock.lock();
602    try {
603      if (m_rpcCallPoller != 0) {
604        m_rpcCallWaitQueue = true;
605        NetworkTablesJNI.cancelPollRpc(m_rpcCallPoller);
606        while (m_rpcCallWaitQueue) {
607          try {
608            if (timeout < 0) {
609              m_rpcCallWaitQueueCond.await();
610            } else {
611              return m_rpcCallWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
612            }
613          } catch (InterruptedException ex) {
614            Thread.currentThread().interrupt();
615            return true;
616          }
617        }
618      }
619    } finally {
620      m_rpcCallLock.unlock();
621    }
622    return true;
623  }
624
625  /*
626   * Client/Server Functions
627   */
628
629  /**
630   * Set the network identity of this node.
631   * This is the name used during the initial connection handshake, and is
632   * visible through ConnectionInfo on the remote node.
633   * @param name      identity to advertise
634   */
635  public void setNetworkIdentity(String name) {
636    NetworkTablesJNI.setNetworkIdentity(m_handle, name);
637  }
638
639  /**
640   * Get the current network mode.
641   * @return Bitmask of NetworkMode.
642   */
643  public int getNetworkMode() {
644    return NetworkTablesJNI.getNetworkMode(m_handle);
645  }
646
647  /**
648   * Starts a server using the networktables.ini as the persistent file,
649   * using the default listening address and port.
650   */
651  public void startServer() {
652    startServer("networktables.ini");
653  }
654
655  /**
656   * Starts a server using the specified persistent filename, using the default
657   * listening address and port.
658   *
659   * @param persistFilename  the name of the persist file to use
660   */
661  public void startServer(String persistFilename) {
662    startServer(persistFilename, "");
663  }
664
665  /**
666   * Starts a server using the specified filename and listening address,
667   * using the default port.
668   *
669   * @param persistFilename  the name of the persist file to use
670   * @param listenAddress    the address to listen on, or empty to listen on any
671   *                         address
672   */
673  public void startServer(String persistFilename, String listenAddress) {
674    startServer(persistFilename, listenAddress, kDefaultPort);
675  }
676
677  /**
678   * Starts a server using the specified filename, listening address, and port.
679   *
680   * @param persistFilename  the name of the persist file to use
681   * @param listenAddress    the address to listen on, or empty to listen on any
682   *                         address
683   * @param port             port to communicate over
684   */
685  public void startServer(String persistFilename, String listenAddress, int port) {
686    NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port);
687  }
688
689  /**
690   * Stops the server if it is running.
691   */
692  public void stopServer() {
693    NetworkTablesJNI.stopServer(m_handle);
694  }
695
696  /**
697   * Starts a client.  Use SetServer to set the server name and port.
698   */
699  public void startClient() {
700    NetworkTablesJNI.startClient(m_handle);
701  }
702
703  /**
704   * Starts a client using the specified server and the default port
705   *
706   * @param serverName  server name
707   */
708  public void startClient(String serverName) {
709    startClient(serverName, kDefaultPort);
710  }
711
712  /**
713   * Starts a client using the specified server and port
714   *
715   * @param serverName  server name
716   * @param port        port to communicate over
717   */
718  public void startClient(String serverName, int port) {
719    NetworkTablesJNI.startClient(m_handle, serverName, port);
720  }
721
722  /**
723   * Starts a client using the specified servers and default port.  The
724   * client will attempt to connect to each server in round robin fashion.
725   *
726   * @param serverNames   array of server names
727   */
728  public void startClient(String[] serverNames) {
729    startClient(serverNames, kDefaultPort);
730  }
731
732  /**
733   * Starts a client using the specified servers and port number.  The
734   * client will attempt to connect to each server in round robin fashion.
735   *
736   * @param serverNames   array of server names
737   * @param port          port to communicate over
738   */
739  public void startClient(String[] serverNames, int port) {
740    int[] ports = new int[serverNames.length];
741    for (int i = 0; i < serverNames.length; i++) {
742      ports[i] = port;
743    }
744    startClient(serverNames, ports);
745  }
746
747  /**
748   * Starts a client using the specified (server, port) combinations.  The
749   * client will attempt to connect to each server in round robin fashion.
750   *
751   * @param serverNames   array of server names
752   * @param ports         array of port numbers
753   */
754  public void startClient(String[] serverNames, int[] ports) {
755    NetworkTablesJNI.startClient(m_handle, serverNames, ports);
756  }
757
758  /**
759   * Starts a client using commonly known robot addresses for the specified
760   * team using the default port number.
761   *
762   * @param team        team number
763   */
764  public void startClientTeam(int team) {
765    startClientTeam(team, kDefaultPort);
766  }
767
768  /**
769   * Starts a client using commonly known robot addresses for the specified
770   * team.
771   *
772   * @param team        team number
773   * @param port        port to communicate over
774   */
775  public void startClientTeam(int team, int port) {
776    NetworkTablesJNI.startClientTeam(m_handle, team, port);
777  }
778
779  /**
780   * Stops the client if it is running.
781   */
782  public void stopClient() {
783    NetworkTablesJNI.stopClient(m_handle);
784  }
785
786  /**
787   * Sets server address and port for client (without restarting client).
788   * Changes the port to the default port.
789   *
790   * @param serverName  server name
791   */
792  public void setServer(String serverName) {
793    setServer(serverName, kDefaultPort);
794  }
795
796  /**
797   * Sets server address and port for client (without restarting client).
798   *
799   * @param serverName  server name
800   * @param port        port to communicate over
801   */
802  public void setServer(String serverName, int port) {
803    NetworkTablesJNI.setServer(m_handle, serverName, port);
804  }
805
806  /**
807   * Sets server addresses and port for client (without restarting client).
808   * Changes the port to the default port.  The client will attempt to connect
809   * to each server in round robin fashion.
810   *
811   * @param serverNames   array of server names
812   */
813  public void setServer(String[] serverNames) {
814    setServer(serverNames, kDefaultPort);
815  }
816
817  /**
818   * Sets server addresses and port for client (without restarting client).
819   * The client will attempt to connect to each server in round robin fashion.
820   *
821   * @param serverNames   array of server names
822   * @param port          port to communicate over
823   */
824  public void setServer(String[] serverNames, int port) {
825    int[] ports = new int[serverNames.length];
826    for (int i = 0; i < serverNames.length; i++) {
827      ports[i] = port;
828    }
829    setServer(serverNames, ports);
830  }
831
832  /**
833   * Sets server addresses and ports for client (without restarting client).
834   * The client will attempt to connect to each server in round robin fashion.
835   *
836   * @param serverNames   array of server names
837   * @param ports         array of port numbers
838   */
839  public void setServer(String[] serverNames, int[] ports) {
840    NetworkTablesJNI.setServer(m_handle, serverNames, ports);
841  }
842
843  /**
844   * Sets server addresses and port for client (without restarting client).
845   * Changes the port to the default port.  The client will attempt to connect
846   * to each server in round robin fashion.
847   *
848   * @param team        team number
849   */
850  public void setServerTeam(int team) {
851    setServerTeam(team, kDefaultPort);
852  }
853
854  /**
855   * Sets server addresses and port for client (without restarting client).
856   * Connects using commonly known robot addresses for the specified team.
857   *
858   * @param team        team number
859   * @param port        port to communicate over
860   */
861  public void setServerTeam(int team, int port) {
862    NetworkTablesJNI.setServerTeam(m_handle, team, port);
863  }
864
865  /**
866   * Starts requesting server address from Driver Station.
867   * This connects to the Driver Station running on localhost to obtain the
868   * server IP address, and connects with the default port.
869   */
870  public void startDSClient() {
871    startDSClient(kDefaultPort);
872  }
873
874  /**
875   * Starts requesting server address from Driver Station.
876   * This connects to the Driver Station running on localhost to obtain the
877   * server IP address.
878   *
879   * @param port server port to use in combination with IP from DS
880   */
881  public void startDSClient(int port) {
882    NetworkTablesJNI.startDSClient(m_handle, port);
883  }
884
885  /**
886   * Stops requesting server address from Driver Station.
887   */
888  public void stopDSClient() {
889    NetworkTablesJNI.stopDSClient(m_handle);
890  }
891
892  /**
893   * Set the periodic update rate.
894   * Sets how frequently updates are sent to other nodes over the network.
895   *
896   * @param interval update interval in seconds (range 0.01 to 1.0)
897   */
898  public void setUpdateRate(double interval) {
899    NetworkTablesJNI.setUpdateRate(m_handle, interval);
900  }
901
902  /**
903   * Flushes all updated values immediately to the network.
904   * Note: This is rate-limited to protect the network from flooding.
905   * This is primarily useful for synchronizing network updates with
906   * user code.
907   */
908  public void flush() {
909    NetworkTablesJNI.flush(m_handle);
910  }
911
912  /**
913   * Gets information on the currently established network connections.
914   * If operating as a client, this will return either zero or one values.
915   * @return array of connection information
916   */
917  public ConnectionInfo[] getConnections() {
918    return NetworkTablesJNI.getConnections(m_handle);
919  }
920
921  /**
922   * Return whether or not the instance is connected to another node.
923   * @return True if connected.
924   */
925  public boolean isConnected() {
926    return NetworkTablesJNI.isConnected(m_handle);
927  }
928
929  /**
930   * Saves persistent keys to a file.  The server does this automatically.
931   *
932   * @param filename file name
933   * @throws PersistentException if error saving file
934   */
935  public void savePersistent(String filename) throws PersistentException {
936    NetworkTablesJNI.savePersistent(m_handle, filename);
937  }
938
939  /**
940   * Loads persistent keys from a file.  The server does this automatically.
941   *
942   * @param filename file name
943   * @return List of warnings (errors result in an exception instead)
944   * @throws PersistentException if error reading file
945   */
946  public String[] loadPersistent(String filename) throws PersistentException {
947    return NetworkTablesJNI.loadPersistent(m_handle, filename);
948  }
949
950  /**
951   * Save table values to a file.  The file format used is identical to
952   * that used for SavePersistent.
953   * @param filename  filename
954   * @param prefix    save only keys starting with this prefix
955   * @throws PersistentException if error saving file
956   */
957  public void saveEntries(String filename, String prefix) throws PersistentException {
958    NetworkTablesJNI.saveEntries(m_handle, filename, prefix);
959  }
960
961  /**
962   * Load table values from a file.  The file format used is identical to
963   * that used for SavePersistent / LoadPersistent.
964   * @param filename  filename
965   * @param prefix    load only keys starting with this prefix
966   * @return List of warnings (errors result in an exception instead)
967   * @throws PersistentException if error saving file
968   */
969  public String[] loadEntries(String filename, String prefix) throws PersistentException {
970    return NetworkTablesJNI.loadEntries(m_handle, filename, prefix);
971  }
972
973  private final ReentrantLock m_loggerLock = new ReentrantLock();
974  private final Map<Integer, Consumer<LogMessage>> m_loggers = new HashMap<>();
975  private Thread m_loggerThread;
976  private int m_loggerPoller;
977  private boolean m_loggerWaitQueue;
978  private final Condition m_loggerWaitQueueCond = m_loggerLock.newCondition();
979
980  private void startLogThread() {
981    m_loggerThread = new Thread(() -> {
982      boolean wasInterrupted = false;
983      while (!Thread.interrupted()) {
984        LogMessage[] events;
985        try {
986          events = NetworkTablesJNI.pollLogger(this, m_loggerPoller);
987        } catch (InterruptedException ex) {
988          Thread.currentThread().interrupt();
989          wasInterrupted = true; // don't try to destroy poller, as its handle is likely no longer valid
990          break;
991        }
992        for (LogMessage event : events) {
993          Consumer<LogMessage> logger;
994          m_loggerLock.lock();
995          try {
996            logger = m_loggers.get(event.logger);
997          } finally {
998            m_loggerLock.unlock();
999          }
1000          if (logger != null) {
1001            try {
1002              logger.accept(event);
1003            } catch (Throwable throwable) {
1004              System.err.println("Unhandled exception during logger callback: " + throwable.toString());
1005              throwable.printStackTrace();
1006            }
1007          }
1008        }
1009      }
1010      m_loggerLock.lock();
1011      try {
1012        if (!wasInterrupted) {
1013          NetworkTablesJNI.destroyLoggerPoller(m_loggerPoller);
1014        }
1015        m_rpcCallPoller = 0;
1016      } finally {
1017        m_loggerLock.unlock();
1018      }
1019    }, "NTLogger");
1020    m_loggerThread.setDaemon(true);
1021    m_loggerThread.start();
1022  }
1023
1024  /**
1025   * Add logger callback function.  By default, log messages are sent to stderr;
1026   * this function sends log messages with the specified levels to the provided
1027   * callback function instead.  The callback function will only be called for
1028   * log messages with level greater than or equal to minLevel and less than or
1029   * equal to maxLevel; messages outside this range will be silently ignored.
1030   *
1031   * @param func        log callback function
1032   * @param minLevel    minimum log level
1033   * @param maxLevel    maximum log level
1034   * @return Logger handle
1035   */
1036  public int addLogger(Consumer<LogMessage> func, int minLevel, int maxLevel) {
1037    m_loggerLock.lock();
1038    try {
1039      if (m_loggerPoller == 0) {
1040        m_loggerPoller = NetworkTablesJNI.createLoggerPoller(m_handle);
1041        startLogThread();
1042      }
1043      int handle = NetworkTablesJNI.addPolledLogger(m_loggerPoller, minLevel, maxLevel);
1044      m_loggers.put(handle, func);
1045      return handle;
1046    } finally {
1047      m_loggerLock.unlock();
1048    }
1049  }
1050
1051  /**
1052   * Remove a logger.
1053   * @param logger Logger handle to remove
1054   */
1055  public void removeLogger(int logger) {
1056    m_loggerLock.lock();
1057    try {
1058      m_loggers.remove(logger);
1059    } finally {
1060      m_loggerLock.unlock();
1061    }
1062    NetworkTablesJNI.removeLogger(logger);
1063  }
1064
1065  /**
1066   * Wait for the incoming log event queue to be empty.  This is primarily useful
1067   * for deterministic testing.  This blocks until either the log event
1068   * queue is empty (e.g. there are no more events that need to be passed along
1069   * to callbacks or poll queues) or the timeout expires.
1070   * @param timeout   timeout, in seconds.  Set to 0 for non-blocking behavior,
1071   *                  or a negative value to block indefinitely
1072   * @return False if timed out, otherwise true.
1073   */
1074  public boolean waitForLoggerQueue(double timeout) {
1075    if (!NetworkTablesJNI.waitForLoggerQueue(m_handle, timeout)) {
1076      return false;
1077    }
1078    m_loggerLock.lock();
1079    try {
1080      if (m_loggerPoller != 0) {
1081        m_loggerWaitQueue = true;
1082        NetworkTablesJNI.cancelPollLogger(m_loggerPoller);
1083        while (m_loggerWaitQueue) {
1084          try {
1085            if (timeout < 0) {
1086              m_loggerWaitQueueCond.await();
1087            } else {
1088              return m_loggerWaitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
1089            }
1090          } catch (InterruptedException ex) {
1091            Thread.currentThread().interrupt();
1092            return true;
1093          }
1094        }
1095      }
1096    } finally {
1097      m_loggerLock.unlock();
1098    }
1099    return true;
1100  }
1101
1102  @Override
1103  public boolean equals(Object o) {
1104    if (o == this) {
1105      return true;
1106    }
1107    if (!(o instanceof NetworkTableInstance)) {
1108      return false;
1109    }
1110    NetworkTableInstance other = (NetworkTableInstance) o;
1111    return m_handle == other.m_handle;
1112  }
1113
1114  @Override
1115  public int hashCode() {
1116    return m_handle;
1117  }
1118
1119  private boolean m_owned;
1120  private int m_handle;
1121}