iOS SBeacon Framework Trip Management - scopetech/Mobile-Public GitHub Wiki

Trip Management

iOS SBeacon Framework v1.0.8

This guide covers trip lifecycle monitoring, trip status detection, and retrieving trip data from SBeacon devices.


Overview

The SBeacon device automatically detects when a trip starts and ends using motion sensors. Your app can monitor trip status changes in real-time and retrieve historical trip records stored in the beacon's memory.


Prerequisites


Trip Lifecycle States

TripStatus Enum

public enum TripStatus: Int {
    case Idle = 0      // No trip in progress
    case Start = 1     // Trip just started
    case Active = 2    // Trip in progress
    case End = 3       // Trip just ended
    case Timeout = 4   // Trip ended due to timeout
    case Unknown = 6   // Unknown state
}

State Transitions

IDLE → START → ACTIVE → END → IDLE
                   ↓
                TIMEOUT → IDLE

Flow:

  1. IDLE: Vehicle parked, no motion detected
  2. START: Motion detected, trip beginning
  3. ACTIVE: Trip in progress, vehicle moving
  4. END: Motion stopped, trip ending
  5. IDLE: Trip complete, data available for sync

Monitoring Trip Status

Real-Time Trip Status Callback

import BLEFramework

class TripMonitor {
    init() {
        setupTripMonitoring()
    }

    private func setupTripMonitoring() {
        SBeaconManager.shared.tripStatusChanged = { [weak self] status in
            self?.handleTripStatusChange(status)
        }
    }

    private func handleTripStatusChange(_ status: TripStatus) {
        switch status {
        case .Idle:
            print("No trip in progress")

        case .Start:
            print("Trip starting!")
            onTripStart()

        case .Active:
            print("Trip in progress")

        case .End:
            print("Trip ending")

        case .Timeout:
            print("Trip timed out")
            // Request data before transitioning to IDLE
            requestTripData()

        case .Unknown:
            print("Unknown trip status")
        }
    }

    private func onTripStart() {
        // Start tracking, update UI, etc.
    }

    private func requestTripData() {
        // Fetch trip records after trip ends
    }

    deinit {
        SBeaconManager.shared.tripStatusChanged = nil
    }
}

Trip Information (Counts)

Request Trip Information

Get counts of unsynced data on the beacon:

func requestTripInformation(for beacon: SCPBeacon) {
    // Set callback
    SBeaconManager.shared.tripInformationReceived = { [weak self] tripCount, incidentCount, dbEventCount in
        print("Unsynced trips: \(tripCount)")
        print("Incidents: \(incidentCount)")
        print("Driving behavior events: \(dbEventCount)")

        self?.handleTripInformation(
            trips: tripCount,
            incidents: incidentCount,
            dbEvents: dbEventCount
        )
    }

    // Request information
    SBeaconManager.shared.requestTripInformation(beacon: beacon)
}

private func handleTripInformation(trips: Int, incidents: Int, dbEvents: Int) {
    if trips > 0 {
        // Trip data available - fetch it
        requestTripRecords()
    }

    if incidents > 0 {
        // Incident data available
    }

    if dbEvents > 0 {
        // Driving behavior events available
    }
}

Trip Records

TripRecord Model

Each trip record contains:

public struct TripRecord {
    let index: UInt8              // Record index (0-99)
    let startTimestamp: UInt32     // Start time (beacon seconds)
    let endTimestamp: UInt32       // End time (beacon seconds)
    var actualStartTimestamp: Int64?  // Real-world start time (ms)
    var actualEndTimestamp: Int64?    // Real-world end time (ms)

    // Computed properties
    var startDate: Date?           // Start date/time
    var endDate: Date?             // End date/time
    var duration: UInt32           // Duration in seconds
    var durationInterval: TimeInterval // Duration as TimeInterval
}

Request Trip Records

func requestTripRecords(for beacon: SCPBeacon) {
    // Set callback
    SBeaconManager.shared.tripRecordsReceived = { [weak self] records in
        print("Received \(records.count) trip records")
        self?.processTripRecords(records)
    }

    // Request records
    SBeaconManager.shared.requestTripRecords(beacon: beacon)
}

