项目要求读取设备(Iec61850协议)的数据并且实现开关的控制,我主要使用iec61850bean框架来实现。
iec61850bean框架的主页https://www.beanit.com/iec-61850/
项目地址https://github.com/beanit/iec61850bean
Android 作为客户端,要读取的设备装置作为服务端。
在项目/app/build.gradle中添加
implementation 'com.beanit:iec61850bean:1.9.0'
客户端连接:
import com.beanit.iec61850bean.ClientAssociation
import com.beanit.iec61850bean.ClientEventListener
import com.beanit.iec61850bean.ClientSap
import java.net.InetAddress
class Iec61850Client {
private var IP = ""
private var PORT = 102
private var clientAssociation: ClientAssociation? = null
fun startClient(IP:String,PORT:Int,listener: ClientEventListener?): ClientAssociation?{
val clientSap = ClientSap()
// clientSap.setResponseTimeout(2000)
clientAssociation = clientSap.associate(InetAddress.getByName(IP), PORT, "", listener)
this.IP = IP
this.PORT = PORT
return clientAssociation
}
fun disconnect() {
if (clientAssociation != null && clientAssociation!!.isOpen) {
clientAssociation!!.close()
}
}
val isConnect: Boolean
get() = clientAssociation != null && clientAssociation!!.isOpen
val getIp: String
get() = IP
val getPort: Int
get() = PORT
}
连接至设备服务器:
val iecClient = Iec61850Client()
try {
val clientAssociation = iecClient?.startClient(ip, port.toInt(), listener)
val serverModel = clientAssociation?.retrieveModel()
} catch (e: Exception) {
callback.onFailure(e)
e.printStackTrace()
iecClient?.disconnect()
}
其中clientAssociation?.retrieveModel()用来获取服务器模型。
然后如果想通过上传报告来更新遥测数据,则开启报告
var measReportRef = "TEMPLATEMEAS/LLN0.urcbAin01"
private fun enableReporting() {
try {
val urcb: Urcb? = serverModel?.getUrcb(measReportRef)
clientAssociation?.getRcbValues(urcb)
// iecClient?.clientAssociation?.reserveUrcb(urcb)
clientAssociation?.enableReporting(urcb)
clientAssociation?.startGi(urcb)
} catch (e: Exception) {
e.printStackTrace()
println("设置失败" + e.message)
}
}
开启报告后你会收到定时发送的报告数据更新:
override fun newReport(report: Report) {
println("got a report.${report}")
try {
val list = report.values
for (itemModel in list) {
val dataRef = itemModel.reference.toString()
println("dataRef:$dataRef")
when (report.dataSetRef) {
"TEMPLATEMEAS/LLN0.dsAin" -> {
when {
dataRef.contains(".HzBus.mag.f") || dataRef.contains(".Hz.mag.f") -> {//SIUnit == 33
if (itemModel is BdaFloat32) {
mydata.f = itemModel.float.toTwoDigitDouble()
}
}
dataRef.contains(".TotPF.mag.f") -> {
if (itemModel is BdaFloat32) {
mydata.PF = itemModel.float.toTwoDigitDouble()
}
}
dataRef.contains(".TotW.mag.f") -> {//单位W 1000000
if (itemModel is BdaFloat32) {
mydata.activePower = itemModel.float.toTwoDigitDouble() * 1000
}
}
}
mydata.time = System.currentTimeMillis()
}
}
}
uiHandler.post {
showYCView()
}
} catch (e: ServiceError) {
e.printStackTrace()
}
}
我们也可以通过路径来主动读取:
clientAssociation.getAllDataValues()读取所有路径和数据,调用后所有设备服务器的实时数据都会更新至serverModel变量中。
我们也可以只取我们想要的部分值(通常是读取一些开关量状态值),先定义路径map
private var dataMapSt = mutableMapOf<String, String>()
private fun initFCDAMap() {
dataMapSt.clear()
dataMapSt["DlqState"] = "TEMPLATECTRL/INDGGIO1.Ind1.stVal"
}
然后通过clientAssociation.GetDataValues()来读取
private fun readBrcbRelayDin() {
lifecycleScope.launch(Dispatchers.IO) {
for (item in dataMapSt) {
val dataRef = item.value
val node = serverModel?.findModelNode(dataRef, Fc.ST)
if (node != null) {
try {
clientAssociation?.getDataValues(node as FcModelNode?) // 只获取这一个节点的值
if (node is BdaBoolean) {
when (item.key) {
"DlqState" -> {val dlqState = node.value}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
以上是读取数据的两种方式。
关于开关量的控制,我在项目中使用的是SBOw控制,即带值选择,然后再控制,但是Iec61850客户端只实现了普通的不带值选择并控制。所以需要我们自己实现带值选择。
private fun selectWithValue(
association: ClientAssociation?,
controlObject: FcModelNode,
controlValue: Boolean
): Boolean {
var result = false
try {
// 获取控制对象中用于携带值的SBOw属性(例如一个BdaVisibleString节点)
val sbow = controlObject?.getChild("SBOw", Fc.CO) as? ConstructedDataAttribute
println(sbow)
sbow?.let {
(sbow.getChild("ctlVal") as BdaBoolean).value = controlValue
(sbow.getChild("origin").getChild("orCat") as BdaInt8).value = 3
(sbow.getChild("origin").getChild("orIdent") as BdaOctetString).value =
ConvertUtils.string2Bytes("myClient")
(sbow.getChild("ctlNum") as BdaInt8U).value = 1.toShort()
(sbow.getChild("T") as BdaTimestamp).instant = Instant.now()
association?.setDataValues(sbow)
result = true
}
} catch (e: Exception) {
e.printStackTrace()
}
return result
}
private fun operate(
association: ClientAssociation?,
controlObject: FcModelNode,
controlValue: Boolean
): Boolean {
var result = false
try {
val oper = controlObject.getChild("Oper", Fc.CO) as? ConstructedDataAttribute
oper?.let {
(oper.getChild("ctlVal") as BdaBoolean).value = controlValue
(oper.getChild("origin").getChild("orCat") as BdaInt8).value = 3
(oper.getChild("origin").getChild("orIdent") as BdaOctetString).value =
ConvertUtils.string2Bytes("myClient")
(oper.getChild("ctlNum") as BdaInt8U).value = 1.toShort()
(oper.getChild("T") as BdaTimestamp).instant = Instant.now()
association?.setDataValues(oper)
result = true
}
} catch (e: Exception) {
e.printStackTrace()
}
return result
}
以上是我们需要自己实现的带值选择并控制,然后调用上述的方法来控制开关。
//先找到要控制的节点
val sboNode =
serverModel?.findModelNode("TEMPLATECTRL/TC1CSWI1.Pos", Fc.CO) as FcModelNode
if (sboNode != null) {
try {
println("dlqSwitch61850dlqSwitch61850$controlValue ")
association?.getDataValues(sboNode as FcModelNode?)
val selectResult = selectWithValue(association, sboNode, controlValue)
if (selectResult) {
val operateResult = operate(association, sboNode, controlValue)
if (operateResult) {
callback.onSuccess(controlValue)
operationRecord.result = "操作成功"
GreenDaoApi.getGreenDaoApi().addOperationRecord(operationRecord)
} else {
callback.onFailure(controlValue,TipsException("断路器分合闸失败operateResult$operateResult"))
operationRecord.result = "操作失败"
GreenDaoApi.getGreenDaoApi().addOperationRecord(operationRecord)
}
} else {
callback.onFailure(controlValue,TipsException("断路器分合闸失败selectResult$selectResult"))
operationRecord.result = "操作失败"
GreenDaoApi.getGreenDaoApi().addOperationRecord(operationRecord)
}
} catch (e: Exception) {
e.printStackTrace()
callback.onFailure(controlValue,TipsException("断路器分合闸失败${e.message}"))
operationRecord.result = "操作失败"
GreenDaoApi.getGreenDaoApi().addOperationRecord(operationRecord)
}
}