How can I detect a server ping using URLSessionWebSocketTask? - ios

Using URLSessionWebSocketTask, a server ping is automatically responded to by a pong from the client.
Still, can the client observe that a ping/pong occurred?
(a related but different question)

Usually, right after setting a connection, you'd need to run ping in a loop in the app:
Timer.TimerPublisher(interval: 10, runLoop: .main).sink(receiveValue: { _ in
task.sendPing(pongReceiveHandler: { error in
guard let error = error else {
log("Received pong")
return
}
// Handle error
})
}).store(in: &subscriptions)
UPD:
If you're interested in observing pong, then in subscription, check if you received 0x9 as a data:
task.receive { result in
switch result {
case .success(let message):
switch message {
case .data(let data):
//
...
}
...
}
}
It will be:
close connection: 0x8
ping: 0x9
pong: 0xA
So you can create an enum:
enum Code: UInt8 {
case closeConnection = 0x8
case ping = 0x9
case pong = 0xA
}
And initialise it with data you receive: Code(rawValue: YOURDATA)
Or you can send "PING" as a text from your cloud server instead, that will simplify things for you

Related

How to create NWConnection

Premise
I'm using Apple's Network framework to create a peer-to-peer app. What I'm currently trying to do is:
Device A establishes a connection with Device B and sends data.
Device B saves the NWConnection information of Device A.
Device B establishes a connection with Device A again using the saved MWConnection information and sends data.
I modelled off Apple's demo, which is essentially this source code here. Unlike Apple's demo, which establishes a single two-way connection and maintains that connection at all times, my app connects to multiple devices that can drop in and out of the connections at any time. This is why I want to be able to a) distinguish a specific device and b) initiate a new connection.
Problem
The problem I'm having is being able to reconstruct the NWConnection object using the information provided when a connection has been established. There are two ways of instantiating this object.
init(host: NWEndpoint.Host, port: NWEndpoint.Port, using: NWParameters)
init(to: NWEndpoint, using: NWParameters)
My attempts have been trying to gather the endpoint information like the host and the port while the connection with the desired device has been established and instantiate NWEndpoint to be used in NWConnection. But, I haven't been able to re-establish the connection thus far.
Following is the portion of the object that is used to initiate a connection. My full code is modified, but Apple's counterpart can be seen here.
class PeerConnection {
weak var delegate: PeerConnectionDelegate?
weak var statusDelegate: StatusDelegate?
var connection: NWConnection?
let initiatedConnection: Bool
init(endpoint: NWEndpoint, interface: NWInterface?, passcode: String, delegate: PeerConnectionDelegate) {
self.delegate = delegate
self.initiatedConnection = true
let connection = NWConnection(to: endpoint, using: NWParameters(passcode: passcode))
self.connection = connection
startConnection()
}
init(connection: NWConnection, delegate: PeerConnectionDelegate) {
self.delegate = delegate
self.connection = connection
self.initiatedConnection = false
startConnection()
}
// Handle starting the peer-to-peer connection for both inbound and outbound connections.
func startConnection() {
guard let connection = connection else {
return
}
connection.stateUpdateHandler = { newState in
switch newState {
case .ready:
print("\(connection) established")
// When the connection is ready, start receiving messages.
self.receiveNextMessage()
// Notify your delegate that the connection is ready.
if let delegate = self.statusDelegate {
delegate.showConnectionStatus(.connectionSuccess("Connection Success"))
}
case .failed(let error):
print("\(connection) failed with \(error)")
// Cancel the connection upon a failure.
connection.cancel()
// Notify your delegate that the connection failed.
if let delegate = self.statusDelegate {
delegate.showConnectionStatus(.connectionFail("Connection Fail"))
}
default:
break
}
}
// Start the connection establishment.
connection.start(queue: .main)
}
}
This is how I receive data when another device sends it.
func receiveNextMessage() {
guard let connection = connection else {
return
}
/// Has to call itself again within the closure because the maximum message is once.
connection.receiveMessage { (content, context, isComplete, error) in
// Extract your message type from the received context.
if let message = context?.protocolMetadata(definition: GameProtocol.definition) as? NWProtocolFramer.Message {
self.delegate?.receivedMessage(content: content, message: message, connection: connection)
}
if error == nil {
// Continue to receive more messages until you receive and error.
self.receiveNextMessage()
}
}
}
Finally, following is how I attempt to reconstruct MWConnection:
func receivedMessage(content: Data?, message: NWProtocolFramer.Message, connection: NWConnection) {
switch(connection.endpoint) {
case .hostPort(let host, let port):
/// first attempt
let endpoint = NWEndpoint.hostPort(host: host, port: port)
let newConnection = PeerConnection(endpoint: endpoint, interface: nil, passcode: passcode, delegate: self)
/// second attempt
let connection = NWConnection(host: host, port: port, using: NWParameters(passcode: passcode))
let newConnection = PeerConnection(connection: connection, delegate: self)
default:
break
}
}
NWConnection is instantiated inside initializer of PeerConnection.