private func processTripRecords(_ records: [TripRecord]) {
    for record in records {
        guard let startDate = record.startDate,
              let endDate = record.endDate else {
            continue
        }

        let durationMinutes = Int(record.duration / 60)

        print("Trip \(record.index):")
        print("  Start: \(startDate)")
        print("  End: \(endDate)")
        print("  Duration: \(durationMinutes) minutes")

        // Upload to server, save to database, etc.
        uploadTripRecord(record)
    }
}

Timestamp Handling

Beacon Time vs. Real Time

SBeacon uses its own internal timer that starts at 0 when powered on. To convert to real-world time:

// Beacon stores a "start time" offset in milliseconds
// This is set when the beacon connects

let beaconStartTimeMs = beacon.timestamp?.int64Value ?? 0

// Trip record timestamps are in beacon seconds
let tripStartBeaconSeconds = record.startTimestamp  // e.g., 86400 (1 day)

// Convert to real-world time
let actualStartTimeMs = beaconStartTimeMs + (Int64(tripStartBeaconSeconds) * 1000)
let actualStartDate = Date(timeIntervalSince1970: Double(actualStartTimeMs) / 1000.0)

Framework automatically handles this conversion:

  • TripRecord.actualStartTimestamp - Real time in milliseconds
  • TripRecord.startDate - Swift Date object

Complete Integration Example

SwiftUI Trip Management View

import SwiftUI
import BLEFramework

struct TripManagementView: View {
    @StateObject private var viewModel: TripManagementViewModel
    let beacon: SCPBeacon

    init(beacon: SCPBeacon) {
        self.beacon = beacon
        _viewModel = StateObject(wrappedValue: TripManagementViewModel(beacon: beacon))
    }

    var body: some View {
        List {
            Section("Current Status") {
                HStack {
                    Text("Trip Status")
                    Spacer()
                    Text(viewModel.currentStatus.description)
                        .foregroundColor(statusColor)
                }
            }

            Section("Trip Information") {
                HStack {
                    Text("Unsynced Trips")
                    Spacer()
                    Text("\(viewModel.tripCount)")
                }

                HStack {
                    Text("Incidents")
                    Spacer()
                    Text("\(viewModel.incidentCount)")
                }

                HStack {
                    Text("DB Events")
                    Spacer()
                    Text("\(viewModel.dbEventCount)")
                }

                Button("Refresh Information") {
                    viewModel.requestInformation()
                }
            }

            Section("Trip Records") {
                if viewModel.tripRecords.isEmpty {
                    Text("No trip records")
                        .foregroundColor(.secondary)
                } else {
                    ForEach(viewModel.tripRecords, id: \.index) { record in
                        TripRecordRow(record: record)
                    }
                }

                Button("Fetch Trip Records") {
                    viewModel.requestTripRecords()
                }
                .disabled(viewModel.tripCount == 0)
            }
        }
        .navigationTitle("Trip Management")
        .onAppear {
            viewModel.startMonitoring()
        }
        .onDisappear {
            viewModel.stopMonitoring()
        }
    }

    private var statusColor: Color {
        switch viewModel.currentStatus {
        case .Active: return .green
        case .Start: return .blue
        case .End: return .orange
        case .Idle: return .secondary
        default: return .gray
        }
    }
}

struct TripRecordRow: View {
    let record: TripRecord

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Trip #\(record.index)")
                .font(.headline)

            if let startDate = record.startDate {
                Text("Start: \(startDate.formatted())")
                    .font(.caption)
            }

            if let endDate = record.endDate {
                Text("End: \(endDate.formatted())")
                    .font(.caption)
            }

            Text("Duration: \(formattedDuration)")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding(.vertical, 4)
    }

    private var formattedDuration: String {
        let minutes = Int(record.duration / 60)
        let hours = minutes / 60
        let remainingMinutes = minutes % 60

        if hours > 0 {
            return "\(hours)h \(remainingMinutes)m"
        } else {
            return "\(minutes)m"
        }
    }
}

