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