Skip to content
Open
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ After connecting, the Viam specific extensions for reading and writing can be ca
await device.writeRobotPartConfig(
partId: 'id',
secret: 'secret',
// apiKey is the preferred but optional authentication method
apiKey: APIKey(
id: 'keyID',
key: 'key'
),
);
await device.exitProvisioning();
```
Expand Down
12 changes: 12 additions & 0 deletions lib/src/api_key.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
part of '../viam_bluetooth_provisioning.dart';

class APIKey {
final String id;
final String key;

APIKey({required this.id, required this.key});

String toJson() {
return jsonEncode({'id': id, 'key': key});
}
}
10 changes: 10 additions & 0 deletions lib/src/bluetooth_device_extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ extension ViamWriting on BluetoothDevice {
Future<void> writeRobotPartConfig({
required String partId,
required String secret,
APIKey? apiKey,
String appAddress = 'https://app.viam.com:443',
String psk = 'viamsetup',
}) async {
Expand Down Expand Up @@ -186,6 +187,15 @@ extension ViamWriting on BluetoothDevice {
orElse: () => throw Exception('appAddressCharacteristic not found'),
);
await appAddressCharacteristic.write(encodedAppAddress);

if (apiKey != null) {
final encodedApiKey = encoder.process(utf8.encode('$psk:${apiKey.toJson()}'));
final apiKeyCharacteristic = bleService.characteristics.firstWhere(
(char) => char.uuid.str == ViamBluetoothUUIDs.apiKeyCredsUUID,
orElse: () => throw Exception('apiKeyCharacteristic not found'),
);
await apiKeyCharacteristic.write(encodedApiKey);
}
Comment on lines +190 to +198
Copy link
Member Author

@danielbotros danielbotros Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking to make this optional since everything else was written to be backwards compatible with secrets, and if an apiKey is included then it will be used over secret so I don't want to force api key usage before these changes have been fully validated

}

Future<void> exitProvisioning({String psk = 'viamsetup'}) async {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/viam_bluetooth_uuids.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class ViamBluetoothUUIDs {
static String pskUUID = _uuid.v5(_namespace, _pskKey);
static String robotPartUUID = _uuid.v5(_namespace, _robotPartIDKey);
static String robotPartSecretUUID = _uuid.v5(_namespace, _robotPartSecretKey);
static String apiKeyCredsUUID = _uuid.v5(_namespace, _apiKeyCredsKey);
static String appAddressUUID = _uuid.v5(_namespace, _appAddressKey);
static String statusUUID = _uuid.v5(_namespace, _statusKey);
static String cryptoUUID = _uuid.v5(_namespace, _cryptoKey);
Expand All @@ -25,6 +26,7 @@ class ViamBluetoothUUIDs {
static const String _pskKey = 'psk';
static const String _robotPartIDKey = 'id';
static const String _robotPartSecretKey = 'secret';
static const String _apiKeyCredsKey = 'api_key';
static const String _appAddressKey = 'app_address';
static const String _statusKey = 'status';
static const String _cryptoKey = 'pub_key';
Expand Down
1 change: 1 addition & 0 deletions lib/viam_bluetooth_provisioning.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ part 'src/wifi_network.dart';
part 'src/viam_bluetooth_uuids.dart';
part 'src/viam_bluetooth_provisioning.dart';
part 'src/bluetooth_device_extensions.dart';
part 'src/api_key.dart';
20 changes: 20 additions & 0 deletions viam_example_app/lib/provision_peripheral_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class _ProvisionPeripheralScreen extends State<ProvisionPeripheralScreen> {
final TextEditingController _partIdTextController = TextEditingController();
final TextEditingController _secretTextController = TextEditingController();
final TextEditingController _appAddressTextController = TextEditingController();
final TextEditingController _apiKeyIdTextController = TextEditingController();
final TextEditingController _apiKeyKeyTextController = TextEditingController();

List<String> _networkList = [];

Expand Down Expand Up @@ -47,6 +49,8 @@ class _ProvisionPeripheralScreen extends State<ProvisionPeripheralScreen> {
_partIdTextController.dispose();
_secretTextController.dispose();
_appAddressTextController.dispose();
_apiKeyIdTextController.dispose();
_apiKeyKeyTextController.dispose();
super.dispose();
}

Expand Down Expand Up @@ -137,10 +141,18 @@ class _ProvisionPeripheralScreen extends State<ProvisionPeripheralScreen> {
final partId = _partIdTextController.text;
final secret = _secretTextController.text;
final appAddress = _appAddressTextController.text;
final apiKeyId = _apiKeyIdTextController.text.trim();
final apiKeyKey = _apiKeyKeyTextController.text.trim();

APIKey? apiKey;
if (apiKeyId.isNotEmpty && apiKeyKey.isNotEmpty) {
apiKey = APIKey(id: apiKeyId, key: apiKeyKey);
}
await widget.device.writeRobotPartConfig(
partId: partId,
secret: secret,
appAddress: appAddress,
apiKey: apiKey,
);
_showSnackBar('Wrote robot part config');
} catch (e) {
Expand Down Expand Up @@ -230,6 +242,14 @@ class _ProvisionPeripheralScreen extends State<ProvisionPeripheralScreen> {
controller: _secretTextController,
decoration: const InputDecoration(labelText: 'Secret'),
),
TextField(
controller: _apiKeyIdTextController,
decoration: const InputDecoration(labelText: 'API Key ID', hintText: '(will be used instead of secret if provided)'),
),
TextField(
controller: _apiKeyKeyTextController,
decoration: const InputDecoration(labelText: 'API Key', hintText: '(will be used instead of secret if provided)'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it does seem that we will still write the secret if it's present - but we talked and that doesn't seem like an issue. You could take the writing logic one step further and avoid writing secret if an APIKey is passed in

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoiding that for now since older viam-server versions don't how to handle only api keys

),
TextField(
controller: _appAddressTextController,
decoration: const InputDecoration(labelText: 'App Address'),
Expand Down