Is is possible to open SwiftNIO based server socket within XCTest test app?

I have an XCTest which works with UI components. I tried to open a server socket within the xctext function using SwiftNIO.
I took the echo server example from here. and I simplified, removed the args with hardcoded values for the sake of a dirty test.
import XCTest
import NIOCore
import NIOPosix
private final class EchoHandler: ChannelInboundHandler {
public typealias InboundIn = ByteBuffer
public typealias OutboundOut = ByteBuffer
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.write(data, promise: nil)
}
// Flush it out. This can make use of gathering writes if multiple buffers are pending
public func channelReadComplete(context: ChannelHandlerContext) {
context.flush()
}
public func errorCaught(context: ChannelHandlerContext, error: Error) {
print("error: ", error)
// As we are not really interested getting notified on success or failure we just pass nil as promise to
// reduce allocations.
context.close(promise: nil)
}
}
class MyXCTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
// Catch system alerts such as "allow connecting to Wi-fi network"
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
alert.buttons["Join"].tap()
return true
}
}
override func tearDownWithError() throws {
}
func testXYZ() throws {
app.launch()
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let bootstrap = ServerBootstrap(group: group)
// Specify backlog and enable SO_REUSEADDR for the server itself
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
// Set the handlers that are appled to the accepted Channels
.childChannelInitializer { channel in
// Ensure we don't read faster than we can write by adding the BackPressureHandler into the pipeline.
channel.pipeline.addHandler(BackPressureHandler()).flatMap { v in
channel.pipeline.addHandler(EchoHandler())
}
}
// Enable SO_REUSEADDR for the accepted Channels
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
.childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
defer {
try! group.syncShutdownGracefully()
}
let channel = try { () -> Channel in
return try bootstrap.bind(host: "0.0.0.0", port: 1234).wait()
}()
print("============= Server started and listening on \(channel.localAddress!)")
// then some XCTest code which works here was cut from this snippet
}
The test runs correctly, it also prints
============= Server started and listening on [IPv4]0.0.0.0/0.0.0.0:1234
But in reality EchoClient from here doesn't work
swift run NIOEchoClient localhost 1234 ๎‚ฒ ๏€Œ ๎‚ฒ 1785
[0/0] Build complete!
Please enter line to send to the server
dfsdfd
Swift/ErrorType.swift:200: Fatal error: Error raised at top level: NIOPosix.NIOConnectionError(host: "localhost", port: 1234, dnsAError: nil, dnsAAAAError: nil, connectionErrors: [NIOPosix.SingleConnectionFailure(target: [IPv6]localhost/::1:1234, error: connection reset (error set): Connection refused (errno: 61)), NIOPosix.SingleConnectionFailure(target: [IPv4]localhost/127.0.0.1:1234, error: connection reset (error set): Connection refused (errno: 61))])
[1] 28213 trace trap swift run NIOEchoClient localhost 1234
The listening socket also unavailable with
sudo lsof -PiTCP -sTCP:LISTEN
I was also trying UITestEntitlements to set com.apple.security.app-sandbox to false.
Is there a way to allow server sockets from XCTest?
Originally I was trying to embed a Swift-GRPC endpoint, to allow more finer grained control from a HW in the loop controller. The intent is to start an XCTest using command line xcodebuild, which in turn is starting a long running test, but instead of the test code written in Swift, I would expose the events when to tap some buttons, right outside of the test process through a grpc endpoint.
Since the grpc endpoint didn't worked, I reduced the problem to the one above.
Anybody have a hint, how to pass through this issue, or have a hint why it will never be possible to open server socket within an XCTest app, don't hesitate to reply here.
Yes, that is possible, you can find many examples of this in the AsyncHTTPClient and SwiftNIO test suites.
The reason that yours doesn't work is because you shut down the MultiThreadedEventLoopGroup right after binding the socket. So essentially you're starting everything up and then you shut it down again.
Also, for unit tests, I'd recommend binding to 127.0.0.1 only because you probably don't want connections from elsewhere. Another good idea is to use an ephemeral port, ie. have the system pick a free, random port automatically. You can achieve this by specifying port 0. After you bind the server Channel you can then interrogate the server channel by using serverChannel.localAddress?.port! about the port it picked.
Here's a full example with a client and a server in a test case.
import XCTest
import NIO
final class ExampleTests: XCTestCase {
// We keep the `EventLoopGroup` where all the I/O runs alive during the test
private var group: EventLoopGroup!
// Same for the server channel.
private var serverChannel: Channel!
private var serverAddress: SocketAddress {
return self.serverChannel.localAddress!
}
// We set up the server in `setUp`...
override func setUp() {
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
XCTAssertNoThrow(self.serverChannel = try ServerBootstrap(group: self.group)
.childChannelInitializer { channel in
channel.pipeline.addHandler(UppercasingHandler())
}
.bind(host: "127.0.0.1", port: 0) // port 0 means: pick a free port.
.wait())
}
// ... and tear it down in `tearDown`.
override func tearDown() {
XCTAssertNoThrow(try self.serverChannel?.close().wait())
XCTAssertNoThrow(try self.group?.syncShutdownGracefully())
}
func testExample() throws {
// Here we just bootstrap a little client that sends "Hello world!\n"
let clientChannel = try ClientBootstrap(group: self.group)
.channelInitializer { channel in
channel.pipeline.addHandler(PrintEverythingHandler())
}
.connect(to: self.serverAddress)
.wait()
XCTAssertNoThrow(try clientChannel
.writeAndFlush(ByteBuffer(string: "Hello world!\n"))
.wait())
XCTAssertNoThrow(try clientChannel.closeFuture.wait())
}
}
// Just a handler that uses the C `toupper` function which uppercases characters.
final class UppercasingHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
typealias OutboundOut = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let inBuffer = self.unwrapInboundIn(data)
var outBuffer = context.channel.allocator.buffer(capacity: inBuffer.readableBytes)
// Here we just upper case each byte using the C stdlib's `toupper` function.
outBuffer.writeBytes(inBuffer.readableBytesView.map { UInt8(toupper(CInt($0))) })
context.writeAndFlush(self.wrapOutboundOut(outBuffer),
promise: nil)
}
// We want to close the connection on any error really.
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("server: unexpected error \(error), closing")
context.close(promise: nil)
}
}
// This handler just prints everything using the `write` system call. And closes the connection on a newline.
final class PrintEverythingHandler: ChannelInboundHandler {
typealias InboundIn = ByteBuffer
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let inBuffer = self.unwrapInboundIn(data)
guard inBuffer.readableBytes > 0 else {
return
}
// We're using Unsafe* stuff here because we're using the `write` system call, which is a C function.
_ = inBuffer.withUnsafeReadableBytes { ptr in
write(STDOUT_FILENO, ptr.baseAddress!, ptr.count)
}
// If we see a newline, then let's actually close the connection...
if inBuffer.readableBytesView.contains(UInt8(ascii: "\n")) {
print("found newline, closing...")
context.close(promise: nil)
}
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
print("client: unexpected error \(error), closing")
context.close(promise: nil)
}
}

