Android Question Strange issues with socket/asyncstream [Solved]

Kevin

Well-Known Member
Licensed User
I used the B4X Serializator tutorial as the basis for an android app that communicates with my ESP-12E. For the most part it works great, however, I have noticed some strange issues:

If I don't "disconnect" when pausing the activity (such as when exiting the app), then it seems to stay connected, which is fine. However, if I then want to connect to it using the B4J app, it cannot connect, presumably because the ESP is still connected to the Android app.

So in Activity_Pause of the Android app, I call the Disconnect sub in the Starter service. This solves the problem above, and works fine if I am using the ESP's IP address in the B4A app. In this case, opening the app again later re-connects or if I use the B4J app then it works and all is well.

But if I use my DDNS URL (with a port forwarded in the router to the ESP, with the router likely using hairpin routing), it always fails to re-connect when using the B4A app again. However, if I do NOT call Disconnect when pausing the B4A app, it continues to work when opening the app again later.

Another problem I am having is that when away from my WiFi (or if I just switch my phone over to 4G), when using the DDNS URL for the ESP, it cannot connect. The port is definitely forwarded to the correct port and IP address of the ESP. I have many devices on my network with forwarded ports and they all connect fine using the DDNS URL.

I'm at a loss for why I am seeing this strange behavior. I hope I have explained it well.

In short:

1 On my local WiFI, if I disconnect/close the connection when the B4A app pauses and later open the B4A app again:
*Using the ESP's IP address re-connects fine
*Using my DDNS URL instead of the IP address causes the app to fail to re-connect.
*I can switch back and forth between the B4A and B4J apps and they connect fine.

2 On my local WiFI, if I do NOT disconnect/close the connection when the B4A app pauses and later open the B4A app again:
*Using the ESP's IP address re-connects fine
*Using my DDNS URL instead of the IP address re-connects fine.
*I cannot then use the B4J app to connect to the ESP because it is still connected to the B4A app.

3 Even though I have the proper port forwarded in my router pointing to the ESP, I cannot connect to it outside of my WiFi network using DDNS, even though this works fine with other network devices with their ports forwarded in my router.

MAIN:
B4X:
#Region  Project Attributes
    #ApplicationLabel: Pool Minder
    #VersionCode: 1
    #VersionName: 1.0
    'SupportedOrientations possible values: unspecified, landscape or portrait.
    #SupportedOrientations: portrait
    #CanInstallToExternalStorage: False
    #BridgeLogger: True
#End Region

#Region  Activity Attributes
    #FullScreen: False
    #IncludeTitle: True
#End Region

' Data Objects:
' 0 = AirTemperature
' 1 = AirHumidity
' 2 = AirHeatIndex
' 3 = PoolTemperature
' 4 = HeatPumpRun (boolean)

Sub Process_Globals
    'These global variables will be declared once when the application starts.
    'These variables can be accessed from all modules.

End Sub

Sub Globals

    Private PanelConfig As Panel
    Private btnConnectAPMode As Button
    
    Private txtIP As EditText
    Private txtBoardName As EditText
    Private txtSSID As EditText
    Private txtPW As EditText
    
    Private PanelMain As Panel
    Private btnRefresh As Button
    Private btnConfig As Button
    Private lblPoolTemp As Label
    Private lblAirTemp As Label
    Private lblHumidity As Label
    Private lblHeatIndex As Label
    Private lblHeatRun As Label
    
    Private btnHeatRelayToggle As Button
    Private lblDandT As Label
End Sub

Sub Activity_Create(FirstTime As Boolean)
    Activity.LoadLayout("1")
    
    Starter.BoardIP = StateManager.GetSetting2 ("sIP", "192.168.4.1")
    Log ("Board IP = " & Starter.BoardIP)
    txtIP.Text = Starter.BoardIP
End Sub

Public Sub StateChanged
'    If Starter.connected Then
'        lblState.Text = "Connected"
'    Else
'        lblState.Text = "Disconnected"
'    End If
End Sub

