Skip to content

iOS SBeacon Framework Calibration

AigarsM edited this page Feb 2, 2026 · 1 revision

Calibration

iOS SBeacon Framework v1.0.8

This guide covers accelerometer calibration for SBeacon devices. Calibration is critical for accurate driving behavior detection.


Why Calibration is Critical

SBeacon uses a 3-axis accelerometer to detect driving events. Without calibration:

  • G-force readings are inaccurate
  • False positives/negatives in event detection
  • Trip detection may fail

Calibration ensures the accelerometer knows "level" - establishing a baseline for detecting motion and impacts.


Prerequisites

  • Beacon Pairing - Beacon must be paired and connected
  • Physical requirements (see below)

Physical Requirements

Surface Requirements

CRITICAL: Place beacon on a flat, stable, horizontal surface during calibration:

  • ✅ Level table or desk
  • ✅ Flat floor
  • ✅ Dashboard of parked vehicle (engine off)
  • ❌ Angled surface
  • ❌ Moving vehicle
  • ❌ Unstable surface

Orientation Requirements

Follow ISO 8855 vehicle axis standard:

       ^ Z (up/down)
       |
       |
       +----> Y (forward/backward)
      /
     /
    X (left/right)

Mount beacon in vehicle with:

  • X-axis: Side-to-side (lateral)
  • Y-axis: Front-to-back (longitudinal)
  • Z-axis: Up-down (vertical)

Checking Calibration Status

Check if Beacon is Calibrated

import BLEFramework

// Method 1: Check beacon property
if beacon.isCalibrated {
    print("Beacon is already calibrated")
} else {
    print("Beacon needs calibration")
}

// Method 2: Query beacon directly
SBeaconManager.shared.checkBeaconCalibration()

// Result updates beacon.isCalibrated property automatically

Performing Calibration

Basic Calibration

func calibrateBeacon() {
    // Set callback
    SBeaconManager.shared.calibrationComplete = { [weak self] error in
        if let error = error {
            print("Calibration failed: \(error)")
            self?.handleCalibrationFailure(error)
        } else {
            print("Calibration successful!")
            self?.onCalibrationSuccess()
        }
    }

    // Start calibration (beacon must be on flat surface)
    SBeaconManager.shared.calibrateBeacon()
}

Calibration with Timeout

import Foundation
import BLEFramework

class CalibrationManager {
    private var calibrationTimer: Timer?
    private let calibrationTimeout: TimeInterval = 15.0

    func calibrateBeacon(completion: @escaping (Result<Void, CalibrationError>) -> Void) {
        // Set callback
        SBeaconManager.shared.calibrationComplete = { [weak self] error in
            guard let self = self else { return }

            // Cancel timeout
            self.calibrationTimer?.invalidate()
            self.calibrationTimer = nil

            if let error = error {
                completion(.failure(error))
            } else {
                completion(.success(()))
            }

            // Clean up callback
            SBeaconManager.shared.calibrationComplete = nil
        }

        // Start timeout
        calibrationTimer = Timer.scheduledTimer(withTimeInterval: calibrationTimeout, repeats: false) { [weak self] _ in
            guard let self = self else { return }

            SBeaconManager.shared.calibrationComplete = nil
            completion(.failure(.timeout))
        }

        // Start calibration
        SBeaconManager.shared.calibrateBeacon()
    }

    enum CalibrationError: Error {
        case noBeacon
        case timeout
        case notOnFlatSurface
    }
}

SwiftUI Example

Complete Calibration View

import SwiftUI
import BLEFramework

struct CalibrationView: View {
    @StateObject private var viewModel: CalibrationViewModel
    let beacon: SCPBeacon

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

