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
005// THIS FILE WAS AUTO-GENERATED BY ./ntcore/generate_topics.py. DO NOT MODIFY
006
007package edu.wpi.first.networktables;
008
009import edu.wpi.first.datalog.DataLog;
010import edu.wpi.first.util.WPIUtilJNI;
011import edu.wpi.first.util.concurrent.Event;
012import edu.wpi.first.util.protobuf.Protobuf;
013import edu.wpi.first.util.struct.Struct;
014import java.nio.charset.StandardCharsets;
015import java.util.EnumSet;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Map;
019import java.util.OptionalLong;
020import java.util.Set;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.concurrent.ConcurrentMap;
023import java.util.concurrent.TimeUnit;
024import java.util.concurrent.locks.Condition;
025import java.util.concurrent.locks.ReentrantLock;
026import java.util.function.Consumer;
027import us.hebi.quickbuf.ProtoMessage;
028
029/**
030 * NetworkTables Instance.
031 *
032 * <p>Instances are completely independent from each other. Table operations on one instance will
033 * not be visible to other instances unless the instances are connected via the network. The main
034 * limitation on instances is that you cannot have two servers on the same network port. The main
035 * utility of instances is for unit testing, but they can also enable one program to connect to two
036 * different NetworkTables networks.
037 *
038 * <p>The global "default" instance (as returned by {@link #getDefault()}) is always available, and
039 * is intended for the common case when there is only a single NetworkTables instance being used in
040 * the program.
041 *
042 * <p>Additional instances can be created with the {@link #create()} function. A reference must be
043 * kept to the NetworkTableInstance returned by this function to keep it from being garbage
044 * collected.
045 */
046public final class NetworkTableInstance implements AutoCloseable {
047  /** Client/server mode flag values (as returned by {@link #getNetworkMode()}). */
048  public enum NetworkMode {
049    /** Running in server mode. */
050    kServer(0x01),
051
052    /** Running in client mode. */
053    kClient(0x04),
054
055    /** Currently starting up (either client or server). */
056    kStarting(0x08),
057
058    /** Running in local-only mode. */
059    kLocal(0x10);
060
061    private final int value;
062
063    NetworkMode(int value) {
064      this.value = value;
065    }
066
067    /**
068     * Returns the network mode value.
069     *
070     * @return The network mode value.
071     */
072    public int getValue() {
073      return value;
074    }
075  }
076
077  /** The default port that network tables operates on. */
078  public static final int kDefaultPort = 5810;
079
080  /**
081   * Construct from native handle.
082   *
083   * @param handle Native handle
084   */
085  private NetworkTableInstance(int handle) {
086    m_owned = false;
087    m_handle = handle;
088  }
089
090  /** Destroys the instance (if created by {@link #create()}). */
091  @Override
092  public synchronized void close() {
093    if (m_owned && m_handle != 0) {
094      m_listeners.close();
095      m_schemas.forEach((k, v) -> v.close());
096      NetworkTablesJNI.destroyInstance(m_handle);
097      m_handle = 0;
098    }
099  }
100
101  /**
102   * Determines if the native handle is valid.
103   *
104   * @return True if the native handle is valid, false otherwise.
105   */
106  public boolean isValid() {
107    return m_handle != 0;
108  }
109
110  /* The default instance. */
111  private static NetworkTableInstance s_defaultInstance;
112
113  /**
114   * Get global default instance.
115   *
116   * @return Global default instance
117   */
118  public static synchronized NetworkTableInstance getDefault() {
119    if (s_defaultInstance == null) {
120      s_defaultInstance = new NetworkTableInstance(NetworkTablesJNI.getDefaultInstance());
121    }
122    return s_defaultInstance;
123  }
124
125  /**
126   * Create an instance. Note: A reference to the returned instance must be retained to ensure the
127   * instance is not garbage collected.
128   *
129   * @return Newly created instance
130   */
131  public static NetworkTableInstance create() {
132    NetworkTableInstance inst = new NetworkTableInstance(NetworkTablesJNI.createInstance());
133    inst.m_owned = true;
134    return inst;
135  }
136
137  /**
138   * Create an instance from a native handle. This instance is not owned, closing will do nothing.
139   *
140   * @param handle Native instance handle
141   * @return Instance
142   */
143  public static NetworkTableInstance fromNativeHandle(int handle) {
144    return new NetworkTableInstance(handle);
145  }
146
147  /**
148   * Gets the native handle for the instance.
149   *
150   * @return Native handle
151   */
152  public int getHandle() {
153    return m_handle;
154  }
155
156  /**
157   * Get (generic) topic.
158   *
159   * @param name topic name
160   * @return Topic
161   */
162  public Topic getTopic(String name) {
163    Topic topic = m_topics.get(name);
164    if (topic == null) {
165      int handle = NetworkTablesJNI.getTopic(m_handle, name);
166      topic = new Topic(this, handle);
167      Topic oldTopic = m_topics.putIfAbsent(name, topic);
168      if (oldTopic != null) {
169        topic = oldTopic;
170      }
171      // also cache by handle
172      m_topicsByHandle.putIfAbsent(handle, topic);
173    }
174    return topic;
175  }
176
177  /**
178   * Get boolean topic.
179   *
180   * @param name topic name
181   * @return BooleanTopic
182   */
183  public BooleanTopic getBooleanTopic(String name) {
184    Topic topic = m_topics.get(name);
185    if (topic instanceof BooleanTopic t) {
186      return t;
187    }
188
189    int handle;
190    if (topic == null) {
191      handle = NetworkTablesJNI.getTopic(m_handle, name);
192    } else {
193      handle = topic.getHandle();
194    }
195
196    BooleanTopic wrapTopic = new BooleanTopic(this, handle);
197    m_topics.put(name, wrapTopic);
198
199    // also cache by handle
200    m_topicsByHandle.put(handle, wrapTopic);
201
202    return wrapTopic;
203  }
204
205  /**
206   * Get long topic.
207   *
208   * @param name topic name
209   * @return IntegerTopic
210   */
211  public IntegerTopic getIntegerTopic(String name) {
212    Topic topic = m_topics.get(name);
213    if (topic instanceof IntegerTopic t) {
214      return t;
215    }
216
217    int handle;
218    if (topic == null) {
219      handle = NetworkTablesJNI.getTopic(m_handle, name);
220    } else {
221      handle = topic.getHandle();
222    }
223
224    IntegerTopic wrapTopic = new IntegerTopic(this, handle);
225    m_topics.put(name, wrapTopic);
226
227    // also cache by handle
228    m_topicsByHandle.put(handle, wrapTopic);
229
230    return wrapTopic;
231  }
232
233  /**
234   * Get float topic.
235   *
236   * @param name topic name
237   * @return FloatTopic
238   */
239  public FloatTopic getFloatTopic(String name) {
240    Topic topic = m_topics.get(name);
241    if (topic instanceof FloatTopic t) {
242      return t;
243    }
244
245    int handle;
246    if (topic == null) {
247      handle = NetworkTablesJNI.getTopic(m_handle, name);
248    } else {
249      handle = topic.getHandle();
250    }
251
252    FloatTopic wrapTopic = new FloatTopic(this, handle);
253    m_topics.put(name, wrapTopic);
254
255    // also cache by handle
256    m_topicsByHandle.put(handle, wrapTopic);
257
258    return wrapTopic;
259  }
260
261  /**
262   * Get double topic.
263   *
264   * @param name topic name
265   * @return DoubleTopic
266   */
267  public DoubleTopic getDoubleTopic(String name) {
268    Topic topic = m_topics.get(name);
269    if (topic instanceof DoubleTopic t) {
270      return t;
271    }
272
273    int handle;
274    if (topic == null) {
275      handle = NetworkTablesJNI.getTopic(m_handle, name);
276    } else {
277      handle = topic.getHandle();
278    }
279
280    DoubleTopic wrapTopic = new DoubleTopic(this, handle);
281    m_topics.put(name, wrapTopic);
282
283    // also cache by handle
284    m_topicsByHandle.put(handle, wrapTopic);
285
286    return wrapTopic;
287  }
288
289  /**
290   * Get String topic.
291   *
292   * @param name topic name
293   * @return StringTopic
294   */
295  public StringTopic getStringTopic(String name) {
296    Topic topic = m_topics.get(name);
297    if (topic instanceof StringTopic t) {
298      return t;
299    }
300
301    int handle;
302    if (topic == null) {
303      handle = NetworkTablesJNI.getTopic(m_handle, name);
304    } else {
305      handle = topic.getHandle();
306    }
307
308    StringTopic wrapTopic = new StringTopic(this, handle);
309    m_topics.put(name, wrapTopic);
310
311    // also cache by handle
312    m_topicsByHandle.put(handle, wrapTopic);
313
314    return wrapTopic;
315  }
316
317  /**
318   * Get byte[] topic.
319   *
320   * @param name topic name
321   * @return RawTopic
322   */
323  public RawTopic getRawTopic(String name) {
324    Topic topic = m_topics.get(name);
325    if (topic instanceof RawTopic t) {
326      return t;
327    }
328
329    int handle;
330    if (topic == null) {
331      handle = NetworkTablesJNI.getTopic(m_handle, name);
332    } else {
333      handle = topic.getHandle();
334    }
335
336    RawTopic wrapTopic = new RawTopic(this, handle);
337    m_topics.put(name, wrapTopic);
338
339    // also cache by handle
340    m_topicsByHandle.put(handle, wrapTopic);
341
342    return wrapTopic;
343  }
344
345  /**
346   * Get boolean[] topic.
347   *
348   * @param name topic name
349   * @return BooleanArrayTopic
350   */
351  public BooleanArrayTopic getBooleanArrayTopic(String name) {
352    Topic topic = m_topics.get(name);
353    if (topic instanceof BooleanArrayTopic t) {
354      return t;
355    }
356
357    int handle;
358    if (topic == null) {
359      handle = NetworkTablesJNI.getTopic(m_handle, name);
360    } else {
361      handle = topic.getHandle();
362    }
363
364    BooleanArrayTopic wrapTopic = new BooleanArrayTopic(this, handle);
365    m_topics.put(name, wrapTopic);
366
367    // also cache by handle
368    m_topicsByHandle.put(handle, wrapTopic);
369
370    return wrapTopic;
371  }
372
373  /**
374   * Get long[] topic.
375   *
376   * @param name topic name
377   * @return IntegerArrayTopic
378   */
379  public IntegerArrayTopic getIntegerArrayTopic(String name) {
380    Topic topic = m_topics.get(name);
381    if (topic instanceof IntegerArrayTopic t) {
382      return t;
383    }
384
385    int handle;
386    if (topic == null) {
387      handle = NetworkTablesJNI.getTopic(m_handle, name);
388    } else {
389      handle = topic.getHandle();
390    }
391
392    IntegerArrayTopic wrapTopic = new IntegerArrayTopic(this, handle);
393    m_topics.put(name, wrapTopic);
394
395    // also cache by handle
396    m_topicsByHandle.put(handle, wrapTopic);
397
398    return wrapTopic;
399  }
400
401  /**
402   * Get float[] topic.
403   *
404   * @param name topic name
405   * @return FloatArrayTopic
406   */
407  public FloatArrayTopic getFloatArrayTopic(String name) {
408    Topic topic = m_topics.get(name);
409    if (topic instanceof FloatArrayTopic t) {
410      return t;
411    }
412
413    int handle;
414    if (topic == null) {
415      handle = NetworkTablesJNI.getTopic(m_handle, name);
416    } else {
417      handle = topic.getHandle();
418    }
419
420    FloatArrayTopic wrapTopic = new FloatArrayTopic(this, handle);
421    m_topics.put(name, wrapTopic);
422
423    // also cache by handle
424    m_topicsByHandle.put(handle, wrapTopic);
425
426    return wrapTopic;
427  }
428
429  /**
430   * Get double[] topic.
431   *
432   * @param name topic name
433   * @return DoubleArrayTopic
434   */
435  public DoubleArrayTopic getDoubleArrayTopic(String name) {
436    Topic topic = m_topics.get(name);
437    if (topic instanceof DoubleArrayTopic t) {
438      return t;
439    }
440
441    int handle;
442    if (topic == null) {
443      handle = NetworkTablesJNI.getTopic(m_handle, name);
444    } else {
445      handle = topic.getHandle();
446    }
447
448    DoubleArrayTopic wrapTopic = new DoubleArrayTopic(this, handle);
449    m_topics.put(name, wrapTopic);
450
451    // also cache by handle
452    m_topicsByHandle.put(handle, wrapTopic);
453
454    return wrapTopic;
455  }
456
457  /**
458   * Get String[] topic.
459   *
460   * @param name topic name
461   * @return StringArrayTopic
462   */
463  public StringArrayTopic getStringArrayTopic(String name) {
464    Topic topic = m_topics.get(name);
465    if (topic instanceof StringArrayTopic t) {
466      return t;
467    }
468
469    int handle;
470    if (topic == null) {
471      handle = NetworkTablesJNI.getTopic(m_handle, name);
472    } else {
473      handle = topic.getHandle();
474    }
475
476    StringArrayTopic wrapTopic = new StringArrayTopic(this, handle);
477    m_topics.put(name, wrapTopic);
478
479    // also cache by handle
480    m_topicsByHandle.put(handle, wrapTopic);
481
482    return wrapTopic;
483  }
484
485
486  /**
487   * Get protobuf-encoded value topic.
488   *
489   * @param <T> value class (inferred from proto)
490   * @param <MessageType> protobuf message type (inferred from proto)
491   * @param name topic name
492   * @param proto protobuf serialization implementation
493   * @return ProtobufTopic
494   */
495  public <T, MessageType extends ProtoMessage<?>>
496      ProtobufTopic<T> getProtobufTopic(String name, Protobuf<T, MessageType> proto) {
497    Topic topic = m_topics.get(name);
498    if (topic instanceof ProtobufTopic<?> t && t.getProto().equals(proto)) {
499      @SuppressWarnings("unchecked")
500      ProtobufTopic<T> wrapTopic = (ProtobufTopic<T>) topic;
501      return wrapTopic;
502    }
503
504    int handle;
505    if (topic == null) {
506      handle = NetworkTablesJNI.getTopic(m_handle, name);
507    } else {
508      handle = topic.getHandle();
509    }
510
511    ProtobufTopic<T> wrapTopic = ProtobufTopic.wrap(this, handle, proto);
512    m_topics.put(name, wrapTopic);
513
514    // also cache by handle
515    m_topicsByHandle.put(handle, wrapTopic);
516
517    return wrapTopic;
518  }
519
520  /**
521   * Get struct-encoded value topic.
522   *
523   * @param <T> value class (inferred from struct)
524   * @param name topic name
525   * @param struct struct serialization implementation
526   * @return StructTopic
527   */
528  public <T>
529      StructTopic<T> getStructTopic(String name, Struct<T> struct) {
530    Topic topic = m_topics.get(name);
531    if (topic instanceof StructTopic<?> t && t.getStruct().equals(struct)) {
532      @SuppressWarnings("unchecked")
533      StructTopic<T> wrapTopic = (StructTopic<T>) topic;
534      return wrapTopic;
535    }
536
537    int handle;
538    if (topic == null) {
539      handle = NetworkTablesJNI.getTopic(m_handle, name);
540    } else {
541      handle = topic.getHandle();
542    }
543
544    StructTopic<T> wrapTopic = StructTopic.wrap(this, handle, struct);
545    m_topics.put(name, wrapTopic);
546
547    // also cache by handle
548    m_topicsByHandle.put(handle, wrapTopic);
549
550    return wrapTopic;
551  }
552
553  /**
554   * Get struct-encoded value array topic.
555   *
556   * @param <T> value class (inferred from struct)
557   * @param name topic name
558   * @param struct struct serialization implementation
559   * @return StructArrayTopic
560   */
561  public <T>
562      StructArrayTopic<T> getStructArrayTopic(String name, Struct<T> struct) {
563    Topic topic = m_topics.get(name);
564    if (topic instanceof StructArrayTopic<?> t && t.getStruct().equals(struct)) {
565      @SuppressWarnings("unchecked")
566      StructArrayTopic<T> wrapTopic = (StructArrayTopic<T>) topic;
567      return wrapTopic;
568    }
569
570    int handle;
571    if (topic == null) {
572      handle = NetworkTablesJNI.getTopic(m_handle, name);
573    } else {
574      handle = topic.getHandle();
575    }
576
577    StructArrayTopic<T> wrapTopic = StructArrayTopic.wrap(this, handle, struct);
578    m_topics.put(name, wrapTopic);
579
580    // also cache by handle
581    m_topicsByHandle.put(handle, wrapTopic);
582
583    return wrapTopic;
584  }
585
586  private Topic[] topicHandlesToTopics(int[] handles) {
587    Topic[] topics = new Topic[handles.length];
588    for (int i = 0; i < handles.length; i++) {
589      topics[i] = getCachedTopic(handles[i]);
590    }
591    return topics;
592  }
593
594  /**
595   * Get all published topics.
596   *
597   * @return Array of topics.
598   */
599  public Topic[] getTopics() {
600    return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, "", 0));
601  }
602
603  /**
604   * Get published topics starting with the given prefix. The results are optionally filtered by
605   * string prefix to only return a subset of all topics.
606   *
607   * @param prefix topic name required prefix; only topics whose name starts with this string are
608   *     returned
609   * @return Array of topic information.
610   */
611  public Topic[] getTopics(String prefix) {
612    return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, 0));
613  }
614
615  /**
616   * Get published topics starting with the given prefix. The results are optionally filtered by
617   * string prefix and data type to only return a subset of all topics.
618   *
619   * @param prefix topic name required prefix; only topics whose name starts with this string are
620   *     returned
621   * @param types bitmask of data types; 0 is treated as a "don't care"
622   * @return Array of topic information.
623   */
624  public Topic[] getTopics(String prefix, int types) {
625    return topicHandlesToTopics(NetworkTablesJNI.getTopics(m_handle, prefix, types));
626  }
627
628  /**
629   * Get published topics starting with the given prefix. The results are optionally filtered by
630   * string prefix and data type to only return a subset of all topics.
631   *
632   * @param prefix topic name required prefix; only topics whose name starts with this string are
633   *     returned
634   * @param types array of data type strings
635   * @return Array of topic information.
636   */
637  public Topic[] getTopics(String prefix, String[] types) {
638    return topicHandlesToTopics(NetworkTablesJNI.getTopicsStr(m_handle, prefix, types));
639  }
640
641  /**
642   * Get information about all topics.
643   *
644   * @return Array of topic information.
645   */
646  public TopicInfo[] getTopicInfo() {
647    return NetworkTablesJNI.getTopicInfos(this, m_handle, "", 0);
648  }
649
650  /**
651   * Get information about topics starting with the given prefix. The results are optionally
652   * filtered by string prefix to only return a subset of all topics.
653   *
654   * @param prefix topic name required prefix; only topics whose name starts with this string are
655   *     returned
656   * @return Array of topic information.
657   */
658  public TopicInfo[] getTopicInfo(String prefix) {
659    return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, 0);
660  }
661
662  /**
663   * Get information about topics starting with the given prefix. The results are optionally
664   * filtered by string prefix and data type to only return a subset of all topics.
665   *
666   * @param prefix topic name required prefix; only topics whose name starts with this string are
667   *     returned
668   * @param types bitmask of data types; 0 is treated as a "don't care"
669   * @return Array of topic information.
670   */
671  public TopicInfo[] getTopicInfo(String prefix, int types) {
672    return NetworkTablesJNI.getTopicInfos(this, m_handle, prefix, types);
673  }
674
675  /**
676   * Get information about topics starting with the given prefix. The results are optionally
677   * filtered by string prefix and data type to only return a subset of all topics.
678   *
679   * @param prefix topic name required prefix; only topics whose name starts with this string are
680   *     returned
681   * @param types array of data type strings
682   * @return Array of topic information.
683   */
684  public TopicInfo[] getTopicInfo(String prefix, String[] types) {
685    return NetworkTablesJNI.getTopicInfosStr(this, m_handle, prefix, types);
686  }
687
688  /* Cache of created entries. */
689  private final ConcurrentMap<String, NetworkTableEntry> m_entries = new ConcurrentHashMap<>();
690
691  /**
692   * Gets the entry for a key.
693   *
694   * @param name Key
695   * @return Network table entry.
696   */
697  public NetworkTableEntry getEntry(String name) {
698    NetworkTableEntry entry = m_entries.get(name);
699    if (entry == null) {
700      entry = new NetworkTableEntry(this, NetworkTablesJNI.getEntry(m_handle, name));
701      NetworkTableEntry oldEntry = m_entries.putIfAbsent(name, entry);
702      if (oldEntry != null) {
703        entry = oldEntry;
704      }
705    }
706    return entry;
707  }
708
709  /* Cache of created topics. */
710  private final ConcurrentMap<String, Topic> m_topics = new ConcurrentHashMap<>();
711  private final ConcurrentMap<Integer, Topic> m_topicsByHandle = new ConcurrentHashMap<>();
712
713  Topic getCachedTopic(String name) {
714    Topic topic = m_topics.get(name);
715    if (topic == null) {
716      int handle = NetworkTablesJNI.getTopic(m_handle, name);
717      topic = new Topic(this, handle);
718      Topic oldTopic = m_topics.putIfAbsent(name, topic);
719      if (oldTopic != null) {
720        topic = oldTopic;
721      }
722      // also cache by handle
723      m_topicsByHandle.putIfAbsent(handle, topic);
724    }
725    return topic;
726  }
727
728  Topic getCachedTopic(int handle) {
729    Topic topic = m_topicsByHandle.get(handle);
730    if (topic == null) {
731      topic = new Topic(this, handle);
732      Topic oldTopic = m_topicsByHandle.putIfAbsent(handle, topic);
733      if (oldTopic != null) {
734        topic = oldTopic;
735      }
736    }
737    return topic;
738  }
739
740  /* Cache of created tables. */
741  private final ConcurrentMap<String, NetworkTable> m_tables = new ConcurrentHashMap<>();
742
743  /**
744   * Gets the table with the specified key.
745   *
746   * @param key the key name
747   * @return The network table
748   */
749  public NetworkTable getTable(String key) {
750    // prepend leading / if not present
751    String theKey;
752    if (key.isEmpty() || "/".equals(key)) {
753      theKey = "";
754    } else if (key.charAt(0) == NetworkTable.PATH_SEPARATOR) {
755      theKey = key;
756    } else {
757      theKey = NetworkTable.PATH_SEPARATOR + key;
758    }
759
760    // cache created tables
761    NetworkTable table = m_tables.get(theKey);
762    if (table == null) {
763      table = new NetworkTable(this, theKey);
764      NetworkTable oldTable = m_tables.putIfAbsent(theKey, table);
765      if (oldTable != null) {
766        table = oldTable;
767      }
768    }
769    return table;
770  }
771
772  /*
773   * Callback Creation Functions
774   */
775
776  private static class ListenerStorage implements AutoCloseable {
777    private final ReentrantLock m_lock = new ReentrantLock();
778    private final Map<Integer, Consumer<NetworkTableEvent>> m_listeners = new HashMap<>();
779
780    @SuppressWarnings("PMD.SingularField")
781    private Thread m_thread;
782
783    private int m_poller;
784    private boolean m_waitQueue;
785    private final Event m_waitQueueEvent = new Event();
786    private final Condition m_waitQueueCond = m_lock.newCondition();
787    private final NetworkTableInstance m_inst;
788
789    ListenerStorage(NetworkTableInstance inst) {
790      m_inst = inst;
791    }
792
793    int add(
794        String[] prefixes,
795        EnumSet<NetworkTableEvent.Kind> eventKinds,
796        Consumer<NetworkTableEvent> listener) {
797      m_lock.lock();
798      try {
799        if (m_poller == 0) {
800          m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
801          startThread();
802        }
803        int h = NetworkTablesJNI.addListener(m_poller, prefixes, eventKinds);
804        m_listeners.put(h, listener);
805        return h;
806      } finally {
807        m_lock.unlock();
808      }
809    }
810
811    int add(
812        int handle,
813        EnumSet<NetworkTableEvent.Kind> eventKinds,
814        Consumer<NetworkTableEvent> listener) {
815      m_lock.lock();
816      try {
817        if (m_poller == 0) {
818          m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
819          startThread();
820        }
821        int h = NetworkTablesJNI.addListener(m_poller, handle, eventKinds);
822        m_listeners.put(h, listener);
823        return h;
824      } finally {
825        m_lock.unlock();
826      }
827    }
828
829    int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> listener) {
830      m_lock.lock();
831      try {
832        if (m_poller == 0) {
833          m_poller = NetworkTablesJNI.createListenerPoller(m_inst.getHandle());
834          startThread();
835        }
836        int h = NetworkTablesJNI.addLogger(m_poller, minLevel, maxLevel);
837        m_listeners.put(h, listener);
838        return h;
839      } finally {
840        m_lock.unlock();
841      }
842    }
843
844    void remove(int listener) {
845      m_lock.lock();
846      try {
847        m_listeners.remove(listener);
848      } finally {
849        m_lock.unlock();
850      }
851      NetworkTablesJNI.removeListener(listener);
852    }
853
854    @Override
855    public void close() {
856      if (m_poller != 0) {
857        NetworkTablesJNI.destroyListenerPoller(m_poller);
858      }
859      m_poller = 0;
860    }
861
862    private void startThread() {
863      m_thread =
864          new Thread(
865              () -> {
866                boolean wasInterrupted = false;
867                int[] handles = new int[] { m_poller, m_waitQueueEvent.getHandle() };
868                while (!Thread.interrupted()) {
869                  try {
870                    WPIUtilJNI.waitForObjects(handles);
871                  } catch (InterruptedException ex) {
872                    m_lock.lock();
873                    try {
874                      if (m_waitQueue) {
875                        m_waitQueue = false;
876                        m_waitQueueCond.signalAll();
877                      }
878                    } finally {
879                      m_lock.unlock();
880                    }
881                    Thread.currentThread().interrupt();
882                    // don't try to destroy poller, as its handle is likely no longer valid
883                    wasInterrupted = true;
884                    break;
885                  }
886                  for (NetworkTableEvent event :
887                      NetworkTablesJNI.readListenerQueue(m_inst, m_poller)) {
888                    Consumer<NetworkTableEvent> listener;
889                    m_lock.lock();
890                    try {
891                      listener = m_listeners.get(event.listener);
892                    } finally {
893                      m_lock.unlock();
894                    }
895                    if (listener != null) {
896                      try {
897                        listener.accept(event);
898                      } catch (Throwable throwable) {
899                        System.err.println(
900                            "Unhandled exception during listener callback: "
901                            + throwable.toString());
902                        throwable.printStackTrace();
903                      }
904                    }
905                  }
906                  m_lock.lock();
907                  try {
908                    if (m_waitQueue) {
909                      m_waitQueue = false;
910                      m_waitQueueCond.signalAll();
911                    }
912                  } finally {
913                    m_lock.unlock();
914                  }
915                }
916                m_lock.lock();
917                try {
918                  if (!wasInterrupted) {
919                    NetworkTablesJNI.destroyListenerPoller(m_poller);
920                  }
921                  m_poller = 0;
922                } finally {
923                  m_lock.unlock();
924                }
925              },
926              "NTListener");
927      m_thread.setDaemon(true);
928      m_thread.start();
929    }
930
931    boolean waitForQueue(double timeout) {
932      m_lock.lock();
933      try {
934        if (m_poller != 0) {
935          m_waitQueue = true;
936          m_waitQueueEvent.set();
937          while (m_waitQueue) {
938            try {
939              if (timeout < 0) {
940                m_waitQueueCond.await();
941              } else {
942                return m_waitQueueCond.await((long) (timeout * 1e9), TimeUnit.NANOSECONDS);
943              }
944            } catch (InterruptedException ex) {
945              Thread.currentThread().interrupt();
946              return true;
947            }
948          }
949        }
950      } finally {
951        m_lock.unlock();
952      }
953      return true;
954    }
955  }
956
957  private final ListenerStorage m_listeners = new ListenerStorage(this);
958
959  /**
960   * Remove a connection listener.
961   *
962   * @param listener Listener handle to remove
963   */
964  public void removeListener(int listener) {
965    m_listeners.remove(listener);
966  }
967
968  /**
969   * Wait for the listener queue to be empty. This is primarily useful for deterministic
970   * testing. This blocks until either the listener queue is empty (e.g. there are no
971   * more events that need to be passed along to callbacks or poll queues) or the timeout expires.
972   *
973   * @param timeout timeout, in seconds. Set to 0 for non-blocking behavior, or a negative value to
974   *     block indefinitely
975   * @return False if timed out, otherwise true.
976   */
977  public boolean waitForListenerQueue(double timeout) {
978    return m_listeners.waitForQueue(timeout);
979  }
980
981  /**
982   * Add a connection listener. The callback function is called asynchronously on a separate
983   * thread, so it's important to use synchronization or atomics when accessing any shared state
984   * from the callback function.
985   *
986   * @param immediateNotify Notify listener of all existing connections
987   * @param listener Listener to add
988   * @return Listener handle
989   */
990  public int addConnectionListener(
991      boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
992    EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kConnection);
993    if (immediateNotify) {
994      eventKinds.add(NetworkTableEvent.Kind.kImmediate);
995    }
996    return m_listeners.add(m_handle, eventKinds, listener);
997  }
998
999  /**
1000   * Add a time synchronization listener. The callback function is called asynchronously on a
1001   * separate thread, so it's important to use synchronization or atomics when accessing any shared
1002   * state from the callback function.
1003   *
1004   * @param immediateNotify Notify listener of current time synchronization value
1005   * @param listener Listener to add
1006   * @return Listener handle
1007   */
1008  public int addTimeSyncListener(
1009      boolean immediateNotify, Consumer<NetworkTableEvent> listener) {
1010    EnumSet<NetworkTableEvent.Kind> eventKinds = EnumSet.of(NetworkTableEvent.Kind.kTimeSync);
1011    if (immediateNotify) {
1012      eventKinds.add(NetworkTableEvent.Kind.kImmediate);
1013    }
1014    return m_listeners.add(m_handle, eventKinds, listener);
1015  }
1016
1017  /**
1018   * Add a listener for changes on a particular topic. The callback function is called
1019   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1020   * accessing any shared state from the callback function.
1021   *
1022   * <p>This creates a corresponding internal subscriber with the lifetime of the
1023   * listener.
1024   *
1025   * @param topic Topic
1026   * @param eventKinds set of event kinds to listen to
1027   * @param listener Listener function
1028   * @return Listener handle
1029   */
1030  public int addListener(
1031      Topic topic,
1032      EnumSet<NetworkTableEvent.Kind> eventKinds,
1033      Consumer<NetworkTableEvent> listener) {
1034    if (topic.getInstance().getHandle() != m_handle) {
1035      throw new IllegalArgumentException("topic is not from this instance");
1036    }
1037    return m_listeners.add(topic.getHandle(), eventKinds, listener);
1038  }
1039
1040  /**
1041   * Add a listener for changes on a subscriber. The callback function is called
1042   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1043   * accessing any shared state from the callback function. This does NOT keep the subscriber
1044   * active.
1045   *
1046   * @param subscriber Subscriber
1047   * @param eventKinds set of event kinds to listen to
1048   * @param listener Listener function
1049   * @return Listener handle
1050   */
1051  public int addListener(
1052      Subscriber subscriber,
1053      EnumSet<NetworkTableEvent.Kind> eventKinds,
1054      Consumer<NetworkTableEvent> listener) {
1055    if (subscriber.getTopic().getInstance().getHandle() != m_handle) {
1056      throw new IllegalArgumentException("subscriber is not from this instance");
1057    }
1058    return m_listeners.add(subscriber.getHandle(), eventKinds, listener);
1059  }
1060
1061  /**
1062   * Add a listener for changes on a subscriber. The callback function is called
1063   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1064   * accessing any shared state from the callback function. This does NOT keep the subscriber
1065   * active.
1066   *
1067   * @param subscriber Subscriber
1068   * @param eventKinds set of event kinds to listen to
1069   * @param listener Listener function
1070   * @return Listener handle
1071   */
1072  public int addListener(
1073      MultiSubscriber subscriber,
1074      EnumSet<NetworkTableEvent.Kind> eventKinds,
1075      Consumer<NetworkTableEvent> listener) {
1076    if (subscriber.getInstance().getHandle() != m_handle) {
1077      throw new IllegalArgumentException("subscriber is not from this instance");
1078    }
1079    return m_listeners.add(subscriber.getHandle(), eventKinds, listener);
1080  }
1081
1082  /**
1083   * Add a listener for changes on an entry. The callback function is called
1084   * asynchronously on a separate thread, so it's important to use synchronization or atomics when
1085   * accessing any shared state from the callback function.
1086   *
1087   * @param entry Entry
1088   * @param eventKinds set of event kinds to listen to
1089   * @param listener Listener function
1090   * @return Listener handle
1091   */
1092  public int addListener(
1093      NetworkTableEntry entry,
1094      EnumSet<NetworkTableEvent.Kind> eventKinds,
1095      Consumer<NetworkTableEvent> listener) {
1096    if (entry.getTopic().getInstance().getHandle() != m_handle) {
1097      throw new IllegalArgumentException("entry is not from this instance");
1098    }
1099    return m_listeners.add(entry.getHandle(), eventKinds, listener);
1100  }
1101
1102  /**
1103   * Add a listener for changes to topics with names that start with any of the given
1104   * prefixes. The callback function is called asynchronously on a separate thread, so it's
1105   * important to use synchronization or atomics when accessing any shared state from the callback
1106   * function.
1107   *
1108   * <p>This creates a corresponding internal subscriber with the lifetime of the
1109   * listener.
1110   *
1111   * @param prefixes Topic name string prefixes
1112   * @param eventKinds set of event kinds to listen to
1113   * @param listener Listener function
1114   * @return Listener handle
1115   */
1116  public int addListener(
1117      String[] prefixes,
1118      EnumSet<NetworkTableEvent.Kind> eventKinds,
1119      Consumer<NetworkTableEvent> listener) {
1120    return m_listeners.add(prefixes, eventKinds, listener);
1121  }
1122
1123  /*
1124   * Client/Server Functions
1125   */
1126
1127  /**
1128   * Get the current network mode.
1129   *
1130   * @return Enum set of NetworkMode.
1131   */
1132  public EnumSet<NetworkMode> getNetworkMode() {
1133    int flags = NetworkTablesJNI.getNetworkMode(m_handle);
1134    EnumSet<NetworkMode> rv = EnumSet.noneOf(NetworkMode.class);
1135    for (NetworkMode mode : NetworkMode.values()) {
1136      if ((flags & mode.getValue()) != 0) {
1137        rv.add(mode);
1138      }
1139    }
1140    return rv;
1141  }
1142
1143  /**
1144   * Starts local-only operation. Prevents calls to startServer or startClient from taking effect.
1145   * Has no effect if startServer or startClient has already been called.
1146   */
1147  public void startLocal() {
1148    NetworkTablesJNI.startLocal(m_handle);
1149  }
1150
1151  /**
1152   * Stops local-only operation. startServer or startClient can be called after this call to start
1153   * a server or client.
1154   */
1155  public void stopLocal() {
1156    NetworkTablesJNI.stopLocal(m_handle);
1157  }
1158
1159  /**
1160   * Starts a server using the networktables.json as the persistent file, using the default
1161   * listening address and port.
1162   */
1163  public void startServer() {
1164    startServer("networktables.json");
1165  }
1166
1167  /**
1168   * Starts a server using the specified persistent filename, using the default listening address
1169   * and port.
1170   *
1171   * @param persistFilename the name of the persist file to use
1172   */
1173  public void startServer(String persistFilename) {
1174    startServer(persistFilename, "");
1175  }
1176
1177  /**
1178   * Starts a server using the specified filename and listening address, using the default port.
1179   *
1180   * @param persistFilename the name of the persist file to use
1181   * @param listenAddress the address to listen on, or empty to listen on any address
1182   */
1183  public void startServer(String persistFilename, String listenAddress) {
1184    startServer(persistFilename, listenAddress, kDefaultPort);
1185  }
1186
1187  /**
1188   * Starts a server using the specified filename, listening address, and port.
1189   *
1190   * @param persistFilename the name of the persist file to use
1191   * @param listenAddress the address to listen on, or empty to listen on any address
1192   * @param port port to communicate over
1193   */
1194  public void startServer(String persistFilename, String listenAddress, int port) {
1195    NetworkTablesJNI.startServer(m_handle, persistFilename, listenAddress, port);
1196  }
1197
1198  /** Stops the server if it is running. */
1199  public void stopServer() {
1200    NetworkTablesJNI.stopServer(m_handle);
1201  }
1202
1203  /**
1204   * Starts a client. Use SetServer or SetServerTeam to set the server name and port.
1205   *
1206   * @param identity network identity to advertise (cannot be empty string)
1207   */
1208  public void startClient(String identity) {
1209    NetworkTablesJNI.startClient(m_handle, identity);
1210  }
1211
1212  /** Stops the client if it is running. */
1213  public void stopClient() {
1214    NetworkTablesJNI.stopClient(m_handle);
1215  }
1216
1217  /**
1218   * Sets server address and port for client (without restarting client). Changes the port to the
1219   * default port.
1220   *
1221   * @param serverName server name
1222   */
1223  public void setServer(String serverName) {
1224    setServer(serverName, 0);
1225  }
1226
1227  /**
1228   * Sets server address and port for client (without restarting client).
1229   *
1230   * @param serverName server name
1231   * @param port port to communicate over (0=default)
1232   */
1233  public void setServer(String serverName, int port) {
1234    NetworkTablesJNI.setServer(m_handle, serverName, port);
1235  }
1236
1237  /**
1238   * Sets server addresses and port for client (without restarting client). Changes the port to the
1239   * default port. The client will attempt to connect to each server in round robin fashion.
1240   *
1241   * @param serverNames array of server names
1242   */
1243  public void setServer(String[] serverNames) {
1244    setServer(serverNames, 0);
1245  }
1246
1247  /**
1248   * Sets server addresses and port for client (without restarting client). The client will attempt
1249   * to connect to each server in round robin fashion.
1250   *
1251   * @param serverNames array of server names
1252   * @param port port to communicate over (0=default)
1253   */
1254  public void setServer(String[] serverNames, int port) {
1255    int[] ports = new int[serverNames.length];
1256    for (int i = 0; i < serverNames.length; i++) {
1257      ports[i] = port;
1258    }
1259    setServer(serverNames, ports);
1260  }
1261
1262  /**
1263   * Sets server addresses and ports for client (without restarting client). The client will
1264   * attempt to connect to each server in round robin fashion.
1265   *
1266   * @param serverNames array of server names
1267   * @param ports array of port numbers (0=default)
1268   */
1269  public void setServer(String[] serverNames, int[] ports) {
1270    NetworkTablesJNI.setServer(m_handle, serverNames, ports);
1271  }
1272
1273  /**
1274   * Sets server addresses and port for client (without restarting client). Changes the port to the
1275   * default port. The client will attempt to connect to each server in round robin fashion.
1276   *
1277   * @param team team number
1278   */
1279  public void setServerTeam(int team) {
1280    setServerTeam(team, 0);
1281  }
1282
1283  /**
1284   * Sets server addresses and port for client (without restarting client). Connects using commonly
1285   * known robot addresses for the specified team.
1286   *
1287   * @param team team number
1288   * @param port port to communicate over (0=default)
1289   */
1290  public void setServerTeam(int team, int port) {
1291    NetworkTablesJNI.setServerTeam(m_handle, team, port);
1292  }
1293
1294  /**
1295   * Disconnects the client if it's running and connected. This will automatically start
1296   * reconnection attempts to the current server list.
1297   */
1298  public void disconnect() {
1299    NetworkTablesJNI.disconnect(m_handle);
1300  }
1301
1302  /**
1303   * Starts requesting server address from Driver Station. This connects to the Driver Station
1304   * running on localhost to obtain the server IP address, and connects with the default port.
1305   */
1306  public void startDSClient() {
1307    startDSClient(0);
1308  }
1309
1310  /**
1311   * Starts requesting server address from Driver Station. This connects to the Driver Station
1312   * running on localhost to obtain the server IP address.
1313   *
1314   * @param port server port to use in combination with IP from DS (0=default)
1315   */
1316  public void startDSClient(int port) {
1317    NetworkTablesJNI.startDSClient(m_handle, port);
1318  }
1319
1320  /** Stops requesting server address from Driver Station. */
1321  public void stopDSClient() {
1322    NetworkTablesJNI.stopDSClient(m_handle);
1323  }
1324
1325  /**
1326   * Flushes all updated values immediately to the local client/server. This does not flush to the
1327   * network.
1328   */
1329  public void flushLocal() {
1330    NetworkTablesJNI.flushLocal(m_handle);
1331  }
1332
1333  /**
1334   * Flushes all updated values immediately to the network. Note: This is rate-limited to protect
1335   * the network from flooding. This is primarily useful for synchronizing network updates with
1336   * user code.
1337   */
1338  public void flush() {
1339    NetworkTablesJNI.flush(m_handle);
1340  }
1341
1342  /**
1343   * Gets information on the currently established network connections. If operating as a client,
1344   * this will return either zero or one values.
1345   *
1346   * @return array of connection information
1347   */
1348  public ConnectionInfo[] getConnections() {
1349    return NetworkTablesJNI.getConnections(m_handle);
1350  }
1351
1352  /**
1353   * Return whether or not the instance is connected to another node.
1354   *
1355   * @return True if connected.
1356   */
1357  public boolean isConnected() {
1358    return NetworkTablesJNI.isConnected(m_handle);
1359  }
1360
1361  /**
1362   * Get the time offset between server time and local time. Add this value to local time to get
1363   * the estimated equivalent server time. In server mode, this always returns 0. In client mode,
1364   * this returns the time offset only if the client and server are connected and have exchanged
1365   * synchronization messages. Note the time offset may change over time as it is periodically
1366   * updated; to receive updates as events, add a listener to the "time sync" event.
1367   *
1368   * @return Time offset in microseconds (optional)
1369   */
1370  public OptionalLong getServerTimeOffset() {
1371    return NetworkTablesJNI.getServerTimeOffset(m_handle);
1372  }
1373
1374  /**
1375   * Starts logging entry changes to a DataLog.
1376   *
1377   * @param log data log object; lifetime must extend until StopEntryDataLog is called or the
1378   *     instance is destroyed
1379   * @param prefix only store entries with names that start with this prefix; the prefix is not
1380   *     included in the data log entry name
1381   * @param logPrefix prefix to add to data log entry names
1382   * @return Data logger handle
1383   */
1384  public int startEntryDataLog(DataLog log, String prefix, String logPrefix) {
1385    return NetworkTablesJNI.startEntryDataLog(m_handle, log, prefix, logPrefix);
1386  }
1387
1388  /**
1389   * Stops logging entry changes to a DataLog.
1390   *
1391   * @param logger data logger handle
1392   */
1393  public static void stopEntryDataLog(int logger) {
1394    NetworkTablesJNI.stopEntryDataLog(logger);
1395  }
1396
1397  /**
1398   * Starts logging connection changes to a DataLog.
1399   *
1400   * @param log data log object; lifetime must extend until StopConnectionDataLog is called or the
1401   *     instance is destroyed
1402   * @param name data log entry name
1403   * @return Data logger handle
1404   */
1405  public int startConnectionDataLog(DataLog log, String name) {
1406    return NetworkTablesJNI.startConnectionDataLog(m_handle, log, name);
1407  }
1408
1409  /**
1410   * Stops logging connection changes to a DataLog.
1411   *
1412   * @param logger data logger handle
1413   */
1414  public static void stopConnectionDataLog(int logger) {
1415    NetworkTablesJNI.stopConnectionDataLog(logger);
1416  }
1417
1418  /**
1419   * Add logger callback function. By default, log messages are sent to stderr; this function sends
1420   * log messages with the specified levels to the provided callback function instead. The callback
1421   * function will only be called for log messages with level greater than or equal to minLevel and
1422   * less than or equal to maxLevel; messages outside this range will be silently ignored.
1423   *
1424   * @param minLevel minimum log level
1425   * @param maxLevel maximum log level
1426   * @param func callback function
1427   * @return Listener handle
1428   */
1429  public int addLogger(int minLevel, int maxLevel, Consumer<NetworkTableEvent> func) {
1430    return m_listeners.addLogger(minLevel, maxLevel, func);
1431  }
1432
1433  /**
1434   * Returns whether there is a data schema already registered with the given name that this
1435   * instance has published. This does NOT perform a check as to whether the schema has already
1436   * been published by another node on the network.
1437   *
1438   * @param name Name (the string passed as the data type for topics using this schema)
1439   * @return True if schema already registered
1440   */
1441  public boolean hasSchema(String name) {
1442    return m_schemas.containsKey("/.schema/" + name);
1443  }
1444
1445  /**
1446   * Registers a data schema. Data schemas provide information for how a certain data type string
1447   * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
1448   * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas
1449   * are published just like normal topics, with the name being generated from the provided name:
1450   * "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
1451   *
1452   * @param name Name (the string passed as the data type for topics using this schema)
1453   * @param type Type of schema (e.g. "protobuf", "struct", etc)
1454   * @param schema Schema data
1455   */
1456  public void addSchema(String name, String type, byte[] schema) {
1457    m_schemas.computeIfAbsent("/.schema/" + name, k -> {
1458      RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}");
1459      pub.setDefault(schema);
1460      return pub;
1461    });
1462  }
1463
1464  /**
1465   * Registers a data schema. Data schemas provide information for how a certain data type string
1466   * can be decoded. The type string of a data schema indicates the type of the schema itself (e.g.
1467   * "protobuf" for protobuf schemas, "struct" for struct schemas, etc). In NetworkTables, schemas
1468   * are published just like normal topics, with the name being generated from the provided name:
1469   * "/.schema/name". Duplicate calls to this function with the same name are silently ignored.
1470   *
1471   * @param name Name (the string passed as the data type for topics using this schema)
1472   * @param type Type of schema (e.g. "protobuf", "struct", etc)
1473   * @param schema Schema data
1474   */
1475  public void addSchema(String name, String type, String schema) {
1476    m_schemas.computeIfAbsent("/.schema/" + name, k -> {
1477      RawPublisher pub = getRawTopic(k).publishEx(type, "{\"retained\":true}");
1478      pub.setDefault(StandardCharsets.UTF_8.encode(schema));
1479      return pub;
1480    });
1481  }
1482
1483  /**
1484   * Registers a protobuf schema. Duplicate calls to this function with the same name are silently
1485   * ignored.
1486   *
1487   * @param proto protobuf serialization object
1488   */
1489  public void addSchema(Protobuf<?, ?> proto) {
1490    proto.forEachDescriptor(
1491        this::hasSchema,
1492        (typeString, schema) -> addSchema(typeString, "proto:FileDescriptorProto", schema));
1493  }
1494
1495  /**
1496   * Registers a struct schema. Duplicate calls to this function with the same name are silently
1497   * ignored.
1498   *
1499   * @param struct struct serialization object
1500   */
1501  public void addSchema(Struct<?> struct) {
1502    addSchemaImpl(struct, new HashSet<>());
1503  }
1504
1505  @Override
1506  public boolean equals(Object other) {
1507    return other == this || other instanceof NetworkTableInstance inst && m_handle == inst.m_handle;
1508  }
1509
1510  @Override
1511  public int hashCode() {
1512    return m_handle;
1513  }
1514
1515  private void addSchemaImpl(Struct<?> struct, Set<String> seen) {
1516    String typeString = struct.getTypeString();
1517    if (hasSchema(typeString)) {
1518      return;
1519    }
1520    if (!seen.add(typeString)) {
1521      throw new UnsupportedOperationException(typeString + ": circular reference with " + seen);
1522    }
1523    addSchema(typeString, "structschema", struct.getSchema());
1524    for (Struct<?> inner : struct.getNested()) {
1525      addSchemaImpl(inner, seen);
1526    }
1527    seen.remove(typeString);
1528  }
1529
1530  private boolean m_owned;
1531  private int m_handle;
1532  private final ConcurrentMap<String, RawPublisher> m_schemas = new ConcurrentHashMap<>();
1533}