diff --git a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/GenericChipDeviceListener.kt b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/GenericChipDeviceListener.kt index 1d38c9c5c9c244..4ccf1ba6e6a836 100644 --- a/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/GenericChipDeviceListener.kt +++ b/src/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/GenericChipDeviceListener.kt @@ -23,6 +23,14 @@ open class GenericChipDeviceListener : ChipDeviceController.CompletionListener { // No op } + override fun onReadCommissioningInfo(vendorId: Int,productId: Int, wifiEndpointId: Int, threadEndpointId: Int) { + // No op + } + + override fun onCommissioningStatusUpdate(nodeId: Long, stage: String, errorCode: Int) { + // No op + } + override fun onNotifyChipConnectionClosed() { // No op } diff --git a/src/controller/AutoCommissioner.cpp b/src/controller/AutoCommissioner.cpp index 4ddd081e46ed04..0feec835699252 100644 --- a/src/controller/AutoCommissioner.cpp +++ b/src/controller/AutoCommissioner.cpp @@ -45,6 +45,12 @@ void AutoCommissioner::SetOperationalCredentialsDelegate(OperationalCredentialsD CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParameters & params) { mParams = params; + if (params.GetFailsafeTimerSeconds().HasValue()) + { + ChipLogProgress(Controller, "Setting failsafe timer from parameters"); + mParams.SetFailsafeTimerSeconds(params.GetFailsafeTimerSeconds().Value()); + } + if (params.GetThreadOperationalDataset().HasValue()) { ByteSpan dataset = params.GetThreadOperationalDataset().Value(); @@ -57,6 +63,13 @@ CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParam ChipLogProgress(Controller, "Setting thread operational dataset from parameters"); mParams.SetThreadOperationalDataset(ByteSpan(mThreadOperationalDataset, dataset.size())); } + + if (params.GetAttemptThreadNetworkScan().HasValue()) + { + ChipLogProgress(Controller, "Setting attempt thread scan from parameters"); + mParams.SetAttemptThreadNetworkScan(params.GetAttemptThreadNetworkScan().Value()); + } + if (params.GetWiFiCredentials().HasValue()) { WiFiCredentials creds = params.GetWiFiCredentials().Value(); @@ -73,6 +86,12 @@ CHIP_ERROR AutoCommissioner::SetCommissioningParameters(const CommissioningParam WiFiCredentials(ByteSpan(mSsid, creds.ssid.size()), ByteSpan(mCredentials, creds.credentials.size()))); } + if (params.GetAttemptWiFiNetworkScan().HasValue()) + { + ChipLogProgress(Controller, "Setting attempt wifi scan from parameters"); + mParams.SetAttemptWiFiNetworkScan(params.GetAttemptWiFiNetworkScan().Value()); + } + if (params.GetCountryCode().HasValue()) { auto & code = params.GetCountryCode().Value(); @@ -159,6 +178,25 @@ CommissioningStage AutoCommissioner::GetNextCommissioningStageInternal(Commissio } return CommissioningStage::kArmFailsafe; case CommissioningStage::kArmFailsafe: + if (mNeedsNetworkSetup) + { + // if there is a WiFi or a Thread endpoint, then perform scan + if ((mParams.GetAttemptWiFiNetworkScan().ValueOr(false) && + mDeviceCommissioningInfo.network.wifi.endpoint != kInvalidEndpointId) || + (mParams.GetAttemptThreadNetworkScan().ValueOr(false) && + mDeviceCommissioningInfo.network.thread.endpoint != kInvalidEndpointId)) + { + return CommissioningStage::kScanNetworks; + } + ChipLogProgress(Controller, "No NetworkScan enabled or WiFi/Thread endpoint not specified, skipping ScanNetworks"); + } + else + { + ChipLogProgress(Controller, "Not a BLE connection, skipping ScanNetworks"); + } + // skip scan step + return CommissioningStage::kConfigRegulatory; + case CommissioningStage::kScanNetworks: return CommissioningStage::kConfigRegulatory; case CommissioningStage::kConfigRegulatory: return CommissioningStage::kSendPAICertificateRequest; @@ -490,26 +528,68 @@ CHIP_ERROR AutoCommissioner::CommissioningStepFinished(CHIP_ERROR err, Commissio { completionStatus.err = err; } + mParams.SetCompletionStatus(completionStatus); + + if (mCommissioningPaused) + { + mPausedStage = nextStage; + + if (GetDeviceProxyForStep(nextStage) == nullptr) + { + ChipLogError(Controller, "Invalid device for commissioning"); + return CHIP_ERROR_INCORRECT_STATE; + } + return CHIP_NO_ERROR; + } + return PerformStep(nextStage); +} - DeviceProxy * proxy = mCommissioneeDeviceProxy; +DeviceProxy * AutoCommissioner::GetDeviceProxyForStep(CommissioningStage nextStage) +{ if (nextStage == CommissioningStage::kSendComplete || (nextStage == CommissioningStage::kCleanup && mOperationalDeviceProxy != nullptr)) { - proxy = mOperationalDeviceProxy; + return mOperationalDeviceProxy; } + return mCommissioneeDeviceProxy; +} +CHIP_ERROR AutoCommissioner::PerformStep(CommissioningStage nextStage) +{ + DeviceProxy * proxy = GetDeviceProxyForStep(nextStage); if (proxy == nullptr) { ChipLogError(Controller, "Invalid device for commissioning"); return CHIP_ERROR_INCORRECT_STATE; } - mParams.SetCompletionStatus(completionStatus); mCommissioner->PerformCommissioningStep(proxy, nextStage, mParams, this, GetEndpoint(nextStage), GetCommandTimeout(proxy, nextStage)); return CHIP_NO_ERROR; } +void AutoCommissioner::PauseCommissioning() +{ + mCommissioningPaused = true; +} + +CHIP_ERROR AutoCommissioner::ResumeCommissioning() +{ + VerifyOrReturnError(mCommissioningPaused, CHIP_ERROR_INCORRECT_STATE); + mCommissioningPaused = false; + + // if no new step was attempted + if (mPausedStage == CommissioningStage::kError) + { + return CHIP_NO_ERROR; + } + + CommissioningStage nextStage = mPausedStage; + mPausedStage = CommissioningStage::kError; + + return PerformStep(nextStage); +} + void AutoCommissioner::ReleaseDAC() { if (mDAC != nullptr) diff --git a/src/controller/AutoCommissioner.h b/src/controller/AutoCommissioner.h index b6df47deb6bf64..422b6ef26911d9 100644 --- a/src/controller/AutoCommissioner.h +++ b/src/controller/AutoCommissioner.h @@ -38,11 +38,32 @@ class AutoCommissioner : public CommissioningDelegate CHIP_ERROR CommissioningStepFinished(CHIP_ERROR err, CommissioningDelegate::CommissioningReport report) override; + /** + * @brief + * This function puts the AutoCommissioner in a paused state to prevent advancing to the next stage. + * It is expected that a DevicePairingDelegate may call this method when processing the + * OnCommissioningStatusUpdate, for example, in order to obtain network credentials from the user based + * upon the results of the NetworkScan. + * Use ResumeCommissioning to continue the commissioning process. + * + */ + void PauseCommissioning(); + + /** + * @brief + * An error return value means resume failed, for example: + * - AutoCommissioner was not in a paused state. + * - AutoCommissioner was unable to continue (no DeviceProxy) + */ + CHIP_ERROR ResumeCommissioning(); + protected: CommissioningStage GetNextCommissioningStage(CommissioningStage currentStage, CHIP_ERROR & lastErr); DeviceCommissioner * GetCommissioner() { return mCommissioner; } + CHIP_ERROR PerformStep(CommissioningStage nextStage); private: + DeviceProxy * GetDeviceProxyForStep(CommissioningStage nextStage); void ReleaseDAC(); void ReleasePAI(); @@ -75,6 +96,9 @@ class AutoCommissioner : public CommissioningDelegate bool mNeedsNetworkSetup = false; ReadCommissioningInfo mDeviceCommissioningInfo; + CommissioningStage mPausedStage = CommissioningStage::kError; + bool mCommissioningPaused = false; + // TODO: Why were the nonces statically allocated, but the certs dynamically allocated? uint8_t * mDAC = nullptr; uint16_t mDACLen = 0; diff --git a/src/controller/CHIPDeviceController.cpp b/src/controller/CHIPDeviceController.cpp index 0a720609c1ac1d..d8217d2833b89b 100644 --- a/src/controller/CHIPDeviceController.cpp +++ b/src/controller/CHIPDeviceController.cpp @@ -1556,6 +1556,7 @@ void DeviceCommissioner::CommissioningStageComplete(CHIP_ERROR err, Commissionin { mPairingDelegate->OnCommissioningStatusUpdate(PeerId(GetCompressedFabricId(), nodeId), mCommissioningStage, err); } + if (mCommissioningDelegate == nullptr) { return; @@ -1711,20 +1712,27 @@ void DeviceCommissioner::OnDone(app::ReadClient *) { if (features.Has(app::Clusters::NetworkCommissioning::NetworkCommissioningFeature::kWiFiNetworkInterface)) { + ChipLogProgress(Controller, "----- NetworkCommissioning Features: has WiFi. endpointid = %u", + path.mEndpointId); info.network.wifi.endpoint = path.mEndpointId; } else if (features.Has( app::Clusters::NetworkCommissioning::NetworkCommissioningFeature::kThreadNetworkInterface)) { + ChipLogProgress(Controller, "----- NetworkCommissioning Features: has Thread. endpointid = %u", + path.mEndpointId); info.network.thread.endpoint = path.mEndpointId; } else if (features.Has( app::Clusters::NetworkCommissioning::NetworkCommissioningFeature::kEthernetNetworkInterface)) { + ChipLogProgress(Controller, "----- NetworkCommissioning Features: has Ethernet. endpointid = %u", + path.mEndpointId); info.network.eth.endpoint = path.mEndpointId; } else { + ChipLogProgress(Controller, "----- NetworkCommissioning Features: no features."); // TODO: Gross workaround for the empty feature map on all clusters. Remove. if (info.network.thread.endpoint == kInvalidEndpointId) { @@ -1773,6 +1781,12 @@ void DeviceCommissioner::OnDone(app::ReadClient *) } mAttributeCache = nullptr; mReadClient = nullptr; + + if (mPairingDelegate != nullptr) + { + mPairingDelegate->OnReadCommissioningInfo(info); + } + CommissioningDelegate::CommissioningReport report; report.Set(info); CommissioningStageComplete(return_err, report); @@ -1811,6 +1825,39 @@ void DeviceCommissioner::OnSetRegulatoryConfigResponse( commissioner->CommissioningStageComplete(err, report); } +void DeviceCommissioner::OnScanNetworksFailure(void * context, CHIP_ERROR error) +{ + ChipLogProgress(Controller, "Received ScanNetworks failure response %" CHIP_ERROR_FORMAT, error.Format()); + + DeviceCommissioner * commissioner = static_cast(context); + if (commissioner->GetPairingDelegate() != nullptr) + { + commissioner->GetPairingDelegate()->OnScanNetworksFailure(error); + } + // need to advance to next step + // clear error so that we don't abort the commissioning when ScanNetworks fails + commissioner->CommissioningStageComplete(CHIP_NO_ERROR); +} + +void DeviceCommissioner::OnScanNetworksResponse(void * context, + const NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & data) +{ + CommissioningDelegate::CommissioningReport report; + + ChipLogProgress(Controller, "Received ScanNetwork response, networkingStatus=%u debugText=%s", + to_underlying(data.networkingStatus), + (data.debugText.HasValue() ? std::string(data.debugText.Value().data(), data.debugText.Value().size()).c_str() + : "none provided")); + DeviceCommissioner * commissioner = static_cast(context); + + if (commissioner->GetPairingDelegate() != nullptr) + { + commissioner->GetPairingDelegate()->OnScanNetworksSuccess(data); + } + // need to advance to next step + commissioner->CommissioningStageComplete(CHIP_NO_ERROR); +} + void DeviceCommissioner::OnNetworkConfigResponse(void * context, const NetworkCommissioning::Commands::NetworkConfigResponse::DecodableType & data) { @@ -1940,6 +1987,16 @@ void DeviceCommissioner::PerformCommissioningStep(DeviceProxy * proxy, Commissio mReadClient = std::move(readClient); } break; + case CommissioningStage::kScanNetworks: { + NetworkCommissioning::Commands::ScanNetworks::Type request; + if (params.GetWiFiCredentials().HasValue()) + { + request.ssid.Emplace(params.GetWiFiCredentials().Value().ssid); + } + request.breadcrumb.Emplace(breadcrumb); + SendCommand(proxy, request, OnScanNetworksResponse, OnScanNetworksFailure, endpoint, timeout); + break; + } case CommissioningStage::kConfigRegulatory: { // To set during config phase: // UTC time diff --git a/src/controller/CHIPDeviceController.h b/src/controller/CHIPDeviceController.h index c647ea4dc4a10b..e963a300ef47a3 100644 --- a/src/controller/CHIPDeviceController.h +++ b/src/controller/CHIPDeviceController.h @@ -761,8 +761,12 @@ class DLL_EXPORT DeviceCommissioner : public DeviceController, void * context, const chip::app::Clusters::GeneralCommissioning::Commands::SetRegulatoryConfigResponse::DecodableType & data); static void + OnScanNetworksResponse(void * context, + const app::Clusters::NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & data); + static void OnScanNetworksFailure(void * context, CHIP_ERROR err); + static void OnNetworkConfigResponse(void * context, - const chip::app::Clusters::NetworkCommissioning::Commands::NetworkConfigResponse::DecodableType & data); + const app::Clusters::NetworkCommissioning::Commands::NetworkConfigResponse::DecodableType & data); static void OnConnectNetworkResponse( void * context, const chip::app::Clusters::NetworkCommissioning::Commands::ConnectNetworkResponse::DecodableType & data); static void OnCommissioningCompleteResponse( diff --git a/src/controller/CommissioningDelegate.cpp b/src/controller/CommissioningDelegate.cpp index 82834ce3854ae8..01d9c6ecd90f88 100644 --- a/src/controller/CommissioningDelegate.cpp +++ b/src/controller/CommissioningDelegate.cpp @@ -41,6 +41,10 @@ const char * StageToString(CommissioningStage stage) return "ArmFailSafe"; break; + case kScanNetworks: + return "ScanNetworks"; + break; + case kConfigRegulatory: return "ConfigRegulatory"; break; diff --git a/src/controller/CommissioningDelegate.h b/src/controller/CommissioningDelegate.h index 58cc4131569690..22d651e2c4be03 100644 --- a/src/controller/CommissioningDelegate.h +++ b/src/controller/CommissioningDelegate.h @@ -52,6 +52,9 @@ enum CommissioningStage : uint8_t kFindOperational, kSendComplete, kCleanup, + // ScanNetworks can happen anytime after kArmFailsafe. + // However, the circ tests fail if it is earlier in the list + kScanNetworks, }; const char * StageToString(CommissioningStage stage); @@ -261,10 +264,12 @@ class CommissioningParameters return *this; } + // If a ThreadOperationalDataset is provided, then the ThreadNetworkScan will not be attempted CommissioningParameters & SetThreadOperationalDataset(ByteSpan threadOperationalDataset) { mThreadOperationalDataset.SetValue(threadOperationalDataset); + mAttemptThreadNetworkScan = MakeOptional(static_cast(false)); return *this; } // This parameter should be set with the information returned from kSendOpCertSigningRequest. It must be set before calling @@ -352,6 +357,26 @@ class CommissioningParameters Credentials::DeviceAttestationDelegate * GetDeviceAttestationDelegate() const { return mDeviceAttestationDelegate; } + // If an SSID is provided, and AttemptWiFiNetworkScan is true, + // then a directed scan will be performed using the SSID provided in the WiFiCredentials object + Optional GetAttemptWiFiNetworkScan() const { return mAttemptWiFiNetworkScan; } + CommissioningParameters & SetAttemptWiFiNetworkScan(bool attemptWiFiNetworkScan) + { + mAttemptWiFiNetworkScan = MakeOptional(attemptWiFiNetworkScan); + return *this; + } + + // If a ThreadOperationalDataset is provided, then the ThreadNetworkScan will not be attempted + Optional GetAttemptThreadNetworkScan() const { return mAttemptThreadNetworkScan; } + CommissioningParameters & SetAttemptThreadNetworkScan(bool attemptThreadNetworkScan) + { + if (!mThreadOperationalDataset.HasValue()) + { + mAttemptThreadNetworkScan = MakeOptional(attemptThreadNetworkScan); + } + return *this; + } + private: // Items that can be set by the commissioner Optional mFailsafeTimerSeconds; @@ -379,6 +404,8 @@ class CommissioningParameters CompletionStatus completionStatus; Credentials::DeviceAttestationDelegate * mDeviceAttestationDelegate = nullptr; // Delegate to handle device attestation failures during commissioning + Optional mAttemptWiFiNetworkScan; + Optional mAttemptThreadNetworkScan; // This automatically gets set to false when a ThreadOperationalDataset is set }; struct RequestedCertificate diff --git a/src/controller/DevicePairingDelegate.h b/src/controller/DevicePairingDelegate.h index 8f89cea7050723..2e9fc0558c2221 100644 --- a/src/controller/DevicePairingDelegate.h +++ b/src/controller/DevicePairingDelegate.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -75,6 +76,26 @@ class DLL_EXPORT DevicePairingDelegate {} virtual void OnCommissioningStatusUpdate(PeerId peerId, CommissioningStage stageCompleted, CHIP_ERROR error) {} + + /** + * @brief + * Called with the ReadCommissioningInfo returned from the target + */ + virtual void OnReadCommissioningInfo(const ReadCommissioningInfo & info) {} + + /** + * @brief + * Called with the NetworkScanResponse returned from the target + */ + virtual void + OnScanNetworksSuccess(const app::Clusters::NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & dataResponse) + {} + + /** + * @brief + * Called when the NetworkScan request fails. + */ + virtual void OnScanNetworksFailure(CHIP_ERROR error) {} }; } // namespace Controller diff --git a/src/controller/java/AndroidDeviceControllerWrapper.cpp b/src/controller/java/AndroidDeviceControllerWrapper.cpp index 494bb5694c86e1..6ccdb94fc2abfe 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.cpp +++ b/src/controller/java/AndroidDeviceControllerWrapper.cpp @@ -74,7 +74,8 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( chip::Inet::EndPointManager * tcpEndPointManager, chip::Inet::EndPointManager * udpEndPointManager, AndroidOperationalCredentialsIssuerPtr opCredsIssuerPtr, jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, jbyteArray nodeOperationalCertificate, - jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, CHIP_ERROR * errInfoOnFailure) + jbyteArray ipkEpochKey, uint16_t listenPort, uint16_t controllerVendorId, uint16_t failsafeTimerSeconds, + bool attemptNetworkScanWiFi, bool attemptNetworkScanThread, CHIP_ERROR * errInfoOnFailure) { if (errInfoOnFailure == nullptr) { @@ -146,10 +147,17 @@ AndroidDeviceControllerWrapper * AndroidDeviceControllerWrapper::AllocateNew( setupParams.controllerVendorId = static_cast(controllerVendorId); setupParams.pairingDelegate = wrapper.get(); setupParams.operationalCredentialsDelegate = opCredsIssuer; + setupParams.defaultCommissioner = &wrapper->mAutoCommissioner; initParams.fabricIndependentStorage = wrapperStorage; wrapper->mGroupDataProvider.SetStorageDelegate(wrapperStorage); + CommissioningParameters params = wrapper->mAutoCommissioner.GetCommissioningParameters(); + params.SetFailsafeTimerSeconds(failsafeTimerSeconds); + params.SetAttemptWiFiNetworkScan(attemptNetworkScanWiFi); + params.SetAttemptThreadNetworkScan(attemptNetworkScanThread); + wrapper->UpdateCommissioningParameters(params); + CHIP_ERROR err = wrapper->mGroupDataProvider.Init(); if (err != CHIP_NO_ERROR) { @@ -358,6 +366,13 @@ CHIP_ERROR AndroidDeviceControllerWrapper::ApplyNetworkCredentials(chip::Control return err; } +CHIP_ERROR AndroidDeviceControllerWrapper::UpdateCommissioningParameters(const chip::Controller::CommissioningParameters & params) +{ + // this will wipe out any custom attestationNonce and csrNonce that was being used. + // however, Android APIs don't allow these to be set to custom values today. + return mAutoCommissioner.SetCommissioningParameters(params); +} + void AndroidDeviceControllerWrapper::OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) { chip::DeviceLayer::StackUnlock unlock; @@ -403,6 +418,210 @@ void AndroidDeviceControllerWrapper::OnCommissioningComplete(NodeId deviceId, CH } } +void AndroidDeviceControllerWrapper::OnCommissioningStatusUpdate(PeerId peerId, chip::Controller::CommissioningStage stageCompleted, + CHIP_ERROR error) +{ + chip::DeviceLayer::StackUnlock unlock; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + jmethodID onCommissioningStatusUpdateMethod; + CHIP_ERROR err = JniReferences::GetInstance().FindMethod(env, mJavaObjectRef, "onCommissioningStatusUpdate", + "(JLJAVA/LANG/STRING;I)V", &onCommissioningStatusUpdateMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Error finding Java method: %" CHIP_ERROR_FORMAT, err.Format())); + + UtfString jStageCompleted(env, StageToString(stageCompleted)); + env->CallVoidMethod(mJavaObjectRef, onCommissioningStatusUpdateMethod, static_cast(peerId.GetNodeId()), + jStageCompleted.jniValue(), error.AsInteger()); +} + +void AndroidDeviceControllerWrapper::OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) +{ + // calls: onReadCommissioningInfo(int vendorId, int productId, int wifiEndpointId, int threadEndpointId) + chip::DeviceLayer::StackUnlock unlock; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + jmethodID onReadCommissioningInfoMethod; + CHIP_ERROR err = JniReferences::GetInstance().FindMethod(env, mJavaObjectRef, "onReadCommissioningInfo", "(IIII)V", + &onReadCommissioningInfoMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Controller, "Error finding Java method: %" CHIP_ERROR_FORMAT, err.Format())); + + env->CallVoidMethod(mJavaObjectRef, onReadCommissioningInfoMethod, static_cast(info.basic.vendorId), + static_cast(info.basic.productId), static_cast(info.network.wifi.endpoint), + static_cast(info.network.thread.endpoint)); +} + +void AndroidDeviceControllerWrapper::OnScanNetworksSuccess( + const chip::app::Clusters::NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & dataResponse) +{ + chip::DeviceLayer::StackUnlock unlock; + CHIP_ERROR err = CHIP_NO_ERROR; + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + jmethodID javaMethod; + + VerifyOrReturn(env != nullptr, ChipLogError(Zcl, "Error invoking Java callback: no JNIEnv")); + + err = JniReferences::GetInstance().FindMethod( + env, mJavaObjectRef, "onScanNetworksSuccess", + "(Ljava/lang/Integer;Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;)V", &javaMethod); + VerifyOrReturn(err == CHIP_NO_ERROR, ChipLogError(Zcl, "Error invoking Java callback: %s", ErrorStr(err))); + + jobject NetworkingStatus; + std::string NetworkingStatusClassName = "java/lang/Integer"; + std::string NetworkingStatusCtorSignature = "(I)V"; + chip::JniReferences::GetInstance().CreateBoxedObject( + NetworkingStatusClassName.c_str(), NetworkingStatusCtorSignature.c_str(), + static_cast(dataResponse.networkingStatus), NetworkingStatus); + jobject DebugText; + if (!dataResponse.debugText.HasValue()) + { + chip::JniReferences::GetInstance().CreateOptional(nullptr, DebugText); + } + else + { + jobject DebugTextInsideOptional; + DebugTextInsideOptional = + env->NewStringUTF(std::string(dataResponse.debugText.Value().data(), dataResponse.debugText.Value().size()).c_str()); + chip::JniReferences::GetInstance().CreateOptional(DebugTextInsideOptional, DebugText); + } + jobject WiFiScanResults; + if (!dataResponse.wiFiScanResults.HasValue()) + { + chip::JniReferences::GetInstance().CreateOptional(nullptr, WiFiScanResults); + } + else + { + // TODO: use this + jobject WiFiScanResultsInsideOptional; + chip::JniReferences::GetInstance().CreateArrayList(WiFiScanResultsInsideOptional); + + auto iter_WiFiScanResultsInsideOptional = dataResponse.wiFiScanResults.Value().begin(); + while (iter_WiFiScanResultsInsideOptional.Next()) + { + auto & entry = iter_WiFiScanResultsInsideOptional.GetValue(); + jobject newElement_security; + std::string newElement_securityClassName = "java/lang/Integer"; + std::string newElement_securityCtorSignature = "(I)V"; + chip::JniReferences::GetInstance().CreateBoxedObject(newElement_securityClassName.c_str(), + newElement_securityCtorSignature.c_str(), + entry.security.Raw(), newElement_security); + jobject newElement_ssid; + jbyteArray newElement_ssidByteArray = env->NewByteArray(static_cast(entry.ssid.size())); + env->SetByteArrayRegion(newElement_ssidByteArray, 0, static_cast(entry.ssid.size()), + reinterpret_cast(entry.ssid.data())); + newElement_ssid = newElement_ssidByteArray; + jobject newElement_bssid; + jbyteArray newElement_bssidByteArray = env->NewByteArray(static_cast(entry.bssid.size())); + env->SetByteArrayRegion(newElement_bssidByteArray, 0, static_cast(entry.bssid.size()), + reinterpret_cast(entry.bssid.data())); + newElement_bssid = newElement_bssidByteArray; + jobject newElement_channel; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.channel, + newElement_channel); + jobject newElement_wiFiBand; + chip::JniReferences::GetInstance().CreateBoxedObject( + "java/lang/Integer", "(I)V", static_cast(entry.wiFiBand), newElement_wiFiBand); + jobject newElement_rssi; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.rssi, newElement_rssi); + + jclass wiFiInterfaceScanResultStructClass; + err = chip::JniReferences::GetInstance().GetClassRef( + env, "chip/devicecontroller/ChipStructs$NetworkCommissioningClusterWiFiInterfaceScanResult", + wiFiInterfaceScanResultStructClass); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Could not find class ChipStructs$NetworkCommissioningClusterWiFiInterfaceScanResult"); + return; + } + jmethodID wiFiInterfaceScanResultStructCtor = + env->GetMethodID(wiFiInterfaceScanResultStructClass, "", + "(Ljava/lang/Integer;[B[BLjava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)V"); + if (wiFiInterfaceScanResultStructCtor == nullptr) + { + ChipLogError(Zcl, "Could not find ChipStructs$NetworkCommissioningClusterWiFiInterfaceScanResult constructor"); + return; + } + + jobject newElement = + env->NewObject(wiFiInterfaceScanResultStructClass, wiFiInterfaceScanResultStructCtor, newElement_security, + newElement_ssid, newElement_bssid, newElement_channel, newElement_wiFiBand, newElement_rssi); + chip::JniReferences::GetInstance().AddToList(WiFiScanResultsInsideOptional, newElement); + } + chip::JniReferences::GetInstance().CreateOptional(WiFiScanResultsInsideOptional, WiFiScanResults); + } + jobject ThreadScanResults; + if (!dataResponse.threadScanResults.HasValue()) + { + chip::JniReferences::GetInstance().CreateOptional(nullptr, ThreadScanResults); + } + else + { + jobject ThreadScanResultsInsideOptional; + chip::JniReferences::GetInstance().CreateArrayList(ThreadScanResultsInsideOptional); + + auto iter_ThreadScanResultsInsideOptional = dataResponse.threadScanResults.Value().begin(); + while (iter_ThreadScanResultsInsideOptional.Next()) + { + auto & entry = iter_ThreadScanResultsInsideOptional.GetValue(); + jobject newElement_panId; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.panId, + newElement_panId); + jobject newElement_extendedPanId; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Long", "(J)V", entry.extendedPanId, + newElement_extendedPanId); + jobject newElement_networkName; + newElement_networkName = env->NewStringUTF(std::string(entry.networkName.data(), entry.networkName.size()).c_str()); + jobject newElement_channel; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.channel, + newElement_channel); + jobject newElement_version; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.version, + newElement_version); + jobject newElement_extendedAddress; + jbyteArray newElement_extendedAddressByteArray = env->NewByteArray(static_cast(entry.extendedAddress.size())); + env->SetByteArrayRegion(newElement_extendedAddressByteArray, 0, static_cast(entry.extendedAddress.size()), + reinterpret_cast(entry.extendedAddress.data())); + newElement_extendedAddress = newElement_extendedAddressByteArray; + jobject newElement_rssi; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.rssi, newElement_rssi); + jobject newElement_lqi; + chip::JniReferences::GetInstance().CreateBoxedObject("java/lang/Integer", "(I)V", entry.lqi, newElement_lqi); + + jclass threadInterfaceScanResultStructClass; + err = chip::JniReferences::GetInstance().GetClassRef( + env, "chip/devicecontroller/ChipStructs$NetworkCommissioningClusterThreadInterfaceScanResult", + threadInterfaceScanResultStructClass); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "Could not find class ChipStructs$NetworkCommissioningClusterThreadInterfaceScanResult"); + return; + } + jmethodID threadInterfaceScanResultStructCtor = + env->GetMethodID(threadInterfaceScanResultStructClass, "", + "(Ljava/lang/Integer;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/" + "Integer;[BLjava/lang/Integer;Ljava/lang/Integer;)V"); + if (threadInterfaceScanResultStructCtor == nullptr) + { + ChipLogError(Zcl, "Could not find ChipStructs$NetworkCommissioningClusterThreadInterfaceScanResult constructor"); + return; + } + + jobject newElement = + env->NewObject(threadInterfaceScanResultStructClass, threadInterfaceScanResultStructCtor, newElement_panId, + newElement_extendedPanId, newElement_networkName, newElement_channel, newElement_version, + newElement_extendedAddress, newElement_rssi, newElement_lqi); + chip::JniReferences::GetInstance().AddToList(ThreadScanResultsInsideOptional, newElement); + } + chip::JniReferences::GetInstance().CreateOptional(ThreadScanResultsInsideOptional, ThreadScanResults); + } + + env->CallVoidMethod(mJavaObjectRef, javaMethod, NetworkingStatus, DebugText, WiFiScanResults, ThreadScanResults); +} + +void AndroidDeviceControllerWrapper::OnScanNetworksFailure(CHIP_ERROR error) +{ + chip::DeviceLayer::StackUnlock unlock; + + CallJavaMethod("onScanNetworksFailure", static_cast(error.AsInteger())); +} + CHIP_ERROR AndroidDeviceControllerWrapper::SyncGetKeyValue(const char * key, void * value, uint16_t & size) { ChipLogProgress(chipTool, "KVS: Getting key %s", key); diff --git a/src/controller/java/AndroidDeviceControllerWrapper.h b/src/controller/java/AndroidDeviceControllerWrapper.h index 668c51d5e4c0cb..e5809340fea39c 100644 --- a/src/controller/java/AndroidDeviceControllerWrapper.h +++ b/src/controller/java/AndroidDeviceControllerWrapper.h @@ -70,17 +70,35 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel */ CHIP_ERROR ApplyNetworkCredentials(chip::Controller::CommissioningParameters & params, jobject networkCredentials); + /** + * Update the CommissioningParameters used by the active device commissioner + */ + CHIP_ERROR UpdateCommissioningParameters(const chip::Controller::CommissioningParameters & params); + // DevicePairingDelegate implementation void OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) override; void OnPairingComplete(CHIP_ERROR error) override; void OnPairingDeleted(CHIP_ERROR error) override; void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR error) override; + void OnCommissioningStatusUpdate(chip::PeerId peerId, chip::Controller::CommissioningStage stageCompleted, + CHIP_ERROR error) override; + void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override; + void OnScanNetworksSuccess( + const chip::app::Clusters::NetworkCommissioning::Commands::ScanNetworksResponse::DecodableType & dataResponse) override; + void OnScanNetworksFailure(CHIP_ERROR error) override; // PersistentStorageDelegate implementation CHIP_ERROR SyncSetKeyValue(const char * key, const void * value, uint16_t size) override; CHIP_ERROR SyncGetKeyValue(const char * key, void * buffer, uint16_t & size) override; CHIP_ERROR SyncDeleteKeyValue(const char * key) override; + chip::Controller::AutoCommissioner * GetAutoCommissioner() { return &mAutoCommissioner; } + + const chip::Controller::CommissioningParameters & GetCommissioningParameters() const + { + return mAutoCommissioner.GetCommissioningParameters(); + } + static AndroidDeviceControllerWrapper * FromJNIHandle(jlong handle) { return reinterpret_cast(handle); @@ -114,6 +132,9 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel * @param[in] ipkEpochKey the IPK epoch key to use for this node * @param[in] listenPort the UDP port to listen on * @param[in] controllerVendorId the vendor ID identifying the controller + * @param[in] failsafeTimerSeconds the failsafe timer in seconds + * @param[in] attemptNetworkScanWiFi whether to attempt a network scan when configuring the network for a WiFi device + * @param[in] attemptNetworkScanThread whether to attempt a network scan when configuring the network for a Thread device * @param[out] errInfoOnFailure a pointer to a CHIP_ERROR that will be populated if this method returns nullptr */ static AndroidDeviceControllerWrapper * @@ -122,7 +143,8 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel chip::Inet::EndPointManager * udpEndPointManager, AndroidOperationalCredentialsIssuerPtr opCredsIssuer, jobject keypairDelegate, jbyteArray rootCertificate, jbyteArray intermediateCertificate, jbyteArray nodeOperationalCertificate, jbyteArray ipkEpochKey, - uint16_t listenPort, uint16_t controllerVendorId, CHIP_ERROR * errInfoOnFailure); + uint16_t listenPort, uint16_t controllerVendorId, uint16_t failsafeTimerSeconds, bool attemptNetworkScanWiFi, + bool attemptNetworkScanThread, CHIP_ERROR * errInfoOnFailure); private: using ChipDeviceControllerPtr = std::unique_ptr; @@ -146,6 +168,8 @@ class AndroidDeviceControllerWrapper : public chip::Controller::DevicePairingDel jbyteArray operationalDatasetBytes = nullptr; jbyte * operationalDataset = nullptr; + chip::Controller::AutoCommissioner mAutoCommissioner; + AndroidDeviceControllerWrapper(ChipDeviceControllerPtr controller, AndroidOperationalCredentialsIssuerPtr opCredsIssuer) : mController(std::move(controller)), mOpCredsIssuer(std::move(opCredsIssuer)) {} diff --git a/src/controller/java/CHIPDeviceController-JNI.cpp b/src/controller/java/CHIPDeviceController-JNI.cpp index 59f6f417f1d762..3e8db12d8f0794 100644 --- a/src/controller/java/CHIPDeviceController-JNI.cpp +++ b/src/controller/java/CHIPDeviceController-JNI.cpp @@ -172,6 +172,20 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr jmethodID getControllerVendorId; err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getControllerVendorId", "()I", &getControllerVendorId); + + jmethodID getFailsafeTimerSeconds; + err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getFailsafeTimerSeconds", "()I", + &getFailsafeTimerSeconds); + SuccessOrExit(err); + + jmethodID getAttemptNetworkScanWiFi; + err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getAttemptNetworkScanWiFi", "()Z", + &getAttemptNetworkScanWiFi); + SuccessOrExit(err); + + jmethodID getAttemptNetworkScanThread; + err = chip::JniReferences::GetInstance().FindMethod(env, controllerParams, "getAttemptNetworkScanThread", "()Z", + &getAttemptNetworkScanThread); SuccessOrExit(err); jmethodID getKeypairDelegate; @@ -205,13 +219,17 @@ JNI_METHOD(jlong, newDeviceController)(JNIEnv * env, jobject self, jobject contr jbyteArray intermediateCertificate = (jbyteArray) env->CallObjectMethod(controllerParams, getIntermediateCertificate); jbyteArray operationalCertificate = (jbyteArray) env->CallObjectMethod(controllerParams, getOperationalCertificate); jbyteArray ipk = (jbyteArray) env->CallObjectMethod(controllerParams, getIpk); + uint16_t failsafeTimerSeconds = env->CallIntMethod(controllerParams, getFailsafeTimerSeconds); + bool attemptNetworkScanWiFi = env->CallIntMethod(controllerParams, getAttemptNetworkScanWiFi); + bool attemptNetworkScanThread = env->CallIntMethod(controllerParams, getAttemptNetworkScanThread); std::unique_ptr opCredsIssuer( new chip::Controller::AndroidOperationalCredentialsIssuer()); wrapper = AndroidDeviceControllerWrapper::AllocateNew( sJVM, self, kLocalDeviceId, chip::kUndefinedCATs, &DeviceLayer::SystemLayer(), DeviceLayer::TCPEndPointManager(), DeviceLayer::UDPEndPointManager(), std::move(opCredsIssuer), keypairDelegate, rootCertificate, intermediateCertificate, - operationalCertificate, ipk, listenPort, controllerVendorId, &err); + operationalCertificate, ipk, listenPort, controllerVendorId, failsafeTimerSeconds, attemptNetworkScanWiFi, + attemptNetworkScanThread, &err); SuccessOrExit(err); } @@ -384,6 +402,48 @@ JNI_METHOD(void, establishPaseConnectionByAddress) } } +JNI_METHOD(void, pauseCommissioning) +(JNIEnv * env, jobject self, jlong handle) +{ + ChipLogProgress(Controller, "pauseCommissioning() called"); + chip::DeviceLayer::StackLock lock; + AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); + + wrapper->GetAutoCommissioner()->PauseCommissioning(); +} + +JNI_METHOD(void, resumeCommissioning) +(JNIEnv * env, jobject self, jlong handle) +{ + ChipLogProgress(Controller, "resumeCommissioning() called"); + chip::DeviceLayer::StackLock lock; + AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); + + wrapper->GetAutoCommissioner()->ResumeCommissioning(); +} + +JNI_METHOD(void, updateCommissioningNetworkCredentials) +(JNIEnv * env, jobject self, jlong handle, jobject networkCredentials) +{ + ChipLogProgress(Controller, "updateCommissioningNetworkCredentials() called"); + chip::DeviceLayer::StackLock lock; + AndroidDeviceControllerWrapper * wrapper = AndroidDeviceControllerWrapper::FromJNIHandle(handle); + + CommissioningParameters commissioningParams = wrapper->GetCommissioningParameters(); + CHIP_ERROR err = wrapper->ApplyNetworkCredentials(commissioningParams, networkCredentials); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "ApplyNetworkCredentials failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); + JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); + } + err = wrapper->UpdateCommissioningParameters(commissioningParams); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Controller, "UpdateCommissioningParameters failed. Err = %" CHIP_ERROR_FORMAT, err.Format()); + JniReferences::GetInstance().ThrowError(env, sChipDeviceControllerExceptionCls, err); + } +} + JNI_METHOD(jbyteArray, convertX509CertToMatterCert) (JNIEnv * env, jobject self, jbyteArray x509Cert) { diff --git a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java index 032f373e424a81..6b1197ae28cdc5 100644 --- a/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java +++ b/src/controller/java/src/chip/devicecontroller/ChipDeviceController.java @@ -23,7 +23,9 @@ import chip.devicecontroller.GetConnectedDeviceCallbackJni.GetConnectedDeviceCallback; import chip.devicecontroller.model.ChipAttributePath; import chip.devicecontroller.model.ChipEventPath; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** Controller to interact with the CHIP device. */ public class ChipDeviceController { @@ -31,6 +33,7 @@ public class ChipDeviceController { private long deviceControllerPtr; private int connectionId; private CompletionListener completionListener; + private ScanNetworksListener scanNetworksListener; /** * To load class and jni, we need to new AndroidChipPlatform after jni load but before new @@ -54,6 +57,10 @@ public void setCompletionListener(CompletionListener listener) { completionListener = listener; } + public void setScanNetworksListener(ScanNetworksListener listener) { + scanNetworksListener = listener; + } + public void pairDevice( BluetoothGatt bleServer, int connId, @@ -168,6 +175,28 @@ public void commissionDevice( commissionDevice(deviceControllerPtr, deviceId, csrNonce, networkCredentials); } + public void pauseCommissioning() { + pauseCommissioning(deviceControllerPtr); + } + + public void resumeCommissioning() { + resumeCommissioning(deviceControllerPtr); + } + + /** + * Update the network credentials held by the commissioner for the current commissioning session. + * The updated values will be used by the commissioner if the network credentials haven't already + * been sent to the device. + * + *

Its expected that this method will be called in response to the NetworkScan or the + * ReadCommissioningInfo callbacks. + * + * @param networkCredentials the credentials (Wi-Fi or Thread) to use in commissioning + */ + public void updateCommissioningNetworkCredentials(NetworkCredentials networkCredentials) { + updateCommissioningNetworkCredentials(deviceControllerPtr, networkCredentials); + } + public void unpairDevice(long deviceId) { unpairDevice(deviceControllerPtr, deviceId); } @@ -220,6 +249,39 @@ public void onCommissioningComplete(long nodeId, int errorCode) { } } + public void onCommissioningStatusUpdate(long nodeId, String stage, int errorCode) { + if (completionListener != null) { + completionListener.onCommissioningStatusUpdate(nodeId, stage, errorCode); + } + } + + public void onReadCommissioningInfo( + int vendorId, int productId, int wifiEndpointId, int threadEndpointId) { + if (completionListener != null) { + completionListener.onReadCommissioningInfo( + vendorId, productId, wifiEndpointId, threadEndpointId); + } + } + + public void onScanNetworksFailure(int errorCode) { + if (scanNetworksListener != null) { + scanNetworksListener.onScanNetworksFailure(errorCode); + } + } + + public void onScanNetworksSuccess( + Integer networkingStatus, + Optional debugText, + Optional> + wiFiScanResults, + Optional> + threadScanResults) { + if (scanNetworksListener != null) { + scanNetworksListener.onScanNetworksSuccess( + networkingStatus, debugText, wiFiScanResults, threadScanResults); + } + } + public void onOpCSRGenerationComplete(byte[] csr) { if (completionListener != null) { completionListener.onOpCSRGenerationComplete(csr); @@ -567,6 +629,13 @@ private native boolean openPairingWindowWithPINCallback( private native byte[] getAttestationChallenge(long deviceControllerPtr, long devicePtr); + private native void pauseCommissioning(long deviceControllerPtr); + + private native void resumeCommissioning(long deviceControllerPtr); + + private native void updateCommissioningNetworkCredentials( + long deviceControllerPtr, NetworkCredentials networkCredentials); + private native void shutdownSubscriptions(long deviceControllerPtr, long devicePtr); private native void shutdownCommissioning(long deviceControllerPtr); @@ -585,6 +654,20 @@ protected void finalize() throws Throwable { } } + /** Interface to listen for callbacks from CHIPDeviceController. */ + public interface ScanNetworksListener { + /** Notifies when scan networks call fails. */ + void onScanNetworksFailure(int errorCode); + + void onScanNetworksSuccess( + Integer networkingStatus, + Optional debugText, + Optional> + wiFiScanResults, + Optional> + threadScanResults); + } + /** Interface to listen for callbacks from CHIPDeviceController. */ public interface CompletionListener { @@ -603,6 +686,13 @@ public interface CompletionListener { /** Notifies the completion of commissioning. */ void onCommissioningComplete(long nodeId, int errorCode); + /** Notifies the completion of each stage of commissioning. */ + void onReadCommissioningInfo( + int vendorId, int productId, int wifiEndpointId, int threadEndpointId); + + /** Notifies the completion of each stage of commissioning. */ + void onCommissioningStatusUpdate(long nodeId, String stage, int errorCode); + /** Notifies that the Chip connection has been closed. */ void onNotifyChipConnectionClosed(); diff --git a/src/controller/java/src/chip/devicecontroller/ControllerParams.java b/src/controller/java/src/chip/devicecontroller/ControllerParams.java index 1caca6903cac09..87fb85a375adf1 100644 --- a/src/controller/java/src/chip/devicecontroller/ControllerParams.java +++ b/src/controller/java/src/chip/devicecontroller/ControllerParams.java @@ -12,6 +12,9 @@ public final class ControllerParams { @Nullable private final byte[] intermediateCertificate; @Nullable private final byte[] operationalCertificate; @Nullable private final byte[] ipk; + private final int failsafeTimerSeconds = 30; + private final boolean attemptNetworkScanWiFi = false; + private final boolean attemptNetworkScanThread = true; private static final int LEGACY_GLOBAL_CHIP_PORT = 5540; @@ -55,6 +58,18 @@ public byte[] getIpk() { return ipk; } + public int getFailsafeTimerSeconds() { + return failsafeTimerSeconds; + } + + public boolean getAttemptNetworkScanWiFi() { + return attemptNetworkScanWiFi; + } + + public boolean getAttemptNetworkScanThread() { + return attemptNetworkScanThread; + } + /** Returns parameters with ephemerally generated operational credentials */ public static Builder newBuilder() { return new Builder(); @@ -82,6 +97,9 @@ public static class Builder { @Nullable private byte[] intermediateCertificate = null; @Nullable private byte[] operationalCertificate = null; @Nullable private byte[] ipk = null; + private int failsafeTimerSeconds = 30; + private boolean attemptNetworkScanWiFi = false; + private boolean attemptNetworkScanThread = true; private Builder() {} @@ -93,12 +111,29 @@ public Builder setUdpListenPort(int udpListenPort) { return this; } - /** Sets the vendor ID associated with this controller instance. */ public Builder setControllerVendorId(int controllerVendorId) { this.controllerVendorId = controllerVendorId; return this; } + public Builder setFailsafeTimerSeconds(int failsafeTimerSeconds) { + if (failsafeTimerSeconds < 1 || failsafeTimerSeconds > 900) { + throw new IllegalArgumentException("failsafeTimerSeconds must be between 0 and 900"); + } + this.failsafeTimerSeconds = failsafeTimerSeconds; + return this; + } + + public Builder setAttemptNetworkScanWiFi(boolean attemptNetworkScanWiFi) { + this.attemptNetworkScanWiFi = attemptNetworkScanWiFi; + return this; + } + + public Builder setAttemptNetworkScanThread(boolean attemptNetworkScanThread) { + this.attemptNetworkScanThread = attemptNetworkScanThread; + return this; + } + public Builder setKeypairDelegate(KeypairDelegate keypairDelegate) { this.keypairDelegate = keypairDelegate; return this;