    var body: some View {
        VStack(spacing: 24) {
            // Status
            if viewModel.isCalibrating {
                ProgressView("Calibrating...")
                    .scaleEffect(1.5)

                Text("Keep beacon on flat surface")
                    .font(.caption)
                    .foregroundColor(.secondary)

            } else if viewModel.isCalibrated {
                Image(systemName: "checkmark.circle.fill")
                    .font(.system(size: 64))
                    .foregroundColor(.green)

                Text("Beacon Calibrated")
                    .font(.headline)

                if let date = beacon.isCalibratedOn {
                    Text("Calibrated on \(date.formatted())")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }

            } else {
                Image(systemName: "location.north.circle")
                    .font(.system(size: 64))
                    .foregroundColor(.orange)

                Text("Calibration Required")
                    .font(.headline)

                VStack(alignment: .leading, spacing: 8) {
                    InstructionRow(icon: "1.circle.fill", text: "Place beacon on flat surface")
                    InstructionRow(icon: "2.circle.fill", text: "Ensure surface is stable and level")
                    InstructionRow(icon: "3.circle.fill", text: "Tap 'Calibrate' button")
                }
                .padding()

                Button("Calibrate Beacon") {
                    viewModel.startCalibration()
                }
                .buttonStyle(.borderedProminent)
            }

            if let error = viewModel.errorMessage {
                Text(error)
                    .foregroundColor(.red)
                    .font(.caption)
            }
        }
        .padding()
        .navigationTitle("Beacon Calibration")
        .onAppear {
            viewModel.checkCalibration()
        }
    }
}

struct InstructionRow: View {
    let icon: String
    let text: String

    var body: some View {
        HStack {
            Image(systemName: icon)
                .foregroundColor(.blue)
            Text(text)
                .font(.callout)
        }
    }
}

class CalibrationViewModel: ObservableObject {
    @Published var isCalibrating = false
    @Published var isCalibrated = false
    @Published var errorMessage: String?

    private let beacon: SCPBeacon
    private var calibrationTimer: Timer?

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

    func checkCalibration() {
        SBeaconManager.shared.checkBeaconCalibration()

        // Update after check
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
            self?.isCalibrated = self?.beacon.isCalibrated ?? false
        }
    }

    func startCalibration() {
        isCalibrating = true
        errorMessage = nil

        SBeaconManager.shared.calibrationComplete = { [weak self] error in
            guard let self = self else { return }

            self.calibrationTimer?.invalidate()
            self.isCalibrating = false

            if let error = error {
                self.errorMessage = "Calibration failed: \(error.localizedDescription)"
            } else {
                self.isCalibrated = true
                self.beacon.isCalibrated = true
            }

            SBeaconManager.shared.calibrationComplete = nil
        }

        // Timeout protection
        calibrationTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
            guard let self = self else { return }

            if self.isCalibrating {
                self.isCalibrating = false
                self.errorMessage = "Calibration timeout. Please try again."
                SBeaconManager.shared.calibrationComplete = nil
            }
        }

        SBeaconManager.shared.calibrateBeacon()
    }

    deinit {
        calibrationTimer?.invalidate()
        SBeaconManager.shared.calibrationComplete = nil
    }
}

UIKit Example

import UIKit
import BLEFramework

class CalibrationViewController: UIViewController {
    private let beacon: SCPBeacon
    private let statusLabel = UILabel()
    private let instructionsLabel = UILabel()
    private let calibrateButton = UIButton(type: .system)
    private let activityIndicator = UIActivityIndicatorView(style: .large)

    private var calibrationTimer: Timer?

    init(beacon: SCPBeacon) {
        self.beacon = beacon
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        checkCalibrationStatus()
    }

