diff --git a/docs/gestures.md b/docs/gestures.md new file mode 100644 index 0000000..1c3f86c --- /dev/null +++ b/docs/gestures.md @@ -0,0 +1,211 @@ +# Gesture Callbacks + +This document describes gesture detection from Even G2 glasses BLE traffic, including tap, swipe, and long press events. + +## Overview + +Gestures are transmitted as part of status packets on Service `0x0101`, except for long press which uses Service `0x0D01`. Events are embedded within protobuf-encoded payloads and identified by specific byte patterns. + +## Gesture Types + +### Tap (Single/Double) + +**Service:** `0x0101` +**Pattern:** `320b...06 0801 1202 1001` + +``` +Raw packet fragment: +...320b...060801120210011803... + +Decoded: + Service: 0x0101 (Status) + Gesture: Tap + Counter: Increments with each tap +``` + +**Important:** Single tap and double tap are **NOT distinguishable** at the protocol level. Both produce identical packets. The Even app likely implements double-tap detection through timing logic in software. + +### Swipe Forward + +**Service:** `0x0101` +**Pattern:** `320d...08 0801 1204 0801 10XX` + +``` +Raw packet fragment: +aa12170f01010101...320d...08080112040801100b... + +Decoded: + Service: 0x0101 (Status) + Gesture: Swipe + Direction: 0x01 (Forward) + Counter: 0x0b (11) - increments per gesture +``` + +The key identifier is `1204 0801` where `0801` indicates forward direction. + +### Swipe Backward + +**Service:** `0x0101` +**Pattern:** `320d...08 0801 1204 0802 10XX` + +``` +Raw packet fragment: +aa12170f01010101...320d...08080112040802100c... + +Decoded: + Service: 0x0101 (Status) + Gesture: Swipe + Direction: 0x02 (Backward) + Counter: 0x0c (12) - increments per gesture +``` + +The key identifier is `1204 0802` where `0802` indicates backward direction. + +### Long Press + +**Service:** `0x0D01` +**Pattern:** `aa12XX0a01010d0108011a0408011003` + +``` +Full packet: +aa12XX0a01010d0108011a0408011003YYYY + +Decoded: + Service: 0x0D01 (Acknowledgment/Control) + Event: Long Press + Action: Triggers Even AI on-device +``` + +**Note:** Long press is primarily handled on-device to activate Even AI. The BLE packet serves as a notification to the phone app. In some capture sessions, long press events did not appear in BLE traffic at all, suggesting the glasses may handle them entirely locally when AI mode is active. + +## Detection Algorithm + +### Pattern Matching (Python) + +```python +def detect_gesture(packet_hex: str) -> str | None: + """Detect gesture type from G2 packet hex string.""" + + # Long press (service 0d01) + if "01010d01" in packet_hex and "1a0408011003" in packet_hex: + return "long_press" + + # Check for gesture patterns in status packets (service 0101) + if "320d" in packet_hex: + if "12040801" in packet_hex: + return "swipe_forward" + elif "12040802" in packet_hex: + return "swipe_backward" + + if "320b" in packet_hex and "08011202" in packet_hex: + return "tap" + + return None +``` + +### Swift Implementation + +```swift +enum G2Gesture { + case tap + case swipeForward + case swipeBackward + case longPress +} + +func parseGesture(from data: Data) -> G2Gesture? { + let hex = data.map { String(format: "%02x", $0) }.joined() + + // Long press (service 0d01) + if hex.contains("01010d01") && hex.contains("1a0408011003") { + return .longPress + } + + // Swipe gestures (service 0101, pattern 320d) + if hex.contains("320d") { + if hex.contains("12040801") { + return .swipeForward + } else if hex.contains("12040802") { + return .swipeBackward + } + } + + // Tap gesture (service 0101, pattern 320b) + if hex.contains("320b") && hex.contains("08011202") { + return .tap + } + + return nil +} +``` + +## Gesture Counter + +Each gesture packet includes a counter byte at position `10XX` that increments with each gesture event. This can be used to: + +1. Detect missed gestures (gaps in sequence) +2. Debounce rapid repeated gestures +3. Track gesture frequency for analytics + +## Timing Characteristics + +From capture analysis: + +| Gesture | Typical Packet Delay | Notes | +|---------|---------------------|-------| +| Tap | ~50-100ms | Near-instant response | +| Swipe | ~50-100ms | Near-instant response | +| Long Press | ~500-800ms | Delay for press duration detection | + +## Implementation Notes + +### Double-Tap Detection + +Since single and double taps are identical at the protocol level, implement double-tap detection in your app: + +```swift +class GestureHandler { + private var lastTapTime: Date? + private let doubleTapThreshold: TimeInterval = 0.3 + + func handleTap() { + let now = Date() + + if let lastTap = lastTapTime, + now.timeIntervalSince(lastTap) < doubleTapThreshold { + // Double tap detected + onDoubleTap() + lastTapTime = nil + } else { + // Schedule single tap (may be cancelled by double tap) + lastTapTime = now + DispatchQueue.main.asyncAfter(deadline: .now() + doubleTapThreshold) { + if self.lastTapTime != nil { + self.onSingleTap() + self.lastTapTime = nil + } + } + } + } +} +``` + +### Glasses Sleep Behavior + +The glasses enter sleep mode after approximately 10-15 seconds of inactivity. When sleeping: + +- Gesture packets are not transmitted +- Wake the display before expecting gesture events +- Consider implementing a keep-alive mechanism if continuous gesture input is required + +## Capture Method + +Gestures were captured using: +- iOS device with Bluetooth logging profile +- Apple PacketLogger +- Isolated gesture testing with 5-second intervals between actions +- Handle filter: `btatt.handle == 0x0844` (notify characteristic) + +## Contributing + +If you discover additional gesture types or patterns (e.g., multi-finger gestures, pressure sensitivity), please contribute packet dumps with timestamps and actions performed. diff --git a/docs/navigation.md b/docs/navigation.md new file mode 100644 index 0000000..0b298a0 --- /dev/null +++ b/docs/navigation.md @@ -0,0 +1,279 @@ +# Navigation Protocol + +This document describes the turn-by-turn navigation format for Even G2 glasses, including distance, instructions, ETA, and maneuver icons. + +## Overview + +Navigation data is transmitted on Service `0x0820` (Dashboard & Navigation) via BLE Handle `0x0842`. The format uses protobuf encoding with specific field tags for each navigation element. + +## BLE Characteristics + +| Handle | Direction | Purpose | +|--------|-----------|---------| +| `0x0842` | Phone → Glasses | Navigation commands (Write) | +| `0x0844` | Glasses → Phone | Acknowledgments (Notify) | +| `0x0864` | Glasses → Phone | Map rendering data | + +## Packet Structure + +### Navigation Update (Service 0x0820) + +``` +[AA 21] [Seq] [Len] [01 01] [08 20] [NavPayload...] [CRC:2 LE] +``` + +### Navigation Payload (Protobuf) + +``` +Field 08: Navigation state/mode +Field 12: Nested navigation message + ├─ Field 04 (20): Distance to maneuver ("86 m") + ├─ Field 1a: Instruction text ("Turn left") + ├─ Field 22: Time remaining ("7 min") + ├─ Field 2a: Total distance ("701 m") + ├─ Field 32: ETA string ("ETA: 13:07") + ├─ Field 3a: Current speed ("0.0 km/h") + └─ Field 40: Maneuver icon type +``` + +### Field Descriptions + +| Protobuf Tag | Field | Type | Description | +|--------------|-------|------|-------------| +| `08` | State | varint | Navigation mode (07 = active navigation) | +| `12 04` / `20` | Distance | string | Distance to next maneuver | +| `1a` | Instruction | string | Turn instruction text | +| `22` | TimeRemaining | string | Estimated time to destination | +| `2a` | TotalDistance | string | Total remaining distance | +| `32` | ETA | string | Arrival time (formatted) | +| `3a` | Speed | string | Current speed | +| `40` | IconType | varint | Maneuver icon identifier | + +## Example: Full Navigation Packet + +### Raw Packet + +``` +aa21413f0101082008072a39080412043836206d1a095475726e206c6566742205 +37206d696e2a05373031206d320a4554413a2031333a30373a08302e30206b6d2f +6840011768 +``` + +### Decoded + +``` +Header: + Prefix: aa21 (write command) + Sequence: 41 + Length: 3f (63 bytes) + Packet: 01 01 (single packet) + Service: 08 20 (Navigation) + +Payload: + State: 08 07 (active navigation) + Container: 2a 39 (nested message, 57 bytes) + +Navigation Fields: + Distance: 08 04 12 04 "86 m" + Instruction: 1a 09 "Turn left" + Time: 22 05 "7 min" + TotalDist: 2a 05 "701 m" + ETA: 32 0a "ETA: 13:07" + Speed: 3a 08 "0.0 km/h" + Icon: 40 01 + +CRC: 1768 +``` + +## Icon Types + +Based on captured traffic, the `IconType` field (tag `40`) indicates the maneuver type: + +| Value | Maneuver | +|-------|----------| +| `01` | Turn left | +| `02` | Turn right (assumed) | +| `03` | Straight/Continue (assumed) | +| `04` | U-turn (assumed) | + +*Note: Only `01` (turn left) was directly captured. Other values are inferred from typical navigation patterns.* + +## Dashboard Widget + +Navigation can also appear as a dashboard widget with simplified data: + +``` +Service: 0x0820 +Subtype: 02 (widget mode vs 07 for active navigation) + +Example: +aa213e13010108200802220d080112064f66666963651a01029a79 + +Decoded: + Service: 0820 + Mode: 02 (dashboard widget) + Content: "Office" (destination/calendar location) +``` + +## Implementation + +### Building a Navigation Packet (Python) + +```python +import struct + +def build_navigation_packet( + distance: str, + instruction: str, + time_remaining: str, + total_distance: str, + eta: str, + speed: str, + icon_type: int = 1, + sequence: int = 0 +) -> bytes: + """Build a G2 navigation packet.""" + + # Build protobuf payload + def encode_string(tag: int, value: str) -> bytes: + data = value.encode('utf-8') + return bytes([tag, len(data)]) + data + + nav_fields = b'' + nav_fields += bytes([0x08, 0x04]) # Distance container + nav_fields += encode_string(0x12, distance) + nav_fields += encode_string(0x1a, instruction) + nav_fields += encode_string(0x22, time_remaining) + nav_fields += encode_string(0x2a, total_distance) + nav_fields += encode_string(0x32, eta) + nav_fields += encode_string(0x3a, speed) + nav_fields += bytes([0x40, icon_type]) + + # Wrap in container + payload = bytes([0x08, 0x07, 0x2a, len(nav_fields)]) + nav_fields + + # Build packet header + service = bytes([0x08, 0x20]) + pkt_info = bytes([0x01, 0x01]) # Single packet + length = len(pkt_info) + len(service) + len(payload) + + header = bytes([0xaa, 0x21, sequence, length]) + + # Calculate CRC (CRC-16/CCITT on payload only) + full_payload = pkt_info + service + payload + crc = crc16_ccitt(full_payload) + + return header + full_payload + struct.pack(' int: + """CRC-16/CCITT calculation.""" + crc = 0xFFFF + for byte in data: + crc ^= byte << 8 + for _ in range(8): + if crc & 0x8000: + crc = (crc << 1) ^ 0x1021 + else: + crc <<= 1 + crc &= 0xFFFF + return crc +``` + +### Swift Implementation + +```swift +struct G2Navigation { + let distance: String // "86 m" + let instruction: String // "Turn left" + let timeRemaining: String // "7 min" + let totalDistance: String // "701 m" + let eta: String // "ETA: 13:07" + let speed: String // "0.0 km/h" + let iconType: UInt8 // 1 = left turn + + func buildPacket(sequence: UInt8) -> Data { + var navFields = Data() + + // Distance container + navFields.append(contentsOf: [0x08, 0x04]) + navFields.append(encodeString(tag: 0x12, value: distance)) + navFields.append(encodeString(tag: 0x1a, value: instruction)) + navFields.append(encodeString(tag: 0x22, value: timeRemaining)) + navFields.append(encodeString(tag: 0x2a, value: totalDistance)) + navFields.append(encodeString(tag: 0x32, value: eta)) + navFields.append(encodeString(tag: 0x3a, value: speed)) + navFields.append(contentsOf: [0x40, iconType]) + + // Wrap in container + var payload = Data([0x08, 0x07, 0x2a, UInt8(navFields.count)]) + payload.append(navFields) + + // Build packet + let service = Data([0x08, 0x20]) + let pktInfo = Data([0x01, 0x01]) + let fullPayload = pktInfo + service + payload + + var packet = Data([0xaa, 0x21, sequence, UInt8(fullPayload.count)]) + packet.append(fullPayload) + + // Add CRC + let crc = crc16CCITT(fullPayload) + packet.append(contentsOf: [UInt8(crc & 0xFF), UInt8(crc >> 8)]) + + return packet + } + + private func encodeString(tag: UInt8, value: String) -> Data { + let utf8 = value.utf8 + return Data([tag, UInt8(utf8.count)]) + Data(utf8) + } +} +``` + +## Map Rendering + +Full map rendering data is transmitted on Handle `0x0864` as bitmap/image data: + +- Multiple packets with same sequence, incrementing serial number +- Contains turn arrow overlays and route visualization +- Large payloads (observed: 500+ bytes across multiple packets) + +*Full map rendering protocol not yet documented.* + +## Integration with Apple Maps + +To integrate with Apple Maps on iOS: + +1. Use `MKDirections` to get route steps +2. Extract maneuver type, distance, and instructions +3. Convert to G2 navigation format +4. Send packets on `0x0842` characteristic + +```swift +func sendNavigationStep(_ step: MKRoute.Step) { + let nav = G2Navigation( + distance: formatDistance(step.distance), + instruction: step.instructions, + timeRemaining: formatTime(remainingTime), + totalDistance: formatDistance(remainingDistance), + eta: formatETA(arrivalTime), + speed: formatSpeed(currentSpeed), + iconType: mapManeuverToIcon(step.maneuverType) + ) + + let packet = nav.buildPacket(sequence: nextSequence()) + bleManager.write(packet, to: g2WriteCharacteristic) +} +``` + +## Capture Method + +Navigation data was captured using: +- iOS device with Bluetooth logging profile +- Apple PacketLogger during active Google Maps navigation +- Handle filter: `btatt.handle == 0x0842` + +## Contributing + +If you capture additional navigation states (rerouting, arrival, highway mode), please contribute packet dumps with context about the navigation scenario. diff --git a/docs/notifications.md b/docs/notifications.md new file mode 100644 index 0000000..73d7e7f --- /dev/null +++ b/docs/notifications.md @@ -0,0 +1,205 @@ +# Notification Protocol + +This document describes the full notification format for Even G2 glasses, including message body, title, actions, and timestamps. + +## Overview + +Notifications are transmitted on a separate BLE characteristic (Handle `0x0021`) from the main G2 protocol. The format follows an ANCS-like (Apple Notification Center Service) structure with field-length-value encoding. + +## BLE Characteristics + +| Handle | Purpose | +|--------|---------| +| `0x001b` | Notification metadata/control | +| `0x001e` | Notification request | +| `0x0021` | Full notification data | + +## Packet Structure + +### Full Notification (Handle 0x0021) + +``` +[00] [NotifID:2 LE] [Type:2 LE] [00 00] +[BundleLen:2 LE] [BundleID:UTF8] +[01] [Len:2 LE] [Title:UTF8] +[02] [Len:2 LE] [Subtitle:UTF8] +[03] [Len:2 LE] [Body:UTF8] +[04] [Len:2 LE] [InternalID:UTF8] +[05] [Len:2 LE] [Timestamp:ISO8601] +[06] [Len:2 LE] [Action1:UTF8] +[07] [Len:2 LE] [Action2:UTF8] +``` + +### Field Descriptions + +| Field | ID | Description | +|-------|-----|-------------| +| NotifID | Header | 2-byte little-endian notification identifier | +| Type | Header | Notification type (0x02 observed for standard notifications) | +| BundleID | - | App bundle identifier (e.g., `com.apple.mobilephone`) | +| Title | 01 | Primary title or sender name | +| Subtitle | 02 | Secondary text (may be empty) | +| Body | 03 | Main notification content, supports emoji/UTF-8 | +| InternalID | 04 | Internal reference ID as string | +| Timestamp | 05 | ISO 8601 format: `YYYYMMDDTHHmmss` | +| Action1 | 06 | Primary action button text | +| Action2 | 07 | Secondary action button text | + +### App Name Response + +When the app display name is requested: + +``` +[01] [BundleID:UTF8] [00 00] [07] [Len:2 LE] [DisplayName:UTF8] +``` + +## Examples + +### Incoming Phone Call + +``` +Raw: 0027020000001500636f6d2e6170706c652e6d6f62696c6570686f6e65 + 0111002b31202836363229203234312d36303030 + 0209005370616d205269736b + 030d00496e636f6d696e672043616c6c + 0402003133 + 050f003230323630313038543132353933 + 060600416e73776572 + 0707004465636c696e65 + +Decoded: + NotifID: 0x0027 + Type: 0x0002 + BundleID: com.apple.mobilephone + Title: +1 (662) 241-6000 + Subtitle: Spam Risk + Body: Incoming Call + ID: 13 + Timestamp: 20260108T125937 + Action1: Answer + Action2: Decline +``` + +### Missed Call + +``` +Decoded: + NotifID: 0x0028 + BundleID: com.apple.mobilephone + Title: Spam Risk + Subtitle: (empty) + Body: Missed Call + ID: 11 + Timestamp: 20260108T125947 + Action1: Dial + Action2: Clear +``` + +### Location Update (Life360) + +``` +Decoded: + NotifID: 0x002f + BundleID: com.life360.safetymap + Title: Hope + Subtitle: (empty) + Body: 📍 Arrived at Home + ID: 20 + Timestamp: 20260108T131921 + Action1: (empty) + Action2: Clear +``` + +## Implementation Notes + +### Parsing (Python) + +```python +def parse_notification(data: bytes) -> dict: + """Parse notification from Handle 0x0021""" + result = {} + pos = 0 + + # Header + result['notif_id'] = int.from_bytes(data[1:3], 'little') + result['type'] = int.from_bytes(data[3:5], 'little') + pos = 6 + + # Bundle ID + bundle_len = int.from_bytes(data[pos:pos+2], 'little') + pos += 2 + result['bundle_id'] = data[pos:pos+bundle_len].decode('utf-8') + pos += bundle_len + + # Fields 01-07 + field_names = ['title', 'subtitle', 'body', 'internal_id', + 'timestamp', 'action1', 'action2'] + + for i, name in enumerate(field_names, start=1): + if pos >= len(data): + break + field_id = data[pos] + if field_id != i: + continue + pos += 1 + field_len = int.from_bytes(data[pos:pos+2], 'little') + pos += 2 + if field_len > 0: + result[name] = data[pos:pos+field_len].decode('utf-8') + else: + result[name] = '' + pos += field_len + + return result +``` + +### Swift Implementation + +```swift +struct G2Notification { + let notifId: UInt16 + let bundleId: String + let title: String + let subtitle: String + let body: String + let timestamp: String + let action1: String + let action2: String +} + +func parseNotification(data: Data) -> G2Notification? { + guard data.count > 6 else { return nil } + + let notifId = data.subdata(in: 1..<3).withUnsafeBytes { + $0.load(as: UInt16.self) + } + + var pos = 6 + + // Parse bundle ID + let bundleLen = Int(data[pos]) | (Int(data[pos+1]) << 8) + pos += 2 + let bundleId = String(data: data.subdata(in: pos..