Sub Activity_Resume
    btnRefresh_Click 'ToDo  Disabled for now until further testing
End Sub

Sub Activity_Pause (UserClosed As Boolean)
    If Starter.BoardIP <> Starter.BoardIPAPMode Then
        StateManager.SetSetting ("sIP", Starter.BoardIP)
        StateManager.SaveSettings
    End If
    'If UserClosed Then CallSub (Starter, "Disconnect")
    'CallSub (Starter, "Disconnect") '<-------------------------------------------------
End Sub

Sub Activity_KeyPress (KeyCode As Int) As Boolean 'Return True to consume the event
    If KeyCode = KeyCodes.KEYCODE_BACK And PanelConfig.Visible = True Then
        SaveAppSettings
        PanelConfig.Visible = False
        Return True
    End If
End Sub

Sub SaveAppSettings
    If Starter.BoardIP <> Starter.BoardIPAPMode Then
        StateManager.SetSetting ("sIP", Starter.BoardIP)
        StateManager.SaveSettings
    End If
End Sub

#Region Main Screen

Sub btnRefresh_Click
    Starter.Mode = 3
    ProgressDialogShow ("Refreshing data...")
    CallSub2 (Starter, "Connect", True)
End Sub

Sub btnSetConfig_Click
    If txtIP.Text = "" Then
        Msgbox ("Please enter the Pool Minder device's IP address.", Application.LabelName)
         Return
    End If
    
    If txtSSID.text = "" Or txtPW.Text = "" Then
        Msgbox ("Please enter your WiFi SSID and PW.", Application.LabelName)
        Return
    End If
    
    Dim p As Phone
    p.HideKeyboard (Activity)
    
    Starter.Mode = 2
    ProgressDialogShow ("Uploading configuration...")
    CallSub2 (Starter, "Connect", False)
End Sub

Sub btnGetConfig_Click
    If txtIP.Text = "" Then
        Msgbox ("Please enter the Pool Minder device's IP address.", Application.LabelName)
        Return
    End If
    
    txtBoardName.Text = ""
    txtSSID.Text = ""
    txtPW.Text = ""
    
    Dim p As Phone
    p.HideKeyboard (Activity)
    
    Starter.Mode = 1
    ProgressDialogShow ("Retrieving configuration")
    CallSub2 (Starter, "Connect", True)
End Sub

Sub btnConfig_Click
    PanelConfig.Visible = True
End Sub

Sub UpdateDataFields (Record As BoardData)
    ' Data Objects:
    ' 0 = AirTemperature
    ' 1 = AirHumidity
    ' 2 = AirHeatIndex
    ' 3 = PoolTemperature
    ' 4 = HeatPumpRun (boolean)
    ' 5 = Date & Time
    lblDandT.Text = Record.DandT
    lblAirTemp.Text = Record.AirTemp & " F"
    lblHumidity.Text = Record.AirHumidity & "%"
    lblHeatIndex.Text = Record.AirHeatIndex & " F"
    lblPoolTemp.Text = Record.PoolTemp & " F"
    Select Case Record.HeatPumpRun
        Case True
            lblHeatRun.Text = "Auto"
        Case False
            lblHeatRun.Text = "Standby"
    End Select

End Sub

Sub btnHeatRelayToggle_Click
    If lblHeatRun.Text = "" Then
        Msgbox ("Please sync the app with the Pool Minder device first in order to retrieve the current setting.", Application.LabelName)
    End If
    
    Select Case lblHeatRun.Text
            Case "Auto"
                Starter.Mode = 5 ' Change to Standby (HEATOFF)
                ProgressDialogShow ("Please wait...")
                CallSub2 (Starter, "Connect", False)
            Case "Standby"
            Starter.Mode = 4 ' Change to Auto (HEATON)
            ProgressDialogShow ("Please wait...")
                CallSub2 (Starter, "Connect", False)
    End Select
