Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 20 additions & 3 deletions android/app/src/main/kotlin/net/guacamaya/aware/AwareConfig.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
package net.guacamaya.aware

import net.guacamaya.ble.BleConfig

/**
* Constants for Wi-Fi Aware (NAN). Service name is the wire prefix subscribers
* match on; GuacaMalla uses a stable prefix so any nearby node can subscribe to
* the whole family via "guacamalla::*" semantics (Android matches on exact prefix).
*
* SSI (Service Specific Info) carries up to 255 B at the MAC layer — no IP, no
* auth, no user-facing pairing. See docs/protocol-flows.md Flow 4.
* auth, no user-facing pairing. GuacaMalla publishes the same 119 B frame used
* by BLE service-data, including the mutable hop-TTL byte, so the normal
* FloodRouter can verify/persist/relay frames from either radio plane.
* See docs/protocol-flows.md Flow 4.
*/
object AwareConfig {

/** NAN service name prefix. */
const val SERVICE_NAME = "net.guacamaya.v1"

/** Service Specific Info payload — same 118 B layout as the BLE frame. */
const val SSI_SIZE = 22 + 32 + 64
/** Service Specific Info payload — same 119 B layout as BLE service-data. */
const val SSI_SIZE = BleConfig.SERVICE_DATA_SIZE

/** Max bytes the NAN SSI field can carry per IEEE 802.11v. */
const val SSI_MAX = 255

fun packFrame(ttl: Int, payload22: ByteArray, pub32: ByteArray, sig64: ByteArray): ByteArray {
require(payload22.size == 22) { "payload must be 22 B" }
require(pub32.size == 32) { "pubkey must be 32 B" }
require(sig64.size == 64) { "signature must be 64 B" }
return ByteArray(SSI_SIZE).also {
it[BleConfig.TTL_OFFSET] = ttl.coerceIn(0, 255).toByte()
payload22.copyInto(it, BleConfig.PAYLOAD_OFFSET)
pub32.copyInto(it, BleConfig.PUBKEY_OFFSET)
sig64.copyInto(it, BleConfig.SIG_OFFSET)
}
}
}
110 changes: 66 additions & 44 deletions android/app/src/main/kotlin/net/guacamaya/aware/NanMessenger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import java.util.concurrent.atomic.AtomicReference
import net.guacamaya.ble.BleConfig