NWConnection timeout

this is my first post to the community, so please be patient ;-)
I am using Apples Network framework to build a TCP connection to a server. It locks like this:
let testConnection = NWConnection(host: NWEndpoint.Host(server!.serverAddress), port: NWEndpoint.Port(server!.serverPort)!, using: .tcp)
testConnection.stateUpdateHandler = ({ state in
print("TCP state change to: \(state)")
switch state {
case .setup:
break
case .waiting(let error):
print("Waiting Error \(error)")
testConnection.cancel()
break
case .preparing:
break
case .ready:
beginCommunication()
break
case .failed(let error):
print("\(error)")
break
case .cancelled:
break
default:
break
}
})
The serverAddress and serverPort is entered by the user. I want to test the connection. Now my problem is, that if the user is entering an invalid address / port combination (the service is not offered by the server). I stuck in the preparing state for quite a long time. After that I get to the waiting stage (with error message POSIXErrorCode: Operation timed out).
Is there any way to set the timeout for this first connection process ?
Thanks for your ideas
You can set connection timed out by NWProtocolTCP.Options
lazy var tcpOptions: NWProtocolTCP.Options = {
let options = NWProtocolTCP.Options()
options.connectionTimeout = 5 // connection timed out
return options
}()
lazy var parames: NWParameters = {
let parames = NWParameters(tls: nil, tcp: self.tcpOptions)
if let isOption = parames.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
isOption.version = .v4
}
parames.preferNoProxies = true
return parames
}()
lazy var connection: NWConnection = {
let connection = NWConnection(host: "x.x.x.x", port: xxxx, using: self.parames)
return connection
}()
When trigger the timed out event, the .waiting state will call back.
2021-08-30 23:05:17.xxxxxx+xxxx Demo[xxxx:xxxxxx] [connection] nw_socket_handle_socket_event [C1:1] Socket SO_ERROR [60: Operation timed out]
The connection is waiting for a network path change with: POSIXErrorCode: Operation timed out
reference from Developer Fourms

