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