Hello,
To assist with the investigation, I have created a simplified test project that isolates the issue I’m encountering. This specific implementation is where I am seeing the most consistent problems.
Could you please take a look at this example and let me know if you see anything that might be causing the failure? The test code is provided below:
// TCPClient.swift
import Foundation
import Network
import Combine
class TCPClient: ObservableObject {
private var connection: NWConnection?
private let queue = DispatchQueue(label: "TelnetQueue")
@Published var currentMessage: String = "---"
@Published var isConnected: Bool = false
// Contador de soquetes solicitado para monitoramento
private var socketAttemptCount: Int = 0
private let host: NWEndpoint.Host = "192.168.1.50"
private let port: NWEndpoint.Port = 23
private let allowedCommands = ["HRS", "BTz", "BTd"]
private var buffer = Data()
func toggleConnection() {
if isConnected {
disconnect()
} else {
connect()
}
}
func connect() {
// Incrementa e imprime a contagem toda vez que tenta abrir uma nova conexão
socketAttemptCount += 1
print("TENTATIVA DE ABERTURA DE SOQUETE Nº: \(socketAttemptCount)")
guard connection == nil else {
print("AVISO: Já existe uma instância de conexão. Abortando duplicata.")
return
}
let tcpOptions = NWProtocolTCP.Options()
tcpOptions.noDelay = true
// Configuração de Keep-Alive de baixo nível (estilo Android)
tcpOptions.enableKeepalive = true
tcpOptions.keepaliveIdle = 5 // Tempo (segundos) de espera antes de testar a conexão
tcpOptions.keepaliveInterval = 1 // Intervalo entre testes se não houver resposta
tcpOptions.keepaliveCount = 3 // Tentativas antes de derrubar
let parameters = NWParameters(tls: nil, tcp: tcpOptions)
parameters.prohibitedInterfaceTypes = [.cellular] // Garante uso do Wi-Fi local
connection = NWConnection(host: host, port: port, using: parameters)
connection?.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }
print("STATE UPDATE:", state)
switch state {
case .ready:
print("ESTADO: Pronto (Conectado à balança)")
DispatchQueue.main.async {
self.isConnected = true
self.currentMessage = "CONECTADO"
self.receive()
// Envia o comando inicial uma única vez
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.isConnected { self.sendHRS() }
}
}
case .failed(let error):
print("ESTADO: Falha crítica: \(error)")
self.disconnect()
case .cancelled:
print("ESTADO: Soquete cancelado e memória liberada.")
DispatchQueue.main.async { self.isConnected = false }
case .waiting(let error):
print("ESTADO: Aguardando rede (Pode ser erro de porta no ESP32): \(error)")
default:
break
}
}
connection?.start(queue: queue)
}
func disconnect() {
print("DISCONNECT CHAMADO - Limpando recursos...")
// Zera o contador e o estado ao desconectar
socketAttemptCount = 0
DispatchQueue.main.async {
self.isConnected = false
self.connection?.stateUpdateHandler = nil // Remove o handler antes de cancelar para evitar loops
self.connection?.cancel()
self.connection = nil
}
DispatchQueue.main.async {
self.currentMessage = "DESCONECTADO"
self.buffer.removeAll()
print("Contador de soquetes zerado.")
}
}
private func receive() {
// Verificação de segurança: soquete deve existir e o app deve querer estar conectado
guard let connection = connection, isConnected else { return }
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, error in
guard let self = self else { return }
if let data = data, !data.isEmpty {
self.buffer.append(data)
// Processa apenas mensagens completas (terminadas em CRLF)
while let range = self.buffer.range(of: Data([0x0d, 0x0a])) {
let lineData = self.buffer.subdata(in: 0..<range.lowerBound)
self.buffer.removeSubrange(0..<range.upperBound)
if let text = String(data: lineData, encoding: .ascii) {
DispatchQueue.main.async {
// Limpa espaços e exibe o valor da balança
self.currentMessage = text.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
}
}
// Se o ESP32 enviar um pacote de fechamento (FIN)
if isComplete {
print("AVISO: O servidor (ESP32) encerrou a conexão.")
self.disconnect()
return
}
if error != nil {
self.disconnect()
return
}
// Mantém a escuta ativa enquanto estiver conectado
if self.isConnected {
self.receive()
}
}
}
func send(_ command: String) {
guard isConnected, let connection = connection, connection.state == .ready else { return }
guard allowedCommands.contains(where: { command.starts(with: $0) }) else { return }
guard let data = (command + "\r\n").data(using: .ascii) else { return }
// Envio com proteção de processamento
connection.send(content: data, completion: .contentProcessed { [weak self] error in
if let error = error {
print("Erro ao enviar comando: \(error)")
self?.disconnect()
}
})
}
// Funções auxiliares mantidas conforme sua estrutura original
func buildHRSCommand() -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm dd/MM/yy"
return "HRS" + formatter.string(from: Date())
}
func sendHRS() { send(buildHRSCommand()) }
func sendBTz() { send("BTz") }
func sendBTd() { send("BTd") }
}