Class UltiPanelWebSocketClient

java.lang.Object
org.java_websocket.WebSocketAdapter
org.java_websocket.AbstractWebSocket
org.java_websocket.client.WebSocketClient
com.ultikits.ultitools.websocket.UltiPanelWebSocketClient
All Implemented Interfaces:
Runnable, org.java_websocket.WebSocket, org.java_websocket.WebSocketListener

public class UltiPanelWebSocketClient extends org.java_websocket.client.WebSocketClient
UltiPanel WebSocket Client

New WebSocket client based on Java-WebSocket library

  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    private LongSupplier
    Clock.
    private final com.google.gson.Gson
     
    private static final long
    Heartbeat interval, in seconds.
     
    private ScheduledFuture<?>
     
    private static final long
     
    private boolean
     
    private boolean
     
    private long
    Timestamp of the most recent ping sent.
    private long
    Timestamp of the most recent pong received.
    private long
    Most recently measured round-trip latency, in milliseconds.
    private static final int
     
    private Consumer<com.google.gson.JsonObject>
     
    private Runnable
     
    private Runnable
     
    private Consumer<String>
     
    private Runnable
     
    private static final long
    Threshold for declaring a connection "silently dead": no pong received across two heartbeat intervals.
    private int
     
    private final String
     
    private final String
     

    Fields inherited from class org.java_websocket.client.WebSocketClient

    uri
  • Constructor Summary

    Constructors
    Constructor
    Description
    UltiPanelWebSocketClient(String url, String serverId, String token)
    Constructor.
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    Connects to the WebSocket server.
    void
    Disconnects the WebSocket connection.
    private static Map<String,String>
     
    long
    Timestamp of the most recent pong received; 0 if none has ever been received.
    long
    Most recently measured round-trip latency, in milliseconds; -1 if not yet measured.
    (package private) void
    One heartbeat: check liveness first, then send a ping.
    boolean
    isAlive(long timeoutMs)
    Determines whether the connection is still alive, based on pong responses.
    void
    onClose(int code, String reason, boolean remote)
     
    void
     
    void
    onMessage(String message)
     
    void
    onOpen(org.java_websocket.handshake.ServerHandshake handshakedata)
     
    private void
    Called when a pong is received: records the time and computes the round-trip latency.
    void
    sendMessage(com.google.gson.JsonObject message)
    Sends a message to the server.
    void
    Sends a ping message.
    (package private) void
    Test-only clock substitution.
    void
    setMessageHandler(Consumer<com.google.gson.JsonObject> handler)
    Sets the message handler.
    void
    Sets the on-connect handler.
    void
    Sets the on-disconnect handler.
    void
    Sets the error handler.
    void
    Sets the reconnect-exhausted handler (invoked once every reconnection attempt has failed).
    private void
    Starts the heartbeat task.
    void
    Subscribes to a server's status.
    void
    Unsubscribes from a server's status.

    Methods inherited from class org.java_websocket.client.WebSocketClient

    addHeader, clearHeaders, close, close, close, closeBlocking, closeConnection, connectBlocking, connectBlocking, getAttachment, getConnection, getConnections, getDraft, getLocalSocketAddress, getLocalSocketAddress, getProtocol, getReadyState, getRemoteSocketAddress, getRemoteSocketAddress, getResourceDescriptor, getSocket, getSSLSession, getURI, hasBufferedData, hasSSLSupport, isClosed, isClosing, isFlushAndClose, isOpen, onCloseInitiated, onClosing, onMessage, onSetSSLParameters, onWebsocketClose, onWebsocketCloseInitiated, onWebsocketClosing, onWebsocketError, onWebsocketMessage, onWebsocketMessage, onWebsocketOpen, onWriteDemand, reconnect, reconnectBlocking, removeHeader, run, send, send, send, sendFragmentedFrame, sendFrame, sendFrame, setAttachment, setDnsResolver, setProxy, setSocket, setSocketFactory

    Methods inherited from class org.java_websocket.AbstractWebSocket

    getConnectionLostTimeout, isReuseAddr, isTcpNoDelay, setConnectionLostTimeout, setReuseAddr, setTcpNoDelay, startConnectionLostTimer, stopConnectionLostTimer

    Methods inherited from class org.java_websocket.WebSocketAdapter

    onPreparePing, onWebsocketHandshakeReceivedAsClient, onWebsocketHandshakeReceivedAsServer, onWebsocketHandshakeSentAsClient, onWebsocketPing, onWebsocketPong

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • serverId

      private final String serverId
    • token

      private final String token
    • heartbeatExecutor

      private final ScheduledExecutorService heartbeatExecutor
    • isConnected

      private boolean isConnected
    • heartbeatTask

      private ScheduledFuture<?> heartbeatTask
    • gson

      private final com.google.gson.Gson gson
    • messageHandler

      private Consumer<com.google.gson.JsonObject> messageHandler
    • onConnectHandler

      private Runnable onConnectHandler
    • onDisconnectHandler

      private Runnable onDisconnectHandler
    • onErrorHandler

      private Consumer<String> onErrorHandler
    • onReconnectExhaustedHandler

      private Runnable onReconnectExhaustedHandler
    • MAX_RECONNECT_ATTEMPTS

      private static final int MAX_RECONNECT_ATTEMPTS
      See Also:
    • INITIAL_RECONNECT_DELAY_MS

      private static final long INITIAL_RECONNECT_DELAY_MS
      See Also:
    • reconnectAttempts

      private int reconnectAttempts
    • intentionalDisconnect

      private boolean intentionalDisconnect
    • HEARTBEAT_INTERVAL_SECONDS

      private static final long HEARTBEAT_INTERVAL_SECONDS
      Heartbeat interval, in seconds.
      See Also:
    • PONG_TIMEOUT_MS

      private static final long PONG_TIMEOUT_MS
      Threshold for declaring a connection "silently dead": no pong received across two heartbeat intervals.

      Two intervals rather than one, to tolerate a single dropped packet or one scheduling jitter - the cost of a false positive is kicking a good connection into reconnecting, while a false negative only delays detection by one interval.

      See Also:
    • lastPingTime

      private volatile long lastPingTime
      Timestamp of the most recent ping sent.
    • lastPongTime

      private volatile long lastPongTime
      Timestamp of the most recent pong received. 0 means none has ever been received.
    • latencyMs

      private volatile long latencyMs
      Most recently measured round-trip latency, in milliseconds.
    • clock

      private volatile LongSupplier clock
      Clock. In production this is simply System::currentTimeMillis; tests can substitute a fake clock.

      The only reason this exists is so the "no pong received past the threshold" check can be covered by a unit test without Thread.sleep - actually waiting out two heartbeat intervals is 120 seconds, and nobody keeps a test like that around.

  • Constructor Details

    • UltiPanelWebSocketClient

      public UltiPanelWebSocketClient(String url, String serverId, String token) throws URISyntaxException
      Constructor.
      Parameters:
      url - the WebSocket server URL
      serverId - the server ID
      token - the auth token
      Throws:
      URISyntaxException - if the URL is malformed
  • Method Details

    • getHeaders

      private static Map<String,String> getHeaders(String token)
    • connect

      public void connect()
      Connects to the WebSocket server.
      Overrides:
      connect in class org.java_websocket.client.WebSocketClient
    • disconnect

      public void disconnect()
      Disconnects the WebSocket connection.
    • sendMessage

      public void sendMessage(com.google.gson.JsonObject message)
      Sends a message to the server.
      Parameters:
      message - the JSON message object
    • sendPing

      public void sendPing()
      Sends a ping message.
      Specified by:
      sendPing in interface org.java_websocket.WebSocket
      Overrides:
      sendPing in class org.java_websocket.client.WebSocketClient
    • subscribeToServer

      public void subscribeToServer(String serverId)
      Subscribes to a server's status.
      Parameters:
      serverId - the server ID to subscribe to
    • unsubscribeFromServer

      public void unsubscribeFromServer(String serverId)
      Unsubscribes from a server's status.
      Parameters:
      serverId - the server ID to unsubscribe from
    • setMessageHandler

      public void setMessageHandler(Consumer<com.google.gson.JsonObject> handler)
      Sets the message handler.
      Parameters:
      handler - the message handler
    • setOnConnectHandler

      public void setOnConnectHandler(Runnable handler)
      Sets the on-connect handler.
      Parameters:
      handler - the on-connect handler
    • setOnDisconnectHandler

      public void setOnDisconnectHandler(Runnable handler)
      Sets the on-disconnect handler.
      Parameters:
      handler - the on-disconnect handler
    • setOnErrorHandler

      public void setOnErrorHandler(Consumer<String> handler)
      Sets the error handler.
      Parameters:
      handler - the error handler
    • setOnReconnectExhaustedHandler

      public void setOnReconnectExhaustedHandler(Runnable handler)
      Sets the reconnect-exhausted handler (invoked once every reconnection attempt has failed).
      Parameters:
      handler - the reconnect-exhausted handler
    • startHeartbeat

      private void startHeartbeat()
      Starts the heartbeat task.
    • heartbeatTick

      void heartbeatTick()
      One heartbeat: check liveness first, then send a ping.

      "Check liveness first" is the reason this method exists. Before it, the only signal of connection health was whether the socket had closed - and a TCP connection can go silently dead without ever producing onClose (an intermediate device timing out, the peer process hanging, a NAT table entry expiring). In that state pings still go out, onClose never fires, and reconnection logic never starts - the connection looks fine but is actually dead.

      When a silent failure is declared, this method **routes through close() rather than starting a second reconnection path of its own**: closing triggers onClose, reusing the existing reconnect state machine (including the global budget #181 added). Starting a second path would become a second place deciding whether to reconnect - exactly what caused #181.

    • recordPong

      private void recordPong()
      Called when a pong is received: records the time and computes the round-trip latency.

      Called by onMessage(String) before dispatching to messageHandler - a pong is a link-layer fact and should not depend on whether an upper-layer handler happens to be wired up.

    • isAlive

      public boolean isAlive(long timeoutMs)
      Determines whether the connection is still alive, based on pong responses.

      Returns true when no pong has ever been received. This is deliberate: otherwise a newly established connection would be declared dead within its first heartbeat interval. The cost is that this mechanism cannot detect "the peer never answers pong at all" - but that is the safe direction: if the panel simply never implemented pong, a good connection should not be repeatedly kicked. What it can detect is "used to answer, and stopped" - that is, a silent failure.

      Parameters:
      timeoutMs - the liveness threshold, in milliseconds
      Returns:
      true if the most recent pong is within the threshold, or none has ever been received
    • getLatencyMs

      public long getLatencyMs()
      Most recently measured round-trip latency, in milliseconds; -1 if not yet measured.
      Returns:
      the latency in milliseconds, or -1
    • getLastPongTime

      public long getLastPongTime()
      Timestamp of the most recent pong received; 0 if none has ever been received.
      Returns:
      the timestamp, in milliseconds
    • setClock

      @Internal void setClock(LongSupplier clock)
      Test-only clock substitution.
    • onOpen

      public void onOpen(org.java_websocket.handshake.ServerHandshake handshakedata)
      Specified by:
      onOpen in class org.java_websocket.client.WebSocketClient
    • onMessage

      public void onMessage(String message)
      Specified by:
      onMessage in class org.java_websocket.client.WebSocketClient
    • onClose

      public void onClose(int code, String reason, boolean remote)
      Specified by:
      onClose in class org.java_websocket.client.WebSocketClient
    • onError

      public void onError(Exception ex)
      Specified by:
      onError in class org.java_websocket.client.WebSocketClient