Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
f88f4bf
feat(kit): serve the Visor kit to apps and to adb
macedonga Nov 7, 2026
6d02933
refactor(vdk): rename the kit to VDK, the Visor Development Kit
macedonga Nov 7, 2026
27f6487
feat(vdk): route questions to apps, offer apps' tools to the model, p…
macedonga Nov 7, 2026
0a70ed2
refactor(kit): go back to the kit names in code
macedonga Nov 7, 2026
e95ae5c
feat(kit): open a network door with JSON-RPC over WebSocket and MCP
macedonga Nov 7, 2026
b21500b
feat(kit): answer raw.send and raw.messages while Developer mode is on
macedonga Nov 7, 2026
e69e267
feat(kit): answer glasses.pairs and glasses.select
macedonga Nov 7, 2026
839a911
feat(kit): post network clients' events to their webhooks
macedonga Nov 7, 2026
67c131c
feat(kit): fill the thermal level and throttling in glasses.status
macedonga Nov 7, 2026
ec1bdad
feat(kit): serve camera.live and camera.live.photo from the Live view
macedonga Nov 7, 2026
02a3cf2
feat(kit): serve glasses.inputs from svc28's touch and button pushes
macedonga Nov 7, 2026
4c7114f
feat(kit): serve voice.transcripts from the glasses' headset microphone
macedonga Nov 7, 2026
62b6fc1
Merge branch 'main' into kit
macedonga Nov 7, 2026
a435093
fix(kit): report the band the Display holds in band.status
macedonga Nov 7, 2026
c7563c4
Merge branch 'main' into kit
macedonga Nov 7, 2026
37e7a83
Merge branch 'main' into kit
macedonga Nov 7, 2026
f481639
Merge branch 'main' into kit
macedonga Nov 7, 2026
dbba6bb
Merge branch 'main' into kit
macedonga Nov 7, 2026
cac772b
feat(kit): serve settings.* from the glasses' settings catalogue
macedonga Nov 7, 2026
250ce46
feat(kit): serve assistant.turns and assistant.history from the assis…
macedonga Nov 8, 2026
66aa178
feat(protocol): speak Meta's toolkit session on svc7009 and its app o…
macedonga Nov 8, 2026
32bb7e4
feat(dat): draw kit layouts in the Display toolkit app's Bloks format
macedonga Nov 8, 2026
c766adf
feat(dat): draw every DAT layout element and option as the toolkit's …
macedonga Nov 8, 2026
b5126ed
feat(kit): filter glasses.inputs by the sources an app asks for
macedonga Nov 8, 2026
fa14d16
feat(kit): hear partial transcripts and the recogniser's confidence
macedonga Nov 8, 2026
d3e2af7
feat(kit): start a VDK app for a voice phrase it declares
macedonga Nov 8, 2026
96255aa
feat(kit): let the person allow an app's scopes only this time
macedonga Nov 8, 2026
2a2c2de
feat(setup): keep the glasses' receipt key, which their manifest name…
macedonga Nov 8, 2026
6268779
feat(debug): change the owner again and print each pair's receipt key
macedonga Nov 8, 2026
87288a0
feat(dat): open Visor's own app link to the Display and run a toolkit…
macedonga Nov 8, 2026
c4ce59d
feat(identity): build constellation manifests as Meta's builder does,…
macedonga Nov 8, 2026
38e2cd5
fix(identity): wrap the manifest's content key with the authority key
macedonga Nov 8, 2026
72cca77
feat(dat): set the glasses up for apps and serve motion and layouts
macedonga Nov 8, 2026
14fc77a
feat(dat): show the toolkit setup in a sheet like the rest of Visor
macedonga Nov 8, 2026
8604fe2
fix(dat): show nothing on the Display while only motion runs
macedonga Nov 8, 2026
1d4d676
fix(band): always hand the band back to the Display after a borrow
macedonga Nov 8, 2026
6b9466c
fix(dat): give the band the toolkit's manifest and authority key
macedonga Nov 8, 2026
d256709
fix(connection): read link leases as the Meta app does
macedonga Nov 8, 2026
e454fdb
feat(dat): keep every device on Visor's manifest, as the Meta app does
macedonga Nov 8, 2026
9cc534a
feat(debug): open the app link probe with Visor's toolkit key
macedonga Nov 8, 2026
054cb30
fix(dat): close an ended toolkit session's link
macedonga Nov 8, 2026
ffe70b6
feat(kit): read the glasses' motion with nothing on their screen
macedonga Nov 8, 2026
0cf9204
feat(debug): probe the glasses' data collection daemon for motion
macedonga Nov 8, 2026
05f1dcf
feat(kit): read band gestures through a session, the band left on the…
macedonga Nov 8, 2026
5ce72b7
feat(kit): touchpad and band input on the Display through a session
macedonga Nov 8, 2026
e3ba0f6
fix(kit): band gestures from any band on the Display, derived filled in
macedonga Nov 8, 2026
38eb046
fix(session): open a new channel after a service answers not found
macedonga Nov 8, 2026
e2555aa
feat(kit): orientation in the glasses' motion, fused on the phone
macedonga Nov 8, 2026
4acc984
feat(session): move the session to Wi-Fi beside Bluetooth, channels kept
macedonga Nov 8, 2026
2b62c3b
feat(kit): the Display's navigation in glasses.inputs while a layout …
macedonga Nov 8, 2026
79c0fad
feat(kit): videos in Display layouts, with display.video
macedonga Nov 8, 2026
25327f0
chore(debug): add and remove network clients, count band samples in t…
macedonga Nov 8, 2026
366f38c
feat(kit): the glasses speak text unasked, through svc1027
macedonga Nov 8, 2026
d492073
feat(kit): serve voice.speak through the glasses' data collection speech
macedonga Nov 8, 2026
3ae3d5b
chore(debug): extra sensor entries for the data collection probe
macedonga Nov 8, 2026
0671f6e
fix(session): start over on Bluetooth when a switched-to Wi-Fi link d…
macedonga Nov 8, 2026
901edbc
feat(kit): app cards on the Briefing, with a detail view a click opens
macedonga Nov 8, 2026
9550951
feat(kit): Briefing card details, pages, charts and Meta's look on th…
macedonga Nov 8, 2026
3947e07
feat(kit): scale big data: images on the phone, refuse cards past the…
macedonga Nov 8, 2026
b8c187e
feat(kit): the Display asks for the Briefing every 15 s while apps ha…
macedonga Nov 8, 2026
8dae1a3
feat(kit): clicks in a Briefing card's detail go to the card's app
macedonga Nov 8, 2026
484ba3d
feat(kit): voice.transcripts on the Display through the toolkit app's…
macedonga Nov 8, 2026
2f15da6
docs: the README names the VDK and apps' Briefing cards
macedonga Nov 9, 2026
6d922b8
feat(band): share the band with the Display through the phone's relay
macedonga Nov 9, 2026
869a831
feat(band): band.motion from the shared band's IMU streams
macedonga Nov 9, 2026
a1712af
feat(display): web apps get the phone's sensors, location check and r…
macedonga Nov 9, 2026
566fa99
fix(band): a shared band that does not work goes back to the Display
macedonga Nov 9, 2026
6f49ee2
feat(display): the VDK for web apps, and one sheet per web app
macedonga Nov 9, 2026
bfa1937
feat(assistant): web apps' WebMCP tools for the wearer's own model
macedonga Nov 9, 2026
44c4de3
docs(readme): web apps and the shared band in what Visor gives apps
macedonga Nov 9, 2026
84c1735
docs: developer mode's page moves to the site
macedonga Nov 9, 2026
327ede5
docs(readme): link the website and the protocol reference
macedonga Nov 9, 2026
3c0061b
docs(readme): the website link stays in the nav, not as a badge
macedonga Nov 9, 2026
8c19eb8
fix(settings): rows on a sheet line up with its title
macedonga Nov 9, 2026
d1cd328
build(deps): VDK 0.1.1
macedonga Nov 9, 2026
b618cca
feat(settings): Apps, the VDK's home in Settings
macedonga Nov 9, 2026
e5caffa
fix(transport): after the handshake, bit 7 starts a COMMAND frame
macedonga Nov 9, 2026
6019e11
feat(settings): web apps lose Open
macedonga Nov 9, 2026
dd25a92
fix(kit): glasses.status names the model as Meta sells it
macedonga Nov 9, 2026
2ca013c
Merge branch 'main' into kit
macedonga Nov 9, 2026
7068eb8
fix(kit): end a network client's sockets when its token is replaced o…
macedonga Nov 9, 2026
b17189e
fix(kit): grant capture URIs to Android apps only
macedonga Nov 9, 2026
cc42da4
fix(kit): keep the demo switch on for computers and web apps with the…
macedonga Nov 9, 2026
2ab56cc
fix(kit): count a Live watcher only once it joined
macedonga Nov 9, 2026
87c62e4
fix(kit): take a malformed scan filter as no VDK frame
macedonga Nov 9, 2026
1ee359c
fix(band): a loss left from the last round does not end the relay's n…
macedonga Nov 9, 2026
4ebf9c3
fix(session): a link switch and its timeout cannot both win
macedonga Nov 9, 2026
28e8cca
fix(transport): put the permission suppression back on close()
macedonga Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ the frame style and language packs. Works on real glasses as of 2026-10-05.
## Working on it

