Overview
Zebra's WS101 Bluetooth Communication Badge is a rugged Bluetooth comm device that can add hands-free voice capabilities to Zebra mobile computers and tablets. Small and light enough to be worn or pocketed, WS101-series devices provide workers with a means to easily communicate with fellow employees through Zebra's voice-enabled apps or a company's own. Staff can make calls, look up inventory items, hear responses and receive instructions, all without touching a mobile device or tablet.
![]() |
![]() |
![]() |
|||
| The WS101 in healthcare, pharma and retail. |
Main Features
- Button Events: P1, P2, P3
- Voice Triggers: Wake Word, Duress Word
- SDK version query:
(getSDKVersion()) - Voice Control: Leverages SiriKit for hands-free app operation
- Hardware Integration: Use of physical buttons on the Bluetooth speaker for core app functions
- Safety Features: Implements a duress/panic button feature triggered by either physical button or dedicated wake word
App-dev Requirements
- Apple Xcode 15 (or later)
- Apple iPhone or iPad running iOS 16 (or later) with:
- Bluetooth
- Microphone
- Siri
- Background modes enabled:
- Use Bluetooth LE accessories
- Act as a Bluetooth LE accessory
- Voice over IP
- Remote notifications
- Background processing
- Background fetch
- Audio, AirPlay, Picture-in-Picture
- WS101 device connected via Bluetooth for "Phone calls" and "Media audio"
Also required, the following must be addded to the info.plist file:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to connect to Zebra Audio Devices.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to the microphone to record audio for voice notes.</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-peripheral</string>
<string>bluetooth-central</string>
<string>fetch</string>
<string>processing</string>
<string>audio</string>
<string>voip</string>
<string>remote-notification</string>
</array>
What is SCO Audio?
Synchronous Connection-oriented (SCO) Audio is a Bluetooth protocol designed for usage scenarios that can benefit from low-latency communications, such as those for voice. SCO Audio delivers its payload with less delay than other protocols by using reserved time slots. The trade-off is lower bandwidth, making SCO Audio unsuitable for music streaming and other high-fidelity audio content.
WS101 Series Controls
Front View
| Control | Description | |
|---|---|---|
| 1 | Microphone | Captures voice and ambient sounds. |
| 2 | Badge clip tower bar | Enables the attachment of a lanyard and/or badge clip. |
| 3 | Power | Performs multiple functions: Power: Press and hold to turn device on or off. Device enters pairing mode each time it's turned on. Pairing: Hold to re-enter pairing mode after device has been powered up. Incoming call: Press once to answer; press twice to reject. On a call: Press twice to hang up. |
| 4 | P1 programmable button | Sends an event to a registered app (unprogrammed by default). Zebra recommends programming this button for push-to-talk function. |
| 5 | LED ring | Multi-colored visual indicator for multiple functions, including pairing, battery charging and level, incoming call, call on hold. |
Side Views
| Control | Description | |
|---|---|---|
| 1 | Volume up/down | Adjusts volume of the device speaker or call volume for headset. |
| 2 | P2 programmable button | Sends an event to a registered app. By default, mutes/unmutes the mic during calls. |
| 3 | Barcode | Enables the "scan-to-pair" Bluetooth function. |
| 4 | NFC "N-Mark" | Location for the "tap-to-pair" Bluetooth function. |
Top View
| Control | Description | |
|---|---|---|
| 1 | 3.5mm audio jack* | Input for headphones or headset; supports push-to-talk functions. |
| 2 | Top LED | Visual status indicator for incoming calls, call on hold. |
| 3 | Speaker | Projects audio upward, toward device user. |
| 4 | P3 programmable button (red) | Sends an event to a registered app (unprogrammed by default). |
* Feature not present on "-H" (blue) models.
Installation
Step I. Add Zebra AudioConnect SDK
Drag and drop
project.ZebraAudioConnectSDK.xcframeworkinto the project.Go to Project > General > Frameworks, Libraries, and Embedded Content.
Set
project.ZebraAudioConnectSDK.xcframeworkto Embed > Sign.If an error appears relating to the
plist, perform the following:
a. Go to target Build Phases.
b. Expand "Copy Bundle Resources" (see image, below).
c. SelectInfo.plistfrom the list and remove it using the minus ("-") button:
Click image to enlarge; ESC to exit.
Step II. Initialize BLE Manager
1. Import the SDK in ViewController:
import ZebraAudioConnectSDK
2. Create an instance of BLEmanager and assign the delegate:
class ViewController: UIViewController, BLEManagerDelegate {
var bleManager: BLEManager?
override func viewDidLoad() {
super.viewDidLoad()
bleManager = BLEManager()
bleManager?.delegate = self
}
}
Step III. Implement Delegate Methods
The BLEManagerDelegate methods allow the app to receive real-time events from the BLE device. Supported methods are shown below.
| Delegate Method | Description |
|---|---|
func didDetectButtonEvent (button: ZWAButton, state: ButtonState) |
Triggered when a button event occurs. Provides details about the button involved (e.g. PTT, DND, DURESS) and its current state (e.g., pressed, released, long-pressed). |
func didDetectVoiceTrigger (triggerType: VoiceTriggerType) |
Called when a voice trigger is detected. Provides details about the type of voice command that was recognized, such as a Wake Word or Duress Word. |
func deviceDidConnect (device: ZWABluetoothDevice) |
Called when a Zebra WS101 device successfully connects. Passes the ZWABluetooth object, which includes device name, connection status, and type (e.g. hDeviceBLE) |
func ZWAdeviceDidDisconnect (device: ZWABluetoothDevice, reason: DisconnectReareason) |
Called when a device disconnects. Provides the ZWABluetoothDevice object with device details and a DisconnectReason to explain why disconnection occurred. |
Example Delegate Method
func didDetectButtonEvent(button: ZebraAudioConnectSDK.ZWAButton, state: ZebraAudioConnectSDK.ButtonState) {
switch button {
case .p1:
print("P1 button \(state)")
case .p2:
print("P2 button \(state)")
case .p3:
print("P3 button \(state)")
@unknown default:
print("Unknown Button Detected")
}
}
func didDetectVoiceTrigger(triggerType: ZebraAudioConnectSDK.VoiceTriggerType) {
switch triggerType {
case .wakeWord:
print("Wakeword Voice Trigger Detected")
case .duressWord:
print("Duressword Voice Trigger Detected")
@unknown default:
print("Unknown Voice Trigger Detected")
}
}
func deviceDidConnect(device: ZebraAudioConnectSDK.ZWABluetoothDevice) {
print("Model Name: \(device.name)")
}
func deviceDidDisconnect(device: ZebraAudioConnectSDK.ZWABluetoothDevice, reason: ZebraAudioConnectSDK.DisconnectReason) {
print("Device Disconnected - Name: \(device.name) Reason: \(reason)")
}
Step IV. State Restoration Support
The SDK retains state information from previous connections and can reconnect to previously connected devices automatically when the app is subsequently launched or termination.
Background Mode Support
To receive BLE events in the background:
- Ensure
bluetooth-centralis enabled in Background Modes. - Avoid blocking main thread operations inside delegate methods.
Step V. Testing the Integration
Zebra recommends following the procedures listed below to test the app.
NOTE: Testing must be done on physical devices; BLE does not support simulation.
- Ensure that a BLE audio device (e.g., WS101) is nearby, powered up and connected to the host device.
- Trigger actions (P1, P2 and P3 button events, and Duress and Wake Word voice triggers) and confirm callbacks are received.
- Press and hold P1; an audio recording should begin.
- Release P1; the recording should stop.
- Press and release P2; the last-saved audio recording should play.
- Speak the wake word to determine whether the WS101 badge detects the word and wakes.
- Speak the command "Start recording on SDK tester" to verify that the app records audio for 10 seconds.
- Speak the Wake Word to wake the device.
- Speak the command "Play the recording of SDK tester" to verify that the app plays back the last saved audio recording.
- Long-press P3 (Duress) button for at least three (3) seconds to verify that the app immediately displays an incoming call screen to simulate a "Duress Call" and provides a pretext for the user to leave a situation.
- Speak the Duress Wake Word to verify that the app immediately displays the same incoming test call screen.
Data Structures
Button Event Listener
public protocol BLEManagerDelegate: AnyObject {
func didDetectButtonEvent(button: ZWAButton, state: ButtonState)
}
public enum ZWAButton {
case p1 //Programmable Button 1
case p2 //Programmable Button 2
case p3 //Programmable Button 3
}
public enum ButtonState {
case press // Button pressed down
case release // Button released
case longPress // Button held down for longer than 1 second
}
Connection Event Listener Interface
public protocol BLEManagerDelegate: AnyObject {
func systemConnectedDeviceStatus(isConnected: Bool)
func deviceDidConnect(device: ZWABluetoothDevice)
func deviceDidDisconnect(device: ZWABluetoothDevice, reason: DisconnectReason)
}
public struct ZWABluetoothDevice {
public var name: String // Device name (e.g., "Zebra WS101")
public var isConnected: Bool // Current connection status
public var deviceType: DeviceType // BLE
public init(name: String, isConnected: Bool, deviceType: DeviceType) {
self.name = name
self.isConnected = isConnected
self.deviceType = deviceType
}
}
public enum DeviceType {
case ble
case classic
}
public enum DisconnectReason {
case connectionFailed
case unknownError
case bluetoothDisabled
}
Voice Trigger Listener Interface
public protocol BLEManagerDelegate: AnyObject {
func didDetectVoiceTrigger(triggerType: VoiceTriggerType)
}
public enum VoiceTriggerType {
case wakeWord
case duressWord
}
Wakeup Word: Triggers app-defined actions (e.g. launch a voice assistant app). Duress Word: Triggers actions associated with Duress word
Internal SDK Behavior
Bluetooth Handling
- The SDK automatically manages Bluetooth connections.
- A device actively connected with audio from system settings automatically connects from the client app.
- Once a Zebra WS101 device is connected, button events and voice triggers start streaming, provided the device was configured to send SDK events on button and voice triggers using the Bluetooth Comms Utility.
- No explicit handling is required by the client app.
Wake and Duress Word Processing
- The SDK listens for predefined events when a voice trigger is detected on the Bluetooth device.
- Wakeup Duress words can be used to trigger actions defined by the app or a customer.
Usage Example
The iOS application code snippet below demonstrates how to connect to Zebra's Bluetooth audio accessories, receive hardware button-press events (P1, P2, P3), and detect voice triggers such as "Wake Word" and "Duress Word." All activities from the connected device are displayed on the screen in real time.
import UIKit
import ZebraAudioConnectSDK
import AVFoundation
class ViewController: UIViewController, BLEManagerDelegate, UITableViewDataSource, UITableViewDelegate {
var bleManager: BLEManager?
@IBOutlet weak var sdkVersionLabel: UILabel!
@IBOutlet weak var tableView: UITableView!
private var messages: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
bleManager = BLEManager()
bleManager?.delegate = self
tableView.dataSource = self
tableView.delegate = self
// Get and display the SDK version
if let sdkVersion = bleManager?.getSDKVersion() {
sdkVersionLabel.text = "SDK Version: \(sdkVersion)"
}
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MessageCell")
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioRouteChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: nil)
}
@objc private func appWillEnterForeground() {
if bleManager == nil {
bleManager = BLEManager()
bleManager?.delegate = self
print("BLEManager initialized")
} else {
print("BLEManager already exists")
}
bleManager?.getSystemConnectedDeviceStatus()
}
func systemConnectedDeviceStatus(isConnected: Bool) {
if !isConnected{
self.updateMessages(with: "Ensure the BT device is turned on and connected to the host via System Settings.")
}
}
func didDetectButtonEvent(button: ZebraAudioConnectSDK.ZWAButton, state: ZebraAudioConnectSDK.ButtonState) {
switch button {
case .p1:
self.updateMessages(with: "P1 button \(state)")
case .p2:
self.updateMessages(with: "P2 button \(state)")
case .p3:
self.updateMessages(with: "P3 button \(state)")
}
}
func didDetectVoiceTrigger(triggerType: ZebraAudioConnectSDK.VoiceTriggerType) {
switch triggerType {
case .wakeWord:
self.updateMessages(with: "Wakeword Voice Trigger Detected")
case .duressWord:
self.updateMessages(with: "Duressword Voice Trigger Detected")
@unknown default:
self.updateMessages(with: "Unknown Voice Trigger Detected")
}
}
func deviceDidConnect(device: ZebraAudioConnectSDK.ZWABluetoothDevice) {
self.updateMessages(with: "Device Connected - Name: \(device.name)")
}
func deviceDidDisconnect(device: ZebraAudioConnectSDK.ZWABluetoothDevice, reason: ZebraAudioConnectSDK.DisconnectReason) {
self.updateMessages(with: "Device Disconnected - Name: \(device.name) Reason: \(reason)")
}
private func updateMessages(with newText: String) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.messages.append(newText)
self.tableView.reloadData()
}
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath)
cell.textLabel?.text = messages[indexPath.row]
cell.textLabel?.numberOfLines = 0
return cell
}
@objc func handleAudioRouteChange(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
bleManager?.getSystemConnectedDeviceStatus()
bleManager?.startScanning()
case .oldDeviceUnavailable:
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs {
print("Disconnected device: \(output.portName)")
bleManager?.disconnectActiveDevice(named: output.portName)
}
}
bleManager?.startScanning()
case .categoryChange: break
case .routeConfigurationChange: break
default: break
}
}
}
Physical Button Control
This feature permits use of WS101 buttons to record and play audio when the app is open on the screen.
Button actions work only when the SDKTester app is in the foreground.
P1 Button
- Long-press to start Recording:
- Press and hold the P1 button to start recording audio
- Release to stop and save recording
P2 Button
- Press to play last recording:
- Press the P2 button to play the most recent recording
Siri Usage Example
StartRecordingIntent AppIntent
struct StartRecordingIntent: AppIntent {
static var title = "Start Recording"
static var openAppWhenRun = true
@MainActor
func perform() async throws -> some IntentResult {
NotificationCenter.default.post(name: .startRecording, object: nil)
return .result()
}
}
- User speaks the
"standard" wake word >> THIS IS NOT DEFINED ANYWHERE(detected by the WS101 badge hardware). - User then says "Start recording on SDK Tester."
- Siri runs this intent.
- The intent sends a small message to the app using NotificationCenter.
- The app listens for this notification and starts a 10-second recording using the Audio Manager.
PlayRecordingIntent AppIntent
struct PlayRecordingIntent: AppIntent {
static var title = "Play Recording"
static var openAppWhenRun = true
@MainActor
func perform() async throws -> some IntentResult {
NotificationCenter.default.post(name: .playRecording, object: nil)
return .result()
}
}
- User speaks wake word.
- User then says "Play recording on SDK Tester."
- Siri runs this intent.
- The intent sends a small message to the app using NotificationCenter.
- App receives message and starts playing the latest saved recording.
SDKTesterAppShortcuts AppShortcutsProvider
The app uses an AppShortcutsProvider to register voice commands that Siri can recognize. This makes it easy for the user to start recording or playing saved audio using natural phrases. The shortcuts help Siri understand multiple ways of saying the command.
import AppIntents
struct SDKTesterAppShortcuts: AppShortcutsProvider {
static var shortcutTileColor: ShortcutTileColor = .lightBlue
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
// Start Recording Shortcut
AppShortcut(
intent: StartRecordingIntent(),
phrases: [
"Start recording on \(.applicationName)",
"Start recording in \(.applicationName)",
"Record audio in \(.applicationName)"
],
shortTitle: LocalizedStringResource(stringLiteral: "Record"),
systemImageName: "record.circle.fill"
)
// Play Recording Shortcut
AppShortcut(
intent: PlayRecordingIntent(),
phrases: [
"Play recording on \(.applicationName)",
"Play recording in \(.applicationName)",
"Play last recording in \(.applicationName)"
],
shortTitle: LocalizedStringResource(stringLiteral: "Play"),
systemImageName: "play.circle.fill"
)
}
Start Recording Shortcuts
- These phrases allow Siri to start a 10-second recording and are linked to
StartRecordingIntent:- “Start recording in SDKTester”
- “Start recording on SDKTester”
- “Record audio in SDKTester”
Play Recording Shortcuts
- These phrases allow Siri to play the most recent audio clip and are linked to
PlayRecordingIntent:- “Play recording on SDKTester”
- “Play recording in SDKTester”
- “Play last recording in SDKTester”
CallKit Usage Example
// MARK: - CallKit Setup & Handling
extension ViewController: CXProviderDelegate {
func setupCallKit() {
let providerConfiguration = CXProviderConfiguration(localizedName: "Duress Call")
providerConfiguration.supportsVideo = false
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.generic]
callProvider = CXProvider(configuration: providerConfiguration)
callProvider?.setDelegate(self, queue: nil)
}
/// Entry point when wakeword detected in background – shows an incoming call UI
func handleWakewordDetected() {
appendMessage("Wakeword detected. Initiating VoIP call...")
// Generate a unique call UUID
let uuid = UUID()
currentCallUUID = uuid
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: "Duress Call")
update.hasVideo = false
callProvider?.reportNewIncomingCall(with: uuid, update: update) { [weak self] error in
if let error = error {
self?.appendMessage("Failed to report incoming call: \(error.localizedDescription)")
} else {
self?.appendMessage("Incoming call reported successfully.")
}
}
}
func endCall(uuid: UUID) {
let endCallAction = CXEndCallAction(call: uuid)
let transaction = CXTransaction(action: endCallAction)
callController.request(transaction) { [weak self] error in
if let error = error {
print("Failed to end call: \(error.localizedDescription)")
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self?.startAutoRecordingForWakeword()
}
print("Call ended successfully.")
}
}
}
// MARK: - CXProviderDelegate
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
appendMessage("Call accepted. Starting recording...")
isCallAccept = true
if UIApplication.shared.applicationState == .active {
if isCallAccept {
if let callUUID = currentCallUUID {
endCall(uuid: callUUID)
}
isCallAccept = false
}
}
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
appendMessage("Call ended.")
isCallAccept = false
action.fulfill()
}
func providerDidReset(_ provider: CXProvider) {
appendMessage("Call provider has been reset.")
// Clean up any ongoing call-related resources
stopRecordingIfNeeded()
currentCallUUID = nil
}
}
// MARK: - VoIP Setup
extension ViewController: PKPushRegistryDelegate {
func setupVoIP() {
voipRegistry = PKPushRegistry(queue: .main)
voipRegistry?.delegate = self
voipRegistry?.desiredPushTypes = [.voIP]
}
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
appendMessage("VoIP push credentials updated.")
// Send the credentials to your server for push notifications
}
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void) {
appendMessage("Received VoIP push notification.")
handleWakewordDetected()
completion()
}
}
Duress/Panic Feature
If the app is in the foreground or background, the user can long-press the P3 button to initialize a call. Once the user -- DO YOU MEAN "CALL RECIENT"? accepts the call, recording will start.
Releasing the P3 button stops the recording.
If a call is triggered by a duress word voice event, the recording stops automatically after 5 seconds.
Multi-app Considerations
- Button/Voice events: Broadcast to all registered listeners across apps.
- SCO sessions: Only one active session is allowed at a time. Subsequent
startScoConnection()calls fail; handle usingonScoFailed().
Frequently Asked Questions
Why isn't the WS101 mic working?
The WS101 requires exclusive use of the microphone via Synchronous Connection-oriented (SCO) audio connection for voice capture. Some of the most common causes of mic failure are listed below.
Causes for mic failure:
- Wrong connection type: SCO requires the Bluetooth "Hands-free" or "Headset" profile to be active. The WS101 must be connected for "Phone calls" and "Media audio" in the Android System settings panel.
- Another app using the microphone: Only one app can use Bluetooth SCO audio at a time. Quit other app(s) that use the mic (e.g. phone dialer, voice recorder) before running yours.
- Missing permissions: If the device is running Android 12 or later, the problem could be that your app lacks special Bluetooth permissions required at runtime.
- Also see next question.
Why doesn't my app work on Android 12?
In addition to a declaration in the app manifest (as indicated under Permissions above), devices running Android 12 (or later) enforce additional Bluetooth permissions that must be requested at runtime. Heightened security introduced with Android 12 requires that apps be granted permission for "Nearby devices" or "Bluetooth" when they start up. To address the issue, try the steps below:
- When the app first runs, grant the "Nearby devices" permission when prompted.
- If previously denied, go to Settings > Apps > [Your App Name] > Permissions and enable "Nearby devices."
- Restart the app to activate the permission changes.
Lack of background permission also might be hindering the app. Try granting the app background permission if the issue persists.
Why isn't my app receiving WS101 button presses or wake-word events?
Button presses and voice commands use the Bluetooth "Hands-free Profile" control channel, which is enabled only when Bluetooth is configured for audio and for calls. If the device is configured only for audio, the channel that carries button and voice signals is not available for your app to use.
Check WS101 settings:
- Go to Settings > Connections (or) Connected Devices* and look for a WS101 device.
- If there's none, tap "Pair new device" and/or follow the normal process for Bluetooth pairing.
The message "Connected for Phone calls and Media audio" should appear.* - Otherwise, tap the gear icon." The "Phone calls" and "Media audio" toggle controls should both be active.
* Exact processes and wording vary by Android version.
Other things to check:
- Pairing vs. Active Connection - Check that the device is not only paired but also connected.
Previously paired (and remembered) devices don't always reconnect when a host device comes into range. - Wrong button or voice trigger mappings - Zebra recommends using the Zebra Bluetooth Comms Utility (coming soon) to create and verify button mappings to configure wake- and duress-word settings.
Can I check whether a WS101 is connected before initializing the SDK?
Yes. The sample Java code below shows how to detect existing WS101 connections using Android Bluetooth APIs:
/**
* Checks if any supported Zebra WS101 devices are currently connected
* Supports device name matching ("ws101"), MAC prefix ("94:FB:29"),
* or Zebra manufacturer ID filtering
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private fun checkConnectedDevices(context: Context): Boolean {
return try {
val bluetoothAdapter = (context.getSystemService(BLUETOOTH_SERVICE) as? BluetoothManager)?.
adapter
if (bluetoothAdapter?.isEnabled != true) return false
bluetoothAdapter.bondedDevices?.any { device ->
val supported = isDeviceSupported(device.name, device.address)
val connected = isDeviceConnected(device)
supported && connected
} ?: false
} catch (e: Exception) {
Log.e(TAG, "Error checking connected devices", e)
false
}
}
private fun isDeviceSupported(deviceName: String?, macAddress: String?): Boolean {
val nameMatches = deviceName?.lowercase()?.contains("ws101") == true
val macMatches = macAddress?.uppercase()?.startsWith("94:FB:29") == true //MAC address prefix 94:
FB:29 is registered to Zebra Technologies Inc.
return nameMatches || macMatches
}
/**
* Checks if a Bluetooth device is currently connected to the Headset/Hands-free profile
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private fun isDeviceConnected(device: BluetoothDevice): Boolean {
return try {
val bluetoothAdapter = (getSystemService(BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
?: return false
// Check Headset (HFP) connection state - required for WS101 button/voice events
val headsetState = bluetoothAdapter.getProfileConnectionState(android.bluetooth.
BluetoothProfile.HEADSET)
headsetState == android.bluetooth.BluetoothProfile.STATE_CONNECTED
} catch (e: Exception) {
Log.w(TAG, "Unable to check headset profile connection for ${device.name}: ${e.message}")
false
}
}
The program code above is provided for demonstration purposes only, and is NOT intended for use in production environments.
Also See
- Zebra WS101 Product Reference Guide (HTML) | Complete usage and technical reference in searchable online format
- Zebra WS101 Product Reference Guide (PDF) | Complete usage and technical reference in portable document format (PDF)