/**
* Wi-Fi Aware messenger for payloads up to 255 B (BLE frame layout, 118 B).
* Wi-Fi Aware messenger for payloads up to 255 B (BLE frame layout, 119 B).
*
* Two roles:
* - [publish] — radiate the SSI as part of the NAN service discovery frame.
Expand All @@ -39,7 +40,7 @@ class NanMessenger private constructor(
private val subscribeRef = AtomicReference<SubscribeDiscoverySession?>(null)

fun interface Listener {
fun onFrame(payload22: ByteArray, pub32: ByteArray, sig64: ByteArray, peer: PeerHandle)
fun onFrame(payload22: ByteArray, pub32: ByteArray, sig64: ByteArray, ttl: Int, peer: PeerHandle)
}

private var listener: Listener? = null
Expand All @@ -59,18 +60,26 @@ class NanMessenger private constructor(
onFailed(-1)
return
}
manager.attach(object : AttachCallback() {
override fun onAttached(session: WifiAwareSession?) {
sessionRef.set(session)
Log.i(tag, "Aware attached")
onAttached()
}

override fun onAttachFailed() {
Log.e(tag, "Aware attach failed")
onFailed(-2)
}
}, handler)
try {
manager.attach(object : AttachCallback() {
override fun onAttached(session: WifiAwareSession?) {
sessionRef.set(session)
Log.i(tag, "Aware attached")
onAttached()
}

override fun onAttachFailed() {
Log.e(tag, "Aware attach failed")
onFailed(-2)
}
}, handler)
} catch (se: SecurityException) {
Log.w(tag, "Aware attach denied — NEARBY_WIFI_DEVICES? ${se.message}")
onFailed(-3)
} catch (t: Throwable) {
Log.w(tag, "Aware attach unavailable: ${t.message}")
onFailed(-4)
}
}

/**
Expand Down Expand Up @@ -101,16 +110,22 @@ class NanMessenger private constructor(
return
}

session.publish(config, object : DiscoverySessionCallback() {
override fun onPublishStarted(session: PublishDiscoverySession) {
publishRef.set(session)
Log.i(tag, "publish started")
}
try {
session.publish(config, object : DiscoverySessionCallback() {
override fun onPublishStarted(session: PublishDiscoverySession) {
publishRef.set(session)
Log.i(tag, "publish started")
}

override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
Log.d(tag, "msg from peer=${peerHandle.hashCode()} size=${message.size}")
}
}, handler)
override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
Log.d(tag, "msg from peer=${peerHandle.hashCode()} size=${message.size}")
}
}, handler)
} catch (se: SecurityException) {
Log.w(tag, "publish denied — NEARBY_WIFI_DEVICES? ${se.message}")
} catch (t: Throwable) {
Log.w(tag, "publish failed: ${t.message}")
}
}

fun stopPublish() {
Expand All @@ -129,28 +144,35 @@ class NanMessenger private constructor(
val config = SubscribeConfig.Builder()
.setServiceName(AwareConfig.SERVICE_NAME)
.build()
session.subscribe(config, object : DiscoverySessionCallback() {
override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
subscribeRef.set(session)
Log.i(tag, "subscribe started")
}

override fun onServiceDiscovered(
peerHandle: PeerHandle,
serviceSpecificInfo: ByteArray,
matchFilter: MutableList<ByteArray>,
) {
val ssi = serviceSpecificInfo
if (ssi.size != AwareConfig.SSI_SIZE) {
Log.w(tag, "discovered but malformed ssi size=${ssi.size}")
return
try {
session.subscribe(config, object : DiscoverySessionCallback() {
override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
subscribeRef.set(session)
Log.i(tag, "subscribe started")
}
val p22 = ssi.copyOfRange(0, 22)
val pub32 = ssi.copyOfRange(22, 22 + 32)
val sig64 = ssi.copyOfRange(22 + 32, 22 + 32 + 64)
listener?.onFrame(p22, pub32, sig64, peerHandle)
}
}, handler)

override fun onServiceDiscovered(
peerHandle: PeerHandle,
serviceSpecificInfo: ByteArray,
matchFilter: MutableList<ByteArray>,
) {
val ssi = serviceSpecificInfo
if (ssi.size != AwareConfig.SSI_SIZE) {
Log.w(tag, "discovered but malformed ssi size=${ssi.size}")
return
}
val ttl = ssi[BleConfig.TTL_OFFSET].toInt() and 0xFF
val p22 = ssi.copyOfRange(BleConfig.PAYLOAD_OFFSET, BleConfig.PUBKEY_OFFSET)
val pub32 = ssi.copyOfRange(BleConfig.PUBKEY_OFFSET, BleConfig.SIG_OFFSET)
val sig64 = ssi.copyOfRange(BleConfig.SIG_OFFSET, AwareConfig.SSI_SIZE)
listener?.onFrame(p22, pub32, sig64, ttl, peerHandle)
}
}, handler)
} catch (se: SecurityException) {
Log.w(tag, "subscribe denied — NEARBY_WIFI_DEVICES? ${se.message}")
} catch (t: Throwable) {
Log.w(tag, "subscribe failed: ${t.message}")
}
}

fun stopSubscribe() {
Expand Down
23 changes: 17 additions & 6 deletions android/app/src/main/kotlin/net/guacamaya/ble/BleMeshRuntime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,17 @@ object BleMeshRuntime {
private var router: FloodRouter? = null

fun ensureObserving(ctx: Context): Boolean {
ensureRouter(ctx.applicationContext)
val app = ctx.applicationContext
if (observer == null || router == null) {
if (observer == null) {
val obs = Observer.create(app) ?: run {
Log.i(PROBE, "observe fail Observer.create=null")
return false
}
val dao = GuacamayaDatabase.get(app).messageDao()
val bcast = Broadcaster.create(app)
val r = FloodRouter(dao = dao, broadcaster = bcast, scope = scope)
obs.setListener { p22, pub32, sig64, ttl, rssi ->
r.onFrame(p22, pub32, sig64, ttl, rssi)
router?.onFrame(p22, pub32, sig64, ttl, rssi)
}
observer = obs
router = r
}
val obs = observer ?: return false
if (obs.isScanning) {
Expand All @@ -46,6 +43,20 @@ object BleMeshRuntime {
return obs.isScanning
}

fun routeFrame(ctx: Context, payload22: ByteArray, pub32: ByteArray, sig64: ByteArray, ttl: Int, rssi: Int) {
ensureRouter(ctx.applicationContext)
router?.onFrame(payload22, pub32, sig64, ttl, rssi)
}

private fun ensureRouter(ctx: Context) {
val app = ctx.applicationContext
if (router == null) {
val dao = GuacamayaDatabase.get(app).messageDao()
val bcast = Broadcaster.create(app)
router = FloodRouter(dao = dao, broadcaster = bcast, scope = scope)
}
}

fun stopObserving() {
observer?.stop()
Log.i(PROBE, "BleMeshRuntime stopped")
Expand Down
Loading