    private func setupUI() {
        view.backgroundColor = .systemBackground
        title = "Calibration"

        statusLabel.textAlignment = .center
        statusLabel.font = .systemFont(ofSize: 24, weight: .bold)

        instructionsLabel.numberOfLines = 0
        instructionsLabel.textAlignment = .center
        instructionsLabel.font = .systemFont(ofSize: 16)
        instructionsLabel.textColor = .secondaryLabel

        calibrateButton.setTitle("Calibrate Beacon", for: .normal)
        calibrateButton.addTarget(self, action: #selector(calibrateButtonTapped), for: .touchUpInside)

        // Layout (simplified - use Auto Layout in production)
        // Add subviews and constraints...
    }

    private func checkCalibrationStatus() {
        if beacon.isCalibrated {
            showCalibrated()
        } else {
            showNotCalibrated()
        }
    }

    @objc private func calibrateButtonTapped() {
        startCalibration()
    }

    private func startCalibration() {
        calibrateButton.isEnabled = false
        activityIndicator.startAnimating()
        statusLabel.text = "Calibrating..."
        instructionsLabel.text = "Keep beacon on flat surface"

        SBeaconManager.shared.calibrationComplete = { [weak self] error in
            self?.onCalibrationComplete(error: error)
        }

        calibrationTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
            self?.onCalibrationTimeout()
        }

        SBeaconManager.shared.calibrateBeacon()
    }

    private func onCalibrationComplete(error: CalibrationError?) {
        calibrationTimer?.invalidate()
        activityIndicator.stopAnimating()
        calibrateButton.isEnabled = true

        if let error = error {
            showError("Calibration failed: \(error.localizedDescription)")
        } else {
            beacon.isCalibrated = true
            showCalibrated()
        }

        SBeaconManager.shared.calibrationComplete = nil
    }

    private func onCalibrationTimeout() {
        activityIndicator.stopAnimating()
        calibrateButton.isEnabled = true
        showError("Calibration timeout. Please try again.")
        SBeaconManager.shared.calibrationComplete = nil
    }

    private func showCalibrated() {
        statusLabel.text = "✓ Calibrated"
        statusLabel.textColor = .systemGreen
        instructionsLabel.text = "Beacon is ready for use"
        calibrateButton.isHidden = true
    }

    private func showNotCalibrated() {
        statusLabel.text = "Calibration Required"
        statusLabel.textColor = .systemOrange
        instructionsLabel.text = "Place beacon on flat surface and tap Calibrate"
        calibrateButton.isHidden = false
    }

    private func showError(_ message: String) {
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }

    deinit {
        calibrationTimer?.invalidate()
        SBeaconManager.shared.calibrationComplete = nil
    }
}

Best Practices

  1. Calibrate on Flat Surface: Most common cause of calibration failure
  2. Keep Beacon Still: No movement during calibration (15 seconds)
  3. Use Timeout Protection: Prevent indefinite waiting
  4. Verify Afterward: Check beacon.isCalibrated property
  5. Recalibrate When Needed: After firmware updates, battery replacement, or if detection seems off
  6. Store Calibration Date: Track when calibration was last performed
  7. User Instructions: Clearly guide users through process

Troubleshooting

Calibration Never Completes

Problem: calibrationComplete callback never fires

Solutions:

  • Ensure beacon is connected
  • Check timeout protection is implemented
  • Verify beacon is on flat surface (tilt affects calibration)
  • Try power cycling beacon (remove/reinstall battery)

Calibration Fails with Error

Problem: calibrationComplete returns error

Solutions:

  • Check CalibrationError.noBeacon - beacon disconnected
  • Ensure service characteristic is available
  • Reconnect beacon and retry

Calibration Succeeds but Events Still Inaccurate

Problem: isCalibrated == true but detection is off

Solutions:

  • Verify beacon mounting orientation in vehicle matches ISO8855
  • Ensure beacon is securely mounted (not loose/vibrating)
  • Check beacon isn't mounted on angled surface in vehicle
  • Recalibrate in vehicle (parked, level surface)

Calibration Status Not Persisting

Problem: isCalibrated resets to false

Solutions:

  • CoreData may not be saving - call BeaconCoreDataManager.shared.saveContext()
  • Check beacon object isn't being recreated/replaced
  • Verify isCalibratedOn date is being set

Related Pages

Clone this wiki locally