NWNetworking / NWListener how to correctly receive UDP broadcast messages?

I have the following code running fine when receiving direct messages via UDP:
import UIKit
import Network
class ViewController: UIViewController {
var udpListener:NWListener?
var backgroundQueueUdpListener = DispatchQueue(label: "udp-lis.bg.queue", attributes: [])
var backgroundQueueUdpConnection = DispatchQueue(label: "udp-con.bg.queue", attributes: [])
var connections = [NWConnection]()
override func viewDidLoad() {
super.viewDidLoad()
myOnButton(self)
}
#IBAction func myOnButton(_ sender: Any) {
guard self.udpListener == nil else {
print(" ๐Ÿงจ Already listening. Not starting again")
return
}
do {
self.udpListener = try NWListener(using: .udp, on: 2000)
self.udpListener?.stateUpdateHandler = { (listenerState) in
switch listenerState {
case .setup:
print("Listener: Setup")
case .waiting(let error):
print("Listener: Waiting \(error)")
case .ready:
print("Listener: Ready and listens on port: \(self.udpListener?.port?.debugDescription ?? "-")")
case .failed(let error):
print("Listener: Failed \(error)")
case .cancelled:
print("Listener: Cancelled by myOffButton")
for connection in self.connections {
connection.cancel()
}
self.udpListener = nil
default:
break;
}
}
self.udpListener?.start(queue: backgroundQueueUdpListener)
self.udpListener?.newConnectionHandler = { (incomingUdpConnection) in
print ("๐Ÿ’ New connection \(incomingUdpConnection.debugDescription)")
incomingUdpConnection.stateUpdateHandler = { (udpConnectionState) in
switch udpConnectionState {
case .setup:
print("Connection: setup")
case .waiting(let error):
print("Connection: waiting: \(error)")
case .ready:
print("Connection: ready")
self.connections.append(incomingUdpConnection)
self.processData(incomingUdpConnection)
case .failed(let error):
print("Connection: failed: \(error)")
self.connections.removeAll(where: {incomingUdpConnection === $0})
case .cancelled:
print("Connection: cancelled")
self.connections.removeAll(where: {incomingUdpConnection === $0})
default:
break
}
}
incomingUdpConnection.start(queue: self.backgroundQueueUdpConnection)
}
} catch {
print("๐Ÿงจ")
}
}
#IBAction func myOffButton(_ sender: Any) {
udpListener?.cancel()
}
func processData(_ incomingUdpConnection :NWConnection) {
incomingUdpConnection.receiveMessage(completion: {(data, context, isComplete, error) in
if let data = data, !data.isEmpty {
if let string = String(data: data, encoding: .ascii) {
print ("DATA = \(string)")
}
}
if error == nil {
self.processData(incomingUdpConnection)
}
})
}
}
Sending endless UDP packages directly to the device like this:
echo -n "Testdata" | socat - udp-datagram:192.168.2.126:2000,broadcast,sourceport=2000
works as expected, and all messages arrive:
Listener: Waiting POSIXErrorCode: Network is down
Listener: Ready and listens on port: 2000
๐Ÿ’ New connection [C1 192.168.2.134:2000 udp, local: 192.168.2.126:2000, indefinite, server, path satisfied (Path is satisfied), interface: en0, scoped, ipv4, ipv6, dns]
Connection: ready
DATA = Testdata
DATA = Testdata
DATA = Testdata
DATA = Testdata
...
But trying the same with broadcast messages
echo -n "Testat" | socat - udp-datagram:255.255.255.255:2000,broadcast,sourceport=2000
results in only receiving the first message, and a new connection with each received UDP package.
Listener: Waiting POSIXErrorCode: Network is down
Listener: Ready and listens on port: 2000
๐Ÿ’ New connection [C1 192.168.2.134:2000 udp, local: 0.0.0.0:2000, indefinite, server, path satisfied (Path is satisfied), interface: en0, ipv4, ipv6, dns]
Connection: ready
DATA = Testdata
2020-06-28 14:22:03.668116+0200 networking[25440:13489733] [] nw_channel_reclassify_input reclassify failed, could not find client for slot 77926755-A281-4AFD-9649-92FBD1A21FA6
๐Ÿ’ New connection [C2 192.168.2.134:2000 udp, local: 0.0.0.0:2000, indefinite, server, path satisfied (Path is satisfied), interface: en0, ipv4, ipv6, dns]
2020-06-28 14:22:04.384443+0200 networking[25440:13489733] [] nw_channel_reclassify_input reclassify failed, could not find client for slot 7EAC98FA-F665-43C1-9B15-B68B74A56BBC
๐Ÿ’ New connection [C3 192.168.2.134:2000 udp, local: 0.0.0.0:2000, indefinite, server, path satisfied (Path is satisfied), interface: en0, ipv4, ipv6, dns]
2020-06-28 14:22:05.096808+0200 networking[25440:13489733] [] nw_channel_reclassify_input reclassify failed, could not find client for slot AA3EE3BA-D891-4D07-87AF-0CB0A9382CDF
๐Ÿ’ New connection [C4 192.168.2.134:2000 udp, local: 0.0.0.0:2000, indefinite, server, path satisfied (Path is satisfied), interface: en0, ipv4, ipv6, dns]
2020-06-28 14:22:05.714651+0200 networking[25440:13489733] [] nw_channel_reclassify_input reclassify failed, could not find client for slot ED108E8B-E9BC-43C3-8AA7-B4BD0515EB54
Workaround would be to .cancel the connection after each received package, but I doubt this is the way how this was intended to be used to receive UDP broadcast messages. So what is the correct approach to receive UDP broadcast packages in an endless loop?
Ok, I got sort of an answer.
I had the same basic issue in Objective-C. I took the step of cancelling the connection in the handler, which sort of works, but you end up with data loss - about 30% in my experience - as the stream fails to pick up broadcast data.
However, I did get feedback from dts, which is basically this - Networking Library is not designed for receiving broadcast UDP.
I've reverted to BSD sockets.