- `reference/lensbreak`, a snapshot of the desktop Python client, already does most of what
this app needs. Look there first; the protocol write-ups are on the GitHub wiki.
this app needs. Look there first; the protocol write-ups are in `../visor-site/src/content/protocol`
(visor.macedonga.com/protocol), the only copy. The wiki is retired.
- The glasses are the test. Builds get tried on hardware, so there are no test suites.
- Keep it small: short comments, no planning documents.
- No backend, analytics or crash reporting.
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<p align="center">
<b>An Android app for Meta glasses that replaces the Meta AI app.</b><br>
Your photos stay on your phone, and "Hey Meta" answers with the model you choose.
Your photos stay on your phone, "Hey Meta" answers with the model you choose, and your apps use the glasses too.
</p>

<p align="center">
Expand All @@ -16,6 +16,7 @@
<p align="center">
<a href="https://visor.macedonga.com">Website</a> |
<a href="#what-it-does">Features</a> |
<a href="#apps-for-your-glasses">Apps</a> |
<a href="#install">Install</a> |
<a href="#what-it-sends-and-where">Privacy</a> |
<a href="#good-to-know">Good to know</a> |
Expand All @@ -39,10 +40,23 @@ Visor works with Ray-Ban Meta, Oakley Meta and the other Meta glasses. It speaks
| **Notifications** | Message notifications go to the glasses, which announce the sender by the name in your contacts. |
| **Settings** | Every setting the glasses have, the way the Meta app shows them. Advanced holds the rest, assistant language packs and the firmware's feature flags. |
| **Outside the app** | A home screen widget, a Quick Settings tile, launcher shortcuts, and a Live Update notification while the Live view runs. |
| **Developer mode** | Other apps and automations can take photos and videos, import captures and hear about new ones. See [Developer mode](https://visor.macedonga.com/developers/docs/developer-mode/). |
| **More than one pair** | Keep several pairs on one phone. Home swipes between them, each with its name, batteries and a Connect button, and ends with a page to set up another. |
| **Meta Ray-Ban Display** | A teleprompter, live captions and translation, the Neural Band's gestures, the Briefing panel and saved Wi-Fi networks. The glasses' own apps reach the internet through the phone, with Instagram's Reels and messages among them. |
| **Neural Band** | Set up from the Display's settings, with its battery on Home and its wrist in Settings. |
| **Meta Ray-Ban Display** | A teleprompter, live captions and translation, the Neural Band's gestures, the Briefing panel with your apps' own cards, and saved Wi-Fi networks. The glasses' own apps reach the internet through the phone, with Instagram's Reels and messages among them. Web apps get the phone's location, motion sensors and nearby Bluetooth devices, and can give your assistant tools. |
| **Neural Band** | Set up from the Display's settings, with its battery on Home and its wrist in Settings. Share it with the phone, and apps buzz it at once and read its gestures and motion while the Display keeps using it. |

## Apps for your glasses

Visor is also where your apps meet the glasses. Through the [Visor Development Kit](https://visor.macedonga.com/developers/)
(VDK), apps on your phone, programs and AI agents on your computer, and web apps on the Display use the camera and
captures, the Display's layouts and Briefing cards, the Neural Band's gestures and motion, voice, the assistant's
tools and the glasses' settings. No Meta account, no Meta AI app, no developer review.

| To | Do this |
|---|---|
| **Use an app** | It asks for what it needs the first time, on Visor's consent screen. **Settings, Apps** lists every app, program and web app with access, and takes any of it back. |
| **Connect a program or an AI agent** | In **Settings, Apps**, turn on **Let programs connect** and add the program, with its own token. Agents get the glasses over MCP. |
| **Build your own** | For Android, Python, TypeScript, or a web app on the Display: start with the [getting started guide](https://visor.macedonga.com/developers/docs/getting-started/). `visor fake` stands in for the glasses while you build. |
| **Automate** | Automation apps such as Tasker take photos and videos and hear about new captures through [Developer mode](https://visor.macedonga.com/developers/docs/developer-mode/)'s intents. |

## Requirements

Expand Down
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.browser)
implementation(libs.visor.kit)
implementation(libs.ktor.server.cio)
implementation(libs.ktor.server.websockets)
implementation(libs.androidx.media)
implementation(libs.kotlinx.coroutines.android)

Expand Down
149 changes: 149 additions & 0 deletions app/src/debug/kotlin/com/macedonga/visor/debug/AppLinkProbe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.macedonga.visor.debug

import android.content.Context
import com.macedonga.visor.core.identity.BundleCredentials
import com.macedonga.visor.core.identity.BundleState
import com.macedonga.visor.core.identity.IdentityBundle
import com.macedonga.visor.core.identity.nowIso
import com.macedonga.visor.core.protocol.Dat
import com.macedonga.visor.core.session.DataXError
import com.macedonga.visor.core.session.GlassesSession
import com.macedonga.visor.core.session.LinkState
import com.macedonga.visor.core.session.LinkStateChanged
import com.macedonga.visor.core.session.ProtocolError
import com.macedonga.visor.core.session.ServiceData
import com.macedonga.visor.core.session.SessionCommand
import com.macedonga.visor.core.transport.BluetoothLeTransport
import com.macedonga.visor.log.EventLog
import java.security.PrivateKey
import java.util.Locale
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
* Debug only: a link of Visor's own to the Display under its app key, trusted on svc79 against the constellation
* manifest as Meta's toolkit's links are, and a toolkit session on it (svc7009, then the toolkit app on svc6500).
* Every step goes to the event log as "AppLink:".
*/
internal object AppLinkProbe {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var session: GlassesSession? = null
private var job: Job? = null
private val ids = AtomicInteger()
private val samples = AtomicInteger()
private val bandSamples = AtomicInteger()

fun start(context: Context, address: String, pem: ByteArray, version: Long, salt: ByteArray, tag: Int, sign: Int, dam: Boolean, log: EventLog): String {
val credentials = BundleCredentials(IdentityBundle(pem, address, "visor-app", nowIso(), "app", BundleState.VERIFIED))
return start(context, address, credentials.appEcKey, credentials.appPublicKey, version, salt, tag, sign, dam, log)
}

fun start(context: Context, address: String, key: PrivateKey, publicKey: ByteArray, version: Long, salt: ByteArray, tag: Int, sign: Int, dam: Boolean, log: EventLog): String {
stop()
val trust = ProbeConstellationTrust(key, publicKey, version, salt, tag, sign) { log.add("AppLink: $it") }
val opened = GlassesSession(BluetoothLeTransport(context, address), trust)
session = opened
job = scope.launch {
opened.events.collect { event ->
when (event) {
is LinkStateChanged -> {
log.add("AppLink: link ${event.state} ${event.reason}".trim())
if (event.state == LinkState.READY) {
log.add("AppLink: trust ${opened.trustResult}")
send(Dat.SESSION_SERVICE, Dat.SESSION_START_REQUEST, Dat.sessionStart("com.macedonga.visor", UUID.randomUUID().toString(), "Visor", "1.0.1", dam))
}
}
is ProtocolError -> log.add("AppLink: ${event.detail}")
is DataXError -> {
log.add("AppLink: svc${event.serviceId} status 0x%04x".format(Locale.ROOT, event.errorId))
// A refused channel is dead: the next request opens a fresh one.
if (event.serviceId == Dat.DWA_SERVICE) dwaChannel = null
}
is ServiceData -> heard(event, log)
else -> Unit
}
}
}
opened.start(scope)
return "connecting to $address with manifest $version"
}

/** Adds a capability on the toolkit app, then sends its first request: motion at [rateHz], or a test layout. */
fun capability(capability: Int, payload: ByteArray?): String {
val now = System.currentTimeMillis()
send(Dat.DWA_SERVICE, Dat.REQUEST, Dat.requests("add-${ids.incrementAndGet()}", capability, Dat.addCapability(capability), null, now).single())
if (payload != null) {
Dat.requests("req-${ids.incrementAndGet()}", capability, null, payload, now).forEach { send(Dat.DWA_SERVICE, Dat.REQUEST, it) }
}
return "sent to capability $capability"
}

fun stop(): String {
val running = session ?: return "no app link"
runBlocking { running.stop() }
job?.cancel()
session = null
return "stopped; ${samples.getAndSet(0)} motion samples"
}

@Volatile
private var dwaChannel: Int? = null

private fun send(service: Int, type: Int, payload: ByteArray) {
session?.submit(
SessionCommand { s ->
val connection = s.connection ?: return@SessionCommand
if (service != Dat.DWA_SERVICE) return@SessionCommand connection.sendOnService(service, type, payload)
// The toolkit app's own channel, opened once it is up, so a refusal before then does not stick. Only the
// opening frame names the service: one that names it again reads as a new open.
val open = dwaChannel
if (open != null) return@SessionCommand connection.send(open, payload, messageType = type)
val channel = connection.openChannel(service).also { dwaChannel = it }
connection.send(channel, payload, messageType = type, serviceId = service)
},
)
}

private fun heard(event: ServiceData, log: EventLog) {
when (event.serviceId) {
Dat.SESSION_SERVICE -> if (event.messageType == Dat.SESSION_START_RESPONSE) {
val started = Dat.sessionStarted(event.payload)
log.add("AppLink: session status ${started.status}, toolkit app ${started.dwaVersion}, launch ${started.dwaLaunch}, compatibility ${started.compatibility}")
// The toolkit app needs about a second to come up after the launch before it hears svc6500.
if (started.status == Dat.START_OK && started.dwaLaunch == 1) scope.launch {
delay(2_000)
log.add("AppLink: ${capability(Dat.IMU, Dat.motionStart(10))}")
}
} else {
log.add("AppLink: svc7009 type ${event.messageType} ${event.payload.toHexString()}")
}
Dat.DWA_SERVICE -> when (event.messageType) {
Dat.RESPONSE -> Dat.response(event.payload).let {
log.add("AppLink: response ${it.requestId} capability ${it.capability} status ${it.status} active ${it.active} error ${it.error} denied ${it.denied}")
}
Dat.EVENT -> Dat.event(event.payload).let { e ->
val motion = if (e.capability == Dat.IMU) e.payload?.let(Dat::motionSamples).orEmpty() else emptyList()
val n = samples.addAndGet(motion.size)
bandSamples.addAndGet(motion.count { it.band })
when {
motion.isNotEmpty() -> if (n % 50 < motion.size) {
val a = motion.last().accelerometer
log.add("AppLink: $n motion samples (${bandSamples.get()} from the band), last accelerometer ${a?.x} ${a?.y} ${a?.z}, orientation ${motion.last().orientation != null}")
}
e.heartbeat -> Unit
else -> log.add("AppLink: event capability ${e.capability} shutdown ${e.shutdown} ${e.payload?.toHexString()}")
}
}
else -> log.add("AppLink: svc6500 type ${event.messageType} ${event.payload.toHexString()}")
}
else -> log.add("AppLink: svc${event.serviceId} type ${event.messageType} ${event.payload.size} bytes")
}
}
}
Loading
Loading