NTP延迟测量优化
Tạo vào: 18 tháng 1, 2025
Tạo vào: 18 tháng 1, 2025
class NtpClient(serverAddress: String, serverPort: Int) {
companion object {
private const val TAG = "NtpClient"
}
textprivate lateinit var clientSocket: DatagramSocket private val sendData = ByteArray(Long.SIZE_BYTES * 3) private val receiveData = ByteArray(Long.SIZE_BYTES * 4) private lateinit var sendPacket: DatagramPacket private lateinit var receivePacket: DatagramPacket private var delay: Long = 0 private var offset: Long = 0 private var currentRequestId = AtomicLong(0) private val oneWayDelays = mutableListOf<Long>() private val roundTripDelays = mutableListOf<Long>() // 新增字段存储最大、最小和平均值 private var maxOneWayDelay: Long = Long.MIN_VALUE private var minOneWayDelay: Long = Long.MAX_VALUE private var avgOneWayDelay: Double = 0.0 private var maxRoundTripDelay: Long = Long.MIN_VALUE private var minRoundTripDelay: Long = Long.MAX_VALUE private var avgRoundTripDelay: Double = 0.0 private var isFirstMeasurement = true init { try { clientSocket = DatagramSocket() sendPacket = DatagramPacket( sendData, sendData.size, InetAddress.getByName(serverAddress), serverPort ) receivePacket = DatagramPacket(receiveData, receiveData.size) } catch (e: Exception) { Log.e(TAG, "Error initializing NtpClient: ${e.message}") } } /** * 计算客户端与服务器之间的时钟差值(offset)。 * 通过多次测量(多次 singleMeasurement),得到平均的offset和RTT,以此来纠正本地时钟偏差。 * 返回值:Pair(delay, offset) * delay:这里delay代表通过多次测量得到的平均RTT值, * offset:通过NTP算法计算得到的时钟偏移量 */ @RequiresApi(Build.VERSION_CODES.O) suspend fun calculateTimeDifference(measurements: Int = 5): Pair<Long, Long> = withContext(Dispatchers.IO) { var totalOffset = 0L var totalDelay = 0L var successfulMeasurements = 0 for (i in 1..measurements) { val (rtt, singleOffset) = singleMeasurement() if (rtt != Long.MIN_VALUE && singleOffset != Long.MIN_VALUE) { totalDelay += rtt totalOffset += singleOffset successfulMeasurements++ } } if (successfulMeasurements > 0) { val avgDelay = totalDelay / successfulMeasurements val avgOffset = totalOffset / successfulMeasurements delay = avgDelay offset = avgOffset return@withContext Pair(delay, offset) } return@withContext Pair(Long.MIN_VALUE, Long.MIN_VALUE) } /** * 单次测量,用于计算offset和基本的RTT,但不重点计算和输出单向时延和详细时延信息。 * 这些详细的时延信息留在sendTime函数中进行计算和输出。 */ @RequiresApi(Build.VERSION_CODES.O) private suspend fun singleMeasurement(): Pair<Long, Long> = withContext(Dispatchers.IO) { try { val requestId = currentRequestId.getAndIncrement() val t1 = System.currentTimeMillis() // 发送请求 (t1) val sendData = ByteArray(Long.SIZE_BYTES * 3) ByteBuffer.wrap(sendData) .putLong(requestId) .putLong(t1) .putLong(0) val outPacket = DatagramPacket(sendData, sendData.size, sendPacket.address, sendPacket.port) clientSocket.send(outPacket) // 接收响应并获取时间戳 (t1, t2, t3) val inData = ByteArray(Long.SIZE_BYTES * 4) val inPacket = DatagramPacket(inData, inData.size) clientSocket.receive(inPacket) val t4 = System.currentTimeMillis() // 客户端接收时间 val buffer = ByteBuffer.wrap(inPacket.data) val receivedRequestId = buffer.getLong() val receivedT1 = buffer.getLong() val t2 = buffer.getLong() // 服务器接收时间 val t3 = buffer.getLong() // 服务器发送时间 if (receivedRequestId != requestId) { Log.e(TAG, "Mismatched request ID: expected $requestId, got $receivedRequestId") return@withContext Pair(Long.MIN_VALUE, Long.MIN_VALUE) } // 标准NTP公式计算RTT和offset val rtt = (t4 - t1) - (t3 - t2) val singleOffset = ((t2 - t1) + (t3 - t4)) / 2 // 这里只打印基本信息,不深入计算单��时延 Log.d( TAG, "singleMeasurement request=$receivedRequestId: rtt=$rtt ms, offset=$singleOffset ms" ) return@withContext Pair(rtt, singleOffset) } catch (e: Exception) { Log.e(TAG, "Error in measurement: ${e.message}") return@withContext Pair(Long.MIN_VALUE, Long.MIN_VALUE) } } /** * 在获得较稳定的offset(通过calculateTimeDifference和singleMeasurement的多次测量之后), * 使用sendTime来计算和输出实际的往返时延和单向时延。 */ @RequiresApi(Build.VERSION_CODES.O) suspend fun sendTime(): Long = withContext(Dispatchers.IO) { try { val requestId = currentRequestId.getAndIncrement() // 使用纳秒级时间戳以提高精度 val t1 = System.nanoTime() / 1_000_000 // 转换为毫秒 // 发送请求 val sendData = ByteArray(Long.SIZE_BYTES * 3) ByteBuffer.wrap(sendData) .putLong(requestId) .putLong(t1) .putLong(0) val outPacket = DatagramPacket(sendData, sendData.size, sendPacket.address, sendPacket.port) clientSocket.send(outPacket) // 接收响应 val inData = ByteArray(Long.SIZE_BYTES * 4) val inPacket = DatagramPacket(inData, inData.size) clientSocket.receive(inPacket) val t4 = System.nanoTime() / 1_000_000 // 转换为毫秒 val buffer = ByteBuffer.wrap(inPacket.data) val receivedRequestId = buffer.getLong() val receivedT1 = buffer.getLong() val t2 = buffer.getLong() val t3 = buffer.getLong() // 添加时钟偏移校正 val correctedT2 = t2 + offset val correctedT3 = t3 + offset // 使用校正后的时间戳计算延迟 val rtt = (t4 - t1) - (correctedT3 - correctedT2) val singleOffset = ((correctedT2 - t1) + (correctedT3 - t4)) / 2 if (receivedRequestId != requestId) { Log.e(TAG, "Mismatched request ID: expected $requestId, got $receivedRequestId") return@withContext Long.MIN_VALUE } // 计算更详细的时延信息 val upstreamDelay = t2 - t1 // 上行时延 val downstreamDelay = t4 - t3 // 下行时延 val serverProcessingTime = t3 - t2 val networkTime = rtt - serverProcessingTime val estimatedOneWayDelay = networkTime / 2 // 打印详细的延时数据 Log.d( TAG, """ Request $receivedRequestId timing details: t1 (client send): $t1 t2 (server receive): $t2 t3 (server send): $t3 t4 (client receive): $t4 Raw delays: Upstream delay (t2-t1): $upstreamDelay ms Downstream delay (t4-t3): $downstreamDelay ms Server processing time: $serverProcessingTime ms Calculated metrics: RTT: $rtt ms Network time (RTT - processing): $networkTime ms Estimated one-way delay: $estimatedOneWayDelay ms Time offset: $singleOffset ms """.trimIndent() ) // 更新全局offset和delay(这里delay用RTT表示) offset = singleOffset delay = rtt oneWayDelays.add(estimatedOneWayDelay) roundTripDelays.add(rtt) // 更新最大、最小和平均值 updateDelayStats(estimatedOneWayDelay, rtt) return@withContext rtt } catch (e: Exception) { Log.e(TAG, "Error sending time: ${e.message}") Long.MIN_VALUE } } private fun updateDelayStats(oneWayDelay: Long, roundTripDelay: Long) { if (isFirstMeasurement) { // 如果是第一次测量,直接设置最大、最小、平均值 maxOneWayDelay = oneWayDelay minOneWayDelay = oneWayDelay avgOneWayDelay = oneWayDelay.toDouble() maxRoundTripDelay = roundTripDelay minRoundTripDelay = roundTripDelay avgRoundTripDelay = roundTripDelay.toDouble() isFirstMeasurement = false } else { if (oneWayDelay > maxOneWayDelay) maxOneWayDelay = oneWayDelay if (oneWayDelay < minOneWayDelay) minOneWayDelay = oneWayDelay avgOneWayDelay = oneWayDelays.average() if (roundTripDelay > maxRoundTripDelay) maxRoundTripDelay = roundTripDelay if (roundTripDelay < minRoundTripDelay) minRoundTripDelay = roundTripDelay avgRoundTripDelay = roundTripDelays.average() } } fun close() { if (::clientSocket.isInitialized && !clientSocket.isClosed) { clientSocket.close() } delay = 0L offset = 0L } fun isReadyToSendTime(): Boolean { return delay != 0L && offset != 0L } fun getDelayStatistics(): Map<String, Double> { return mapOf( "oneWayMaxDelay" to maxOneWayDelay.toDouble(), "oneWayMinDelay" to minOneWayDelay.toDouble(), "oneWayAvgDelay" to avgOneWayDelay, "roundTripMaxDelay" to maxRoundTripDelay.toDouble(), "roundTripMinDelay" to minRoundTripDelay.toDouble(), "roundTripAvgDelay" to avgRoundTripDelay ) }
}
class NtpServer(private val port: Int) {
companion object {
private const val TAG = "NtpServer"
}
textprivate lateinit var serverSocket: DatagramSocket private var serverJob: Job? = null private var timeDifference: Long = 0 private val clientTimestamps: MutableList<Long> = mutableListOf() private val requestTimestamps = mutableMapOf<String, Long>() @RequiresApi(Build.VERSION_CODES.O) fun start() { serverJob = CoroutineScope(Dispatchers.IO).launch { try { serverSocket = DatagramSocket(port) Log.d(TAG, "NTP Server started on port $port") while (isActive) { val receiveData = ByteArray(Long.SIZE_BYTES * 3) val receivePacket = DatagramPacket(receiveData, receiveData.size) serverSocket.receive(receivePacket) val buffer = ByteBuffer.wrap(receivePacket.data) val requestId = buffer.getLong() val t1 = buffer.getLong() buffer.getLong() val t2 = System.nanoTime() / 1_000_000 // 转换为毫秒 val sendData = ByteArray(Long.SIZE_BYTES * 4) val sendBuffer = ByteBuffer.wrap(sendData) sendBuffer.putLong(requestId) sendBuffer.putLong(t1) sendBuffer.putLong(t2) val t3 = System.nanoTime() / 1_000_000 // 转换为毫秒 sendBuffer.putLong(t3) val sendPacket = DatagramPacket(sendData, sendData.size, receivePacket.address, receivePacket.port) serverSocket.send(sendPacket) Log.d(TAG, "Processed request $requestId from ${receivePacket.address.hostAddress}:${receivePacket.port}") } } catch (e: SocketException) { Log.d(TAG, "Socket closed, stopping server") } catch (e: Exception) { Log.e(TAG, "Error in server: ${e.message}") } finally { if (::serverSocket.isInitialized) { serverSocket.close() } } } } fun stop() { serverJob?.cancel() serverJob?.invokeOnCompletion { if (::serverSocket.isInitialized) { serverSocket.close() } } serverJob = null } fun getClientTimes(): List<Long> { return clientTimestamps.toList() } fun clearClientTimes() { clientTimestamps.clear() } private fun Long.toByteArray(): ByteArray { val buffer = ByteArray(8) for (i in 0..7) { buffer[i] = (this shr (8 * (7 - i))).toByte() } return buffer }
}
class S7ClientFragment : Fragment() {
textprivate lateinit var binding: FragmentS7ClientBinding private var isConnected: Boolean = false private lateinit var host: String private val clientMessages = mutableListOf<String>() private val metrics = CommunicationMetrics() private val s7Client = S7Client(metrics) private lateinit var commandItems: MutableList<S7ClientCommandItem> private var commandAdapter: S7CommandAdapter? = null private var ntpClient: NtpClient? = null data class AddressInfo( val dbNumber: Int?, val startAddress: Int, val dataType: String, val quantity: Int = 1, val bitOffset: Int = 0 ) private var isLoopTesting: Boolean = false private lateinit var sharedViewModel: SharedViewModel override fun onAttach(context: Context) { super.onAttach(context) sharedViewModel = ViewModelProvider(requireActivity())[SharedViewModel::class.java] } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentS7ClientBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedViewModel.currentIpAddress.observe(viewLifecycleOwner) { ipAddress -> if (ipAddress != null) { binding.tvLocalIpAddress.text = "本机IP地址: $ipAddress" } else { binding.tvLocalIpAddress.text = "本机IP地址: 无法获取" } } binding.btnConnectServerS7.setOnClickListener { connectToServer() } binding.btnDisconnectServerS7.setOnClickListener { disconnectFromServer() } binding.tvClientMessagesS7.movementMethod = ScrollingMovementMethod() commandItems = mutableListOf( S7ClientCommandItem( functionCode = "01", dataPoint = "DB1.DBW0" ) ) commandAdapter = S7CommandAdapter(commandItems, onDeleteClick = { position -> handleDeleteCommandClick(position) }, onAddClick = { addNewCommandItem() }, onPointInfoClick = { position -> showPointInfoDialog(position - 1, commandItems[position - 1]) }) parentFragmentManager.setFragmentResultListener( "PointInfoDialogFragment", viewLifecycleOwner ) { requestKey, bundle -> val position = bundle.getInt("position") val dataItemsList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { bundle.getParcelableArrayList("dataItems", S7ClientDataItem::class.java) ?.toMutableList() } else { @Suppress("DEPRECATION") bundle.getParcelableArrayList<S7ClientDataItem>("dataItems") ?.toMutableList() } if (position in 0 until commandItems.size && dataItemsList != null) { commandItems[position].dataItems = dataItemsList } commandAdapter?.notifyItemChanged(position + 1) } binding.commandRecyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.commandRecyclerView.adapter = commandAdapter binding.btnShowMetrics.setOnClickListener { ntpClient?.let { client -> metrics.delayStatistics = client.getDelayStatistics() } val fragment = DataDisplayFragment.newInstance(ProtocolType.S7, true, metrics) fragment.show(parentFragmentManager, "DataDisplayFragment") } binding.btnStartLoopTest.setOnClickListener { if (isConnected) { startLoopTest() } else { showToast("请先连接到服务器") } } binding.btnStopLoopTest.setOnClickListener { if (isConnected) { stopLoopTest() } else { showToast("未连接到服务器") } } binding.btnClearMessages.setOnClickListener { clientMessages.clear() binding.tvClientMessagesS7.text = "客户端消息" } } private fun showPointInfoDialog(position: Int, item: S7ClientCommandItem) { val dialog = S7PointInfoDialogFragment.newInstance(position, item.dataPoint, item.dataItems) dialog.show(parentFragmentManager, "PointInfoDialogFragment") } private fun addNewCommandItem() { val newItem = S7ClientCommandItem( functionCode = "01", dataPoint = "DB1.DBW0" ) commandItems.add(newItem) commandAdapter?.notifyItemInserted(commandItems.size) } private fun handleDeleteCommandClick(position: Int) { val adjustedPosition = position - 1 if (adjustedPosition >= 0 && adjustedPosition < commandItems.size) { commandItems.removeAt(adjustedPosition) commandAdapter?.notifyItemRemoved(position) } } private fun startLoopTest() { if (isLoopTesting) { showToast("循环测试已在运行") return } val loopCount = binding.etLoopCount.text.toString().toIntOrNull() ?: 0 val interval = binding.etInterval.text.toString().toIntOrNull() ?: 0 if (loopCount <= 0) { showToast("循环次数必须大于 0") return } if (interval < 10 || interval > 5000) { showToast("间隔时间必须在 10 到 5000 毫秒之间") return } isLoopTesting = true updateLoopTestStatus(-1, 0, "启动中...") Log.d( "S7ClientFragment", "Starting loop test with $loopCount loops and $interval ms interval" ) lifecycleScope.launch(Dispatchers.IO) { for (i in 0 until loopCount) { if (!isLoopTesting) { Log.d("S7ClientFragment", "Loop test stopped by user") break } Log.d("S7ClientFragment", "Loop iteration: ${i + 1}") // --- 执行您的 S7 读/写操作 --- // 循环读取所有命令并执行读/写操作 for (commandItem in commandItems) { if (!isLoopTesting) { Log.d("S7ClientFragment", "Loop test stopped by user") break } for (dataItem in commandItem.dataItems) { if (!isLoopTesting) { Log.d("S7ClientFragment", "Loop test stopped by user") break } Log.d( "S7ClientFragment", "Processing commandItem functionCode: ${commandItem.functionCode}" ) // 根据操作类型执行读或写操作 if (commandItem.functionCode == "01") { Log.d( "S7ClientFragment", "Performing read operation on ${dataItem.address}" ) performReadOperation(dataItem)
// val addressInfo = parseAddressFromInput(dataItem)
// s7Client.testReadData(addressInfo?.dbNumber!!, addressInfo.startAddress, addressInfo.bitOffset)
} else if (commandItem.functionCode == "04") {
Log.d(
"S7ClientFragment",
"Performing write operation on ${dataItem.address} with values:"
)
performWriteOperation(dataItem)
// val addressInfo = parseAddressFromInput(dataItem)
// s7Client.testWriteData(addressInfo?.dbNumber!!, addressInfo.startAddress, addressInfo.bitOffset, true)
}
text} } // S7 读/写操作结束 updateLoopTestStatus(i, loopCount, "运行中...") Log.d( "S7ClientFragment", "Loop iteration ${i + 1} completed, waiting for $interval ms" ) Thread.sleep(interval.toLong()) } isLoopTesting = false updateLoopTestStatus(-1, 0, "已完成") Log.d("S7ClientFragment", "Loop test finished") } } private suspend fun performReadOperation(dataItem: S7ClientDataItem) { val dataType = dataItem.type val quantity = dataItem.quantity try { // 使用 parseAddressFromInput 解析地址 val addressInfo = parseAddressFromInput(dataItem) if (addressInfo != null) { val dbNumber = addressInfo.dbNumber ?: 0 var startAddress = addressInfo.startAddress val bitAddress = addressInfo.bitOffset // 循环读取每个数据 for (i in 0 until quantity) { Log.d( "S7ClientFragment", "Reading data from DB$dbNumber.${addressInfo.dataType}$startAddress, type: $dataType, quantity: 1" ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ntpClient?.isReadyToSendTime() == true) { ntpClient?.sendTime() } val data = if (dataType.equals("BOOL", ignoreCase = true)) { s7Client.readData( dbNumber, startAddress, dataType, bitAddress, quantity ) } else { s7Client.readData(dbNumber, startAddress, dataType, 0, quantity) } if (data != null) { val value = parseData(data, dataType) Log.d( "S7ClientFragment", "Read data successfully: ${dataItem.address} - $value" ) activity?.runOnUiThread { appendClientMessage("读取数据成功: ${dataItem.address} - $value") } } else { Log.w("S7ClientFragment", "Failed to read data from ${dataItem.address}") activity?.runOnUiThread { appendClientMessage("读取数据失败: ${dataItem.address}") } } startAddress += if (dataType.equals("BOOL", ignoreCase = true)) { bitAddress // BOOL 类型递增位地址 } else { getByteLengthFromDataType(dataType, 1) // 其他类型递增字节地址 } } } else { Log.e("S7ClientFragment", "Invalid address format: ${dataItem.address}") activity?.runOnUiThread { appendClientMessage("地址格式错误: ${dataItem.address}") } } } catch (e: Exception) { Log.e( "S7ClientFragment", "Error reading data from ${dataItem.address}: ${e.message}" ) activity?.runOnUiThread { appendClientMessage("读取数据时发生异常: ${e.message}") } } } private suspend fun performWriteOperation(dataItem: S7ClientDataItem) { val address = dataItem.address val dataType = dataItem.type val quantity = dataItem.quantity val values = dataItem.values // 检查数据数量和值数量是否一致 if (quantity != values.size) { Log.e("S7ClientFragment", "数据数量和值数量不一致") activity?.runOnUiThread { showToast("数据数量和值数量不一致") } return } try { // 使用 parseAddressFromInput 解析地址 val addressInfo = parseAddressFromInput(dataItem) if (addressInfo != null) { val dbNumber = addressInfo.dbNumber ?: 0 var startAddress = addressInfo.startAddress val bitAddress = addressInfo.bitOffset // 循环写入每个数据值 for (i in 0 until quantity) { val currentValue = values[i] if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ntpClient?.isReadyToSendTime() == true) { ntpClient?.sendTime() } Log.d( "S7ClientFragment", "Writing data to DB$dbNumber.${addressInfo.dataType}$startAddress, type: $dataType, quantity: 1, value: $currentValue" ) // 根据数据类型将 currentValue 转换为 ByteArray val byteArrayValue = when (dataType.uppercase()) { "BOOL" -> { // 布尔类型转换为单个字节,1 表示 true,0 表示 false byteArrayOf(when (currentValue) { "1" -> 1 "0" -> 0 else -> throw IllegalArgumentException("Invalid currentValue: $currentValue") }) } "BYTE" -> { // 字节类型直接转换为 ByteArray byteArrayOf((currentValue.toInt() and 0xFF).toByte()) } "WORD" -> { ByteBuffer.allocate(2).putShort(currentValue.toInt().toShort()).array() } "DWORD" -> { val dwordValue = currentValue.toLong() // 确保使用Long来处理大于 2^31 - 1 的值 ByteBuffer.allocate(4) .put((dwordValue ushr 24).toByte()) // 拆分出高 8 位 .put((dwordValue ushr 16).toByte()) // 拆分出第二高的 8 位 .put((dwordValue ushr 8).toByte()) // 拆分出第三高的 8 位 .put((dwordValue and 0xFF).toByte()) // 拆分出最低的 8 位 .array() } "INT" -> { // 整数类型转换为 2 个字节的 ByteArray ByteBuffer.allocate(2).putShort(currentValue.toShort()).array() } "DINT" -> { // 双整数类型转换为 4 个字节的 ByteArray ByteBuffer.allocate(4).putInt(currentValue.toInt()).array() } "REAL" -> { // 浮点数类型转换为 4 个字节的 ByteArray ByteBuffer.allocate(4).putFloat(currentValue.toFloat()).array() } else -> { // 不支持的数据类型,返回空 ByteArray byteArrayOf() } } fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) } Log.d("S7ClientFragment", "byteArrayValue: ${byteArrayValue.toHexString()}") // 调用 writeData 函数,传入 byteArrayValue val success = s7Client.writeData( dbNumber, startAddress, dataType, bitAddress, quantity, byteArrayValue ) activity?.runOnUiThread { if (success) { Log.d( "S7ClientFragment", "Write data successfully: ${dataItem.address} - $currentValue" ) appendClientMessage("写入数据成功: ${dataItem.address} - $currentValue") } else { Log.w( "S7ClientFragment", "Failed to write data to ${dataItem.address}" ) appendClientMessage("写入数据失败: ${dataItem.address} - $currentValue") } } // 更新 startAddress,指向下一个数据地址 startAddress += if (dataType.equals("BOOL", ignoreCase = true)) { bitAddress // BOOL 类型递增位地址 } else { getByteLengthFromDataType(dataType, 1) // 其他类型递增字节地址 } } } else { Log.e("S7ClientFragment", "Invalid address format: $address") activity?.runOnUiThread { appendClientMessage("地址格式错误: $address") } } } catch (e: Exception) { Log.e( "S7ClientFragment", "Error writing data to ${dataItem.address}: ${e.message}" ) activity?.runOnUiThread { appendClientMessage("写入数据时发生异常: ${e.message}") } } } private fun stopLoopTest() { isLoopTesting = false binding.tvLoopTestStatus.text = "状态: 停止" } private fun updateLoopTestStatus(currentLoop: Int, totalLoops: Int, status: String) { activity?.runOnUiThread { binding.tvLoopTestStatus.text = if (currentLoop >= 0) { "状态: 运行中 (${currentLoop + 1}/$totalLoops) - $status" } else { "状态: $status" } } } private fun parseData(data: ByteArray, dataType: String): String { return when (dataType) { "BOOL" -> ((data[0].toInt() shr 0) and 1).toString() "BYTE" -> (data[0].toInt() and 0xFF).toString() "WORD" -> ByteBuffer.wrap(data).short.toInt().and(0xFFFF).toString() "DWORD" -> ByteBuffer.wrap(data).int.toLong().and(0xFFFFFFFF).toString() "INT" -> ByteBuffer.wrap(data).short.toString() "DINT" -> ByteBuffer.wrap(data).int.toString() "REAL" -> Float.fromBits(ByteBuffer.wrap(data).int).toString() // 解析 REAL 类型数据 "STRING" -> { val nullTerminatorIndex = data.indexOf(0) val endIndex = if (nullTerminatorIndex != -1) nullTerminatorIndex else data.size String(data, 0, endIndex, Charsets.UTF_8) } else -> throw IllegalArgumentException("Unsupported data type: $dataType") } } private fun connectToServer() { val ip = binding.etClientIpS7.text.toString() val portString = binding.etClientPortS7.text.toString() val rackString = binding.etClientRackS7.text.toString() val slotString = binding.etClientSlotS7.text.toString() if (ip.isEmpty()) { showToast("请输入有效的IP地址") return } if (portString.isEmpty()) { showToast("请输入端口号") return } val port = portString.toIntOrNull() if (port == null) { showToast("端口号必须是数字") return } val rack = rackString.toIntOrNull() ?: 0 val slot = slotString.toIntOrNull() ?: 0 lifecycleScope.launch(Dispatchers.IO) { try { ntpClient = NtpClient(ip, port) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ntpClient?.calculateTimeDifference() } } catch (e: Exception) { ntpClient = null Log.e("ModbusClientFragment", "Error calculating time difference: ${e.message}") } } lifecycleScope.launch { // 连接到 S7 服务器 host = ip isConnected = s7Client.connectToServer(ip, rack, slot, port) if (isConnected) { updateConnectionStatus(true) appendClientMessage(formatS7Message("01")) showToast("连接成功") } else { updateConnectionStatus(false) appendClientMessage("无法连接到服务器: $ip:$port") showToast("连接失败") } } } private fun disconnectFromServer() { ntpClient?.close() if (isConnected) { s7Client.disconnectFromServer() updateConnectionStatus(false) appendClientMessage(formatS7Message("02")) isConnected = false showToast("断开连接成功") } else { showToast("已经断开连接") } } private fun updateConnectionStatus(isConnected: Boolean) { binding.tvConnectionStatus.text = "连接状态: ${if (isConnected) "已连接" else "未连接"}" } private fun updateClientMessages() { binding.tvClientMessagesS7.text = clientMessages.joinToString(separator = "\n") } private fun showToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } private fun appendClientMessage(message: String) { val currentDateTime = Utils.getCurrentDateTime() clientMessages.add("$currentDateTime: $message") updateClientMessages() } private fun formatS7Message( command: String, address: Int? = null, data: String? = null ): String { val header = "03 00" val protocolId = "02 F0 80" val messageType = when (command) { "01" -> "32 01 00 00 00 00 01" // Connect "02" -> "32 01 00 00 00 00 02" // Disconnect "03" -> "32 01 00 00 00 00 03" // Read "04" -> "32 01 00 00 00 00 04" // Write else -> "32 01 00 00 00 00 00" } val paramFieldLength = "00 04" val dataFieldLength = data?.let { "%04X".format(it.length) } ?: "00 00" val addressHex = address?.let { "%04X".format(it) } ?: "00 00" val dataPart = data?.let { it.toByteArray(Charsets.UTF_8).joinToString(" ") { "%02X".format(it) } } ?: "" val messageFields = "$header ${ calculateMessageLength( header, protocolId, messageType, paramFieldLength, dataFieldLength, addressHex, dataPart ) } $protocolId $messageType $paramFieldLength $dataFieldLength $addressHex $dataPart" return "[S7] Msg:$messageFields" } private fun calculateMessageLength(vararg fields: String): String { val totalLength = fields.sumOf { it.replace(" ", "").length / 2 } return "%04X".format(totalLength) } private fun parseAddressFromInput(dataItem: S7ClientDataItem): AddressInfo? { val address = dataItem.address val dataType = dataItem.type val dbRegex = """DB(\d+)\.(\d+)(?:\.(\d+))?(?:\[(\d+)])?""".toRegex() return when { dbRegex.matches(address) -> { val match = dbRegex.find(address) val dbNumber = match?.groupValues?.get(1)?.toIntOrNull() val startAddress = match?.groupValues?.get(2)?.toIntOrNull() ?: 0 val bitAddress = match?.groupValues?.get(3)?.toIntOrNull() ?: 0 if (dbNumber != null) { AddressInfo(dbNumber, startAddress, dataType, dataItem.quantity, bitAddress ) } else { activity?.runOnUiThread { showToast("地址格式错误: $address") } null } } else -> { activity?.runOnUiThread { showToast("地址格式错误: $address") } null } } } private fun getByteLengthFromDataType(dataType: String, length: Int): Int { return when (dataType.uppercase()) { "BOOL" -> (length + 7) / 8 // 向上取整到最接近的字节 "BYTE", "CHAR" -> length "WORD", "INT" -> length * 2 "DWORD", "DINT", "REAL" -> length * 4 "STRING" -> length * 2 + 2 // 添加两个额外的字节 else -> throw IllegalArgumentException("Unsupported data type: $dataType") } }
}
class S7ServerFragment : Fragment() {
private lateinit var binding: FragmentS7ServerBinding
textprivate var isServerRunning = false private val serverMessages = mutableListOf<String>() private val dataItems = mutableListOf<S7DataBlockItem>() private var adapter: S7DataBlockListAdapter? = null private val metrics = CommunicationMetrics() private val s7Server = S7Server(metrics) private lateinit var sharedViewModel: SharedViewModel private var ntpServer: NtpServer? = null override fun onAttach(context: Context) { super.onAttach(context) sharedViewModel = ViewModelProvider(requireActivity())[SharedViewModel::class.java] } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentS7ServerBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.btnStartServerS7.setOnClickListener { if (!isServerRunning) { val port = binding.etServerPortS7.text.toString().toIntOrNull() ?: 1025 val rack = binding.etServerRackS7.text.toString().toIntOrNull() ?: 0 val slot = binding.etServerSlotS7.text.toString().toIntOrNull() ?: 0 lifecycleScope.launch(Dispatchers.IO) { ntpServer = NtpServer(port) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ntpServer?.start() } } startServer(port, rack, slot) } else { showToast("服务器已在运行") } } binding.btnStopServerS7.setOnClickListener { if (isServerRunning) { stopServer() ntpServer?.stop() } else { showToast("服务器未运行") } } binding.btnShowMetrics.setOnClickListener { val clientTimes = ntpServer?.getClientTimes() s7Server.getLatestMetrics(metrics, clientTimes) ntpServer?.clearClientTimes() val fragment = DataDisplayFragment.newInstance(ProtocolType.S7, false, metrics) fragment.show(parentFragmentManager, "DataDisplayFragment") } sharedViewModel.currentIpAddress.observe(viewLifecycleOwner) { ipAddress -> if (ipAddress != null) { binding.tvLocalIpAddress.text = "本机IP地址: $ipAddress" } else { binding.tvLocalIpAddress.text = "本机IP地址: 无法获取" } } binding.btnAddDataBlock.setOnClickListener { if (isServerRunning) { addDataBlock() } else { showToast("服务器未运行,无法添加数据块") } }
// binding.enableTimestampSwitch.setOnCheckedChangeListener { _, isChecked ->
// s7Server.setEnableTimestamp(isChecked)
// val currentDateTime = Utils.getCurrentDateTime()
// serverMessages.add("{if (isChecked) "启用" else "禁用"}")
// updateServerMessages()
// }
textadapter = S7DataBlockListAdapter( dataItems, onDeleteClick = { position -> if (isServerRunning) { // 检查服务器运行状态 handleDeleteClick(position) } else { showToast("服务器未运行,无法删除数据块") } }) binding.dataBlockRecyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.dataBlockRecyclerView.adapter = adapter binding.btnClearMessages.setOnClickListener { serverMessages.clear() binding.tvServerMessagesS7.text = "服务端消息" } } private fun showToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } private fun handleDeleteClick(position: Int) { val adjustedPosition = position - 1 val itemCount = adapter?.itemCount Log.d("S7ServerFragment", "itemCount: $itemCount") if (adjustedPosition >= 0 && adjustedPosition < dataItems.size) { val dataBlockItem = dataItems[adjustedPosition] val blockNumber = dataBlockItem.blockNumber val success = s7Server.unregisterDataBlock(blockNumber) val currentDateTime = Utils.getCurrentDateTime() if (success) { dataItems.removeAt(adjustedPosition) adapter?.notifyItemRemoved(position) Toast.makeText(context, "数据块 DB$blockNumber 反注册成功", Toast.LENGTH_SHORT) .show() serverMessages.add("$currentDateTime: 数据块 DB$blockNumber 反注册成功") } else { Toast.makeText(context, "数据块 DB$blockNumber 反注册失败", Toast.LENGTH_SHORT) .show() serverMessages.add("$currentDateTime: 数据块 DB$blockNumber 反注册失败") } updateServerMessages() } else { Log.e("S7ServerFragment", "无效的 position: $position") } } private fun addDataBlock() { val blockNumber = binding.etDataBlockNumber.text.toString().toIntOrNull() val startAddress = binding.etDataBlockStartAddress.text.toString().toIntOrNull() val length = binding.etDataBlockLength.text.toString().toIntOrNull() val dataType = binding.spinnerDataType.selectedItem.toString() if (blockNumber != null && startAddress != null && length != null) { val byteLength = getByteLengthFromDataType(dataType, length) val result = s7Server.registerDataBlock(blockNumber, startAddress, byteLength) val currentDateTime = Utils.getCurrentDateTime() if (result) { val newDataBlock = S7DataBlockItem(blockNumber, startAddress, length, dataType, "") dataItems.add(newDataBlock) adapter?.notifyItemInserted(dataItems.size) serverMessages.add("$currentDateTime: 数据块注册成功 (DB$blockNumber, 起始地址: $startAddress, 长度: $length, 数据类型: $dataType)") Toast.makeText(context, "数据块注册成功", Toast.LENGTH_SHORT).show() } else { serverMessages.add("$currentDateTime: 数据块注册失败 (DB$blockNumber, 起始地址: $startAddress, 长度: $length, 数据类型: $dataType)") Toast.makeText(context, "数据块注册失败", Toast.LENGTH_SHORT).show() } updateServerMessages() } else { Toast.makeText(context, "请确保所有字段都已填写", Toast.LENGTH_SHORT).show() } } private fun getByteLengthFromDataType(dataType: String, length: Int): Int { return when (dataType.uppercase()) { "BOOL" -> (length + 7) / 8 // 向上取整到最接近的字节 "BYTE", "CHAR" -> length "WORD", "INT" -> length * 2 "DWORD", "DINT", "REAL" -> length * 4 "STRING" -> length * 2 + 2 // 添加两个额外的字节 else -> throw IllegalArgumentException("Unsupported data type: $dataType") } } private fun startServer(port: Int, rack: Int, slot: Int) { if (s7Server.startServer(port, rack, slot)) { isServerRunning = true updateConnectionStatus(true) Toast.makeText(requireContext(), "服务器已在端口$port 上启动", Toast.LENGTH_SHORT) .show() val currentDateTime = Utils.getCurrentDateTime() serverMessages.add("$currentDateTime: 服务器已在端口$port 上启动") updateServerMessages() } else { Toast.makeText(requireContext(), "无法在端口$port 上启动服务器", Toast.LENGTH_SHORT) .show() } } private fun stopServer() { if (isServerRunning) { s7Server.stopServer() isServerRunning = false updateConnectionStatus(false) Toast.makeText(context, "服务器已停止", Toast.LENGTH_SHORT).show() val currentDateTime = Utils.getCurrentDateTime() serverMessages.add("$currentDateTime: 服务器已停止") updateServerMessages() } else { Toast.makeText(context, "服务器未运行", Toast.LENGTH_SHORT).show() } } private fun updateServerMessages() { binding.tvServerMessagesS7.text = serverMessages.joinToString(separator = "\n") } private fun updateConnectionStatus(isRunning: Boolean) { binding.tvConnectionStatus.text = "服务器状态: ${if (isRunning) "运行中" else "未运行"}" } override fun onDestroyView() { super.onDestroyView() ntpServer?.stop() if (isServerRunning) { try { s7Server.stopServer() } catch (e: Exception) { Log.e("S7ServerFragment", "Error stopping server", e) } } }
}
#include <jni.h>
#include "snap7/examples/cpp/snap7.h"
#include <cstdio>
#include <android/log.h>
#include <string>
#include <vector>
#include <deque>
#include <sys/time.h>
#include <mutex>
#include <algorithm>
#include <utility>
#include <limits>
#include <chrono>
#include <mutex>
#include <unordered_map>
#include <deque>
#include <random>
#include <cctype>
#include <iostream>
#include <iomanip>
static TS7Client *Client = nullptr;
#define LOG_TAG "s7-client"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, VA_ARGS)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, VA_ARGS)
// 使用 ConcurrentHashMap 保证线程安全
std::unordered_map<std::string, std::pair<std::atomic<int>, std::atomic<int>>> testItemResults;
// 获取当前时间戳的函数 (单位: 毫秒)
long long getCurrentTimestamp() {
struct timeval tv{};
gettimeofday(&tv, nullptr);
return (long long) tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
// 用于存储指标数据的结构体
struct Metrics {
int sentCommands;
int receivedCommands;
int sentPackets;
int receivedPackets;
int sentBytes;
int receivedBytes;
double sentRate;
double receivedRate;
int connectionAttemptCount;
int connectionSuccessCount;
long long connectionTotalDelay;
int commandAttemptCount;
int commandSuccessCount;
int commandTotalRetryCount;
long long lastTimestamp; // 上一次接收字节的时间戳
int lastIndex; // 上一次接收字节的时间戳
};
// 全局变量,用于存储最新的指标数据
Metrics metrics = {0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0};
// 存储每秒发送的数据包个数
std::vector<int> sentPacketsPerSecond(300, 0);
std::mutex sentPacketsPerSecondMutex;
// 存储每秒接收的数据包个数
std::vector<int> receivedPacketsPerSecond(300, 0);
std::mutex receivedPacketsPerSecondMutex;
void updateTestItemResult(const std::string &testItemName, bool isSuccess) {
auto it = testItemResults.find(testItemName);
if (it == testItemResults.end()) {
// 如果测试项不存在,则创建新的条目
testItemResults[testItemName] = std::make_pair(0, 0); // 初始化为0
it = testItemResults.find(testItemName);
}
textif (isSuccess) { it->second.first++; // 成功次数加 1 } else { it->second.second++; // 失败次数加 1 }
}
void printDataToSend(byte *data, int size, const char *dataTypeStr) {
if (strcmp(dataTypeStr, "BOOL") == 0) {
LOGD("Data: %d", *data);
} else if (strcmp(dataTypeStr, "BYTE") == 0) {
for (int i = 0; i < size; i++) {
LOGD("Byte %d: 0x%02x", i, data[i]);
}
} else if (strcmp(dataTypeStr, "WORD") == 0) {
for (int i = 0; i < size / 2; i++) {
LOGD("Word %d: 0x%04x", i, ((uint16_t *) data)[i]);
}
} else if (strcmp(dataTypeStr, "DWORD") == 0 || strcmp(dataTypeStr, "DINT") == 0) {
for (int i = 0; i < size / 4; i++) {
LOGD("DWORD %d: 0x%08x", i, ((uint32_t *) data)[i]);
}
} else if (strcmp(dataTypeStr, "INT") == 0) {
for (int i = 0; i < size / 2; i++) {
LOGD("INT %d: %d", i, ((int16_t *) data)[i]);
}
} else if (strcmp(dataTypeStr, "REAL") == 0) {
for (int i = 0; i < size / 4; i++) {
LOGD("REAL %d: %f", i, ((float *) data)[i]);
}
} else if (strcmp(dataTypeStr, "STRING") == 0) {
LOGD("String: %s", data + 2); // 跳过前两个字节的长度信息
}
}
// 获取当前秒数在列表中的索引
int getCurrentSecondIndex() {
long long currentSecond = getCurrentTimestamp() / 1000;
return static_cast<int>(currentSecond % 300);
}
void printByteArray(const void *data, int size) {
const unsigned char *bytes = reinterpret_cast<const unsigned char *>(data);
for (int i = 0; i < size; ++i) {
LOGD("Byte %d: 0x%02x", i, bytes[i]);
}
}
void updateMetrics(JNIEnv *env, jobject thiz, int sentBytes, int receivedBytes,
long long roundTripDelay) {
// 在函数开头创建局部引用框架
env->PushLocalFrame(10);
text// 更新指标数据 metrics.sentCommands++; metrics.receivedCommands++; metrics.sentPackets++; metrics.receivedPackets++; metrics.sentBytes += sentBytes; metrics.receivedBytes += receivedBytes; LOGD("updateMetrics - metrics.sentCommands: %d", metrics.sentCommands); LOGD("updateMetrics - metrics.receivedCommands: %d", metrics.receivedCommands); LOGD("updateMetrics - metrics.sentPackets: %d", metrics.sentPackets); LOGD("updateMetrics - metrics.receivedPackets: %d", metrics.receivedPackets); LOGD("updateMetrics - metrics.sentBytes: %d", metrics.sentBytes); LOGD("updateMetrics - metrics.receivedBytes: %d", metrics.receivedBytes); LOGD("updateMetrics - metrics.sentRate: %f", metrics.sentRate); LOGD("updateMetrics - metrics.receivedRate: %f", metrics.receivedRate); LOGD("updateMetrics - roundTripDelay: %lld", roundTripDelay); // 更新 CommunicationMetrics 对象 jclass s7ClientClass = env->GetObjectClass(thiz); jfieldID metricsFieldId = env->GetFieldID( s7ClientClass, "metrics", "Lcom/dotouch/industrial/bean/CommunicationMetrics;"); jobject metricsObject = env->GetObjectField(thiz, metricsFieldId); // 获取 CommunicationMetrics 类 jclass metricsClass = env->GetObjectClass(metricsObject); // 获取 AtomicInteger 和 AtomicLong 的 set 方法 ID jclass atomicIntegerClass = env->FindClass( "java/util/concurrent/atomic/AtomicInteger"); jmethodID atomicIntegerSetMethodId = env->GetMethodID(atomicIntegerClass, "set", "(I)V"); jclass atomicLongClass = env->FindClass("java/util/concurrent/atomic/AtomicLong"); jmethodID atomicLongSetMethodId = env->GetMethodID(atomicLongClass, "set", "(J)V"); // 获取 AtomicDouble 的 set 方法 ID jclass atomicDoubleClass = env->FindClass("com/dotouch/industrial/bean/AtomicDouble"); jmethodID setDoubleMethodId = env->GetMethodID(atomicDoubleClass, "set", "(D)V"); // --- 获取字段 ID --- jfieldID sentCommandsField = env->GetFieldID(metricsClass, "sentCommands", "Ljava/util/concurrent/atomic/AtomicInteger;"); jfieldID sentPacketsField = env->GetFieldID(metricsClass, "sentPackets", "Ljava/util/concurrent/atomic/AtomicInteger;"); jfieldID sentBytesField = env->GetFieldID(metricsClass, "sentBytes", "Ljava/util/concurrent/atomic/AtomicLong;"); jfieldID receivedCommandsField = env->GetFieldID( metricsClass, "receivedCommands", "Ljava/util/concurrent/atomic/AtomicInteger;"); jfieldID receivedPacketsField = env->GetFieldID( metricsClass, "receivedPackets", "Ljava/util/concurrent/atomic/AtomicInteger;"); jfieldID receivedBytesField = env->GetFieldID( metricsClass, "receivedBytes", "Ljava/util/concurrent/atomic/AtomicLong;"); jfieldID deviceRegistrationSuccessRateFieldId = env->GetFieldID( metricsClass, "deviceRegistrationSuccessRate", "Lcom/dotouch/industrial/bean/AtomicDouble;"); jfieldID deviceRegistrationAverageDelayFieldId = env->GetFieldID( metricsClass, "deviceRegistrationAverageDelay", "Lcom/dotouch/industrial/bean/AtomicDouble;"); jfieldID controlCommandSuccessRateFieldId = env->GetFieldID( metricsClass, "controlCommandSuccessRate", "Lcom/dotouch/industrial/bean/AtomicDouble;"); jfieldID controlCommandRetryCountFieldId = env->GetFieldID( metricsClass, "controlCommandRetryCount", "Ljava/util/concurrent/atomic/AtomicInteger;"); jfieldID testItemResultsField = env->GetFieldID(metricsClass, "testItemResults", "Ljava/util/concurrent/ConcurrentHashMap;"); // 获取 Integer 类和构造函数 ID (用于 sentRateHistory 和 receivedRateHistory) jclass integerClass = env->FindClass("java/lang/Integer"); jmethodID integerConstructorId = env->GetMethodID(integerClass, "<init>", "(I)V"); // --- 更新 sentRate 和 receivedRate 的值 --- jfieldID sentRateFieldId = env->GetFieldID(metricsClass, "sentRate", "Lcom/dotouch/industrial/bean/AtomicDouble;"); jfieldID receivedRateFieldId = env->GetFieldID(metricsClass, "receivedRate", "Lcom/dotouch/industrial/bean/AtomicDouble;"); jobject sentRateObject = env->GetObjectField(metricsObject, sentRateFieldId); jobject receivedRateObject = env->GetObjectField(metricsObject, receivedRateFieldId); env->CallVoidMethod(sentRateObject, setDoubleMethodId, metrics.sentRate); env->CallVoidMethod(receivedRateObject, setDoubleMethodId, metrics.receivedRate); // --- 更新 sentRateHistory 和 receivedRateHistory --- jfieldID sentRateHistoryFieldId = env->GetFieldID(metricsClass, "sentRateHistory", "Ljava/util/List;"); jfieldID receivedRateHistoryFieldId = env->GetFieldID(metricsClass, "receivedRateHistory", "Ljava/util/List;"); jobject sentRateHistoryObject = env->GetObjectField(metricsObject, sentRateHistoryFieldId); jobject receivedRateHistoryObject = env->GetObjectField(metricsObject, receivedRateHistoryFieldId); // 获取 List 的 add 方法 ID jclass listClass = env->FindClass("java/util/List"); jmethodID addMethodId = env->GetMethodID(listClass, "add", "(Ljava/lang/Object;)Z"); jmethodID clearMethodId = env->GetMethodID(listClass, "clear", "()V"); env->CallVoidMethod(sentRateHistoryObject, clearMethodId); env->CallVoidMethod(receivedRateHistoryObject, clearMethodId); std::lock_guard<std::mutex> lock1(sentPacketsPerSecondMutex); for (int i = 0; i < sentPacketsPerSecond.size(); ++i) {
// LOGD("sentPacketsPerSecond[%d]: %d", i, sentPacketsPerSecond[i]);
int packetsCount = sentPacketsPerSecond[i];
text// 创建 Integer 对象 jobject integerObject = env->NewObject(integerClass, integerConstructorId, packetsCount); // 将 Integer 对象添加到 List env->CallBooleanMethod(sentRateHistoryObject, addMethodId, integerObject); // 删除局部引用,防止内存泄漏 env->DeleteLocalRef(integerObject); } std::lock_guard<std::mutex> lock2(receivedPacketsPerSecondMutex); for (int i = 0; i < receivedPacketsPerSecond.size(); ++i) {
// LOGD("receivedPacketsPerSecond[%d]: %d", i, receivedPacketsPerSecond[i]);
int packetsCount = receivedPacketsPerSecond[i];
text// 创建 Integer 对象 jobject integerObject = env->NewObject(integerClass, integerConstructorId, packetsCount); // 将 Integer 对象添加到 List env->CallBooleanMethod(receivedRateHistoryObject, addMethodId, integerObject); // 删除局部引用,防止内存泄漏 env->DeleteLocalRef(integerObject); } // --- 更新 AtomicLong 和 AtomicInteger 字段的值 --- jobject sentCommandsObject = env->GetObjectField(metricsObject, sentCommandsField); env->CallVoidMethod(sentCommandsObject, atomicIntegerSetMethodId, metrics.sentCommands); jobject sentPacketsObject = env->GetObjectField(metricsObject, sentPacketsField); env->CallVoidMethod(sentPacketsObject, atomicIntegerSetMethodId, metrics.sentPackets); jobject sentBytesObject = env->GetObjectField(metricsObject, sentBytesField); env->CallVoidMethod(sentBytesObject, atomicLongSetMethodId, (jlong) metrics.sentBytes); jobject receivedCommandsObject = env->GetObjectField(metricsObject, receivedCommandsField); env->CallVoidMethod(receivedCommandsObject, atomicIntegerSetMethodId, metrics.receivedCommands); jobject receivedPacketsObject = env->GetObjectField(metricsObject, receivedPacketsField); env->CallVoidMethod(receivedPacketsObject, atomicIntegerSetMethodId, metrics.receivedPackets); jobject receivedBytesObject = env->GetObjectField(metricsObject, receivedBytesField); env->CallVoidMethod(receivedBytesObject, atomicLongSetMethodId, (jlong) metrics.receivedBytes); jobject controlCommandRetryCountObject = env->GetObjectField( metricsObject, controlCommandRetryCountFieldId); env->CallVoidMethod(controlCommandRetryCountObject, atomicIntegerSetMethodId, metrics.commandTotalRetryCount); jobject deviceRegistrationSuccessRateObject = env->GetObjectField( metricsObject, deviceRegistrationSuccessRateFieldId); double deviceRegistrationSuccessRate = ((double) metrics.connectionSuccessCount / (double) metrics.connectionAttemptCount) * 100; env->CallVoidMethod(deviceRegistrationSuccessRateObject, setDoubleMethodId, deviceRegistrationSuccessRate); jobject deviceRegistrationAverageDelayObject = env->GetObjectField( metricsObject, deviceRegistrationAverageDelayFieldId); double deviceRegistrationAverageDelay = (double) metrics.connectionTotalDelay / (double) metrics.connectionSuccessCount; env->CallVoidMethod(deviceRegistrationAverageDelayObject, setDoubleMethodId, deviceRegistrationAverageDelay); jobject controlCommandSuccessRateObject = env->GetObjectField( metricsObject, controlCommandSuccessRateFieldId); double controlCommandSuccessRate = ((double) metrics.commandSuccessCount / (double) metrics.commandAttemptCount) * 100; env->CallVoidMethod(controlCommandSuccessRateObject, setDoubleMethodId, controlCommandSuccessRate); // --- 更新 testItemResults (ConcurrentHashMap) --- jobject testItemResultsObject = env->GetObjectField(metricsObject, testItemResultsField); // 获取 ConcurrentHashMap 的 put 方法 ID jclass concurrentHashMapClass = env->FindClass( "java/util/concurrent/ConcurrentHashMap"); jmethodID putMethodId = env->GetMethodID(concurrentHashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); // 获取 AtomicInteger 的构造函数 ID jclass atomicIntegerClass2 = env->FindClass( "java/util/concurrent/atomic/AtomicInteger"); jmethodID atomicIntegerConstructorId = env->GetMethodID(atomicIntegerClass2, "<init>", "(I)V"); // 获取 Pair 类和构造函数 ID jclass pairClass = env->FindClass("kotlin/Pair"); jmethodID pairConstructorId = env->GetMethodID(pairClass, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V"); for (const auto &pair: testItemResults) { const std::string &testItemName = pair.first; int successCount = pair.second.first; int failureCount = pair.second.second; // 创建 Java 字符串对象 jstring testItemNameObject = env->NewStringUTF(testItemName.c_str()); // 创建 Java AtomicInteger 对象 jobject successCountObject = env->NewObject(atomicIntegerClass2, atomicIntegerConstructorId, successCount); jobject failureCountObject = env->NewObject(atomicIntegerClass2, atomicIntegerConstructorId, failureCount); // 创建 Java Pair 对象 jobject pairObject = env->NewObject(pairClass, pairConstructorId, successCountObject, failureCountObject); // 将测试项结果添加到 Java 的 ConcurrentHashMap 中 env->CallObjectMethod(testItemResultsObject, putMethodId, testItemNameObject, pairObject); // 删除局部引用,防止内存泄漏 env->DeleteLocalRef(testItemNameObject); env->DeleteLocalRef(successCountObject); env->DeleteLocalRef(failureCountObject); env->DeleteLocalRef(pairObject); } // 在函数结尾释放局部引用框架 env->PopLocalFrame(nullptr);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Client_connectToServer(JNIEnv *env, jobject obj, jstring jip,
jint rack, jint slot, jint port) {
const char *ip = env->GetStringUTFChars(jip, nullptr);
textmetrics.connectionAttemptCount++; // 如果已经存在客户端实例,先断开连接并删除 if (Client != nullptr) { Client->Disconnect(); delete Client; Client = nullptr; // 避免悬挂指针 LOGD("Disconnected from previous connection."); } // 创建新的客户端实例 Client = new TS7Client(); // 设置连接参数 int timeout = 5000; int result = Client->SetParam(p_i32_PingTimeout, &timeout); if (result != 0) { LOGE("Failed to set PingTimeout parameter. Error code: %d", result); delete Client; Client = nullptr; env->ReleaseStringUTFChars(jip, ip); return JNI_FALSE; } // 设置连接类型(如果需要) result = Client->SetConnectionType(CONNTYPE_BASIC); if (result != 0) { LOGE("Failed to set connection type. Error code: %d", result); delete Client; Client = nullptr; env->ReleaseStringUTFChars(jip, ip); return JNI_FALSE; } // 设置端口(如果支持) result = Client->SetParam(p_u16_RemotePort, &port); if (result != 0) { LOGE("Failed to set remote port. Error code: %d", result); delete Client; Client = nullptr; env->ReleaseStringUTFChars(jip, ip); return JNI_FALSE; } // 记录连接开始时间 long long startTime = getCurrentTimestamp(); // 连接到服务器 result = Client->ConnectTo(ip, rack, slot); // 记录连接结束时间 long long endTime = getCurrentTimestamp(); env->ReleaseStringUTFChars(jip, ip); if (result == 0) { // 连接成功 metrics.connectionSuccessCount++; metrics.connectionTotalDelay += endTime - startTime; LOGD("Successfully connected to PLC: %s, rack: %d, slot: %d", ip, rack, slot); return JNI_TRUE; } else { // 连接失败 LOGE("Failed to connect to PLC: %s, rack: %d, slot: %d, Error code: %d", ip, rack, slot, result); delete Client; Client = nullptr; return JNI_FALSE; }
}
extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_dotouch_industrial_s7_S7Client_readData(JNIEnv *env, jobject obj, jint dbNumber,
jint startAddress, jstring dataType,
jint bitAddress,
jint quantity) {
LOGD("readData called");
// 获取 S7Client 对象
if (Client == nullptr) {
LOGE("Not connected to PLC. Call connectToServer first.");
return nullptr;
}
textmetrics.commandAttemptCount++; long long startTime = getCurrentTimestamp(); // 将 jstring 转换为 C 字符串 const char *cDataType = env->GetStringUTFChars(dataType, 0); // 计算单个数据的大小 int dataSize; if (strcmp(cDataType, "BOOL") == 0) { dataSize = 1; // 对于 BOOL,dataSize 始终为 1 (表示按位读取) } else if (strcmp(cDataType, "BYTE") == 0) { dataSize = 1; } else if (strcmp(cDataType, "WORD") == 0 || strcmp(cDataType, "INT") == 0) { dataSize = 2; } else if (strcmp(cDataType, "DWORD") == 0 || strcmp(cDataType, "DINT") == 0 || strcmp(cDataType, "REAL") == 0) { dataSize = 4; } else { LOGE("Unsupported data type: %s", cDataType); env->ReleaseStringUTFChars(dataType, cDataType); return nullptr; } // 更新 s7receivedPacketsPerSecond { std::lock_guard<std::mutex> lock(receivedPacketsPerSecondMutex); long long currentTimestamp = getCurrentTimestamp(); if (metrics.lastTimestamp == 0) { // 第一次接收数据,重置计数为 1,currentSecondIndex 为 0 receivedPacketsPerSecond[0] = 1; metrics.lastTimestamp = currentTimestamp; LOGD("Received first data, resetting s7receivedPacketsPerSecond"); } else { long long elapsedTimeSinceLastData = currentTimestamp - metrics.lastTimestamp; LOGD("elapsedTimeSinceLastData: %lld ms", elapsedTimeSinceLastData); if (elapsedTimeSinceLastData < 1000) { // 仍在同一秒内,使用 lastTimestamp 计算索引 receivedPacketsPerSecond[metrics.lastIndex]++; LOGD("Received data, updating s7receivedPacketsPerSecond[%d]", metrics.lastIndex); } else { // 经过了至少 1 秒,使用 elapsedTimeSinceLastData 计算索引 int currentSecondIndex = (elapsedTimeSinceLastData / 1000) % 300 + metrics.lastIndex; receivedPacketsPerSecond[currentSecondIndex] = 1; // 重置计数为 1 metrics.lastIndex = currentSecondIndex; metrics.lastTimestamp = currentTimestamp; LOGD("Received data, resetting s7receivedPacketsPerSecond[%d]", currentSecondIndex); } } } int result; int retryCount = 0; if (strcmp(cDataType, "BOOL") == 0) { // 处理 BOOL 类型数据 if (quantity == 1) { // 读取单个 BOOL 值 int byteAddress = startAddress; int bitOffset = bitAddress; uint8_t buffer[1]; // 分配一个字节的缓冲区 do { result = Client->DBRead(dbNumber, byteAddress, 1, buffer); if (result == 0) { metrics.commandSuccessCount++; updateTestItemResult("读取操作", true); break; } else { retryCount++; metrics.commandTotalRetryCount++; updateTestItemResult("读取操作", false); LOGE("Error reading data (attempt %d): %s", retryCount, CliErrorText(result).c_str()); usleep(100000); } } while (retryCount < 3); if (result != 0) { env->ReleaseStringUTFChars(dataType, cDataType); return nullptr; } uint8_t bitValue = (buffer[0] >> bitOffset) & 0x01; LOGD("Read bit value: %d", bitValue); jbyteArray jbyteArrayResult = env->NewByteArray(1); jbyte boolValue = bitValue; env->SetByteArrayRegion(jbyteArrayResult, 0, 1, &boolValue); // 计算往返延迟 long long endTime = getCurrentTimestamp(); long long roundTripDelay = endTime - startTime; // 计算接收速率,避免除以 0 的错误 if (roundTripDelay > 0) { metrics.receivedRate = (double) 1 / ((double) roundTripDelay / 1000.0); // bytes/s } else { metrics.receivedRate = 1; // 或其他默认值 } updateMetrics(env, obj, 0, 1, roundTripDelay); env->ReleaseStringUTFChars(dataType, cDataType); return jbyteArrayResult; } else { // 读取多个连续的 BOOL 值 int byteAddress = startAddress; int bitOffset = bitAddress; int totalSize = (quantity + bitOffset + 7) / 8; uint8_t *readBuffer = (uint8_t *)malloc(totalSize); if (readBuffer == nullptr) { LOGE("Failed to allocate memory for read buffer"); env->ReleaseStringUTFChars(dataType, cDataType); return nullptr; } do { result = Client->DBRead(dbNumber, byteAddress, totalSize, readBuffer); if (result == 0) { metrics.commandSuccessCount++; updateTestItemResult("读取操作", true); break; } else { retryCount++; metrics.commandTotalRetryCount++; updateTestItemResult("读取操作", false); LOGE("Error reading data (attempt %d): %s", retryCount, CliErrorText(result).c_str()); usleep(100000); } } while (retryCount < 3); if (result != 0) { free(readBuffer); env->ReleaseStringUTFChars(dataType, cDataType); return nullptr; } jbyteArray jbyteArrayResult = env->NewByteArray(quantity); jbyte *boolValues = (jbyte *)malloc(quantity); if (boolValues == nullptr) { LOGE("Failed to allocate memory for bool values"); free(readBuffer); env->ReleaseStringUTFChars(dataType, cDataType); return nullptr; } for (int i = 0; i < quantity; i++) { int currentByte = (bitOffset + i) / 8; int currentBit = (bitOffset + i) % 8; boolValues[i] = (readBuffer[currentByte] >> currentBit) & 0x01; } env->SetByteArrayRegion(jbyteArrayResult, 0, quantity, boolValues); // 计算往返延迟 long long endTime = getCurrentTimestamp(); long long roundTripDelay = endTime - startTime; // 计算接收速率,避免除以 0 的错误 if (roundTripDelay > 0) { metrics.receivedRate = (double) totalSize / ((double) roundTripDelay / 1000.0); // bytes/s } else { metrics.receivedRate = totalSize; // 或其他默认值 } free(boolValues); free(readBuffer); updateMetrics(env, obj, 0, totalSize, roundTripDelay); env->ReleaseStringUTFChars(dataType, cDataType); return jbyteArrayResult; } } else { // 处理其他数据类型 (BYTE, WORD, DWORD, 等) int totalSize = quantity * dataSize; void *readBuffer = malloc(totalSize); if (readBuffer == nullptr) { LOGE("Failed to allocate memory for read buffer"); env->ReleaseStringUTFChars(dataType, cDataType); return nullptr; } do { result = Client->DBRead(dbNumber, startAddress, totalSize, readBuffer); if (result == 0) { metrics.commandSuccessCount++; updateTestItemResult("读取操作", true); break; } else { retryCount++; metrics.commandTotalRetryCount++; updateTestItemResult("读取操作", false); LOGE("Error reading data (attempt %d): %s", retryCount, CliErrorText(result).c_str()); usleep(100000); } } while (retryCount < 3); env->ReleaseStringUTFChars(dataType, cDataType); if (result != 0) { free(readBuffer); return nullptr; } jbyteArray jbyteArrayResult = env->NewByteArray(totalSize); env->SetByteArrayRegion(jbyteArrayResult, 0, totalSize, (jbyte *)readBuffer); // 计算往返延迟 long long endTime = getCurrentTimestamp(); long long roundTripDelay = endTime - startTime; // 计算接收速率,避免除以 0 的错误 if (roundTripDelay > 0) { metrics.receivedRate = (double) totalSize / ((double) roundTripDelay / 1000.0); // bytes/s } else { metrics.receivedRate = totalSize; // 或其他默认值 } free(readBuffer); updateMetrics(env, obj, 0, totalSize, roundTripDelay); return jbyteArrayResult; }
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Client_writeData(JNIEnv *env, jobject obj, jint dbNumber,
jint startAddress, jstring dataType,
jint bitAddress, jint quantity,
jbyteArray value) {
textLOGD("writeData called"); // 获取 S7Client 对象 if (Client == nullptr) { return JNI_FALSE; } metrics.commandAttemptCount++; // 将 jstring 转换为 C 字符串 const char *cDataType = env->GetStringUTFChars(dataType, 0); // 获取 jbyteArray 的长度和内容 jsize valueLength = env->GetArrayLength(value); jbyte *cValue = env->GetByteArrayElements(value, 0); // 数据类型信息结构体 struct DataTypeInfo { int size; // 可以添加更多字段,例如数据转换函数指针 }; // 数据类型映射表 std::map<std::string, DataTypeInfo> dataTypeMap = { {"BOOL", {1}}, {"BYTE", {1}}, {"WORD", {2}}, {"INT", {2}}, {"DWORD", {4}}, {"DINT", {4}}, {"REAL", {4}} }; // 查找数据类型信息 auto it = dataTypeMap.find(cDataType); if (it == dataTypeMap.end()) { LOGE("Unsupported data type: %s", cDataType); env->ReleaseStringUTFChars(dataType, cDataType); env->ReleaseByteArrayElements(value, cValue, 0); return JNI_FALSE; } int dataSize = it->second.size; int totalSize = quantity * dataSize; // 获取当前时间戳 (用于计算延迟) long long startTime = getCurrentTimestamp(); // 写入数据 (最多重试 3 次) int result; int retryCount = 0; void *writeBuffer = malloc(totalSize); // 在这里分配缓冲区 if (writeBuffer == nullptr) { LOGE("Failed to allocate memory for write buffer"); env->ReleaseStringUTFChars(dataType, cDataType); env->ReleaseByteArrayElements(value, cValue, 0); return JNI_FALSE; } do { if (strcmp(cDataType, "BOOL") == 0) { // BOOL 类型数据写入 int byteOffset = startAddress; int bitOffset = bitAddress; // 读取原字节数据 uint8_t tempBuffer[1]; result = Client->DBRead(dbNumber, byteOffset, 1, tempBuffer); if (result != 0) { LOGE("Error reading byte before write: %s", CliErrorText(result).c_str()); break; // 读取错误时退出循环 } // 修改目标位 if (cValue[0] == 1) { tempBuffer[0] |= (1 << bitOffset); } else { tempBuffer[0] &= ~(1 << bitOffset); } memcpy(writeBuffer, tempBuffer, 1); // 将修改后的字节复制到 writeBuffer result = Client->DBWrite(dbNumber, byteOffset, 1, writeBuffer); } else { // 其他类型数据写入 memcpy(writeBuffer, cValue, totalSize); // 将数据复制到 writeBuffer result = Client->DBWrite(dbNumber, startAddress, totalSize, writeBuffer); } if (result == 0) { metrics.commandSuccessCount++; break; } else { retryCount++; metrics.commandTotalRetryCount++; LOGE("Error writing data (attempt %d): %s", retryCount, CliErrorText(result).c_str()); usleep(100000); } } while (retryCount < 3); // 释放资源 env->ReleaseStringUTFChars(dataType, cDataType); env->ReleaseByteArrayElements(value, cValue, 0); free(writeBuffer); // 检查写入结果 if (result != 0) { updateTestItemResult("写入操作", false); LOGE("Error writing data: %s", CliErrorText(result).c_str()); return JNI_FALSE; } else { updateTestItemResult("写入操作", true); } // 计算往返延迟 long long endTime = getCurrentTimestamp(); long long roundTripDelay = endTime - startTime; // 计算发送速率,避免除以 0 的错误 if (roundTripDelay > 0) { metrics.sentRate = (double) totalSize / ((double) roundTripDelay / 1000.0); // bytes/s } else { metrics.sentRate = 0.0; // 或其他默认值 } // 更新 s7sentPacketsPerSecond { std::lock_guard<std::mutex> lock(sentPacketsPerSecondMutex); long long currentTimestamp = getCurrentTimestamp(); if (metrics.lastTimestamp == 0) { // 第一次发送数据,重置计数为 1,currentSecondIndex 为 0 sentPacketsPerSecond[0] = 1; metrics.lastTimestamp = currentTimestamp; LOGD("Sent first data, resetting s7sentPacketsPerSecond"); } else { long long elapsedTimeSinceLastData = currentTimestamp - metrics.lastTimestamp; LOGD("elapsedTimeSinceLastData: %lld ms", elapsedTimeSinceLastData); if (elapsedTimeSinceLastData < 1000) { // 仍在同一秒内,使用 lastSentBytesTimestamp 计算索引 sentPacketsPerSecond[metrics.lastIndex]++; LOGD("Sent data, updating s7sentPacketsPerSecond[%d]", metrics.lastIndex); } else { // 经过了至少 1 秒,使用 elapsedTimeSinceLastData 计算索引 int currentSecondIndex = (elapsedTimeSinceLastData / 1000) % 300 + metrics.lastIndex; sentPacketsPerSecond[currentSecondIndex] = 1; // 重置计数为 1 metrics.lastIndex = currentSecondIndex; metrics.lastTimestamp = currentTimestamp; LOGD("Sent data, resetting s7sentPacketsPerSecond[%d]", currentSecondIndex); } } } updateMetrics(env, obj, totalSize, 0, roundTripDelay); return JNI_TRUE;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Client_testReadData(JNIEnv *env, jobject obj, jint dbNumber,
jint byteOffset, jint bitOffset) {
// 检查bitOffset是否在有效范围内(0到7)
if (bitOffset < 0 || bitOffset > 7) {
LOGE("Invalid bitOffset: %d", bitOffset);
return JNI_FALSE; // 返回错误
}
textuint8_t buffer[1]; // 用于存储读取的一个字节的数据 // 调用Client的DBRead函数,读取一个字节的数据 int result = Client->DBRead(dbNumber, byteOffset, 1, buffer); // 检查读取结果 if (result != 0) { LOGE("Error read data: %s", CliErrorText(result).c_str()); return JNI_FALSE; // 如果读取失败,返回false } // 提取特定位上的值 uint8_t bitValue = (buffer[0] >> bitOffset) & 0x01; LOGD("Read bit value: %d", bitValue); // 根据位的值返回 JNI_TRUE 或 JNI_FALSE return bitValue == 1 ? JNI_TRUE : JNI_FALSE;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_dotouch_industrial_s7_S7Client_testWriteData(JNIEnv *env, jobject obj, jint dbNumber,
jint byteOffset, jint bitOffset,
jboolean value) {
// 检查bitOffset是否在有效范围内(0到7)
if (bitOffset < 0 || bitOffset > 7) {
LOGE("Invalid bitOffset: %d", bitOffset);
return -1; // 返回错误码,表示bitOffset无效
}
textuint8_t buffer[1]; // 用于读取现有的字节数据 // 调用Client的DBRead函数,读取一个字节的数据 int result = Client->DBRead(dbNumber, byteOffset, 1, buffer); // 检查读取结果 if (result != 0) { LOGE("Error reading byte before write: %s", CliErrorText(result).c_str()); return result; // 如果读取失败,返回错误码 } // 根据value值修改buffer中对应位的数据 if (value == JNI_TRUE) { buffer[0] |= (1 << bitOffset); // 将对应位设置为1 } else { buffer[0] &= ~(1 << bitOffset); // 将对应位设置为0 } // 调用Client的DBWrite函数,写入修改后的字节数据 result = Client->DBWrite(dbNumber, byteOffset, 1, buffer); // 检查写入结果 if (result != 0) { LOGE("Error writing data: %s", CliErrorText(result).c_str()); return result; // 写入失败,返回错误码 } LOGD("Write data success: byte value = %d", buffer[0]); // 打印写入后的字节值 return 0; // 返回0表示写入成功
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Client_disconnectFromServer(
JNIEnv env,
jobject / this */) {
if (Client != nullptr) {
int result = Client->Disconnect();
delete Client;
Client = nullptr;
textif (result == 0) { // 断开连接成功 LOGD("Successfully disconnected from PLC."); return JNI_TRUE; } else { // 断开连接失败 LOGE("Failed to disconnect from PLC. Error code: %d", result); return JNI_FALSE; } } else { // 客户端不存在,视为已断开连接 LOGD("No active connection to disconnect."); return JNI_TRUE; }
}
#include <jni.h>
#include "snap7/examples/cpp/snap7.h"
#include <cstdio>
#include <android/log.h>
#include <map>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <sys/time.h>
#include <deque>
#include <chrono>
#include <algorithm>
#include <utility>
#include <random>
#include <thread>
#include <iostream>
#include <queue>
// 定义日志标签
#define LOG_TAG "s7-server"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, VA_ARGS)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, VA_ARGS)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, VA_ARGS)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, VA_ARGS)
static S7Object server = 0U;
struct DataBlockInfo {
int areaCode;
word dbNumber;
word startAddress;
word length;
void *dataBuffer;
};
static std::queue<long long> requestTimestamps;
static std::mutex requestTimestampsMutex;
// 使用 std::mutex 保护共享资源
std::mutex dataBlockMutex;
// 使用 std::unordered_map 存储已注册的数据块
std::unordered_map<int, DataBlockInfo> registeredDataBlocks;
// 使用原子变量保证线程安全
std::atomic<int> nextDataBlockId{0};
// 获取当前时间戳的函数 (单位: 毫秒)
long long getCurrentTimestamp() {
struct timeval tv{};
gettimeofday(&tv, nullptr);
return (long long) tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
void printReceivedData(uint8_t *data, int size) {
LOGD("Received data (Size: %d):", size);
for (int i = 0; i < size; ++i) {
LOGD(" Byte %d: 0x%02x", i, data[i]);
}
text// 根据数据类型解析并打印数据 (你需要根据实际情况修改这部分代码) // 例如,如果客户端写入的是一个 INT 类型的数据: if (size >= 2) { int intValue = (data[1] << 8) | data[0]; // 假设是大端字节序 LOGD(" INT Value: %d", intValue); } if (size >= 4) { float floatValue; memcpy(&floatValue, data, sizeof(float)); // 假设是大端字节序 LOGD(" REAL Value: %f", floatValue); }
}
void printByteArray(void* data, int size, int wordLen) {
if (wordLen == S7WLBit) { // BOOL 类型
byte* bytes = (byte*)data;
for (int i = 0; i < size; i++) {
for (int j = 0; j < 8; j++) {
LOGD("Bit %d.%d: %d", i, j, (bytes[i] >> j) & 1);
}
}
} else if (wordLen == S7WLByte) { // BYTE 类型
byte* bytes = (byte*)data;
for (int i = 0; i < size; i++) {
LOGD("Byte %d: %d", i, bytes[i]);
}
} else if (wordLen == S7WLWord) { // WORD 类型
short* shorts = (short*)data;
for (int i = 0; i < size / 2; i++) {
LOGD("Word %d: %d", i, shorts[i]);
}
} else if (wordLen == S7WLDWord) { // DWORD 类型
int* ints = (int*)data;
for (int i = 0; i < size / 4; i++) {
LOGD("DWord %d: %d", i, ints[i]);
}
} else if (wordLen == S7WLReal) { // REAL 类型
float* floats = (float*)data;
for (int i = 0; i < size / 4; i++) {
LOGD("Real %d: %f", i, floats[i]);
}
} else if (wordLen == S7WLCounter) {
// 处理 S7WLCounter 类型的代码 (如果需要)
} else if (wordLen == S7WLTimer) {
// 处理 S7WLTimer 类型的代码 (如果需要)
} else { // 其他类型,检查是否是以空字符结尾的字符串
byte* bytes = (byte*)data;
bool isString = false;
for (int i = 0; i < size; i++) {
if (bytes[i] == '\0') {
isString = true;
break;
}
}
textif (isString) { // 是字符串 char* str = (char*)data; LOGD("String: %s", str); } else { // 不是字符串,以十六进制打印 for (int i = 0; i < size; i++) { LOGD("Byte %d: 0x%02X", i, bytes[i]); } } }
}
// 用于存储指标数据的结构体
struct Metrics {
int receivedCommands; // 接收到的命令数量
int receivedPackets; // 接收到的数据包数量
int receivedBytes;
double receivedRate;
int lastIndex; // 上一次接收字节的时间戳
long long lastReceivedBytesTimestamp; // 上一次接收字节的时间戳
long long lastReceivedBytes; // 上一次接收的字节数
};
// 全局变量,用于存储最新的指标数据
Metrics metrics = {0, 0, 0, 0, 0, 0, 0};
// 存储每秒发送的数据包个数
std::vector<int> sentPacketsPerSecond(300, 0);
std::mutex sentPacketsPerSecondMutex;
// 存储每秒接收的数据包个数
std::vector<int> receivedPacketsPerSecond(300, 0);
std::mutex receivedPacketsPerSecondMutex;
// 存储最近一段时间内接收到的字节数和对应的时间戳
std::deque<std::pair<long long, int>> receivedBytesWindow;
std::mutex receivedBytesWindowMutex;
// 获取当前秒数在列表中的索引
int getCurrentSecondIndex() {
long long currentSecond = getCurrentTimestamp() / 1000;
return currentSecond % 300;
}
// 将时间戳转换为中国标准时间字符串
std::string convertTimestampToCST(long long timestamp) {
// 将时间戳转换为 time_t
auto time = static_cast<time_t>(timestamp / 1000); // 毫秒转换为秒
text// 使用 gmtime 将 time_t 转换为 tm 结构体 (UTC 时间) tm *timeinfo = gmtime(&time); // 使用 mktime 将 tm 结构体转换为 time_t (仍然是 UTC 时间) time_t utcTime = mktime(timeinfo); // 使用 localtime 将 time_t 转换为 tm 结构体 (本地时间,考虑了时区) tm *localTimeinfo = localtime(&utcTime); // 使用 strftime 将 tm 结构体格式化为字符串 char buffer[80]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTimeinfo); return {buffer};
}
// 获取网络时间戳 (JNI 调用 Java)
jlong getNetworkTimestampFromJava(JNIEnv *env) {
// 获取 Utils 类
jclass utilsClass = env->FindClass("com/dotouch/industrial/utils/Utils");
if (utilsClass == nullptr) {
LOGE("Error: Could not find Utils class.");
return -1;
}
text// 获取 Utils 类的构造函数 jmethodID constructor = env->GetMethodID(utilsClass, "<init>", "()V"); if (constructor == nullptr) { LOGE("Error: Could not find Utils constructor."); return -1; } // 创建 Utils 类的实例 jobject utilsObject = env->NewObject(utilsClass, constructor); if (utilsObject == nullptr) { LOGE("Error: Could not create Utils object."); return -1; } // 获取 getNetworkTimestamp 方法 jmethodID getNetworkTimestampMethod = env->GetMethodID(utilsClass, "getNetworkTimestamp", "()Ljava/lang/Long;"); if (getNetworkTimestampMethod == nullptr) { LOGE("Error: Could not find getNetworkTimestamp method."); return -1; } // 调用 getNetworkTimestamp 方法 jobject networkTimestampObject = env->CallObjectMethod(utilsObject, getNetworkTimestampMethod); // 检查返回值是否为空 if (networkTimestampObject == nullptr) { LOGE("Error: getNetworkTimestamp returned null."); return -1; } // 获取 Long 对象的值 jmethodID longValueMethod = env->GetMethodID(env->FindClass("java/lang/Long"), "longValue", "()J"); if (longValueMethod == nullptr) { LOGE("Error: Could not find longValue method."); return -1; } jlong networkTimestamp = env->CallLongMethod(networkTimestampObject, longValueMethod); return networkTimestamp;
}
JavaVM *vm = nullptr;
pthread_key_t g_env_key;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
::vm = vm;
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
text// 初始化 TLS 键 if (pthread_key_create(&g_env_key, nullptr) != 0) { LOGE("Error creating TLS key"); return -1; // 或抛出异常 } return JNI_VERSION_1_6;
}
jlong getCurrentTimestampInternal(JNIEnv *env) {
jclass systemClass = env->FindClass("java/lang/System");
jmethodID currentTimeMillisMethod = env->GetStaticMethodID(systemClass, "currentTimeMillis",
"()J");
return env->CallStaticLongMethod(systemClass, currentTimeMillisMethod);
}
// 服务器事件回调函数
void S7API ServerEventCallback(void *usrPtr, PSrvEvent PEvent, int Size) {
LOGD("Event Code: 0x%08X", PEvent->EvtCode);
switch (PEvent->EvtCode) {
case evcClientAdded:
LOGD("Client connected!");
break;
case evcClientDisconnected:
LOGD("Client disconnected!");
break;
}
}
int S7API RWAreaCallback(void *usrPtr, int Sender, int Operation, PS7Tag PTag, void *pUsrData) {
std::lock_guardstd::mutex lock1(dataBlockMutex); // 假设你使用了 dataBlockMutex 来保护数据访问
// 更新与数据块注册无关的指标数据
metrics.receivedCommands++;
metrics.receivedPackets++;
metrics.receivedBytes += PTag->Size;
text// 计算接收速率 long long currentTimestamp = getCurrentTimestamp(); if (metrics.lastReceivedBytesTimestamp > 0) { long long timeDiff = currentTimestamp - metrics.lastReceivedBytesTimestamp; // 时间差,单位毫秒 long long bytesDiff = metrics.receivedBytes - metrics.lastReceivedBytes; // 字节差 if (timeDiff > 0) { metrics.receivedRate = (double)bytesDiff / ((double)timeDiff / 1000.0); // bytes/s } else { metrics.receivedRate = 0; // 或其他默认值 } } // 更新 receivedPacketsPerSecond { std::lock_guard<std::mutex> lock2(receivedPacketsPerSecondMutex); LOGD("Received data, updating lastReceivedBytesTimestamp %lld", metrics.lastReceivedBytesTimestamp); if (metrics.lastReceivedBytesTimestamp == 0) { // 第一次接收数据,重置计数为 1,currentSecondIndex 为 0 receivedPacketsPerSecond[0] = 1; metrics.lastReceivedBytesTimestamp = currentTimestamp; LOGD("Received first data, resetting receivedPacketsPerSecond"); } else { long long elapsedTimeSinceLastData = currentTimestamp - metrics.lastReceivedBytesTimestamp; LOGD("elapsedTimeSinceLastData: %lld ms", elapsedTimeSinceLastData); if (elapsedTimeSinceLastData < 1000) { // 仍在同一秒内,使用 lastReceivedBytesTimestamp 计算索引 receivedPacketsPerSecond[metrics.lastIndex]++; LOGD("Received data, updating receivedPacketsPerSecond[%d]", metrics.lastIndex); } else { // 经过了至少 1 秒,使用 elapsedTimeSinceLastData 计算索引 int currentSecondIndex = (elapsedTimeSinceLastData / 1000) % 300 + metrics.lastIndex; receivedPacketsPerSecond[currentSecondIndex] = 1; // 重置计数为 1 metrics.lastIndex = currentSecondIndex; metrics.lastReceivedBytesTimestamp = currentTimestamp; LOGD("Received data, resetting receivedPacketsPerSecond[%d]", currentSecondIndex); } } } metrics.lastReceivedBytes = metrics.receivedBytes; // 将时间戳添加到队列 { std::lock_guard<std::mutex> lock3(requestTimestampsMutex); requestTimestamps.push(currentTimestamp); } LOGI("Receive Timestamp: %lld", currentTimestamp); if (Operation == OperationRead) { // 读取操作 // 处理读取的数据 (例如,将数据从服务器端复制到 PTag->Data) if (registeredDataBlocks.count(PTag->DBNumber)) { DataBlockInfo& dbInfo = registeredDataBlocks[PTag->DBNumber]; // 检查读取范围是否越界 (使用原始 PTag->Start 和 PTag->Size) if (PTag->Start + PTag->Size <= dbInfo.length) { // 将 dbInfo.dataBuffer 转换成 byte* 指针 memcpy(pUsrData, (byte*)dbInfo.dataBuffer + PTag->Start, PTag->Size); } else { LOGE("Read area out of range: DB[%d], Start[%d], Size[%d]", PTag->DBNumber, PTag->Start, PTag->Size); return evrErrOutOfRange; // 使用已有的常量 } } else { LOGE("DB not found: %d", PTag->DBNumber); return evrErrAreaNotFound; // 使用已有的常量 } } else if (Operation == OperationWrite) { // 写入操作 // 处理写入的数据 (例如,将 PTag->Data 中的数据复制到服务器端) if (registeredDataBlocks.count(PTag->DBNumber)) { DataBlockInfo& dbInfo = registeredDataBlocks[PTag->DBNumber];
// if (enableTimestamp) { // 处理时间戳
// // 1. 检查数据大小是否足够包含时间戳
// if (PTag->Size < sizeof(long long)) {
// LOGE("Data size too small for timestamp");
// return -1;
// }
//
// // 2. 获取客户端发送的时间戳
// long long recvTimestamp = (long long)pUsrData;
//
// LOGD("Received timestamp: %lld", recvTimestamp);
//
// // 3. 跳过时间戳,指向实际数据
// pUsrData = (byte*)pUsrData + sizeof(long long);
// PTag->Size -= sizeof(long long);
//
// // 4. 计算单向时延
// long long currentTime = getCurrentTimestamp();
// LOGD("Current timestamp: %lld", currentTime);
// double oneWayDelay = currentTime - recvTimestamp;
//
// // 计算接收速率,避免除以 0 的错误
// if (oneWayDelay > 0) {
// metrics.receiveTotalDelay += oneWayDelay;
// metrics.receiveMaxDelay = std::max(metrics.receiveMaxDelay, oneWayDelay);
// metrics.receiveMinDelay = (metrics.receiveCount == 1) ? oneWayDelay : std::min(metrics.receiveMinDelay, oneWayDelay);
//
// LOGD("Round Trip Delay: %f ms", oneWayDelay);
// metrics.receivedRate = (double)PTag->Size / ((double)oneWayDelay / 1000.0); // bytes/s
// } else {
// LOGE("Invalid timestamp received oneWayDelay: %f ms", oneWayDelay);
// metrics.receivedRate = PTag->Size; // 或其他默认值
// }
// }
text// 检查写入范围是否越界 (使用更新后的 PTag->Size) if (PTag->Start + PTag->Size <= dbInfo.length) { // 将 dbInfo.dataBuffer 转换成 byte* 指针 memcpy((byte*)dbInfo.dataBuffer + PTag->Start, pUsrData, PTag->Size); // 打印写入的值 LOGD("Write data (DB: %d, Start: %d, Size: %d):", PTag->DBNumber, PTag->Start, PTag->Size); printByteArray(pUsrData, PTag->Size, PTag->WordLen); } else { LOGE("Write area out of range: DB[%d], Start[%d], Size[%d]", PTag->DBNumber, PTag->Start, PTag->Size); return evrErrOutOfRange; // 使用已有的常量 } } else { LOGE("DB not found: %d", PTag->DBNumber); return evrErrAreaNotFound; // 使用已有的常量 } } return 0; // 其他操作,返回 0 表示操作成功
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Server_startServer(
JNIEnv *env,
jobject thiz,
jint port,
jint rack,
jint slot) {
textauto *Server = new TS7Server(); Server->SetEventsMask(0x0FFFF); int Result = Server->SetParam(p_u16_LocalPort, &port); if (Result != 0) { LOGE("Failed to set server port: Error code %d", Result); delete Server; return JNI_FALSE; } LOGD("Server port set to %d", port); Result = Server->Start(); if (Result == 0) { server = reinterpret_cast<S7Object>(Server); auto rackByte = static_cast<uint8_t>(rack); auto slotByte = static_cast<uint8_t>(slot); int success; success = Server->RegisterArea(srvAreaPE, 0, &rackByte, sizeof(uint8_t)); if (success != 0) { LOGE("Failed to register PE area for rack: Error code %d", Result); delete Server; return JNI_FALSE; } else { LOGD("Successfully registered PE area for rack: %d", rack); } success = Server->RegisterArea(srvAreaPA, 0, &slotByte, sizeof(uint8_t)); if (success != 0) { LOGE("Failed to register PA area for slot: Error code %d", Result); delete Server; return JNI_FALSE; } else { LOGD("Successfully registered PA area for slot: %d", slot); }
// // 注册 DB10
// int DBNumber = 1; // DB10 的编号
// int startAddress = 0; // 起始地址
// int byteLength = 1024; // 假设 DB10 的大小为 1024 字节,请根据实际情况修改
//
// DataBlockInfo dataBlockInfo{};
// dataBlockInfo.areaCode = srvAreaDB;
// dataBlockInfo.dbNumber = DBNumber;
// dataBlockInfo.startAddress = startAddress;
//// dataBlockInfo.length = byteLength + sizeof(long long); // 添加额外的空间用于存储时间戳
// dataBlockInfo.length = byteLength; // 添加额外的空间用于存储时间戳
//
// // 分配内存用于数据缓冲区
// dataBlockInfo.dataBuffer = calloc(dataBlockInfo.length, 1);
// if (dataBlockInfo.dataBuffer == nullptr) {
// LOGE("Failed to register DB10: Memory allocation failed.");
// delete Server;
// return JNI_FALSE;
// }
//
// // 使用 SNAP7 服务器注册数据块
// success = Server->RegisterArea(dataBlockInfo.areaCode,
// dataBlockInfo.dbNumber,
// dataBlockInfo.dataBuffer,
// dataBlockInfo.length);
//
// if (success == 0) {
// // 在映射中注册数据块,使用 dbNumber 作为键值
// registeredDataBlocks[DBNumber] = dataBlockInfo; // 假设 registeredDataBlocks 是一个全局的 std::map<int, DataBlockInfo>
//
// LOGD("Successfully registered DB10 (Area: %d, DB: %d, Start: %d, Length: %d)",
// dataBlockInfo.areaCode, dataBlockInfo.dbNumber, dataBlockInfo.startAddress,
// dataBlockInfo.length);
// } else {
// // 释放内存
// free(dataBlockInfo.dataBuffer);
//
// // 使用 Cli_ErrorText 获取更详细的错误信息
// char errorText[1024];
// Cli_ErrorText(success, errorText, sizeof(errorText));
//
// LOGE("Failed to register DB10: Srv_RegisterArea returned error code %d (%s).", success,
// errorText);
// delete Server;
// return JNI_FALSE;
// }
text// 设置读写区域回调函数 Server->SetRWAreaCallback(RWAreaCallback, nullptr); // 设置服务器事件回调函数 Server->SetEventsCallback(ServerEventCallback, nullptr); Server->SetCpuStatus(S7CpuStatusRun); LOGD("S7 server started successfully on port %d (Rack: %d, Slot: %d)", port, rack, slot); return JNI_TRUE; } else { LOGE("Failed to start S7 server: Error code %d", Result); delete Server; return JNI_FALSE; }
}
void printDataBlockInfo(const DataBlockInfo &dataBlockInfo) {
LOGD("Data Block Information:");
LOGD(" Area Code: %d", dataBlockInfo.areaCode);
LOGD(" DB Number: %d", dataBlockInfo.dbNumber);
LOGD(" Start Address: %d", dataBlockInfo.startAddress);
LOGD(" Length: %d", dataBlockInfo.length);
LOGD(" Data Buffer Address: %p", dataBlockInfo.dataBuffer);
// 以下代码需要根据实际数据类型进行调整
if (dataBlockInfo.areaCode == srvAreaDB) {
// 打印 DB 数据
LOGD(" Data (Hex):");
for (int i = 0; i < dataBlockInfo.length; i++) {
LOGD(" %02X", ((uint8_t *) dataBlockInfo.dataBuffer)[i]);
}
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_dotouch_industrial_s7_S7Server_stopServer(JNIEnv *env, jobject obj) {
if (server != 0U) {
auto *Server = reinterpret_cast<TS7Server *>(server);
textServer->SetEventsCallback(nullptr, nullptr); Server->SetRWAreaCallback(nullptr, nullptr); // 先停止服务器 Server->Stop(); // 清理已注册的数据块 { std::lock_guard<std::mutex> lock(dataBlockMutex); for (auto &pair : registeredDataBlocks) { if (pair.second.dataBuffer != nullptr) { free(pair.second.dataBuffer); } } registeredDataBlocks.clear(); } // 删除服务器对象 delete Server; server = 0U; LOGD("S7 server stopped successfully."); } else { LOGW("S7 server is not running."); }
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Server_registerDataBlock(JNIEnv *env, jobject thiz,
jint dbNumber, jint startAddress,
jint byteLength) {
LOGD("Server pointer in registerDataBlock: %u", server);
if (server == 0U) {
LOGE("Failed to register data block: Server is not running.");
return JNI_FALSE;
}
textDataBlockInfo dataBlockInfo{}; dataBlockInfo.areaCode = srvAreaDB; dataBlockInfo.dbNumber = dbNumber; dataBlockInfo.startAddress = startAddress; dataBlockInfo.length = byteLength; // 分配内存用于数据缓冲区 dataBlockInfo.dataBuffer = calloc(dataBlockInfo.length, 1); if (dataBlockInfo.dataBuffer == nullptr) { LOGE("Failed to register data block: Memory allocation failed."); return JNI_FALSE; } // 打印所有参数值 LOGD("Srv_RegisterArea parameters:"); LOGD(" Server handle: %u", server); LOGD(" Area Code: %d", dataBlockInfo.areaCode); LOGD(" DB Number: %d", dataBlockInfo.dbNumber); LOGD(" Start Address: %d", dataBlockInfo.startAddress); LOGD(" Length: %d", dataBlockInfo.length); LOGD(" Data Buffer Address: %p", dataBlockInfo.dataBuffer); // 使用 SNAP7 服务器注册数据块 auto *Server = reinterpret_cast<TS7Server *>(server); int result = Server->RegisterArea(dataBlockInfo.areaCode, dataBlockInfo.dbNumber, dataBlockInfo.dataBuffer, dataBlockInfo.length); if (result == 0) { // 在映射中注册数据块, 使用 dbNumber 作为键值 registeredDataBlocks[dbNumber] = dataBlockInfo; LOGD("Data block registered successfully (Area: %d, DB: %d, Start: %d, Length: %d)", dataBlockInfo.areaCode, dataBlockInfo.dbNumber, dataBlockInfo.startAddress, dataBlockInfo.length); return JNI_TRUE; } else { // 释放内存 free(dataBlockInfo.dataBuffer); // 使用 Cli_ErrorText 获取更详细的错误信息 char errorText[1024]; Cli_ErrorText(result, errorText, sizeof(errorText)); LOGE("Failed to register data block: Srv_RegisterArea returned error code %d (%s).", result, errorText); return JNI_FALSE; }
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_dotouch_industrial_s7_S7Server_unregisterDataBlock(JNIEnv *env, jobject thiz,
jint blockNumber) {
std::lock_guardstd::mutex lock(dataBlockMutex); // 使用互斥锁保护 registeredDataBlocks
text// 查找要反注册的数据块 for (auto it = registeredDataBlocks.begin(); it != registeredDataBlocks.end(); ++it) { if (it->second.dbNumber == blockNumber) { // 找到匹配的数据块 DataBlockInfo &dataBlockInfo = it->second; // 打印所有参数值 LOGD("Srv_UnregisterArea parameters:"); LOGD(" Server handle: %u", server); LOGD(" Area Code: %d", dataBlockInfo.areaCode); LOGD(" DB Number: %d", dataBlockInfo.dbNumber); // 使用 SNAP7 服务器反注册数据块 auto *Server = reinterpret_cast<TS7Server *>(server); int result = Server->UnregisterArea(dataBlockInfo.areaCode, dataBlockInfo.dbNumber); if (result == 0) { // 反注册成功 // 释放内存 free(dataBlockInfo.dataBuffer); // 从 registeredDataBlocks 中移除数据块 registeredDataBlocks.erase(it); LOGD("Data block unregistered successfully (DB Number: %d)", blockNumber); return JNI_TRUE; } else { // 反注册失败 // 使用 Cli_ErrorText 获取更详细的错误信息 char errorText[1024]; Cli_ErrorText(result, errorText, sizeof(errorText)); LOGE("Failed to unregister data block: Srv_UnregisterArea returned error code %d (%s).", result, errorText); return JNI_FALSE; } } } // 没有找到匹配的数据块 LOGW("Data block not found for unregistration (DB Number: %d)", blockNumber); return JNI_FALSE;
}
// 获取最新的指标数据
extern "C"
JNIEXPORT void JNICALL
Java_com_dotouch_industrial_s7_S7Server_getLatestMetrics(JNIEnv *env, jobject thiz, jobject metricsObject,
jobject clientTimes) {
// 获取必要的 Java 类和方法 ID
jclass metricsClass = env->FindClass("com/dotouch/industrial/bean/CommunicationMetrics");
if (metricsClass == nullptr) {
LOGE("Failed to find class: com/dotouch/industrial/bean/CommunicationMetrics");
return;
}
jfieldID receivedCommandsField = env->GetFieldID(metricsClass, "receivedCommands",
"Ljava/util/concurrent/atomic/AtomicInteger;");
jfieldID receivedPacketsField = env->GetFieldID(metricsClass, "receivedPackets",
"Ljava/util/concurrent/atomic/AtomicInteger;");
jfieldID receivedBytesField = env->GetFieldID(metricsClass, "receivedBytes",
"Ljava/util/concurrent/atomic/AtomicLong;");
jfieldID receivedRateField = env->GetFieldID(metricsClass, "receivedRate",
"Lcom/dotouch/industrial/bean/AtomicDouble;");
jfieldID receivedRateHistoryField = env->GetFieldID(metricsClass, "receivedRateHistory",
"Ljava/util/List;");
textjclass atomicIntegerClass = env->FindClass("java/util/concurrent/atomic/AtomicInteger"); jmethodID setIntMethodId = env->GetMethodID(atomicIntegerClass, "set", "(I)V"); // 获取 AtomicInteger 构造函数 ID jmethodID atomicIntegerConstructorId = env->GetMethodID(atomicIntegerClass, "<init>", "(I)V"); jclass atomicLongClass = env->FindClass("java/util/concurrent/atomic/AtomicLong"); jmethodID setLongMethodId = env->GetMethodID(atomicLongClass, "set", "(J)V"); jclass atomicDoubleClass = env->FindClass("com/dotouch/industrial/bean/AtomicDouble"); jmethodID setDoubleMethodId = env->GetMethodID(atomicDoubleClass, "set", "(D)V"); jmethodID getDoubleMethodId = env->GetMethodID(atomicDoubleClass, "get", "()D"); jclass listClass = env->FindClass("java/util/List"); jmethodID addMethodId = env->GetMethodID(listClass, "add", "(Ljava/lang/Object;)Z"); jmethodID sizeMethodId = env->GetMethodID(listClass, "size", "()I"); jmethodID getMethodId = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;"); jmethodID clearMethodId = env->GetMethodID(listClass, "clear", "()V"); jclass concurrentHashMapClass = env->FindClass("java/util/concurrent/ConcurrentHashMap"); jmethodID putMethodId = env->GetMethodID(concurrentHashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); jclass integerClass = env->FindClass("java/lang/Integer"); jmethodID integerConstructorId = env->GetMethodID(integerClass, "<init>", "(I)V"); jclass pairClass = env->FindClass("kotlin/Pair"); jmethodID pairConstructorId = env->GetMethodID(pairClass, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V"); if (receivedCommandsField == nullptr || receivedPacketsField == nullptr || receivedBytesField == nullptr || receivedRateField == nullptr || receivedRateHistoryField == nullptr || setIntMethodId == nullptr || setLongMethodId == nullptr || setDoubleMethodId == nullptr || getDoubleMethodId == nullptr || addMethodId == nullptr || putMethodId == nullptr || integerConstructorId == nullptr || pairConstructorId == nullptr || atomicIntegerConstructorId == nullptr) { // 检查 AtomicInteger 构造函数 ID LOGE("Failed to get field or method ID from CommunicationMetrics"); return; } // --- 获取 S7Metrics 数据 --- int receivedCommands = metrics.receivedCommands; int receivedPackets = metrics.receivedPackets; long long receivedBytes = metrics.receivedBytes; double receivedRate = metrics.receivedRate; jint listSize = 0; if (clientTimes != nullptr) { listSize = env->CallIntMethod(clientTimes, sizeMethodId); // 打印 clientTimes 的值 LOGD("clientTimes values:"); for (jint i = 0; i < listSize; i++) { jobject longObject = env->CallObjectMethod(clientTimes, getMethodId, i); jlong clientTime = env->CallLongMethod(longObject, env->GetMethodID(env->FindClass("java/lang/Long"), "longValue", "()J")); LOGD(" [%d] %lld", i, clientTime); env->DeleteLocalRef(longObject); } } else { LOGD("clientTimes is null, skipping delay calculation."); }
// 打印 requestTimestamps 的值
LOGD("requestTimestamps values:");
std::vector<long long> requestTimestampsVector;
{
std::lock_guardstd::mutex lock(requestTimestampsMutex);
std::queue<long long> tempQueue = requestTimestamps; // 复制队列以避免死锁
while (!tempQueue.empty()) {
requestTimestampsVector.push_back(tempQueue.front());
tempQueue.pop();
}
}
for (size_t i = 0; i < requestTimestampsVector.size(); i++) {
LOGD(" [%zu] %lld", i, requestTimestampsVector[i]);
}
// 从后向前遍历并计算单向时延
int requestTimestampsSize = requestTimestampsVector.size();
textif (listSize > 0){ for (int i = requestTimestampsSize - 1; i >= 0; i--) { long long receiveTimestamp = requestTimestampsVector[i]; // 获取对应的 clientTime int clientTimeIndex = listSize - (requestTimestampsSize - i); if (clientTimeIndex < 0) { LOGE("Not enough elements in clientTimes!"); break; } jobject longObject = env->CallObjectMethod(clientTimes, getMethodId, clientTimeIndex); jlong clientTime = env->CallLongMethod(longObject, env->GetMethodID(env->FindClass("java/lang/Long"), "longValue", "()J")); // 计算差值并打印 long long diff = receiveTimestamp - clientTime; LOGD("clientTime[%d]: %lld, receiveTimestamp[%d]: %lld, diff: %lld", clientTimeIndex, clientTime, i, receiveTimestamp, diff); env->DeleteLocalRef(longObject); // 释放局部引用 } }
// 清空 requestTimestamps 队列
{
std::lock_guardstd::mutex lock(requestTimestampsMutex);
std::queue<long long> empty;
std::swap(requestTimestamps, empty);
}
textLOGD("receivedCommands: %d", receivedCommands); LOGD("receivedPackets: %d", receivedPackets); LOGD("receivedBytes: %lld", receivedBytes); LOGD("receivedRate: %.2f", receivedRate); // receivedCommands jobject receivedCommandsObject = env->GetObjectField(metricsObject, receivedCommandsField); env->CallVoidMethod(receivedCommandsObject, setIntMethodId, receivedCommands); // receivedPackets jobject receivedPacketsObject = env->GetObjectField(metricsObject, receivedPacketsField); env->CallVoidMethod(receivedPacketsObject, setIntMethodId, receivedPackets); // receivedBytes jobject receivedBytesObject = env->GetObjectField(metricsObject, receivedBytesField); env->CallVoidMethod(receivedBytesObject, setLongMethodId, receivedBytes); // receivedRate jobject receivedRateObject = env->GetObjectField(metricsObject, receivedRateField); env->CallVoidMethod(receivedRateObject, setDoubleMethodId, receivedRate); // --- 接收速率历史数据 --- jobject receivedRateHistoryObject = env->GetObjectField(metricsObject, receivedRateHistoryField); env->CallVoidMethod(receivedRateHistoryObject, clearMethodId); // 锁定 receivedPacketsPerSecondMutex std::lock_guard<std::mutex> lock(receivedPacketsPerSecondMutex); for (int i = 0; i < receivedPacketsPerSecond.size(); ++i) { LOGD("receivedPacketsPerSecond[%d]: %d", i, receivedPacketsPerSecond[i]); int packetsCount = receivedPacketsPerSecond[i]; // 创建 Integer 对象 jobject integerObject = env->NewObject(integerClass, integerConstructorId, packetsCount); // 将 Integer 对象添加到 List env->CallBooleanMethod(receivedRateHistoryObject, addMethodId, integerObject); // 删除局部引用,防止内存泄漏 env->DeleteLocalRef(integerObject); } LOGD("receivedPacketsPerSecond数组: %zu", receivedPacketsPerSecond.size());
}
class DataDisplayFragment : DialogFragment() {
textcompanion object { fun newInstance( protocol: ProtocolType, isClient: Boolean, metrics: CommunicationMetrics ): DataDisplayFragment { val fragment = DataDisplayFragment() val args = Bundle() args.putSerializable("protocol", protocol) args.putBoolean("isClient", isClient) args.putSerializable("metrics", metrics) fragment.arguments = args return fragment } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = AlertDialog.Builder(requireActivity()) val inflater = requireActivity().layoutInflater val view = inflater.inflate(R.layout.fragment_data_display, null) // 获取传递的参数 val protocol = arguments?.getSerializable("protocol") as ProtocolType? val isClient = arguments?.getBoolean("isClient") != false val metrics = arguments?.getSerializable("metrics") as CommunicationMetrics? // 获取指标数据并展示 if (metrics != null) { updateMetricsView(metrics, view) configureChart(metrics, view, requireContext()) val recyclerView = view.findViewById<RecyclerView>(R.id.rv_test_items) recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = TestItemAdapter(metrics.testItemResults) } else { Toast.makeText( context, "No metrics found for this protocol and client state.", Toast.LENGTH_SHORT ).show() } // 设置弹窗视图 builder.setView(view) // 创建对话框 val dialog = builder.create() // 设置对话框居中显示 dialog.window?.setGravity(Gravity.CENTER) // 添加关闭按钮 view.findViewById<Button>(R.id.btn_close).setOnClickListener { dismiss() } // 添加导出按钮 view.findViewById<Button>(R.id.btn_export_report).setOnClickListener { exportMetricsToCsv(metrics ?: CommunicationMetrics(), requireContext()) } return dialog } private fun updateMetricsView(metrics: CommunicationMetrics, view: View) { view.findViewById<TextView>(R.id.tv_sent_commands).text = "发送命令个数:${metrics.sentCommands}" view.findViewById<TextView>(R.id.tv_sent_packets).text = "发送包数:${metrics.sentPackets}" view.findViewById<TextView>(R.id.tv_sent_bytes).text = "发送字节数:${metrics.sentBytes} bytes" view.findViewById<TextView>(R.id.tv_sent_rate).text = "发送速率:${String.format("%.2f", metrics.sentRate.get())} B/s" view.findViewById<TextView>(R.id.tv_received_commands).text = "接收命令个数:${metrics.receivedCommands}" view.findViewById<TextView>(R.id.tv_received_packets).text = "接收包数:${metrics.receivedPackets}" view.findViewById<TextView>(R.id.tv_received_bytes).text = "接收字节数:${metrics.receivedBytes} bytes" view.findViewById<TextView>(R.id.tv_received_rate).text = "接收速率:${String.format("%.2f", metrics.receivedRate.get())} B/s" val delayStats = metrics.delayStatistics if (delayStats != null) { // 单向时延 view.findViewById<TextView>(R.id.tv_send_max_delay).text = "单向最大时延:${String.format("%.2f", delayStats["oneWayMaxDelay"])} ms" view.findViewById<TextView>(R.id.tv_send_min_delay).text = "单向最小时延:${String.format("%.2f", delayStats["oneWayMinDelay"])} ms" view.findViewById<TextView>(R.id.tv_send_avg_delay).text = "单向平均时延:${String.format("%.2f", delayStats["oneWayAvgDelay"])} ms" // 往返时延 view.findViewById<TextView>(R.id.tv_receive_max_delay).text = "往返最大时延:${String.format("%.2f", delayStats["roundTripMaxDelay"])} ms" view.findViewById<TextView>(R.id.tv_receive_min_delay).text = "往返最小时延:${String.format("%.2f", delayStats["roundTripMinDelay"])} ms" view.findViewById<TextView>(R.id.tv_receive_avg_delay).text = "往返平均时延:${String.format("%.2f", delayStats["roundTripAvgDelay"])} ms" } view.findViewById<TextView>(R.id.tv_device_registration_success_rate).text = "设备注册成功率:${String.format("%.2f", metrics.deviceRegistrationSuccessRate.get())}%" view.findViewById<TextView>(R.id.tv_device_registration_average_delay).text = "设备注册平均延时:${String.format("%.2f", metrics.deviceRegistrationAverageDelay.get())} ms" view.findViewById<TextView>(R.id.tv_control_command_success_rate).text = "控制指令成功率:${String.format("%.2f", metrics.controlCommandSuccessRate.get())}%" view.findViewById<TextView>(R.id.tv_control_command_retry_count).text = "控制指令重传次数:${metrics.controlCommandRetryCount}" }
// // 函数用于生成模拟数据
// fun generateMockData(size: Int): List<Entry> {
// val entries = mutableListOf<Entry>()
// for (i in 0 until size) {
// val xValue = i.toFloat()
// // 生成一个基于正弦波形但加入随机偏差的Y值
// val yValue = sin(xValue / 10f) + Random.nextFloat() * 100
// entries.add(Entry(xValue, yValue))
// }
// return entries
// }
text// 配置图表的函数,现在接受 metrics 参数 private fun configureChart(metrics: CommunicationMetrics, view: View, context: Context) { val lineChart = view.findViewById<LineChart>(R.id.chart) // 获取发送速率和接收速率历史数据 val sentRateEntries = metrics.sentRateHistory.mapIndexed { index, value -> Entry( index.toFloat(), value.toFloat() ) } val receivedRateEntries = metrics.receivedRateHistory.mapIndexed { index, value -> Entry( index.toFloat(), value.toFloat() ) } Log.d("ChartData", "Sent Rate History: ${metrics.sentRateHistory}") Log.d("ChartData", "Received Rate History: ${metrics.receivedRateHistory}") // 创建数据集 val sentRateDataSet = LineDataSet(sentRateEntries, "发送速率").apply { color = Color.BLUE valueTextColor = Color.WHITE valueTextSize = 10f mode = LineDataSet.Mode.CUBIC_BEZIER setCircleColor(Color.BLUE) lineWidth = 2f // 增加线条宽度 enableDashedLine(10f, 5f, 0f) // 使用虚线 } val receivedRateDataSet = LineDataSet(receivedRateEntries, "接收速率").apply { color = Color.GREEN valueTextColor = Color.WHITE valueTextSize = 10f mode = LineDataSet.Mode.CUBIC_BEZIER setCircleColor(Color.GREEN) } // 将数据集添加到图表数据中 val lineData = LineData(sentRateDataSet, receivedRateDataSet) lineChart.data = lineData // 设置图表样式 lineChart.xAxis.apply { valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return "${value.toInt()} s" } } position = XAxis.XAxisPosition.BOTTOM axisMinimum = 0f axisMaximum = 300f granularity = 1f // 每秒一个标签 setLabelCount(5, true) // 保证有足够的标签 isGranularityEnabled = true } lineChart.axisLeft.apply { textColor = Color.WHITE axisMinimum = 0f granularity = 1f // 设置Y轴刻度间隔为1,确保显示整数 valueFormatter = object : ValueFormatter() { override fun getFormattedValue(value: Float): String { return value.toInt().toString() } } } lineChart.axisRight.isEnabled = false lineChart.legend.textColor = Color.WHITE lineChart.setBackgroundColor(ContextCompat.getColor(context, android.R.color.darker_gray)) // 启用缩放和拖动 lineChart.setTouchEnabled(true) lineChart.isDragEnabled = true lineChart.setScaleEnabled(true) lineChart.setPinchZoom(false) // 禁用双指缩放 lineChart.setDrawGridBackground(false) // 设置初始缩放比例 lineChart.zoomToCenter(1f, 1f) // 设置最大和最小可见范围 lineChart.setVisibleXRangeMaximum(300f) // 设置最大可见范围 lineChart.setVisibleXRangeMinimum(1f) // 设置最小可见范围为1秒 // 强制刷新标签 lineChart.xAxis.setLabelCount(6, true) lineChart.invalidate() // 刷新图表 } private fun exportMetricsToCsv(metrics: CommunicationMetrics, context: Context) { val metricsDir = File(context.getExternalFilesDir(null), "metrics") if (!metricsDir.exists()) { metricsDir.mkdirs() } val currentDateTime = getCurrentDateTime() val fileName = "$currentDateTime.csv" try { // 在 FileOutputStream 中使用 metricsDir 和 fileName FileOutputStream(File(metricsDir, fileName)).use { outputStream -> // 写入 CSV 文件头 outputStream.write("发送命令个数,发送包数,发送字节数,发送速率,单向最大时延,单向最小时延,单向平均时延,接收命令个数,接收包数,接收字节数,接收速率,往返最大时延,往返最小时延,往返平均时延,设备注册成功率,设备注册平均延时,控制指令成功率,控制指令重传次数,测试项结果\n".toByteArray()) // 创建主要指标数据的字符串 val mainMetricsLine = "${metrics.sentCommands},${metrics.sentPackets},${metrics.sentBytes},${metrics.sentRate},${metrics.delayStatistics?.get("oneWayMaxDelay")},${metrics.delayStatistics?.get("oneWayMinDelay")},${metrics.delayStatistics?.get("oneWayAvgDelay")},${metrics.receivedCommands},${metrics.receivedPackets},${metrics.receivedBytes},${metrics.receivedRate},${metrics.delayStatistics?.get("roundTripMaxDelay")},${metrics.delayStatistics?.get("roundTripMinDelay")},${metrics.delayStatistics?.get("roundTripAvgDelay")},${metrics.deviceRegistrationSuccessRate},${metrics.deviceRegistrationAverageDelay},${metrics.controlCommandSuccessRate},${metrics.controlCommandRetryCount}," // 将测试项结果拼接成字符串,使用分号分隔每个测试项 val testItemResultsString = metrics.testItemResults.entries.joinToString(",") { "${it.key}:成功${it.value.first}失败${it.value.second}" } // 将所有数据拼接成一行 val fullLine = mainMetricsLine + testItemResultsString + "\n" // 添加换行符表示一行结束 // 一次性写入所有数据 outputStream.write(fullLine.toByteArray()) } // 使用 ACTION_SEND Intent 分享文件 val file = File(metricsDir, fileName) val intent = Intent(Intent.ACTION_SEND) intent.type = "text/csv" val fileUri = FileProvider.getUriForFile( context, "com.dotouch.industrial.fileprovider", file ) intent.putExtra(Intent.EXTRA_STREAM, fileUri) intent.putExtra(Intent.EXTRA_SUBJECT, "通信指标数据") intent.putExtra(Intent.EXTRA_TEXT, "请查收附件中的通信指标数据。") startActivity(Intent.createChooser(intent, "选择邮件应用")) Toast.makeText(context, "指标数据已保存到: ${file.absolutePath}", Toast.LENGTH_LONG) .show() } catch (e: Exception) { Toast.makeText(context, "保存指标数据失败: ${e.message}", Toast.LENGTH_SHORT).show() } } private fun getCurrentDateTime(): String { val formattedDateTime = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val dateTime = LocalDateTime.now() val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss") dateTime.format(formatter) } else { val dateTime = Calendar.getInstance() val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()) simpleDateFormat.format(dateTime.time) } return formattedDateTime }
}
目前的单向时延、往返时延的计算方式不应该是通过自定义Ntp的请求来实现的,而是应该在s7的c++代码中的读写功能中进行统计,然后在DataDisplayFragment中进行展示,理解我的需求吗?如何做,给我详细的修改后的代码
Understood. To shift the calculation of single-way delay and round-trip delay (RTT) from the custom NTP implementation to being based directly on the S7 read/write operations within your C++ code, you'll need to make several modifications across both your C++ and Kotlin/Java codebases. Here's a comprehensive guide to achieve this:
updateMetrics
FunctionCurrent Function Signature:
cppvoid updateMetrics(JNIEnv *env, jobject thiz, int sentBytes, int receivedBytes, long long roundTripDelay)
Modified Function Signature:
cppvoid updateMetrics(JNIEnv *env, jobject thiz, int sentBytes, int receivedBytes, long long roundTripDelay, double singleWayDelay)
Modified Function Implementation:
cppvoid updateMetrics(JNIEnv *env, jobject thiz, int sentBytes, int receivedBytes, long long roundTripDelay, double singleWayDelay) { // Existing metric updates... // Get the CommunicationMetrics object jclass s7ClientClass = env->GetObjectClass(thiz); jfieldID metricsFieldId = env->GetFieldID(s7ClientClass, "metrics", "Lcom/dotouch/industrial/bean/CommunicationMetrics;"); jobject metricsObject = env->GetObjectField(thiz, metricsFieldId); // Get the CommunicationMetrics class jclass metricsClass = env->GetObjectClass(metricsObject); // Get AtomicDouble class and its set method jclass atomicDoubleClass = env->FindClass("com/dotouch/industrial/bean/AtomicDouble"); jmethodID setDoubleMethodId = env->GetMethodID(atomicDoubleClass, "set", "(D)V"); if (setDoubleMethodId == nullptr) { LOGE("Failed to get set method for AtomicDouble."); return; } // Update singleWayDelay jfieldID singleWayDelayFieldId = env->GetFieldID(metricsClass, "singleWayDelay", "Lcom/dotouch/industrial/bean/AtomicDouble;"); if (singleWayDelayFieldId == nullptr) { LOGE("Failed to get field ID for singleWayDelay."); return; } jobject singleWayDelayObject = env->GetObjectField(metricsObject, singleWayDelayFieldId); env->CallVoidMethod(singleWayDelayObject, setDoubleMethodId, singleWayDelay); // Update roundTripDelay jfieldID roundTripDelayFieldId = env->GetFieldID(metricsClass, "roundTripDelay", "Lcom/dotouch/industrial/bean/AtomicDouble;"); if (roundTripDelayFieldId == nullptr) { LOGE("Failed to get field ID for roundTripDelay."); return; } jobject roundTripDelayObject = env->GetObjectField(metricsObject, roundTripDelayFieldId); env->CallVoidMethod(roundTripDelayObject, setDoubleMethodId, static_cast<double>(roundTripDelay)); }
readData
and writeData
JNI FunctionsFor readData
:
Before Modification:
cppupdateMetrics(env, obj, 0, 1, roundTripDelay);
After Modification:
cppdouble singleWayDelay = static_cast<double>(roundTripDelay) / 2.0; updateMetrics(env, obj, 0, 1, roundTripDelay, singleWayDelay);
For writeData
:
Before Modification:
cppupdateMetrics(env, obj, totalSize, 0, roundTripDelay);
After Modification:
cppdouble singleWayDelay = static_cast<double>(roundTripDelay) / 2.0; updateMetrics(env, obj, totalSize, 0, roundTripDelay, singleWayDelay);
Complete Example for readData
:
cppextern "C" JNIEXPORT jbyteArray JNICALL Java_com_dotouch_industrial_s7_S7Client_readData(JNIEnv *env, jobject obj, jint dbNumber, jint startAddress, jstring dataType, jint bitAddress, jint quantity) { // Existing code... // Calculate RTT and single-way delay long long endTime = getCurrentTimestamp(); long long roundTripDelay = endTime - startTime; double singleWayDelay = static_cast<double>(roundTripDelay) / 2.0; // Update metrics with the new delay information updateMetrics(env, obj, 0, 1, roundTripDelay, singleWayDelay); // Existing code... }
Complete Example for writeData
:
cppextern "C" JNIEXPORT jboolean JNICALL Java_com_dotouch_industrial_s7_S7Client_writeData(JNIEnv *env, jobject obj, jint dbNumber, jint startAddress, jstring dataType, jint bitAddress, jint quantity, jbyteArray value) { // Existing code... // Calculate RTT and single-way delay long long endTime = getCurrentTimestamp(); long long roundTripDelay = endTime - startTime; double singleWayDelay = static_cast<double>(roundTripDelay) / 2.0; // Update metrics with the new delay information updateMetrics(env, obj, totalSize, 0, roundTripDelay, singleWayDelay); // Existing code... }
CommunicationMetrics
ClassEnsure that your CommunicationMetrics
class includes fields for singleWayDelay
and roundTripDelay
. Here's how you can modify it:
kotlin// Assuming this is your existing CommunicationMetrics class import java.io.Serializable import java.util.concurrent.atomic.AtomicDouble import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong data class CommunicationMetrics( val sentCommands: AtomicInteger = AtomicInteger(0), val sentPackets: AtomicInteger = AtomicInteger(0), val sentBytes: AtomicLong = AtomicLong(0), val sentRate: AtomicDouble = AtomicDouble(0.0), val receivedCommands: AtomicInteger = AtomicInteger(0), val receivedPackets: AtomicInteger = AtomicInteger(0), val receivedBytes: AtomicLong = AtomicLong(0), val receivedRate: AtomicDouble = AtomicDouble(0.0), val singleWayDelay: AtomicDouble = AtomicDouble(0.0), val roundTripDelay: AtomicDouble = AtomicDouble(0.0), val testItemResults: MutableMap<String, Pair<Int, Int>> = mutableMapOf() ) : Serializable
Note: Ensure that you have an AtomicDouble
class implemented, as Java doesn't provide one by default. Here's a simple implementation:
kotlinimport java.util.concurrent.atomic.AtomicReference class AtomicDouble(initialValue: Double) : Serializable { private val value = AtomicReference(initialValue) fun set(newValue: Double) { value.set(newValue) } fun get(): Double = value.get() }
DataDisplayFragment
to Show DelaysModify your DataDisplayFragment
to include views that display the singleWayDelay
and roundTripDelay
.
Add TextView
elements to your fragment_data_display.xml
(or corresponding layout file) to display the delays:
xml<!-- Example additions to your layout --> <TextView android:id="@+id/tv_single_way_delay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="单向时延:0.0 ms" /> <TextView android:id="@+id/tv_round_trip_delay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="往返时延:0.0 ms" />
DataDisplayFragment.kt
Modify the updateMetricsView
function to display the new delay metrics:
kotlinprivate fun updateMetricsView(metrics: CommunicationMetrics, view: View) { // Existing metric updates... // Single-way Delay view.findViewById<TextView>(R.id.tv_single_way_delay).text = "单向时延:${String.format("%.2f", metrics.singleWayDelay.get())} ms" // Round-trip Delay view.findViewById<TextView>(R.id.tv_round_trip_delay).text = "往返时延:${String.format("%.2f", metrics.roundTripDelay.get())} ms" // Existing metric updates... }
S7ClientFragment
Since delays are now being calculated based on read/write operations, you should remove or disable the NTP client to prevent redundant or conflicting delay calculations.
In your S7ClientFragment
, remove the following:
The ntpClient
field:
kotlin// Remove or comment out private var ntpClient: NtpClient? = null
The initialization and usage of NtpClient
in connectToServer
:
kotlinlifecycleScope.launch(Dispatchers.IO) { try { ntpClient = NtpClient(ip, port) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ntpClient?.calculateTimeDifference() } } catch (e: Exception) { ntpClient = null Log.e("ModbusClientFragment", "Error calculating time difference: ${e.message}") } }
Remove calls to ntpClient?.sendTime()
in performReadOperation
and performWriteOperation
.
If there are any UI elements or additional logic related to NTP (like showing delay statistics from NTP), remove or disable them to clean up the interface.
Make sure that the C++ side correctly manages thread safety when updating metrics, especially since JNI calls can be made from different threads.
updateMetrics
Functioncppvoid updateMetrics(JNIEnv *env, jobject thiz, int sentBytes, int receivedBytes, long long roundTripDelay, double singleWayDelay) { // Existing metric updates... // Get the CommunicationMetrics object jclass s7ClientClass = env->GetObjectClass(thiz); jfieldID metricsFieldId = env->GetFieldID(s7ClientClass, "metrics", "Lcom/dotouch/industrial/bean/CommunicationMetrics;"); jobject metricsObject = env->GetObjectField(thiz, metricsFieldId); // Get the CommunicationMetrics class jclass metricsClass = env->GetObjectClass(metricsObject); // Get AtomicDouble class and its set method jclass atomicDoubleClass = env->FindClass("com/dotouch/industrial/bean/AtomicDouble"); jmethodID setDoubleMethodId = env->GetMethodID(atomicDoubleClass, "set", "(D)V"); if (setDoubleMethodId == nullptr) { LOGE("Failed to get set method for AtomicDouble."); return; } // Update singleWayDelay jfieldID singleWayDelayFieldId = env->GetFieldID(metricsClass, "singleWayDelay", "Lcom/dotouch/industrial/bean/AtomicDouble;"); if (singleWayDelayFieldId == nullptr) { LOGE("Failed to get field ID for singleWayDelay."); return; } jobject singleWayDelayObject = env->GetObjectField(metricsObject, singleWayDelayFieldId); env->CallVoidMethod(singleWayDelayObject, setDoubleMethodId, singleWayDelay); // Update roundTripDelay jfieldID roundTripDelayFieldId = env->GetFieldID(metricsClass, "roundTripDelay", "Lcom/dotouch/industrial/bean/AtomicDouble;"); if (roundTripDelayFieldId == nullptr) { LOGE("Failed to get field ID for roundTripDelay."); return; } jobject roundTripDelayObject = env->GetObjectField(metricsObject, roundTripDelayFieldId); env->CallVoidMethod(roundTripDelayObject, setDoubleMethodId, static_cast<double>(roundTripDelay)); // Existing code to update other metrics... }
CommunicationMetrics
Classkotlinimport java.io.Serializable import java.util.concurrent.atomic.AtomicDouble import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong data class CommunicationMetrics( val sentCommands: AtomicInteger = AtomicInteger(0), val sentPackets: AtomicInteger = AtomicInteger(0), val sentBytes: AtomicLong = AtomicLong(0), val sentRate: AtomicDouble = AtomicDouble(0.0), val receivedCommands: AtomicInteger = AtomicInteger(0), val receivedPackets: AtomicInteger = AtomicInteger(0), val receivedBytes: AtomicLong = AtomicLong(0), val receivedRate: AtomicDouble = AtomicDouble(0.0), val singleWayDelay: AtomicDouble = AtomicDouble(0.0), val roundTripDelay: AtomicDouble = AtomicDouble(0.0), val testItemResults: MutableMap<String, Pair<Int, Int>> = mutableMapOf() ) : Serializable
DataDisplayFragment
kotlinclass DataDisplayFragment : DialogFragment() { companion object { fun newInstance( protocol: ProtocolType, isClient: Boolean, metrics: CommunicationMetrics ): DataDisplayFragment { val fragment = DataDisplayFragment() val args = Bundle() args.putSerializable("protocol", protocol) args.putBoolean("isClient", isClient) args.putSerializable("metrics", metrics) fragment.arguments = args return fragment } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = AlertDialog.Builder(requireActivity()) val inflater = requireActivity().layoutInflater val view = inflater.inflate(R.layout.fragment_data_display, null) // Get arguments val protocol = arguments?.getSerializable("protocol") as ProtocolType? val isClient = arguments?.getBoolean("isClient") ?: false val metrics = arguments?.getSerializable("metrics") as CommunicationMetrics? // Display metrics if (metrics != null) { updateMetricsView(metrics, view) configureChart(metrics, view, requireContext()) val recyclerView = view.findViewById<RecyclerView>(R.id.rv_test_items) recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = TestItemAdapter(metrics.testItemResults) } else { Toast.makeText( context, "No metrics found for this protocol and client state.", Toast.LENGTH_SHORT ).show() } // Set dialog view builder.setView(view) // Create dialog val dialog = builder.create() // Center the dialog dialog.window?.setGravity(Gravity.CENTER) // Close button view.findViewById<Button>(R.id.btn_close).setOnClickListener { dismiss() } // Export button view.findViewById<Button>(R.id.btn_export_report).setOnClickListener { exportMetricsToCsv(metrics ?: CommunicationMetrics(), requireContext()) } return dialog } private fun updateMetricsView(metrics: CommunicationMetrics, view: View) { view.findViewById<TextView>(R.id.tv_sent_commands).text = "发送命令个数:${metrics.sentCommands}" view.findViewById<TextView>(R.id.tv_sent_packets).text = "发送包数:${metrics.sentPackets}" view.findViewById<TextView>(R.id.tv_sent_bytes).text = "发送字节数:${metrics.sentBytes} bytes" view.findViewById<TextView>(R.id.tv_sent_rate).text = "发送速率:${String.format("%.2f", metrics.sentRate.get())} B/s" view.findViewById<TextView>(R.id.tv_received_commands).text = "接收命令个数:${metrics.receivedCommands}" view.findViewById<TextView>(R.id.tv_received_packets).text = "接收包数:${metrics.receivedPackets}" view.findViewById<TextView>(R.id.tv_received_bytes).text = "接收字节数:${metrics.receivedBytes} bytes" view.findViewById<TextView>(R.id.tv_received_rate).text = "接收速率:${String.format("%.2f", metrics.receivedRate.get())} B/s" // Single-way and Round-trip Delays view.findViewById<TextView>(R.id.tv_single_way_delay).text = "单向时延:${String.format("%.2f", metrics.singleWayDelay.get())} ms" view.findViewById<TextView>(R.id.tv_round_trip_delay).text = "往返时延:${String.format("%.2f", metrics.roundTripDelay.get())} ms" // Existing metric updates... // Delay Statistics val delayStats = metrics.delayStatistics if (delayStats != null) { // Existing delay stats updates... } // Other metrics... } // Existing configureChart and other functions... }
S7ClientFragment
Since delays are now handled within the S7 read/write operations, you should remove the NTP client to prevent any unnecessary computations or conflicts.
Remove the NtpClient
field:
kotlin// Remove or comment out this line private var ntpClient: NtpClient? = null
Remove NTP Client Initialization in connectToServer
:
kotlin// Remove or comment out the entire lifecycleScope.launch block related to NTP lifecycleScope.launch(Dispatchers.IO) { try { ntpClient = NtpClient(ip, port) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ntpClient?.calculateTimeDifference() } } catch (e: Exception) { ntpClient = null Log.e("ModbusClientFragment", "Error calculating time difference: ${e.message}") } }
Remove Calls to ntpClient?.sendTime()
:
kotlin// In performReadOperation and performWriteOperation, remove or comment out the following lines if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ntpClient?.isReadyToSendTime() == true) { ntpClient?.sendTime() }
Remove NTP Statistics and Metrics Display:
DataDisplayFragment
.Your C++ code now calculates singleWayDelay
and roundTripDelay
during each read/write operation and updates the CommunicationMetrics
object via JNI. Ensure that these metrics are correctly passed and displayed in your Kotlin/Java code.
After making these changes, thoroughly test your application to ensure:
Logging: Continue using comprehensive logging in both C++ and Kotlin/Java to trace and debug the delay calculations.
Error Handling: Ensure that any potential errors in JNI calls or metric updates are gracefully handled to prevent crashes or inconsistent states.
Performance: Monitor the performance impact of the added delay calculations, especially if read/write operations are frequent.
By following these steps, you should be able to transition the delay calculations from a separate NTP mechanism to being directly integrated within your S7 read/write operations, providing more accurate and context-specific timing metrics.