End Sub

Sub HideProg
    ProgressDialogHide
End Sub

#End Region

#Region MessageBoxes and Toasts
Sub ShowMessage (sMessage As String)
    Msgbox (sMessage, Application.LabelName)
End Sub

Sub ShowToast (sMessage As String)
    ToastMessageShow (sMessage, True)
End Sub
#End Region


#Region Config Screen

Sub btnConnectAPMode_Click
    If txtIP.Text <> Starter.BoardIPAPMode Then
        txtIP.Text = Starter.BoardIPAPMode
            Else ' It is equal to it
                txtIP.Text = StateManager.GetSetting ("sIP")
    End If
End Sub

Sub txtIP_TextChanged (Old As String, New As String)
    Starter.BoardIP = New
End Sub

Sub txtBoardName_TextChanged (Old As String, New As String)
    Starter.rec.BoardName = New
End Sub

Sub txtSSID_TextChanged (Old As String, New As String)
    Starter.rec.SSID = New
End Sub

Sub txtPW_TextChanged (Old As String, New As String)
    Starter.rec.Password = New
End Sub

Sub UpdateEditTexts (Record As BoardConfig)
    txtBoardName.Text = Record.BoardName
    txtSSID.Text = Record.SSID
    txtPW.Text = Record.Password
End Sub

#End Region


#Region Panel Click Events
Sub PanelConfig_Click
End Sub

Sub PanelMain_Click
End Sub
#End Region
STARTER SERVICE:
B4X:
#Region  Service Attributes
    #StartAtBoot: False
    #ExcludeFromLibrary: True
#End Region

' Data Objects:
' 0 = AirTemperature
' 1 = AirHumidity
' 2 = AirHeatIndex
' 3 = PoolTemperature
' 4 = HeatPumpRun (boolean)

Sub Process_Globals
    Private sock As Socket
    Private astream As AsyncStreams
    Public connected As Boolean
    'Private watchdog As Timer
    'Private lastValueTime As Long
    Private ser As B4RSerializator
    
    Public BoardIP As String 'IP address of ESP board
    Public BoardIPAPMode As String = "192.168.4.1" 'Default IP for ESP-12E
    
    Public Mode As Int '1 = GETCONFIG, 2 = SETCONFIG, 3 = GETDATA, 4 = SETHEATON, 5 - SETHEATOFF
    Type BoardData (AirTemp As String, AirHumidity As String, AirHeatIndex As String, PoolTemp As String, HeatPumpRun As Boolean, DandT As String)
    Type BoardConfig (BoardName As String, SSID As String, Password As String)
    Public rec As BoardConfig
    Private dat As BoardData
End Sub

Sub Service_Create
    'watchdog.Initialize("watchdog", 1000)
    ser.Initialize
    rec.Initialize
End Sub

Sub Service_Start (StartingIntent As Intent)

End Sub

Public Sub Connect (Get As Boolean)
    If sock.IsInitialized = False Then sock.Initialize("sock")
    
    ' New
