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
/**
* Direct WebSocket connection to our own ClawdNode Gateway.
* No authentication, no restrictions - full bidirectional control.
* Direct WebSocket connection to ClawdNode Gateway.
* 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 {
private const val TAG = "DirectGateway"
private const val PROTOCOL_VERSION = "clawdnode/1.0"
// Our gateway - Tailscale IP of james server
private const val GATEWAY_URL = "ws://100.123.216.65:9878"
// Default gateway URL (can be overridden via TokenStore)
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()
.connectTimeout(10, TimeUnit.SECONDS)
@ -34,31 +48,34 @@ object DirectGateway {
var onCallReject: ((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() {
if (webSocket != null) {
Log.d(TAG, "Already connected or connecting")
log("Already connected or connecting")
return
}
Log.i(TAG, "Connecting to $GATEWAY_URL")
DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $GATEWAY_URL")
val url = gatewayUrl
log("Connecting to $url")
DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $url")
val request = Request.Builder()
.url(GATEWAY_URL)
.url(url)
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(ws: WebSocket, response: Response) {
Log.i(TAG, "Connected to gateway")
isConnected = true
DebugClient.lifecycle("DIRECT_GATEWAY", "Connected")
// Send hello
send(mapOf(
"type" to "hello",
"client" to "clawdnode-android",
"version" to "0.1.0"
))
log("WebSocket connected, waiting for welcome...")
DebugClient.lifecycle("DIRECT_GATEWAY", "Connected, waiting for welcome")
// Don't set isConnected yet - wait for protocol handshake
}
override fun onMessage(ws: WebSocket, text: String) {
@ -72,17 +89,19 @@ object DirectGateway {
}
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
Log.i(TAG, "Connection closed: $code $reason")
log("Connection closed: $code $reason")
isConnected = false
webSocket = null
onConnectionChange?.invoke(false)
DebugClient.lifecycle("DIRECT_GATEWAY", "Disconnected: $code $reason")
scheduleReconnect()
}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "Connection failed", t)
log("Connection failed: ${t.message}")
isConnected = false
webSocket = null
onConnectionChange?.invoke(false)
DebugClient.error("DirectGateway connection failed", t)
scheduleReconnect()
}
@ -110,11 +129,45 @@ object DirectGateway {
val type = json.optString("type", "")
when (type) {
"hello" -> {
Log.i(TAG, "Received hello from server")
DebugClient.log("DirectGateway hello received", mapOf(
"clientId" to json.optString("clientId")
"welcome" -> {
// Server welcome - send our hello
val protocol = json.optString("protocol", "unknown")
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" -> {

View File

@ -50,8 +50,8 @@ class TokenStore(context: Context) {
private const val KEY_GATEWAY_TOKEN = "gateway_token"
private const val KEY_NODE_ID = "node_id"
// Default values for testing
private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:18789"
private const val DEFAULT_GATEWAY_TOKEN = "2dee57cc3ce2947c27ce9e848d5c3e95cc452f25a1477462"
// Default values - ClawdNode custom gateway (not Clawdbot)
private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:9878"
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 com.inou.clawdnode.ClawdNodeApp
import com.inou.clawdnode.R
import com.inou.clawdnode.gateway.DirectGateway
import com.inou.clawdnode.protocol.*
import com.inou.clawdnode.ui.MainActivity
@ -22,8 +23,6 @@ class NodeService : Service() {
private val tag = "NodeService"
private val binder = LocalBinder()
private lateinit var gatewayClient: GatewayClient
private var isConnected = false
// Callbacks for UI updates
@ -40,15 +39,29 @@ class NodeService : Service() {
super.onCreate()
Log.i(tag, "NodeService created")
gatewayClient = GatewayClient(
onCommand = { command -> handleCommand(command) },
onConnectionChange = { connected ->
isConnected = connected
updateNotification()
onConnectionChange?.invoke(connected)
},
onLog = { message -> onLogMessage?.invoke(message) }
)
// Use DirectGateway (ClawdNode protocol) instead of GatewayClient (Clawdbot protocol)
DirectGateway.onConnectionChange = { connected ->
isConnected = connected
updateNotification()
onConnectionChange?.invoke(connected)
}
DirectGateway.onLog = { 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 {
@ -57,19 +70,15 @@ class NodeService : Service() {
// Start as foreground service
startForeground(NOTIFICATION_ID, createNotification())
// Connect to gateway
if (ClawdNodeApp.instance.tokenStore.isConfigured) {
gatewayClient.connect()
} else {
Log.w(tag, "Gateway not configured, waiting for setup")
}
// Connect to gateway (DirectGateway uses URL from TokenStore)
DirectGateway.connect()
return START_STICKY
}
override fun onDestroy() {
Log.i(tag, "NodeService destroyed")
gatewayClient.disconnect()
DirectGateway.disconnect()
ClawdNodeApp.instance.auditLog.log("SERVICE_STOP", "NodeService destroyed")
super.onDestroy()
}
@ -79,102 +88,16 @@ class NodeService : Service() {
// ========================================
fun connect() {
gatewayClient.connect()
DirectGateway.connect()
}
fun disconnect() {
gatewayClient.disconnect()
DirectGateway.disconnect()
}
fun isConnected() = isConnected
fun sendEvent(event: NodeEvent) {
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))
}
}
// Note: Command handling is done via DirectGateway callbacks set in onCreate()
// ========================================
// NOTIFICATION