logo

VanMoofKit

一个用于与 VanMoof S3 & X3 自行车 🚲 通信的 Swift Package。

Swift Version Platforms
Build and Test Status Documentation Twitter Mastodon

Example application

import VanMoofKit

let vanMoof = VanMoof()

try await vanMoof.login(
    username: "knight.rider@vanmoof.com",
    password: "********"
)

let bikes = try await vanMoof.bikes()

for bike in bikes {
    try await bike.connect()
    try await bike.playSound()
}

重要提示

VanMoofKit 不是 VanMoof B.V 的官方库。此 Swift Package 允许访问自行车的某些功能,但在某些司法管辖区使用这些功能可能是非法的。由于此库尚未达到官方稳定版本,因此某些功能可能尚未可用或可能无法按预期工作。

特性

示例

查看示例应用程序,了解 VanMoofKit 的实际应用。只需打开 Example/Example.xcodeproj 并运行 "Example" 方案。

CLI

VanMoofKit Swift Package 包含一个 CLI,它允许您轻松导出您的 VanMoof 帐户数据,包括自行车的加密密钥。

$ swift run vanmoof export --username "knight.rider@vanmoof.com" --password "********" --outputDirectory "~/Downloads"

注意

登录凭据不会被持久保存或记录,它们仅用于向 VanMoof API 进行身份验证。

--outputDirectory 参数是可选的。如果未指定,导出内容将自动保存在 ~/Desktop

安装

Swift Package Manager

要使用 Apple 的 Swift Package Manager 进行集成,请将以下内容作为依赖项添加到您的 Package.swift 中:

dependencies: [
    .package(url: "https://github.com/SvenTiigi/VanMoofKit.git", from: "0.0.7")
]

或者,导航到您的 Xcode 项目,然后选择 Swift Packages,单击“+”图标并搜索 VanMoofKit

Info.plist

由于 VanMoofKit 使用 CoreBluetooth 框架建立与自行车的 BLE 连接,因此需要将 NSBluetoothAlwaysUsageDescription 键添加到应用程序的 Info.plist 文件中。

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Establishing a bluetooth connection to your VanMoof Bike.</string>

VanMoof

要检索 VanMoof 帐户的自行车,您首先需要初始化一个 VanMoof 实例。

let vanMoof = VanMoof()

要进行身份验证,只需在 VanMoof 对象的实例上调用 login 函数。

try await vanMoof.login(
    username: "kight.rider@vanmoof.com",
    password: "********"
)

注意

登录凭据不会被持久保存或记录,它们仅用于向 VanMoof API 进行身份验证。

利用 vanMoof.isAuthenticated 属性来检查用户是否已登录。

if vanMoof.isAuthenticated {
    // ...
}

登录会生成一个 VanMoof.Token,该令牌会自动存储在 VanMoofTokenStore 的实例中。您可以在创建 VanMoof 实例时配置令牌的持久性。

let vanMoof = VanMoof(
    // Specify an instance which conforms to the `VanMoofTokenStore` protocol.
    // Predefined implementations:
    // - KeychainVanMoofTokenStore
    // - LARightVanMoofTokenStore
    // - NSUbiquitousVanMoofTokenStore
    // - UserDefaultsVanMoofTokenStore
    // - InMemoryVanMoofTokenStore
    tokenStore: .keychain()
)

注意

默认情况下,将使用 UserDefaultsVanMoofTokenStore 来存储 VanMoof.Token

登录成功后,您可以检索用户个人资料和关联的自行车。

// Retrieve the user
let user: VanMoof.User = try await vanMoof.user()
print("Available Bikes", user.bikes)

// If you want to directly retrieve the bikes call:
let bikes: [VanMoof.Bike] = try vanMoof.bikes()

要注销当前用户,请调用

vanMoof.logout()

注意

注销用户不会影响任何可用的 VanMoof.Bike 实例。开发者有责任终止与 VanMoof.Bike 的任何打开的连接。

VanMoof.Bike 🚲

一些信息,例如自行车的名称、车架号等,无需与自行车建立有效连接即可获得。您可以通过 bike.details 属性访问这些信息。

let details: VanMoof.Bike.Details = bike.details
print(details.name)
print(details.macAddress)
print(details.frameNumber)

// Or access the details properties directly
// (powered by the @dynamicMemberLookup attribute)
print(bike.name, bike.macAddress, bike.frameNumber)

由于 VanMoof.Bike 符合 Codable 协议,您可以从本地 JSON 文件创建实例。

// Retrieve the JSON file url from the main bundle
let bikeJSONFileURL = Bundle.main.url(forResource: "Bike", withExtension: "json")!
// Try to load the contents of the JSON file
let bikeJSONData = try Data(contentsOf: bikeJSONFileURL)
// Try to decode VanMoof Bike from JSON data
let bike: VanMoof.Bike = try JSONDecoder().decode(VanMoof.Bike.self, from: bikeJSONData)