class TripManagementViewModel: ObservableObject {
    @Published var currentStatus: TripStatus = .Idle
    @Published var tripCount = 0
    @Published var incidentCount = 0
    @Published var dbEventCount = 0
    @Published var tripRecords: [TripRecord] = []

    private let beacon: SCPBeacon

    init(beacon: SCPBeacon) {
        self.beacon = beacon
    }

    func startMonitoring() {
        // Monitor trip status changes
        SBeaconManager.shared.tripStatusChanged = { [weak self] status in
            self?.currentStatus = status

            // Auto-fetch data when trip ends
            if status == .End || status == .Timeout {
                self?.requestInformation()
            }
        }

        // Initial request
        requestInformation()
    }

    func stopMonitoring() {
        SBeaconManager.shared.tripStatusChanged = nil
        SBeaconManager.shared.tripInformationReceived = nil
        SBeaconManager.shared.tripRecordsReceived = nil
    }

    func requestInformation() {
        SBeaconManager.shared.tripInformationReceived = { [weak self] trips, incidents, dbEvents in
            self?.tripCount = trips
            self?.incidentCount = incidents
            self?.dbEventCount = dbEvents
        }

        SBeaconManager.shared.requestTripInformation(beacon: beacon)
    }

    func requestTripRecords() {
        SBeaconManager.shared.tripRecordsReceived = { [weak self] records in
            self?.tripRecords = records
        }

        SBeaconManager.shared.requestTripRecords(beacon: beacon)
    }

    deinit {
        stopMonitoring()
    }
}

Best Practices

1. Fetch Data After Trip Ends

Wait for END or TIMEOUT status before requesting data:

SBeaconManager.shared.tripStatusChanged = { [weak self] status in
    if status == .End || status == .Timeout {
        // Wait briefly for beacon to finalize data
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            self?.requestTripInformation()
        }
    }
}

2. Check Counts Before Fetching Records

func requestTripInformation() {
    SBeaconManager.shared.tripInformationReceived = { [weak self] tripCount, _, _ in
        if tripCount > 0 {
            // Only fetch if there's data available
            self?.requestTripRecords()
        }
    }

    SBeaconManager.shared.requestTripInformation(beacon: beacon)
}

3. Clean Up Callbacks

deinit {
    SBeaconManager.shared.tripStatusChanged = nil
    SBeaconManager.shared.tripInformationReceived = nil
    SBeaconManager.shared.tripRecordsReceived = nil
}

4. Handle Beacon Disconnection

NotificationCenter.default.addObserver(
    forName: SBeaconManager.beaconDisconnectedNotification,
    object: nil,
    queue: .main
) { [weak self] _ in
    // Beacon disconnected - stop monitoring
    self?.currentStatus = .Unknown
}

5. Upload Then Clear

After successfully uploading trip data to your server, consider clearing beacon memory (if supported) to prevent re-sync.


Troubleshooting

Trip Status Callback Never Fires

Problem: tripStatusChanged never called

Solutions:

  • Ensure beacon is designated as "car beacon" (SCPBeaconManager.shared.isCarBeacon(beacon))
  • Verify beacon is connected (beacon.isInRange == true)
  • Check beacon is calibrated (beacon.isCalibrated == true)
  • Ensure callback is set before trip status changes

No Trip Records Returned

Problem: tripRecordsReceived callback returns empty array

Solutions:

  • Call requestTripInformation() first to check if data exists
  • Verify trips have occurred since last sync
  • Check beacon timestamp offset is set (beacon.timestamp != nil)
  • Ensure beacon has completed at least one trip cycle (IDLE → START → ACTIVE → END → IDLE)

Incorrect Timestamps

Problem: Trip dates/times are wrong

Solutions:

  • Beacon must be connected at least once to set time offset
  • Check beacon.timestamp is not nil or 0
  • Verify device time is correct
  • Reconnect beacon to update time offset

Trip Not Detected

Problem: Vehicle starts but trip status stays IDLE

Solutions:

  • Beacon must be calibrated (see Calibration guide)
  • Ensure beacon is mounted correctly in vehicle
  • Check beacon battery level (low battery affects detection)
  • Verify beacon is designated as car beacon

Related Pages