B4J Question Single client with multiple AsyncStreamsText connections to a single server

Chris2

Member
Licensed User
I've seen the examples for handling multiple asyncstreams connections at the server end, e.g.
,

But I'm struggling to convert what's needed to this particular situation so any help or pointers would be appreciated....

I'm trying to communicate with a number of devices that are attached via a serial link to the same host (the host is just a transparent serial to Ethernet adaptor).
The devices are passive in that they only respond to specific ASCII string commands that are addressed to them - each has a unique ID which is included in the string command.
If they receive a command they recognise with the correct ID, then they respond with an ASCII string.

I have a class that handles the comms to each individual device;
Class Comms:
Sub Class_Globals
    Private fx As JFX
    Private IsConnected As Boolean
    Private ast As AsyncStreamsText
    Private socket As Socket
    Private mCallback As Object
    Private mEventName As String
'    Private connections As Map
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize (Callback As Object, EventName As String, lid As Int, did As Int)
    mCallback=Callback
    mEventName=EventName
'    connections.Initialize
    Dim str As String = BuildCommsStr(lid, did)   'BuildCommsStr just builds the test request string I'm using.
    TCPConnect(str)
End Sub

Private Sub TCPConnect(msg As String)
    If ast.IsInitialized Then ast.Close
    If socket.IsInitialized Then socket.Close
    socket.Initialize("socket")
    socket.Connect("192.168.1.99", 4050, 3000)
    Wait For Socket_Connected (Successful As Boolean)
    If Successful Then
        ast.Initialize(Me, "ast", socket.InputStream, socket.OutputStream)
'        connections.Put(ast, mCallback)
        ast.Write(msg)
        CallSubDelayed2(mCallback, mEventName & "_DataSent", msg)
        Wait For Reply_Complete' (success As Boolean)
    End If
    ast.Close
    socket.Close
    setState(False)
End Sub

Private Sub ast_NewText (Text As String)
    Log(Text)
'    Dim a As AsyncStreamsText = Sender
    CallSubDelayed2(mCallback, mEventName & "_Reply", Text)
'    CallSubDelayed2(connections.Get(a), mEventName & "_Reply", Text)

    CallSubDelayed(Me, "Reply_Complete")

End Sub
In the main module I have;
Main module:
Sub Button1_Click
    Dim com1 As Comms
    com1.Initialize(Me, "Comm1", 1, 20)
    
'    Dim com2 As Comms
'    com2.Initialize(Me, "Comm2", 2, 20)
    
End Sub

Sub Comm1_DataSent(msg As String)
    txtRequest.Text=msg
End Sub

Sub Comm1_Reply(msg As String)
    txtReply.Text=msg
End Sub

Sub Comm2_DataSent(msg As String)
    txtRequest2.Text=msg
End Sub

Sub Comm2_Reply(msg As String)
    txtReply2.Text=msg
End Sub
Talking to only one of the devices works fine - I can see the request go out in the logs & in txtRequest, and a valid response come back in the logs and txtReply.
But if I instigate more than one instance of the Comms class, the requests still seem to go out correctly, but the response I get in ast_NewText (and in astreams_NewData in the AsyncStreamsText class) is garbled nonsense.
Presumable that's because the replies from both devices are getting scrambled in the same asyncstream?

I've tried use a connections map & the Sender object to ensure that the stream goes to the correct class instance (see the commented bits in the code above), but it makes no difference. I've tried a similar thing within the AsyncStreamsText class.

Is there any way I can split up the comms lines so that each class instance only receives the reply to its own request?
 

Chris2

Member
Licensed User
Thanks @DonManfred , but I thought that was what I was doing already with;
B4X:
Dim com1 As Comms       '1st instance of Comms class
com1.Initialize(Me, "Comm1", 1, 20)
    
Dim com2 As Comms       '2nd instance of Comms class
com2.Initialize(Me, "Comm2", 2, 20)
?
 

Erel

Administrator
Staff member
Licensed User
I guess that your host doesn't support multiple TCP connections, so you need to share the AsyncStreams object between the class instances. You will need to somehow identify which device sent the message and pass it to the correct instance.

If the messages include the device id then store the class instances in a Map with the ids as the keys.
 

Chris2

Member
Licensed User
I realise that this might have been a bit of a niche issue, but just in case anyone else comes across a similar problem in the future here's how I solved it....

There were actually two issues;
1. I found that if a request for data was sent to multiple devices attached to the same host (same IP address) at the same time then the replies got completely garbled and could not be parsed.
The solution I used for this was to use a single instance of the 'Comms' class (and hence a single instance of AsyncStreamsText) for each IP address, not for each device.

The Comms class instances are stored in a map with the IP addresses as the keys.
I use this map to make sure that messages to each device are sent to the correct class instance.

Within the class, messages to be sent to each device are queued in a map so that only one is sent at a time, and it waits for a reply (or timeout) before proceeding with the next.

2. As @Erel said, I also needed to make sure that the reply I was getting was from the correct device.
For this, I modified astreams_NewData in the AsyncStreamsText class to pick out the exact strings I'm looking for.

AsyncStreamsText:
Private Sub astreams_NewData (Buffer() As Byte)
    sb.Append(BytesToString(Buffer, 0, Buffer.Length, charset))
    Dim s As String = sb.ToString
'    Log(s)
'    Log("mlogIDHex=" & mlogIDHex)
'    mlogIDHex is the unique ID of the device and is a parameter of AsyncStreamsText.Initialize
    Dim matchReply As Matcher = Regex.Matcher($"\[.+${mlogIDHex}.+\]"$, s)   
    If matchReply.Find Then
        CallSubDelayed2(mTarget, mEventName & "_NewText", matchReply.Match)
        sb.Initialize
    End If
    
End Sub
 
Top