Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d8282a3
Add Golf Mode: AI caddie with GPS, scorecard tracking, and club recom…
Feb 27, 2026
b5fbdee
Fix meeting mode disconnect and audio-only session state reset
Feb 27, 2026
36fd7ac
Major reliability overhaul: crash fixes, Discord notes, translation m…
Mar 4, 2026
8afc9ac
auto: update gateway-proxy.js,ngrok-url.sh,samples/CameraAccess/Camer…
Mar 9, 2026
3f85e0f
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsManag…
Mar 9, 2026
7f01190
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsManag…
Mar 9, 2026
79e6ff7
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsView.…
Mar 9, 2026
b0dd470
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsView.…
Mar 9, 2026
71878d4
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsView.…
Mar 9, 2026
152c256
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsView.…
Mar 9, 2026
141105f
auto: update samples/CameraAccess/CameraAccess/Gemini/ClubDistanceMod…
Mar 9, 2026
4c269b0
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiSessionVi…
Mar 9, 2026
3ee0e7c
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiSessionVi…
Mar 9, 2026
1640d64
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiSessionVi…
Mar 9, 2026
8116c49
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiSessionVi…
Mar 9, 2026
4f5ea5e
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift
Mar 9, 2026
67e58a6
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift
Mar 9, 2026
45da194
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift
Mar 9, 2026
9b78e29
auto: update samples/CameraAccess/CameraAccess/Views/Components/GolfO…
Mar 9, 2026
470e3e3
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiSessionVi…
Mar 9, 2026
06b6067
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
ef4255a
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
ce824d4
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
91b5d86
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
c5e2ae1
auto: update samples/CameraAccess/CameraAccess/Settings/KeychainHelpe…
Mar 9, 2026
96a2cbe
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsManag…
Mar 9, 2026
1d426cd
auto: update samples/CameraAccess/CameraAccess/Settings/SettingsView.…
Mar 9, 2026
8536d57
auto: update samples/CameraAccess/server/index.js
Mar 9, 2026
c9cc1ee
auto: update samples/CameraAccess/server/index.js
Mar 9, 2026
302fbfe
auto: update samples/CameraAccess/server/index.js
Mar 9, 2026
ad555e1
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
ec2d645
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
2ca2e38
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
d6613e0
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
ae7e044
auto: update samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Mar 9, 2026
2c09be2
auto: update samples/CameraAccess/CameraAccess/Gemini/GeminiConfig.swift
Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
119 changes: 119 additions & 0 deletions gateway-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
// Unified Gateway Proxy for VisionClaw
// Routes through a single ngrok tunnel:
// - WebSocket connections → Signaling server (port 8080)
// - HTTP requests → OpenClaw gateway (port 18789)
//
// This allows one ngrok URL to handle everything.

const http = require("http");
const net = require("net");

const PROXY_PORT = parseInt(process.env.PROXY_PORT || "19000");
const OPENCLAW_PORT = parseInt(process.env.OPENCLAW_PORT || "18789");
const SIGNALING_PORT = parseInt(process.env.SIGNALING_PORT || "8080");
const OPENCLAW_HOST = process.env.OPENCLAW_HOST || "127.0.0.1";
const SIGNALING_HOST = process.env.SIGNALING_HOST || "127.0.0.1";

// Paths that should go to the signaling server (HTTP, not WS)
const SIGNALING_PATHS = ["/api/turn"];

// Cache ngrok URL (refreshed every 60s)
let cachedNgrokURL = null;
let lastNgrokCheck = 0;

async function getNgrokURL() {
const now = Date.now();
if (cachedNgrokURL && now - lastNgrokCheck < 60000) return cachedNgrokURL;
try {
const res = await fetch("http://localhost:4040/api/tunnels");
const data = await res.json();
const tunnel = data.tunnels?.[0];
if (tunnel) {
cachedNgrokURL = tunnel.public_url;
lastNgrokCheck = now;
}
} catch {}
return cachedNgrokURL;
}

// HTTP requests → route based on path
const server = http.createServer(async (req, res) => {
// Auto-discovery endpoint: returns current ngrok tunnel URL
// iOS app calls this over LAN to learn the tunnel URL for 5G use
if (req.url === "/api/tunnel-url") {
const url = await getNgrokURL();
res.writeHead(200, {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
});
res.end(JSON.stringify({ tunnel_url: url || null }));
return;
}

const isSignalingPath = SIGNALING_PATHS.some((p) => req.url.startsWith(p));
const targetHost = isSignalingPath ? SIGNALING_HOST : OPENCLAW_HOST;
const targetPort = isSignalingPath ? SIGNALING_PORT : OPENCLAW_PORT;
const targetLabel = isSignalingPath ? "Signaling" : "OpenClaw";

const proxyReq = http.request(
{
hostname: targetHost,
port: targetPort,
path: req.url,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
}
);

proxyReq.on("error", (err) => {
console.error(`[Proxy] ${targetLabel} error: ${err.message}`);
res.writeHead(502, { "Content-Type": "text/plain" });
res.end(`${targetLabel} unreachable`);
});

req.pipe(proxyReq);
});

