Reputation: 11
I have a Greengrass core (Jetson TX2) broadcasting an isolated access point so different IoT devices can communicate with each other. the access point works as I was able to have one device to communicate to the core through OSC protocol.
Right now I am trying to get my smartwatch (running WearOS) to communicate with AWS Greengrass MQTT. This requires a SSL connection along with a group certificate provided by the core. My implementation works when the core and the smartwatch both connect to a network with active internet connection. However, when I try to communicate them through the isolated access point, the smartwatch is unable to communicate with the core
I am implementing the communication through paho.mqtt.android
with a socketFactory
to store my keys and certifications. I am not sure what cause my app to not be able to communicate with my core processor
I have tried to isolate the problem and it seems like the smartwatch did connect to the access point and retrieve and IP address. However, the paho.mqtt.android
library does not use that IP address to initiate the MQTT connection
this is how I implement the client from the watch
AWSMQTTService.kt
// ... other code
socketFactory = AWSIotSslUtility.getSocketFactory(
applicationContext.resources.openRawResource(R.raw.groupca),
applicationContext.resources.openRawResource(R.raw.fc7334298c_certificate),
applicationContext.resources.openRawResource(R.raw.fc7334298c_private)
)
mqttClient = MqttAndroidClient(this, "ssl://${getEdgeEndpoint()}:8883", getDeviceName())
val mqttOptions = MqttConnectOptions()
mqttOptions.connectionTimeout = 5
mqttOptions.isAutomaticReconnect = true
mqttOptions.socketFactory = socketFactory
mqttClient.connect(mqttOptions, null, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
debugLog("success")
}
override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable) {
debugLog("failure")
Log.e("Jarvis/err", "Failure", exception)
}
})
AWSIotSslUtility.kt
/*
* Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.shelltechworks.jarviswear.lib.utils
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMKeyPair
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import java.io.*
import java.security.cert.CertificateFactory
import java.security.cert.Certificate as Certificate
import java.security.*
import java.security.Security
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
/**
* This is a helper class to facilitate reading of the configurations and
* certificate from the resource files.
*/
object AWSIotSslUtility {
@Throws(Exception::class)
fun getSocketFactory(
caCrtFile: InputStream,
crtFile: InputStream, keyFile: InputStream
): SSLSocketFactory {
Security.addProvider(BouncyCastleProvider())
val cf = CertificateFactory.getInstance("X.509")
val caCert = cf.generateCertificate(caCrtFile)
val cert = cf.generateCertificate(crtFile)
// load client private key
val pemParser = PEMParser(InputStreamReader(keyFile))
val pemRawValue = pemParser.readObject()
val converter = JcaPEMKeyConverter()
.setProvider("BC")
val key: KeyPair
key = converter.getKeyPair(pemRawValue as PEMKeyPair)
pemParser.close()
// CA certificate is used to authenticate server
val caKs = KeyStore.getInstance(KeyStore.getDefaultType())
caKs.load(null, null)
caKs.setCertificateEntry("ca-certificate", caCert)
val tmf = TrustManagerFactory.getInstance("X509")
tmf.init(caKs)
// client key and certificates are sent to server so it can authenticate
// us
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
ks.load(null, null)
ks.setCertificateEntry("certificate", cert)
ks.setKeyEntry(
"private-key", key.private, "".toCharArray(),
arrayOf(cert)
)
val kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm()
)
kmf.init(ks, "".toCharArray())
// finally, create SSL socket factory
val context = SSLContext.getInstance("TLSv1.2")
context.init(kmf.keyManagers, tmf.trustManagers, null)
return context.socketFactory
}
}
I expect this to connect and print success
to the debug log. However, I received this error
2019-05-29 16:18:31.259 13475-13475/com.shelltechworks.jarviswear D/Jarvis/AWSMQTTService: failure
2019-05-29 16:18:31.270 13475-13475/com.shelltechworks.jarviswear E/Jarvis/err: Failure
MqttException (0) - java.net.SocketTimeoutException: failed to connect to /192.168.12.1 (port 8883) from /192.168.167.239 (port 41415) after 5000ms
at org.eclipse.paho.client.mqttv3.internal.ExceptionHelper.createMqttException(ExceptionHelper.java:38)
at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:715)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:457)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Caused by: java.net.SocketTimeoutException: failed to connect to /192.168.12.1 (port 8883) from /192.168.167.239 (port 41415) after 5000ms
at libcore.io.IoBridge.connectErrno(IoBridge.java:185)
at libcore.io.IoBridge.connect(IoBridge.java:130)
at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:129)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:356)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:356)
at java.net.Socket.connect(Socket.java:616)
at org.eclipse.paho.client.mqttv3.internal.TCPNetworkModule.start(TCPNetworkModule.java:80)
at org.eclipse.paho.client.mqttv3.internal.SSLNetworkModule.start(SSLNetworkModule.java:103)
at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:701)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:457)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Upvotes: 0
Views: 1240
Reputation: 11
Finally figured out the solution. Turns out by default Android networkManager
will not bind itself to WiFi connection that doesn't have Internet access. Furthermore, if the device have cellular capability, it will bind to instead.
As far as I know, there are no solution to disable this functionality without rooting the device so I have to manually/explicitly bind the WiFi interface to a process in my app
private fun bindAppToWifiNetwork() {
val builder: NetworkRequest.Builder = NetworkRequest.Builder()
builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
val connectivityManager =
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.requestNetwork(builder.build(), object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
connectivityManager.bindProcessToNetwork(network)
}
})
}
Upvotes: 1