Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ var package = Package(
name: "BluetoothLinux",
type: libraryType,
targets: ["BluetoothLinux"]
),
.executable(
name: "hcitool",
targets: ["hcitool"]
),
.executable(
name: "hciconfig",
targets: ["hciconfig"]
),
.executable(
name: "gatttool",
targets: ["gatttool"]
),
.executable(
name: "gattserver",
targets: ["gattserver"]
),
.executable(
name: "beacon",
targets: ["beacon"]
)
],
dependencies: [
Expand All @@ -29,6 +49,10 @@ var package = Package(
.package(
url: "https://github.com/PureSwift/Socket.git",
branch: "main"
),
.package(
url: "https://github.com/apple/swift-argument-parser.git",
from: "1.5.0"
)
],
targets: [
Expand All @@ -55,6 +79,72 @@ var package = Package(
.define("ENABLE_MOCKING", .when(configuration: .debug))
]
),
.executableTarget(
name: "hcitool",
dependencies: [
"BluetoothLinux",
.product(
name: "BluetoothGAP",
package: "Bluetooth"
),
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
)
]
),
.executableTarget(
name: "hciconfig",
dependencies: [
"BluetoothLinux",
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
)
]
),
.executableTarget(
name: "gatttool",
dependencies: [
"BluetoothLinux",
.product(
name: "BluetoothGATT",
package: "Bluetooth"
),
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
)
]
),
.executableTarget(
name: "gattserver",
dependencies: [
"BluetoothLinux",
.product(
name: "BluetoothGATT",
package: "Bluetooth"
),
.product(
name: "BluetoothGAP",
package: "Bluetooth"
),
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
)
]
),
.executableTarget(
name: "beacon",
dependencies: [
"BluetoothLinux",
.product(
name: "ArgumentParser",
package: "swift-argument-parser"
)
]
),
.target(
name: "CBluetoothLinux"
),
Expand Down
51 changes: 51 additions & 0 deletions Sources/beacon/Beacon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// Beacon.swift
// BluetoothLinux
//
// Broadcast Bluetooth Low Energy beacons.
//

import Foundation
import ArgumentParser
import Bluetooth
import BluetoothHCI
import BluetoothLinux

@main
struct Beacon: AsyncParsableCommand {

static let configuration = CommandConfiguration(
commandName: "beacon",
abstract: "Broadcast Bluetooth Low Energy beacons.",
subcommands: [
IBeacon.self,
Stop.self
]
)
}

extension HostController.ID {

/// Parse a controller identifier from a command line argument (e.g. `hci0` or `0`).
static func parse(_ argument: String) throws -> HostController.ID {
let string = argument.hasPrefix("hci") ? String(argument.dropFirst(3)) : argument
guard let rawValue = UInt16(string) else {
throw ValidationError("Invalid device identifier '\(argument)'")
}
return .init(rawValue: rawValue)
}
}

extension HostController {

/// Resolve the controller to use for a command.
static func command(device id: HostController.ID?) async throws -> HostController {
if let id {
return try await HostController(id: id)
}
guard let controller = await HostController.controllers.first else {
throw ValidationError("No Bluetooth controllers found.")
}
return controller
}
}
50 changes: 50 additions & 0 deletions Sources/beacon/IBeacon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// IBeacon.swift
// BluetoothLinux
//

import Foundation
import ArgumentParser
import Bluetooth
import BluetoothHCI
import BluetoothLinux

struct IBeacon: AsyncParsableCommand {

static let configuration = CommandConfiguration(
commandName: "ibeacon",
abstract: "Advertise as an Apple iBeacon (requires root)."
)

@Option(name: [.customShort("i"), .long], help: "The controller to use (e.g. hci0).", transform: HostController.ID.parse)
var device: HostController.ID?

@Argument(help: "The proximity UUID of the beacon.", transform: { argument in
guard let uuid = UUID(uuidString: argument) else {
throw ValidationError("Invalid UUID '\(argument)'")
}
return uuid
})
var uuid: UUID

@Option(help: "The value identifying a group of beacons.")
var major: UInt16 = 0

@Option(help: "The value identifying a specific beacon within a group.")
var minor: UInt16 = 0

@Option(help: "The measured signal strength (in dBm) at 1 meter.")
var rssi: Int8 = -59

func run() async throws {
let controller = try await HostController.command(device: device)
let beacon = AppleBeacon(
uuid: uuid,
major: major,
minor: minor,
rssi: rssi
)
try await controller.iBeacon(beacon)
print("Advertising iBeacon \(uuid) (major: \(major), minor: \(minor)) on \(controller.name)")
}
}
31 changes: 31 additions & 0 deletions Sources/beacon/Stop.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Stop.swift
// BluetoothLinux
//

import Foundation
import ArgumentParser
import Bluetooth
import BluetoothHCI
import BluetoothLinux

struct Stop: AsyncParsableCommand {

static let configuration = CommandConfiguration(
commandName: "stop",
abstract: "Stop advertising (requires root)."
)

@Option(name: [.customShort("i"), .long], help: "The controller to use (e.g. hci0).", transform: HostController.ID.parse)
var device: HostController.ID?

func run() async throws {
let controller = try await HostController.command(device: device)
do {
try await controller.enableLowEnergyAdvertising(false)
} catch HCIError.commandDisallowed {
// already disabled
}
print("Stopped advertising on \(controller.name)")
}
}
Loading
Loading