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