// WebSocket upgrade → Signaling server
server.on("upgrade", (req, socket, head) => {
console.log(`[Proxy] WebSocket upgrade → signaling (${req.url})`);

const proxySocket = net.connect(SIGNALING_PORT, SIGNALING_HOST, () => {
// Reconstruct the HTTP upgrade request to forward to signaling server
const headers = [`${req.method} ${req.url} HTTP/1.1`];
for (let i = 0; i < req.rawHeaders.length; i += 2) {
headers.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
}
headers.push("", "");

proxySocket.write(headers.join("\r\n"));
if (head && head.length) {
proxySocket.write(head);
}

// Bidirectional pipe
proxySocket.pipe(socket);
socket.pipe(proxySocket);
});

proxySocket.on("error", (err) => {
console.error(`[Proxy] Signaling error: ${err.message}`);
socket.end();
});

socket.on("error", (err) => {
console.error(`[Proxy] Client socket error: ${err.message}`);
proxySocket.end();
});
});

server.listen(PROXY_PORT, "0.0.0.0", () => {
console.log(`[Proxy] Unified gateway running on port ${PROXY_PORT}`);
console.log(`[Proxy] HTTP → OpenClaw (${OPENCLAW_HOST}:${OPENCLAW_PORT})`);
console.log(`[Proxy] WS → Signaling (${SIGNALING_HOST}:${SIGNALING_PORT})`);
});
9 changes: 9 additions & 0 deletions ngrok-url.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
# Gets the current ngrok tunnel URL for OpenClaw gateway
URL=$(curl -s http://localhost:4040/api/tunnels 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); tunnels=d.get('tunnels',[]); [print(t['public_url']) for t in tunnels]" 2>/dev/null)
if [ -z "$URL" ]; then
echo "ngrok not running. Start it: launchctl load ~/Library/LaunchAgents/com.isdc.ngrok-openclaw.plist"
exit 1
fi
echo "Current ngrok URL: $URL"
echo "Set this in VisionClaw app > Settings > Tunnel URL"
32 changes: 28 additions & 4 deletions samples/CameraAccess/CameraAccess.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,19 @@
9DD6CB0E2F3C64F400ED7098 /* WebRTCOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD6CB0D2F3C64F400ED7098 /* WebRTCOverlayView.swift */; };
9DD894B22F4047630090B9B9 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD894AF2F4047630090B9B9 /* SettingsManager.swift */; };
9DD894B32F4047630090B9B9 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD894B02F4047630090B9B9 /* SettingsView.swift */; };
9DD894B42F4047630090B9B9 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD894B52F4047630090B9B9 /* KeychainHelper.swift */; };
9DD895962F405E0E0090B9B9 /* RTCVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD895952F405E0E0090B9B9 /* RTCVideoView.swift */; };
9DD895972F405E0E0090B9B9 /* PiPVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD895942F405E0E0090B9B9 /* PiPVideoView.swift */; };
A1B2C3D42F0A000200000001 /* GeminiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000001 /* GeminiConfig.swift */; };
A1B2C3D42F0A000200000002 /* GeminiLiveService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000002 /* GeminiLiveService.swift */; };
A1B2C3D42F0A000200000003 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000003 /* AudioManager.swift */; };
A1B2C3D42F0A000200000004 /* GeminiSessionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000004 /* GeminiSessionViewModel.swift */; };
A1B2C3D42F0A000200000005 /* GeminiOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000005 /* GeminiOverlayView.swift */; };
A1B2C3D42F0A000200000006 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000006 /* LocationManager.swift */; };
A1B2C3D42F0A000200000009 /* GolfCourseAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000009 /* GolfCourseAPIService.swift */; };
A1B2C3D42F0A00020000000A /* ClubDistanceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A00010000000A /* ClubDistanceModel.swift */; };
A1B2C3D42F0A000200000007 /* GolfOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000007 /* GolfOverlayView.swift */; };
A1B2C3D42F0A000200000008 /* TranslationOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D42F0A000100000008 /* TranslationOverlayView.swift */; };
E66D30242E7DA71900470B48 /* MockDeviceKitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66D30232E7DA71900470B48 /* MockDeviceKitButton.swift */; };
E6A188482EB918740097D0E1 /* StreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A188472EB918740097D0E1 /* StreamView.swift */; };
E6DA451D2E79A63100E3F688 /* MockDeviceCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6DA45182E79A63100E3F688 /* MockDeviceCardView.swift */; };
Expand Down Expand Up @@ -105,6 +111,7 @@
9DD6CB032F3C637D00ED7098 /* WebRTCSessionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCSessionViewModel.swift; sourceTree = "<group>"; };
9DD6CB0D2F3C64F400ED7098 /* WebRTCOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCOverlayView.swift; sourceTree = "<group>"; };
9DD894AF2F4047630090B9B9 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
9DD894B52F4047630090B9B9 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
9DD894B02F4047630090B9B9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
9DD895942F405E0E0090B9B9 /* PiPVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPVideoView.swift; sourceTree = "<group>"; };
9DD895952F405E0E0090B9B9 /* RTCVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCVideoView.swift; sourceTree = "<group>"; };
Expand All @@ -113,6 +120,11 @@
A1B2C3D42F0A000100000003 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000004 /* GeminiSessionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiSessionViewModel.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000005 /* GeminiOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiOverlayView.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000006 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000009 /* GolfCourseAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GolfCourseAPIService.swift; sourceTree = "<group>"; };
A1B2C3D42F0A00010000000A /* ClubDistanceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClubDistanceModel.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000007 /* GolfOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GolfOverlayView.swift; sourceTree = "<group>"; };
A1B2C3D42F0A000100000008 /* TranslationOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationOverlayView.swift; sourceTree = "<group>"; };
E66D30232E7DA71900470B48 /* MockDeviceKitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceKitButton.swift; sourceTree = "<group>"; };
E699CC952E8150670052C240 /* CameraAccessTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CameraAccessTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E6A188472EB918740097D0E1 /* StreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -226,6 +238,8 @@
8FFD5FF32E8422580035E446 /* CustomButton.swift */,
E6DA45172E79A63100E3F688 /* CardView.swift */,
A1B2C3D42F0A000100000005 /* GeminiOverlayView.swift */,
A1B2C3D42F0A000100000007 /* GolfOverlayView.swift */,
A1B2C3D42F0A000100000008 /* TranslationOverlayView.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -258,6 +272,7 @@
isa = PBXGroup;
children = (
9DD894AF2F4047630090B9B9 /* SettingsManager.swift */,
9DD894B52F4047630090B9B9 /* KeychainHelper.swift */,
9DD894B02F4047630090B9B9 /* SettingsView.swift */,
);
path = Settings;
Expand All @@ -270,6 +285,9 @@
A1B2C3D42F0A000100000001 /* GeminiConfig.swift */,
A1B2C3D42F0A000100000002 /* GeminiLiveService.swift */,
A1B2C3D42F0A000100000004 /* GeminiSessionViewModel.swift */,
A1B2C3D42F0A000100000009 /* GolfCourseAPIService.swift */,
A1B2C3D42F0A00010000000A /* ClubDistanceModel.swift */,
A1B2C3D42F0A000100000006 /* LocationManager.swift */,
);
path = Gemini;
sourceTree = "<group>";
Expand Down Expand Up @@ -432,7 +450,13 @@
A1B2C3D42F0A000200000004 /* GeminiSessionViewModel.swift in Sources */,
9DD894B22F4047630090B9B9 /* SettingsManager.swift in Sources */,
9DD894B32F4047630090B9B9 /* SettingsView.swift in Sources */,
9DD894B42F4047630090B9B9 /* KeychainHelper.swift in Sources */,
A1B2C3D42F0A000200000005 /* GeminiOverlayView.swift in Sources */,
A1B2C3D42F0A000200000006 /* LocationManager.swift in Sources */,
A1B2C3D42F0A000200000009 /* GolfCourseAPIService.swift in Sources */,
A1B2C3D42F0A00020000000A /* ClubDistanceModel.swift in Sources */,
A1B2C3D42F0A000200000007 /* GolfOverlayView.swift in Sources */,
A1B2C3D42F0A000200000008 /* TranslationOverlayView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -465,7 +489,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = WY253UX7FC;
DEVELOPMENT_TEAM = T72H2BCN3N;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = "";
INFOPLIST_FILE = CameraAccess/Info.plist;
Expand All @@ -475,7 +499,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xiaoanliu.VisionClaw;
PRODUCT_BUNDLE_IDENTIFIER = com.kikinhochow.VisionClaw;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
Expand All @@ -494,7 +518,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = WY253UX7FC;
DEVELOPMENT_TEAM = T72H2BCN3N;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = CameraAccess/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
Expand All @@ -503,7 +527,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xiaoanliu.VisionClaw;
PRODUCT_BUNDLE_IDENTIFIER = com.kikinhochow.VisionClaw;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
Expand Down
Loading