class NtpClient(serverAddress: String, serverPort: Int) {
companion object {
private const val TAG = "NtpClient"
}
private 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"
}
private 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() {
private 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)
}
}
}
// 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
private 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("$currentDateTime: 时间戳功能已${if (isChecked) "启用" else "禁用"}")
// updateServerMessages()
// }
adapter = 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);
}
if (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);
// 更新指标数据
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];
// 创建 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];
// 创建 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);
metrics.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;
}
metrics.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) {
LOGD("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; // 返回错误
}
uint8_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无效
}
uint8_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;
if (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]);
}
// 根据数据类型解析并打印数据 (你需要根据实际情况修改这部分代码)
// 例如,如果客户端写入的是一个 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;
}
}
if (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); // 毫秒转换为秒
// 使用 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;
}
// 获取 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;
}
// 初始化 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_guard<std::mutex> lock1(dataBlockMutex); // 假设你使用了 dataBlockMutex 来保护数据访问
// 更新与数据块注册无关的指标数据
metrics.receivedCommands++;
metrics.receivedPackets++;
metrics.receivedBytes += PTag->Size;
// 计算接收速率
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; // 或其他默认值
// }
// }
// 检查写入范围是否越界 (使用更新后的 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) {
auto *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;
// }
// 设置读写区域回调函数
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);
Server->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;
}
DataBlockInfo 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_guard<std::mutex> lock(dataBlockMutex); // 使用互斥锁保护 registeredDataBlocks
// 查找要反注册的数据块
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;");
jclass 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_guard<std::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();
if (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_guard<std::mutex> lock(requestTimestampsMutex);
std::queue<long long> empty;
std::swap(requestTimestamps, empty);
}
LOGD("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() {
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)
// 获取传递的参数
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
// }
// 配置图表的函数,现在接受 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中进行展示,理解我的需求吗?如何做,给我详细的修改后的代码