DispatchGroup.wait not waiting

I'm having an issue understanding or using Dispatchgroup. I've read a lot about them however most examples / documentation are very vague or doesn't resemble what I want to do, however every time I mention my problem everyone says "USE DISPATCH GROUPS!".
Here's what I want to do(NOTE: SEQUENTIAL ORDER IS CRUCIAL):
Send Bluetooth Write characteristic.
device receives value, and spits something in response
Read Bluetooth response (via a read characteristic)
Send a new write characteristic (a different command)
device receives NEW command, spits NEW data response
Repeat twice (3 commands total, 3 different responses total).
My code:
func tPodInitialSetUp()
{
print ("* * * * * BEGIN SET-UP * * * * *")
let setupDispatchGroup = DispatchGroup()
setupDispatchGroup.enter()
self.writeValue(command: Data(CommandModeCmd)) //231: Put t-Pod in command mode, burst mode is OFF returns OK
setupDispatchGroup.leave()
setupDispatchGroup.wait()
setupDispatchGroup.enter()
deviceConnected?.readValue(for: deviceConnectedCh1n2Char!)
print("Sent command 231: returned: \(receivedString1)")
if receivedString1.lowercased() == "ok"
{
print("t-Pod burst mode is OFF")
}
setupDispatchGroup.leave()
setupDispatchGroup.wait()
setupDispatchGroup.enter()
self.writeValue(command: Data(loadProbeCalCmd)) //202: load calibration constants of probe, returns ok or 0
setupDispatchGroup.leave()
setupDispatchGroup.wait()
setupDispatchGroup.enter()
deviceConnected?.readValue(for: deviceConnectedCh1n2Char!)
print("Sent command 202: returned: \(receivedString1)")
if receivedString1.lowercased() == "ok"
{
print("Probe Constants loaded")
}
if receivedString1 == "0"
{
print("No probe connected")
}
setupDispatchGroup.leave()
setupDispatchGroup.wait()
setupDispatchGroup.enter()
self.writeValue(command: Data(probeSNCmd)) //205: load probe serial number
setupDispatchGroup.leave()
setupDispatchGroup.wait()
setupDispatchGroup.enter()
deviceConnected?.readValue(for: deviceConnectedCh1n2Char!)
print("Sent command 205: returned: \(receivedString1)")
if (receivedString1.count == 6)
{
print("received Probe SN: \(receivedString1)")
probeSN = receivedString1
}
setupDispatchGroup.leave()
setupDispatchGroup.notify(queue: .main)
{
tPodSN = String(describing: connectedDeviceName!.dropFirst(7))
print ("* * * SET-UP COMPLETE * * *")
self.writeValue(command: Data(resetCmd)) //200: resets t-Pod
self.writeValue(command: Data(beaconOffCmd)) //211: turns beacon off (temperature output)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)
{
self.dataDisplaySubView.isHidden = false
print ("Adding observers!")
NotificationCenter.default.addObserver(self, selector: #selector(self.updateIncomingData), name: NSNotification.Name(rawValue: DATA_PARSED), object: nil) //Run every time you receive data from BLE
NotificationCenter.default.addObserver(self, selector: #selector(self.calculateTNU), name: NSNotification.Name(rawValue: TOGGLESWITCH_TOGGLED), object: nil) //Run in case the toggle switches change and data needs to be re-calculated
NotificationCenter.default.addObserver(self, selector: #selector(self.parseReceivedData), name: NSNotification.Name(rawValue: DEVICE_FINISHED_SENT_DATA), object: nil) //Run every time you receive the notification that the whole data has been sent
}
}
This calls the bluetooth write command which has the following code and confirmation:
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
guard error == nil else {
print("Error writing descriptor: " + (error?.localizedDescription)!)
return
}
print("Descriptor Value sent")
}
Now, Here's my output:
* * * * * BEGIN SET-UP * * * * *
***** WRITING *****
Wrote: 1 bytes
***** WRITING *****
Wrote: 1 bytes
Sent command 231: returned: **T-Pod-9Ch**
***** WRITING *****
Wrote: 1 bytes
Sent command 202: returned: **T-Pod-9Ch**
***** WRITING *****
Wrote: 1 bytes
Sent command 205: returned: **T-Pod-9Ch**
* * * SET-UP COMPLETE * * *
***** WRITING *****
Wrote: 1 bytes
***** WRITING *****
Wrote: 1 bytes
Characteristic Value sent
Adding observers!
Characteristic Value sent
Characteristic Value sent
Characteristic Value sent
Characteristic Value sent
Clearing TNU Array
Now, as you can see "Characteristic Value Sent" is the confirmation the bluetooth function gives when it sends the value, however this output is created AFTER it finished running the entire code, so basically it put the commands in some pipeline, forgot about them did everything else and THEN sent the commands and therefore the response I'm reading are all nonsense! As you can see all received strings are T-Pod-9Ch (which is just its normal burst output), the expected responses I should get from the commands are OK, OK and a 6 digit number (in that order).
Please help, I've read so many times about how dispatch groups are supposed to work but I just can't get them to do what I want.
If I got your question right, you need to wait for an answer before sending new command.
But your writes doesn't have a completion block, that's why in your case using dispatchGroup makes no sense.
The code below is a common example to use dispatch groups
func someMethod(completionHandler: #escaping ()-> Void) {
//we need to get or set some data in separated queue
DispatchQueue.global(qos: .background).async {
let group = DispatchGroup()
//let's say we have an array of urls and we need to load images and put them to an array
for url in someUrlArray {
group.enter()
SomeLoaderClass.load(url) { image in
//add received image
//leave the group
group.leave()
}
}
//now we need to wait until all images will be downloaded
group.wait()
//then we can finish and call the completion in the main queue
DispatchQueue.main.async {
completionHandler()
}
}
}
In your situation you may have several options:
First, if you know that if you send one command and receive an answer exactly for that command, you can call methods in order below:
Call one method to Send command 1
Call another method after an answer for command 1 will be received
Call yet another method to Send command 2
And one more method after getting an answer for command 2
...
n. Finish setup
Like if I need to register a user, I need to send defined credentials first, get token from the server, then run something after it.
So you will have to add an additional method for each command and you will call them according the order
If you can't recognize for which command you're going to get an answer and you sure that you've send only one command and waiting only one answer, then you can use dispatch group in the way described below:
typealias Callback = ()->Void
class SomeManagerClass {
var callback: Callback?
func initiateSetup(){
DispatchQueue.global(qos: .background).async { [weak self] in
let group = DispatchGroup()
//now we can send commands
group.enter()
self?.sendCommand(SomeDataForTheFirstCommand) {
//when the first answer will be received, it will init the callback, so you can leave the group now
group.leave()
}
//sending the second command
group.enter()
self?.sendCommand(SomeDataForTheSecondCommand) {
//waiting for the second answer will be received
group.leave()
}
//.... more commands sending same way
group.wait()
//now all commands was send and you got an answer for each
//finishing setup
DispatchQueue.main.async{
self?.finishSetup()
}
}
}
func sendCommand(_ command: Data, callback: Callback?){
self.writeValue(command: command)
self.callback = callback
}
func answerReceived(){
//this is just an example method that is called when you get an answer for any command
//now we can callback
self.callback?()
}
func finishSetup(){
//do something
}
}
Let me know if you need more details
Example of using Dispatch group
private func performTask1() -> String {
print("Task1")
var result: String?
let background = DispatchQueue.global(qos: .background)
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
background.asyncAfter(deadline: .now() + 3) {
result = "Task1 is performed"
dispatchGroup.leave()
}
dispatchGroup.wait()
return result!
}
Try this out to understand concept of DispactGroup!
Try! this
Required DispatchQueue.global(qos: .background) for leave()
func dispatchGroup() -> String {
var message = ""
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5) {
message = "Hiii Friends"
group.leave()
}
group.wait()
return message
}

Resources