'    If astream.IsInitialized Then
'        astream.Close
'    End If
'    sock.Initialize("sock")
    '------------------
    
    Try
        If sock.Connected = False Then
            sock.Connect(BoardIP, 51041, 10000)
            Wait For sock_Connected (Successful As Boolean)
            If Successful = False Then
                CallSub(Main, "HideProg")  'Hide the progress object
                CallSub2(Main, "ShowMessage", "Failed to connect")
                Log("Failed to connect!")
                Return
            Else
                Log("Connected to board")
            End If
            astream.InitializePrefix(sock.InputStream, False, sock.OutputStream, "astream")
        End If
    Catch
        Log(LastException)
        If astream.IsInitialized Then astream.Close
        sock.close
        CallSub(Main, "HideProg")  'Hide the progress object
        Return
    End Try
    
    If Get Then
                Select Case Mode
                    Case 1 'GETCONFIG
                        Log ("Requesting Config...")
                        Dim HeaderObj As Object = Array ("GETCONFIG")
                        astream.Write(ser.ConvertArrayToBytes(HeaderObj))
                        Wait For astream_NewData (Buffer() As Byte)
                        If Buffer(0) = 0 Then
                            rec.BoardName = "PoolMinder"
                        Else
                            ' We received a 1 as the first byte sent, which means it has been configured
                            Log ("Board appears to have been configured")
                            Wait For (astream) AStream_NewData(Data() As Byte)
                            Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                            Log (ObjectsReceived(0))
                            Select Case ObjectsReceived(0)
                                Case "GETCONFIG"
                                    Log ("Config Header received")
                                    Wait for astream_NewData (Data() As Byte)
                                    Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                                    For Each o As Object In ObjectsReceived
                                        Log("Incoming Object: " & o)
                                    Next
                                    rec = ObjectsToRecord(ser.ConvertBytesToArray(Data))
                                Case Else
                                    Log ("Data was received but without a header!")
                                    Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                                    For Each o As Object In ObjectsReceived
                                        Log("Incoming Object: " & o)
                                    Next
                            End Select
                        End If
                        CallSub2 (Main, "UpdateEditTexts", rec)   'UpdateEditTexts (rec) 'sheet.Set(rec, meta)
                    Case 3 'GETDATA
                        Log ("Requesting Data...")
                        Dim HeaderObj As Object = Array ("GETDATA")
                        astream.Write(ser.ConvertArrayToBytes(HeaderObj))
                        'Sleep(500)
                        Wait for astream_NewData (Data() As Byte)
                        Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                        If ObjectsReceived(0) = "GETDATA" Then
                            Wait for astream_NewData (Data() As Byte)
                            Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                            For Each o As Object In ObjectsReceived
                                Log("Incoming Object: " & o)
                            Next
                            dat = ObjectsToRecordDat(ser.ConvertBytesToArray(Data))
                        End If
                        CallSub2 (Main, "UpdateDataFields", dat)  ' Update data labels
                End Select
            Else 'Put mode, so send Request header and anything else relevant
                Select Case Mode
                    Case 2 'SETCONFIG
                        Log ("Sending Config...")
                        Dim HeaderObj As Object = Array ("SETCONFIG")
                        astream.Write(ser.ConvertArrayToBytes(HeaderObj))
                        'Sleep(250)
                        astream.Write(ser.ConvertArrayToBytes(RecordToObjects(rec)))
                        'Sleep(250)
                        Wait for astream_NewData (Data() As Byte)
                        Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                        If ObjectsReceived(0) = "SETCONFIG-SUCCESS" Then
                            CallSub2 (Main, "ShowToast", "Config settings saved!")
                            Log ("CONFIG command was successful")
                        End If
                    Case 4 'SETHEATON
                        Log ("Sending HEATON request...")
                        Dim HeaderObj As Object = Array ("SETHEATON")
                        astream.Write(ser.ConvertArrayToBytes(HeaderObj))
                        'Sleep(250)
                        Wait for astream_NewData (Data() As Byte)
                        Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                        If ObjectsReceived(0) = "SETHEAT-SUCCESS" Then
                            Log ("SETHEAT command was successful")
                        End If
                        'Disconnect
                        CallSub (Main, "btnRefresh_Click")
                    Case 5 'SETHEATOFF
                    Log ("Sending HEATOFF request...")
                        Dim HeaderObj As Object = Array ("SETHEATOFF")
                        astream.Write(ser.ConvertArrayToBytes(HeaderObj))
                        'Sleep(250)
                        Wait for astream_NewData (Data() As Byte)
                        Dim ObjectsReceived() As Object = (ser.ConvertBytesToArray(Data))
                        If ObjectsReceived(0) = "SETHEAT-SUCCESS" Then
                            Log ("SETHEAT command was successful")
                        End If
                        'Disconnect
                        CallSub (Main, "btnRefresh_Click")
                End Select   
            End If
        'Disconnect
    CallSub (Main, "HideProg")
