Gateway and service improvements

- DirectGateway updates
- TokenStore improvements
- NodeService refinements
This commit is contained in:
James (ClawdBot) 2026-02-04 22:58:46 -05:00
parent b8d1705c9c
commit ed7e23003c
3 changed files with 110 additions and 134 deletions

View File

@ -9,14 +9,28 @@ import org.json.JSONObject
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
* Direct WebSocket connection to our own ClawdNode Gateway. * Direct WebSocket connection to ClawdNode Gateway.
* No authentication, no restrictions - full bidirectional control. * Uses ClawdNode protocol (not Clawdbot protocol).
*
* Protocol:
* 1. Server sends: {"type":"welcome","protocol":"clawdnode/1.0",...}
* 2. Client sends: {"type":"hello","client":"clawdnode-android","version":"..."}
* 3. Server sends: {"type":"ready","message":"Connected"}
* 4. Bidirectional events/commands
*/ */
object DirectGateway { object DirectGateway {
private const val TAG = "DirectGateway" private const val TAG = "DirectGateway"
private const val PROTOCOL_VERSION = "clawdnode/1.0"
// Our gateway - Tailscale IP of james server // Default gateway URL (can be overridden via TokenStore)
private const val GATEWAY_URL = "ws://100.123.216.65:9878" private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:9878"
// Get URL from TokenStore or use default
private val gatewayUrl: String
get() = ClawdNodeApp.instance.tokenStore.gatewayUrl?.let { url ->
// Ensure it's a WebSocket URL
url.replace("http://", "ws://").replace("https://", "wss://").trimEnd('/')
} ?: DEFAULT_GATEWAY_URL
private val client = OkHttpClient.Builder() private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
@ -34,31 +48,34 @@ object DirectGateway {
var onCallReject: ((callId: String) -> Unit)? = null var onCallReject: ((callId: String) -> Unit)? = null
var onCallHangup: ((callId: String) -> Unit)? = null var onCallHangup: ((callId: String) -> Unit)? = null
// Connection state callback
var onConnectionChange: ((Boolean) -> Unit)? = null
var onLog: ((String) -> Unit)? = null
private fun log(message: String) {
Log.d(TAG, message)
onLog?.invoke(message)
}
fun connect() { fun connect() {
if (webSocket != null) { if (webSocket != null) {
Log.d(TAG, "Already connected or connecting") log("Already connected or connecting")
return return
} }
Log.i(TAG, "Connecting to $GATEWAY_URL") val url = gatewayUrl
DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $GATEWAY_URL") log("Connecting to $url")
DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $url")
val request = Request.Builder() val request = Request.Builder()
.url(GATEWAY_URL) .url(url)
.build() .build()
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) { override fun onOpen(ws: WebSocket, response: Response) {
Log.i(TAG, "Connected to gateway") log("WebSocket connected, waiting for welcome...")
isConnected = true DebugClient.lifecycle("DIRECT_GATEWAY", "Connected, waiting for welcome")
DebugClient.lifecycle("DIRECT_GATEWAY", "Connected") // Don't set isConnected yet - wait for protocol handshake
// Send hello
send(mapOf(
"type" to "hello",
"client" to "clawdnode-android",
"version" to "0.1.0"
))
} }
override fun onMessage(ws: WebSocket, text: String) { override fun onMessage(ws: WebSocket, text: String) {
@ -72,17 +89,19 @@ object DirectGateway {
} }
override fun onClosed(ws: WebSocket, code: Int, reason: String) { override fun onClosed(ws: WebSocket, code: Int, reason: String) {
Log.i(TAG, "Connection closed: $code $reason") log("Connection closed: $code $reason")
isConnected = false isConnected = false
webSocket = null webSocket = null
onConnectionChange?.invoke(false)
DebugClient.lifecycle("DIRECT_GATEWAY", "Disconnected: $code $reason") DebugClient.lifecycle("DIRECT_GATEWAY", "Disconnected: $code $reason")
scheduleReconnect() scheduleReconnect()
} }
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "Connection failed", t) log("Connection failed: ${t.message}")
isConnected = false isConnected = false
webSocket = null webSocket = null
onConnectionChange?.invoke(false)
DebugClient.error("DirectGateway connection failed", t) DebugClient.error("DirectGateway connection failed", t)
scheduleReconnect() scheduleReconnect()
} }
@ -110,11 +129,45 @@ object DirectGateway {
val type = json.optString("type", "") val type = json.optString("type", "")
when (type) { when (type) {
"hello" -> { "welcome" -> {
Log.i(TAG, "Received hello from server") // Server welcome - send our hello
DebugClient.log("DirectGateway hello received", mapOf( val protocol = json.optString("protocol", "unknown")
"clientId" to json.optString("clientId") log("Received welcome (protocol: $protocol)")
DebugClient.log("DirectGateway welcome received", mapOf(
"protocol" to protocol
)) ))
// Check protocol compatibility
if (!protocol.startsWith("clawdnode/")) {
log("WARNING: Unexpected protocol: $protocol")
}
// Send hello
webSocket?.send(JSONObject(mapOf(
"type" to "hello",
"client" to "clawdnode-android",
"version" to "0.1.0"
)).toString())
}
"ready" -> {
// Server confirmed connection
log("Connection ready: ${json.optString("message")}")
isConnected = true
onConnectionChange?.invoke(true)
DebugClient.lifecycle("DIRECT_GATEWAY", "Ready - fully connected")
}
"error" -> {
// Server error
val code = json.optString("code")
val message = json.optString("message")
log("Server error [$code]: $message")
DebugClient.error("Gateway error: $code - $message", null)
if (code == "WRONG_PROTOCOL") {
log("ERROR: Connected to wrong server! Check gateway URL and port.")
}
} }
"command" -> { "command" -> {

View File

@ -50,8 +50,8 @@ class TokenStore(context: Context) {
private const val KEY_GATEWAY_TOKEN = "gateway_token" private const val KEY_GATEWAY_TOKEN = "gateway_token"
private const val KEY_NODE_ID = "node_id" private const val KEY_NODE_ID = "node_id"
// Default values for testing // Default values - ClawdNode custom gateway (not Clawdbot)
private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:18789" private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:9878"
private const val DEFAULT_GATEWAY_TOKEN = "2dee57cc3ce2947c27ce9e848d5c3e95cc452f25a1477462" private const val DEFAULT_GATEWAY_TOKEN = "" // No token needed for custom gateway
} }
} }

View File

@ -10,6 +10,7 @@ import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.inou.clawdnode.ClawdNodeApp import com.inou.clawdnode.ClawdNodeApp
import com.inou.clawdnode.R import com.inou.clawdnode.R
import com.inou.clawdnode.gateway.DirectGateway
import com.inou.clawdnode.protocol.* import com.inou.clawdnode.protocol.*
import com.inou.clawdnode.ui.MainActivity import com.inou.clawdnode.ui.MainActivity
@ -22,8 +23,6 @@ class NodeService : Service() {
private val tag = "NodeService" private val tag = "NodeService"
private val binder = LocalBinder() private val binder = LocalBinder()
private lateinit var gatewayClient: GatewayClient
private var isConnected = false private var isConnected = false
// Callbacks for UI updates // Callbacks for UI updates
@ -40,15 +39,29 @@ class NodeService : Service() {
super.onCreate() super.onCreate()
Log.i(tag, "NodeService created") Log.i(tag, "NodeService created")
gatewayClient = GatewayClient( // Use DirectGateway (ClawdNode protocol) instead of GatewayClient (Clawdbot protocol)
onCommand = { command -> handleCommand(command) }, DirectGateway.onConnectionChange = { connected ->
onConnectionChange = { connected -> isConnected = connected
isConnected = connected updateNotification()
updateNotification() onConnectionChange?.invoke(connected)
onConnectionChange?.invoke(connected) }
}, DirectGateway.onLog = { message ->
onLog = { message -> onLogMessage?.invoke(message) } onLogMessage?.invoke(message)
) }
// Wire up command handlers
DirectGateway.onNotificationAction = { notificationId, action, replyText ->
NotificationManager.triggerAction(notificationId, action, replyText)
}
DirectGateway.onCallAnswer = { callId ->
CallManager.answer(callId, null)
}
DirectGateway.onCallReject = { callId ->
CallManager.reject(callId, null)
}
DirectGateway.onCallHangup = { callId ->
CallManager.hangup(callId)
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -57,19 +70,15 @@ class NodeService : Service() {
// Start as foreground service // Start as foreground service
startForeground(NOTIFICATION_ID, createNotification()) startForeground(NOTIFICATION_ID, createNotification())
// Connect to gateway // Connect to gateway (DirectGateway uses URL from TokenStore)
if (ClawdNodeApp.instance.tokenStore.isConfigured) { DirectGateway.connect()
gatewayClient.connect()
} else {
Log.w(tag, "Gateway not configured, waiting for setup")
}
return START_STICKY return START_STICKY
} }
override fun onDestroy() { override fun onDestroy() {
Log.i(tag, "NodeService destroyed") Log.i(tag, "NodeService destroyed")
gatewayClient.disconnect() DirectGateway.disconnect()
ClawdNodeApp.instance.auditLog.log("SERVICE_STOP", "NodeService destroyed") ClawdNodeApp.instance.auditLog.log("SERVICE_STOP", "NodeService destroyed")
super.onDestroy() super.onDestroy()
} }
@ -79,102 +88,16 @@ class NodeService : Service() {
// ======================================== // ========================================
fun connect() { fun connect() {
gatewayClient.connect() DirectGateway.connect()
} }
fun disconnect() { fun disconnect() {
gatewayClient.disconnect() DirectGateway.disconnect()
} }
fun isConnected() = isConnected fun isConnected() = isConnected
fun sendEvent(event: NodeEvent) { // Note: Command handling is done via DirectGateway callbacks set in onCreate()
gatewayClient.send(event)
}
// ========================================
// COMMAND HANDLING
// ========================================
private fun handleCommand(command: NodeCommand) {
Log.d(tag, "Handling command: ${command::class.simpleName}")
try {
when (command) {
is ScreenshotCommand -> handleScreenshot(command)
is NotificationActionCommand -> handleNotificationAction(command)
is NotificationDismissCommand -> handleNotificationDismiss(command)
is CallAnswerCommand -> handleCallAnswer(command)
is CallRejectCommand -> handleCallReject(command)
is CallSilenceCommand -> handleCallSilence(command)
is CallSpeakCommand -> handleCallSpeak(command)
is CallHangupCommand -> handleCallHangup(command)
}
} catch (e: Exception) {
Log.e(tag, "Error handling command", e)
command.requestId?.let {
gatewayClient.sendResponse(it, false, error = e.message)
}
}
}
private fun handleScreenshot(cmd: ScreenshotCommand) {
// TODO: Implement screenshot capture via MediaProjection
Log.d(tag, "Screenshot requested - not yet implemented")
cmd.requestId?.let {
gatewayClient.sendResponse(it, false, error = "Screenshot not yet implemented")
}
}
private fun handleNotificationAction(cmd: NotificationActionCommand) {
// Delegate to NotificationListener
NotificationManager.triggerAction(cmd.notificationId, cmd.action, cmd.text)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("triggered" to true))
}
}
private fun handleNotificationDismiss(cmd: NotificationDismissCommand) {
NotificationManager.dismiss(cmd.notificationId)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("dismissed" to true))
}
}
private fun handleCallAnswer(cmd: CallAnswerCommand) {
CallManager.answer(cmd.callId, cmd.greeting)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("answered" to true))
}
}
private fun handleCallReject(cmd: CallRejectCommand) {
CallManager.reject(cmd.callId, cmd.reason)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("rejected" to true))
}
}
private fun handleCallSilence(cmd: CallSilenceCommand) {
CallManager.silence(cmd.callId)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("silenced" to true))
}
}
private fun handleCallSpeak(cmd: CallSpeakCommand) {
CallManager.speak(cmd.callId, cmd.text, cmd.voice)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("speaking" to true))
}
}
private fun handleCallHangup(cmd: CallHangupCommand) {
CallManager.hangup(cmd.callId)
cmd.requestId?.let {
gatewayClient.sendResponse(it, true, payload = mapOf("hungup" to true))
}
}
// ======================================== // ========================================
// NOTIFICATION // NOTIFICATION