连接

要建立与 VanMoof.Bike 的连接,请调用

// Try to connect to the bike
try await bike.connect()

您可以按以下方式检索连接的当前状态。

// Switch on connectionState
switch bike.connectionState {
case .disconnected:
    print("Disconnected")
case .discovering:
    print("Discovering")
case .connecting:
    print("Connecting")
case .connected:
    print("Connected")
case .disconnecting:
    print("Disconnecting")
}

// Or make use of convience properties such as:
let isConnected: Bool = bike.isConnected
let isDisconnected: Bool = bike.isDisconnected

// Alternatively you can use a Publisher
bike.connectionStatePublisher
    .sink { connectionState in
        // ...
    }

// Or a specialized publisher which only emits connection errors
bike.connectionErrorPublisher
    .sink { connectionError in
        // ...
    }

此外,您可以通过以下方式监控连接的信号强度

// Retrieve the current signal strength
let signalStrength: VanMoof.Bike.SignalStrength = try await bike.signalStrength

// A Publisher that emits the current signal strength in a given update interval
bike.signalStrengthPublisher(
    updateInterval: 5
).sink { signalStrength in
    // ...
}

如果您希望终止连接,只需调用

// Disconnect from the bike
try await bike.disconnect()

ModuleState (模块状态)

switch try await bike.moduleState {
case .on:
    break
case .off:
    break
default:
    break
}

bike.moduleStatePublisher.sink { moduleState in
    // ...
}

try await bike.set(moduleState: .on)

// Alias for setting the module state to on
try await bike.wakeUp()

LockState (锁状态)

switch try await bike.lockState {
case .unlocked:
    break
case .locked:
    break
case .awaitingUnlock:
    break
}

bike.lockStatePublisher.sink { lockState in
    // ...
}

// Unlock Bike
try await bike.unlock()

Battery Level (电池电量)

let batteryLevel = try await bike.batteryLevel

bike.batteryLevelPublisher.sink { batteryLevel in
    // ...
}

Battery State (电池状态)

switch try await bike.batteryState {
case .notCharging;
    break
case .charging:
    break
}

bike.batteryStatePublisher.sink { batteryState in
    // ...
}

Speed Limit (速度限制)

switch try await bike.speedLimit {
case .europe:
    print("Europe 25 km/h")
case .unitedStates:
    print("United States 32 km/h")
case .japan:
    print("Japan 24 km/h")
}

bike.speedLimitPublisher.sink { speedLimit in
    // ...
}

警告

在某些司法管辖区更改速度限制可能是非法的。

try await bike.set(speedLimit: .unitedStates)

Power Level (功率级别)

switch try await bike.powerLevel {
case .off:
    break
case .one:
    break
case .two:
    break
case .three:
    break
case .four:
    break
}

bike.powerLevelPublisher.sink { powerLevel in
    // ...
}

try await bike.set(powerLevel: .four)

Light Mode (灯光模式)

switch try await bike.lightMode {
case .auto:
    break
case .alwaysOn:
    break
case .off:
    break
}

bike.lightModePublisher.sink { lightMode in
    // ...
}

try await bike.set(lightMode: .alwaysOn)

Bell Sound (铃声)

switch try await bike.bellSound {
case .sonar:
    break
case .bell:
    break
case .party:
    break
case .foghorn:
    break
}

bike.bellSoundPublisher.sink { bellSound in
    // ...
}

try await bike.set(bellSound: .party)

Play Sound (播放声音)

try await bike.play(sound: .scrollingTone)
try await bike.play(sound: .beepPositive)
try await bike.play(sound: .alarmStageOne)

Sound Volume (声音音量)

// An integer in the range from 0 to 100
let soundVolume: Int = try await bike.soundVolume

Total Distance (总里程)

let totalDistance: VanMoof.Bike.Distance = try await bike.totalDistance

bike.totalDistancePublisher.sink { totalDistance in
    // ...
}

Unit System (单位系统)

switch try await bike.unitSystem {
case .metric:
    break
case .imperial:
    break
}

bike.unitSystemPublisher.sink { unitSystem in
    // ...
}

try await bike.set(unitSystem: .metric)

Firmware Versions (固件版本)

let bikeFirmwareVersion: String = try await bike.firmwareVersion
let bleChipFirmwareVersion: String = try await bike.bleChipFirmwareVersion
let eShifterFirmwareVersion: String = try await bike.eShifterFirmwareVersion

致谢

VanMoof 自行车蓝牙 API 逆向工程

GitHub 工作流程文件和问题模板