End Sub

Sub RecordToObjects (Record As BoardConfig) As Object()
'    Return Array(Record.BoardName, Record.SSID, Record.Password, Record.AirTemp, Record.AirHumidity, Record.PoolTemp, Record.hpStatus, _
'        Record.hpDay1, Record.hpDay2, Record.hpDay3, Record.hpDay4, Record.hpDay5)
    Return Array(Record.BoardName, Record.SSID, Record.Password)
End Sub

Sub ObjectsToRecord(Objects() As Object) As BoardConfig
    Dim r As BoardConfig
    r.Initialize
    r.BoardName = Objects(0)
    r.SSID = Objects(1)
    r.Password = Objects(2)
'    r.hpDay1 = Objects(7)
'    r.hpDay2 = Objects(8)
'    r.hpDay3 = Objects(9)
'    r.hpDay4 = Objects(10)
'    r.hpDay5 = Objects(11)
'    r.MQTTBroker = Objects(3)
'    r.MQTTPort = Objects(4)
'    r.MQTTUsername = Objects(5)
'    r.MQTTPassword = Objects(6)
    Return r
End Sub

Sub ObjectsToRecordDat(Objects() As Object) As BoardData
    Dim r As BoardData
    r.Initialize
    ' Data Objects:
    ' 0 = AirTemperature
    ' 1 = AirHumidity
    ' 2 = AirHeatIndex
    ' 3 = PoolTemperature
    ' 4 = HeatPumpRun (boolean)
    ' 5 = Date & Time
    r.AirTemp = Objects(0)
    r.AirHumidity = Objects(1)
    r.AirHeatIndex = Objects(2)
    r.PoolTemp = Objects(3)
    r.HeatPumpRun = Objects (4)
    r.DandT = Objects (5)
    Return r
    '    r.hpDay1 = Objects(7)
    '    r.hpDay2 = Objects(8)
    '    r.hpDay3 = Objects(9)
    '    r.hpDay4 = Objects(10)
    '    r.hpDay5 = Objects(11)
End Sub

Sub AStream_Error
    Log("Error")
    CallSub(Main, "HideProg")  'Hide the progress object
    If astream.IsInitialized Then astream.Close
    sock.close
End Sub

Sub AStream_Terminated
    Log("Terminated")
    CallSub(Main, "HideProg")  'Hide the progress object
    If astream.IsInitialized Then astream.Close
End Sub

Sub Disconnect
    Log("Disconnect")
    CallSub(Main, "HideProg")  'Hide the progress object
    If astream.IsInitialized Then astream.Close
    sock.Close
End Sub

'Return true to allow the default exceptions handler to handle the uncaught exception.
Sub Application_Error (Error As Exception, StackTrace As String) As Boolean
    CallSub(Main, "HideProg")  'Hide the progress object
    Return True
End Sub

Sub Service_Destroy
    CallSub(Main, "HideProg")  'Hide the progress object
End Sub
 

Kevin

Well-Known Member
Licensed User
I will do more investigating when I have the time.

Based on what I am describing, do you feel that it would point to a problem in my code? If so, what should I be looking for, and would it be in the B4R server code or the B4A client code? Would you recommend disconnecting after every exchange of messages is complete? Or only when the app is finished (closed)?
 

Kevin

Well-Known Member
Licensed User
Just a quick update as I have solved the problem.

I had been including the http:// when entering the DDNS URL in place of the IP address. For whatever reason that occasionally worked, which threw me off. I just now tried it without the http:// and I was able to connect & reconnect just fine even from outside of my home WiFi.

If it would help anyone else, regarding my question about when I should disconnect, I am keeping the connection open unless the B4A app pauses or if the user changes the IP address field in the app.
 
Top