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:
STARTER SERVICE:
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