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
- Beacon Pairing - Beacon must be paired and connected
- Calibration - Beacon should be calibrated for accurate detection
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:
- IDLE: Vehicle parked, no motion detected
- START: Motion detected, trip beginning
- ACTIVE: Trip in progress, vehicle moving
- END: Motion stopped, trip ending
- 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 millisecondsTripRecord.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.timestampis 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
- Calibration - Required for trip detection
- Driving Behavior - Trip-based event detection
- Event Handling - All callback details
- Data Models - TripRecord structure