Skip to content

iOS SBeacon Framework Trip Management

AigarsM edited this page Feb 2, 2026 · 1 revision

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

Clone this wiki locally