添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

其中,130、133、134类型标识,都是充电桩通信规约中扩展的类型标识,这三个类型的信息对象部分,都是标准的一个信息对象,信息对象包含:信息对象地址、记录类型、业务数据。

而业务数据,针对每种记录类型,都是标准的数据格式,即自上到下的每个字段和字段长度都是标准的,基于这种情况,如果是你用Java去解析,你会如何解析?

我们要做的是一个插件,既要解析南网电动协议,还要把我们桩的自定义协议协议,或者反向转对象等,互转和反向转的交互涉及到几十处。

二 通常的解析方式

通常情况下,针对这种数据,我们可能会对每个记录类型中的每个字段一个一个字段读取,然后处理这些记录类型数据的地方,到处都是写了很多的读取逻辑,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**  
* 充电桩实时数据上传 * @param scuProtocolBody
*/
private void pileRealtime(ScuProtocolBody scuProtocolBody) {
log.info("收到充电桩实时数据!data:{}", scuProtocolBody.getData());
String data = scuProtocolBody.getData();
int startIndex = 0;
int endIndex = TRAN_SN_BYTES * 2;
String tranSn = data.substring(startIndex, endIndex);

startIndex = endIndex;
endIndex = startIndex + PILESN_BYTES * 2;
String pileSn = data.substring(startIndex, endIndex);

startIndex = endIndex;
endIndex = startIndex + GUNID_BYTES * 2;
String gunSn = data.substring(startIndex, endIndex);

startIndex = endIndex;
endIndex = startIndex + RESULT_BYTES * 2;
int gunStatus = StringUtils.hexToInt(data.substring(startIndex, endIndex));
PileOrderRealtimeVO realtimeVO = new PileOrderRealtimeVO().setTranSn(tranSn).setPileSn(pileSn).setGunSn(gunSn).setGunStatus(gunStatus);
if (GunStatusEnum.CHARGING.getCode() == gunStatus){
//如果是充电中,就需要把充电交易数据送到运营中心
startIndex = endIndex + 2 * 2; //跳过了"枪是否归位"和"是否插枪"的协议中不取的字节数
endIndex = startIndex + TWO_BYTES * 2;
int outVoltInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double outVolt = DoubleUtil.intToDoubleForMagn(outVoltInt, 10);

startIndex = endIndex;
endIndex = startIndex + TWO_BYTES * 2;
int outElecInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double outElec = DoubleUtil.intToDoubleForMagn(outElecInt, 10);

startIndex = endIndex + 9 * 2; //跳过了"枪线温度"和"枪线编码"的协议中不取的字节数
endIndex = startIndex + ONE_BYTES * 2;
int soc = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));

startIndex = endIndex + ONE_BYTES * 2; //跳过了协议中"电池组最高温度"不取的字节数
endIndex = startIndex + DATE_MIN_BYTES * 2;
int totalChargeTime = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
//剩余充电时间
startIndex = endIndex ;
endIndex = startIndex + DATE_MIN_BYTES * 2;
int residueChargeTime = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
//充电度数
startIndex = endIndex ;
endIndex = startIndex + FOUR_BYTES * 2;
int chargeDegInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double chargeDeg = DoubleUtil.intToDoubleForMagn(chargeDegInt, 10000);
//计损充电度数
startIndex = endIndex ;
endIndex = startIndex + FOUR_BYTES * 2;
int meterLossChargeDegInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double meterLossChargeDeg = DoubleUtil.intToDoubleForMagn(meterLossChargeDegInt, 10000);
//已充金额
startIndex = endIndex ;
endIndex = startIndex + FOUR_BYTES * 2;
int chargeAmountInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double chargeAmount = DoubleUtil.intToDoubleForMagn(chargeAmountInt, 10000);
//充电枪告警码
startIndex = endIndex ;
endIndex = startIndex + EIGHT_BYTES * 2;
//经历时段个数
startIndex = endIndex ;
endIndex = startIndex + ONE_BYTES * 2;
int timeFrameNum = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double totolElecFee = 0D;
double totolServiceFee = 0D;
for (int i = 1; i <= timeFrameNum; i++) {
startIndex = endIndex ;
endIndex = startIndex + (FOUR_BYTES + EIGHT_BYTES) * 2;//开始时间,结束时间,服务费单价,电费单价,总电量
startIndex = endIndex ;
endIndex = startIndex + (FOUR_BYTES) * 2;//总电费
int elecFeeInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double elecFee = DoubleUtil.intToDoubleForMagn(elecFeeInt, 10000);
totolElecFee += elecFee;
startIndex = endIndex ;
endIndex = startIndex + (FOUR_BYTES) * 2;//总服务费
int serviceFeeInt = StringUtils.hexToIntEndian(data.substring(startIndex, endIndex));
double serviceFee = DoubleUtil.intToDoubleForMagn(serviceFeeInt, 10000);
totolServiceFee += serviceFee;
}
realtimeVO.setOutVolt(outVolt).setOutElec(outElec).setSoc(soc).setTotalChargeTime(totalChargeTime).setResidueChargeTime(residueChargeTime).setChargeDeg(chargeDeg)
.setMeterLossChargeDeg(meterLossChargeDeg).setChargeAmount(chargeAmount).setElecFee(DoubleUtil.get4Point(totolElecFee)).setServiceFee(DoubleUtil.get4Point(totolServiceFee));
}
miniAppChargeOptService.chargeRealTime(realtimeVO);
}

以上这些代码,是对我们桩协议中信息体部分的解析代码,看了以上这段代码,不知道你是一种什么感觉,我就问你易读性如何?如果要修改容易改吗?

如果协议都是标准的,只需要解析这么一次,这样做其实问题也不大,关键是,要对接的记录类型至少十多个,每个都这么写,容不容易出错?出错后容不容易检查定位?

三 优雅的解析或转字节数组做法

通过反射机制和自定义注解方式,做到优雅的读取和对象转字节数组。具体方式如下:

自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 字段字节类型
*
* @author mafgwo
* @since 2022/10/20
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ByteType {

/**
* 字节长度
* @return
*/
int len() default 0;

/**
* 长度字段
* @return
*/
String lenField() default "";

/**
* 小数个数
* @return
*/
int decimalNum() default 0;

/**
* 偏移量 转字节时 减去偏移 转对象时 加上偏移
* @return
*/
int offset() default 0;

/**
* ASCII编码标识 针对String 0 非ascii 1 小端序 2 大端序
* @return
*/
int asciiFlag() default 0;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 字段字节列表类型
*
* @author mafgwo
* @since 2022/10/20
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ListType {

/**
* 对象数对应的字段
* @return
*/
String itemNumField();

}

写转换工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import com.mafgwo.annotation.ByteType;
import com.mafgwo.annotation.ListType;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* 记录类型工具类
*
* @author mafgwo
* @since 2022/10/20
*/
@Slf4j
public class RecordDataUtils {

private RecordDataUtils() {
}

/**
* 云端字节转对象
* @param bytes
* @param cls
* @param <T>
* @return
*/
public static <T> T cloudConvertToObj(byte[] bytes, Class<T> cls) {
return convertToObj(new ByteAndIdx().setBytes(bytes).setIdx(0), cls, true);
}


/**
* 云端对象转字节
* @param obj
* @return
*/
public static byte[] cloudConvertToBytes(Object obj) {
return convertToBytes(obj, true, null);
}

/**
* 本地16进制字符串转对象
* @param hexData
* @param cls
* @param <T>
* @return
*/
public static <T> T localConvertToObj(String hexData, Class<T> cls) {
return convertToObj(new ByteAndIdx().setBytes(ByteUtil.toBytesFromHexString(hexData)).setIdx(0), cls, false);
}

/**
* 本地对象转16进制字符串
* @param obj
* @return
*/
public static String localConvertToHexString(Object obj) {
byte[] bytes = convertToBytes(obj, false, null);
return ByteUtil.byteArrayToHexString(bytes);
}

/**
* 字节转换为对象
*
* @param byteAndIdx
* @param cls
* @param <T>
* @return
*/
public static <T> T convertToObj(ByteAndIdx byteAndIdx, Class<T> cls, boolean bcdReverse) {
byte[] bytes = byteAndIdx.getBytes();
List<Field> declaredFields = ClassUtil.getAllFields(cls);
try {
// 数字类型的字段名与值映射
Map<String, Number> fieldNameValMap = new HashMap<>();
T obj = cls.newInstance();
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
int len = 0;
int offset = 0;
ByteType annotation = declaredField.getDeclaredAnnotation(ByteType.class);
if (annotation != null) {
len = annotation.len();
offset = annotation.offset();
if (!"".equals(annotation.lenField())) {
Number number = fieldNameValMap.get(annotation.lenField());
Assert.notNull(number, "ByteType注解中的" + annotation.lenField() + "属性未找到值");
len = number.intValue();
}
if (len <= 0) {
continue;
}
}
Object val = null;
// 如果是字节则直接取第一个字节
if (declaredField.getType() == Byte.class) {
val = (byte) (bytes[byteAndIdx.idx++] + offset);
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Short 2个字节
else if (declaredField.getType() == Short.class) {
val = (short) (ByteUtil.byteArrayToShort(ByteUtil.getByte(bytes, byteAndIdx.idx, 2)) + offset);
byteAndIdx.idx += 2;
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Integer 4个字节
else if (declaredField.getType() == Integer.class) {
val = ByteUtil.byteArrayToInt(ByteUtil.getByte(bytes, byteAndIdx.idx, 4)) + offset;
byteAndIdx.idx += 4;
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Long 8个字节
else if (declaredField.getType() == Long.class) {
val = ByteUtil.byteArrayToLong(ByteUtil.getByte(bytes, byteAndIdx.idx, 8)) + offset;
byteAndIdx.idx += 8;
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Float或Double类型
else if (declaredField.getType() == Float.class || declaredField.getType() == Double.class) {
Assert.notNull(annotation, "Float/Double类型属性必须包含ByteType注解");
double v = ByteUtil.byteArrayToLong(ByteUtil.getByte(bytes, byteAndIdx.idx, len)) / Math.pow(10, annotation.decimalNum());
if (declaredField.getType() == Float.class) {
val = (float) (v + offset);
} else {
val = v + offset;
}
byteAndIdx.idx += len;
}
// 时间类型
else if (declaredField.getType() == Date.class) {
val = ByteUtil.byte2Hdate(ByteUtil.getByte(bytes, byteAndIdx.idx, 7));
byteAndIdx.idx += 7;
}
// 字符串类型
else if (declaredField.getType() == String.class) {
Assert.notNull(annotation, "String类型属性必须包含ByteType注解");
// 如果是ascii编码
if (annotation.asciiFlag() == 1) {
val = ByteUtil.toAsciiFromBytes(ByteUtil.getByte(bytes, byteAndIdx.idx, len), false);
}
else if (annotation.asciiFlag() == 2) {
val = ByteUtil.toAsciiFromBytes(ByteUtil.getByte(bytes, byteAndIdx.idx, len), true);
} else {
val = Util.byteArray2HexString(ByteUtil.getByte(bytes, byteAndIdx.idx, len), Integer.MAX_VALUE, false, bcdReverse);
}
byteAndIdx.idx += len;
}
// List类型
else if (declaredField.getType() == List.class) {
ListType listType = declaredField.getDeclaredAnnotation(ListType.class);
Assert.notNull(listType, "List类型属性必须包含ListType注解");
Number itemNum = fieldNameValMap.get(listType.itemNumField());
Assert.notNull(itemNum, "List类型注解中的" + listType.itemNumField() + "属性未找到值");
// 如果是List类型,得到其Generic的类型
Type genericType = declaredField.getGenericType();
Assert.notNull(genericType, "List类型的泛型类型不允许为空");
Assert.isTrue(genericType instanceof ParameterizedType, "List类型的泛型类型必须是自定义对象");
Class<?> genericClazz = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
List list = new ArrayList(itemNum.intValue());
for (int i = 0; i < itemNum.intValue(); i++) {
list.add(convertToObj(byteAndIdx, genericClazz, bcdReverse));
}
val = list;
}
declaredField.set(obj, val);
}
return obj;
} catch (Exception e) {
log.error("字节转换为对象异常,bytes:{}", ByteUtil.byteArrayToHexString(bytes), e);
return null;
}
}

/**
* 对象转字节 如果字段为null直接用FF填充
*
* @param obj
* @param bcdReverse
* @param fieldName 字段名称 没有则是所有字段
* @return
*/
public static byte[] convertToBytes(Object obj, boolean bcdReverse, String fieldName) {
try {
// 数字类型的字段名与值映射
Map<String, Number> fieldNameValMap = new HashMap<>();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
List<Field> declaredFields = ClassUtil.getAllFields(obj.getClass());
// int curr = 0;
// 数字类型的字段名与值映射
for (Field declaredField : declaredFields) {
// 如果有传入字段名 字段名不匹配则跳过
if (fieldName != null && !Objects.equals(declaredField.getName(), fieldName)) {
continue;
}
declaredField.setAccessible(true);
Object val = declaredField.get(obj);
ByteType annotation = declaredField.getDeclaredAnnotation(ByteType.class);
int len = 0;
int offset = 0;
if (annotation != null) {
len = annotation.len();
offset = annotation.offset();
if (!"".equals(annotation.lenField())) {
Number number = fieldNameValMap.get(annotation.lenField());
Assert.notNull(number, "ByteType注解中的" + annotation.lenField() + "属性未找到值" );
len = number.intValue();
}
if (len <= 0) {
continue;
}
}
// 如果是字节则直接取第一个字节
if (declaredField.getType() == Byte.class) {
outputStream.write(val != null ? ((byte) val) - offset : 0xFF);
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Short 2个字节
else if (declaredField.getType() == Short.class) {
outputStream.write(val != null ? ByteUtil.shortToByteArray((short) ((short) val - offset)) : ByteUtil.get0xFFBytes(2));
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Integer 4个字节
else if (declaredField.getType() == Integer.class) {
outputStream.write(val != null ? ByteUtil.intToByteArray((int) val - offset) : ByteUtil.get0xFFBytes(4));
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Long 8个字节
else if (declaredField.getType() == Long.class) {
outputStream.write(val != null ? ByteUtil.longToByteArray((long) val - offset) : ByteUtil.get0xFFBytes(8));
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Float或Double类型
else if (declaredField.getType() == Float.class || declaredField.getType() == Double.class) {
Assert.notNull(annotation, "Float/Double类型属性必须包含ByteType注解");
if (val == null) {
outputStream.write(ByteUtil.get0xFFBytes(len));
} else {
long v = Math.round((((Number) val).doubleValue() - offset) * Math.pow(10, annotation.decimalNum()));
outputStream.write(ByteUtil.longToByteArray(v, len));
}
}
// 时间类型
else if (declaredField.getType() == Date.class) {
if (val == null) {
outputStream.write(ByteUtil.get0xFFBytes(7));
} else {
byte[] v = ByteUtil.date2Hbyte((Date) val);
outputStream.write(v);
}
}
// 字符串类型
else if (declaredField.getType() == String.class) {
Assert.notNull(annotation, "String类型属性必须包含ByteType注解");
if (val == null) {
outputStream.write(ByteUtil.get0xFFBytes(len));
} else {
int length = ((String) val).length() / 2;
Assert.isTrue(len >= length, "字符串长度超过ByteType注解中定义长度");
// bcdReverse 反向为前补0
if ((annotation.asciiFlag() == 2 || bcdReverse) && len > length ) {
outputStream.write(ByteUtil.getZeroBytes(len - length));
}
// 如果是ascii编码
if (annotation.asciiFlag() > 0) {
outputStream.write(ByteUtil.toBytesFromAscii((String)val));
} else {
outputStream.write(ByteUtil.toBytesFromHexString((String)val, bcdReverse));
}
// bcdReverse 正向为后补0
if (!(annotation.asciiFlag() == 2 || bcdReverse) && len > length ) {
outputStream.write(ByteUtil.getZeroBytes(len - length));
}
}
}
// List类型
else if (declaredField.getType() == List.class) {
List list = (List) val;
for (Object item : list) {
byte[] itemBytes = convertToBytes(item, bcdReverse, fieldName);
Assert.notNull(itemBytes, "记录对象子项转字节为null,item:" + item);
outputStream.write(itemBytes);
}
}
if (fieldName != null && Objects.equals(declaredField.getName(), fieldName)) {
break;
}
// log.info("----------------{}:{}", declaredField.getName(), outputStream.size() - curr);
// curr = outputStream.size();
}
return outputStream.toByteArray();
} catch (Exception e) {
log.error("对象转换为字节数组异常,obj:{}", obj, e);
return null;
}
}

@Accessors(chain = true)
@Data
public static class ByteAndIdx {
private byte[] bytes;
private int idx;
}

}

依赖的其他工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
* 类工具类
*
* @author mafgwo
* @since 2022/10/21
*/
public class ClassUtil {

private ClassUtil() {
}

/**
* 获取所有的属性(含父类属性)
*
* @param cls
* @return
*/
public static List<Field> getAllFields(Class<?> cls) {
if (cls == Object.class) {
return Collections.emptyList();
}
Field[] declaredFields = cls.getDeclaredFields();
Class<?> genericSuperclass = (Class<?>) cls.getGenericSuperclass();
if (genericSuperclass == Object.class) {
return Arrays.asList(declaredFields);
} else {
List<Field> genericFields = getAllFields(genericSuperclass);
List<Field> allFields = new ArrayList<>(genericFields.size() + declaredFields.length);
allFields.addAll(genericFields);
allFields.addAll(Arrays.asList(declaredFields));
return allFields;
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
import org.springframework.util.StringUtils;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;

/**
* byte 工具类
*
* @author mafgwo
* @since 2022/09/30
*/
public class ByteUtil {

/**
* @param @param i
* @param @return
* @return byte[]
* @throws
* @Title: intToByteArray
* @Description: int 转换成 byte数组
*/
public static byte[] longToByteArray(long val) {
return longToByteArray(val, 8);
}

public static byte[] longToByteArray(long val, int len) {
byte[] result = new byte[len];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (val >> (i * 8) & 0xFF);
}
return result;
}

/**
* @param @param i
* @param @return
* @return byte[]
* @throws
* @Title: intToByteArray
* @Description: int 转换成 byte数组
*/
public static byte[] intToByteArray(int i) {
byte[] result = new byte[4];
result[3] = (byte) ((i >> 24) & 0xFF);
result[2] = (byte) ((i >> 16) & 0xFF);
result[1] = (byte) ((i >> 8) & 0xFF);
result[0] = (byte) (i & 0xFF);
return result;
}

/**
* @param @param val
* @param @return
* @return byte[]
* @throws
* @Title: shortToByteArray
* @Description: short 转换成 byte[]
*/
public static byte[] shortToByteArray(short val) {
byte[] b = new byte[2];
b[1] = (byte) ((val >> 8) & 0xff);
b[0] = (byte) (val & 0xff);
return b;
}

/**
* @param @param bytes
* @param @return
* @return int
* @throws
* @Title: byteArrayToInt
* @Description: byte[] 转换成 int
*/
public static int byteArrayToInt(byte[] bytes) {
int value = 0;
for (int i = 0; i < bytes.length; i++) {
int shift = i * 8;
value += (bytes[i] & 0xFF) << shift;
}
return value;
}

/**
* @param @param bytes
* @param @return
* @return int
* @throws
* @Title: byteArrayToInt
* @Description: byte[] 转换成 int
*/
public static long byteArrayToLong(byte[] bytes) {
int value = 0;
for (int i = 0; i < bytes.length; i++) {
int shift = i * 8;
value += (bytes[i] & 0xFF) << shift;
}
return value;
}

/**
* @param @param bytes
* @param @return
* @return short
* @throws
* @Title: byteArrayToShort
* @Description: byte[] 转换成short
*/
public static short byteArrayToShort(byte[] bytes) {
short value = 0;
for (int i = 0; i < 2; i++) {
int shift = i * 8;
value += (bytes[i] & 0xFF) << shift;
}
return value;
}

/**
* @param @param date
* @param @return
* @return byte[]
* @throws
* @Title: date2HByte
* @Description: 日期转换成 CP56Time2a
*/
public static byte[] date2Hbyte(Date date) {
ByteArrayOutputStream bOutput = new ByteArrayOutputStream();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
// 毫秒需要转换成两个字节其中 低位在前高位在后
// 先转换成short
int millisecond = calendar.get(Calendar.SECOND) * 1000 + calendar.get(Calendar.MILLISECOND);

// 默认的低位在前
byte[] millisecondByte = intToByteArray(millisecond);
bOutput.write(millisecondByte[0]);
bOutput.write(millisecondByte[1]);

// 分钟 只占6个比特位 需要把前两位置为零
bOutput.write((byte) calendar.get(Calendar.MINUTE));
// 小时需要把前三位置零
bOutput.write((byte) calendar.get(Calendar.HOUR_OF_DAY));
// 星期日的时候 week 是0
int week = calendar.get(Calendar.DAY_OF_WEEK);
if (week == Calendar.SUNDAY) {
week = 7;
} else {
week--;
}
// 前三个字节是 星期 因此需要将星期向左移5位 后五个字节是日期 需要将两个数字相加 相加之前需要先将前三位置零
bOutput.write((byte) (week << 5) + (calendar.get(Calendar.DAY_OF_MONTH)));
// 前四字节置零
bOutput.write((byte) ((byte) calendar.get(Calendar.MONTH) + 1));
bOutput.write((byte) (calendar.get(Calendar.YEAR) - 2000));
return bOutput.toByteArray();
}


/**
* @param @param date
* @param @return
* @return byte[]
* @throws
* @Title: date2HByte
* @Description:CP56Time2a转换成 时间
*/
public static Date byte2Hdate(byte[] dataByte) {
int year = (dataByte[6] & 0x7F) + 2000;
int month = dataByte[5] & 0x0F;
int day = dataByte[4] & 0x1F;
int hour = dataByte[3] & 0x1F;
int minute = dataByte[2] & 0x3F;
int second = dataByte[1] > 0 ? dataByte[1] : (int) (dataByte[1] & 0xff);
int millisecond = dataByte[0] > 0 ? dataByte[0] : (int) (dataByte[0] & 0xff);
millisecond = (second << 8) + millisecond;
second = millisecond / 1000;
millisecond = millisecond % 1000;
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
calendar.set(Calendar.MILLISECOND, millisecond);
return calendar.getTime();
}

public static String byteArrayToHexString(byte[] array) {
return Util.byteArray2HexString(array, Integer.MAX_VALUE, false);
}

public static String reverseByteArrayToHexString(byte[] array) {
return Util.byteArray2HexString(array, Integer.MAX_VALUE, false, true);
}

public static String byteArrayToHexString(byte[] array, boolean blank) {
return Util.byteArray2HexString(array, Integer.MAX_VALUE, blank);
}

/**
* 将16进制字符串转换为byte[]
*
* @param str
* @return
*/
public static byte[] toBytesFromHexString(String str) {
return toBytesFromHexString(str, false);
}

/**
* 将16进制字符串转换为byte[] byte倒序
* 南网电动的 桩编号 交易流水号等都是倒序的
* @param str
* @return
*/
private static byte[] toReverseBytesFromHexString(String str) {
return toBytesFromHexString(str, true);
}

/**
* 将16进制字符串转换为byte[]
*
* @param str
* @param reverse 是否倒序 true 倒序 则前面的转为高位 后面的转为低位
* @return
*/
public static byte[] toBytesFromHexString(String str, boolean reverse) {
if (StringUtils.isEmpty(str)) {
return new byte[0];
}
int byteLen = str.length() / 2;
byte[] bytes = new byte[byteLen];
for (int i = 0; i < byteLen; i++) {
String subStr = str.substring(i * 2, i * 2 + 2);
int index = reverse ? byteLen - 1 - i : i;
bytes[index] = (byte) Integer.parseInt(subStr, 16);
}
return bytes;
}

/**
* 返回指定位置的数组
*
* @param bytes
* @param start 开始位置
* @param length 截取长度
* @return
*/
public static byte[] getByte(byte[] bytes, int start, int length) {
length = Math.min(length, bytes.length - start);
byte[] ruleByte = new byte[length];
int index = 0;
while (index < length) {
ruleByte[index++] = bytes[start++];
}
return ruleByte;
}

/**
* 校验指定的数组是否都是0 是则返回true 否则false
*
* @param bytes
* @param start 开始位置
* @param length 截取长度
* @return
*/
public static boolean validBytesZero(byte[] bytes, int start, int length) {
length = Math.min(length, bytes.length - start);
int index = 0;
while (index < length) {
if (bytes[start++] != 0) {
return false;
}
index++;
}
return true;
}

/**
* 16进制转10进制
* @param hex 16进制字符串
* @return
*/
public static int hexToInt(String hex) {
return Integer.parseInt(hex, 16);
}

/**
* 16进制转10进制
* @param hex 16进制字符串
* @return
*/
public static long hexToLong(String hex) {
return Long.parseLong(hex, 16);
}


/**
* 整数转16进制字符串
* @param intVal 整数值
* @param byteLen 转换后的总字节数
* @param littleEndian true=小端序 false=大端序
* @return
*/
public static String intToHex(Integer intVal, int byteLen, boolean littleEndian) {
String hex = Integer.toHexString(intVal).toUpperCase();
int finalLen = byteLen * 2;
int diff = finalLen - hex.length();
StringBuilder sb = new StringBuilder(finalLen);
for (int i = 0; i < diff; i++) {
sb.append("0");
}
sb.append(hex);
if (!littleEndian) {
return sb.toString();
}
else {
return transferEndian(sb.toString());
}
}

/**
* 转换大小端
* @param hex 16进制字符串
* @return
*/
public static String transferEndian(String hex) {
StringBuilder sb = new StringBuilder(hex.length());
for (int i = hex.length(); i > 0; i -= 2) {
sb.append(hex.substring(i - 2, i));
}
return sb.toString();
}

/**
* float类型转4字节数组
* @param fval
* @param remain
* @return
*/
public static byte[] floatToBytes(float fval, int remain) {
int bits = (int) (fval * Math.pow(10, remain));
return intToByteArray(bits);
}

/**
* float类型转2字节数组
* @param fval
* @return
*/
public static byte[] floatToShortBytes(float fval, int remain) {
short bits = (short) (fval * Math.pow(10, remain));
return shortToByteArray(bits);
}

/**
* 创建为0的字节
* @param length
* @return
*/
public static byte[] getZeroBytes(int length) {
byte[] bytes = new byte[length];
for (int i = 0; i < length; i++) {
bytes[i] = 0;
}
return bytes;
}

/**
* 创建为FF的字节
* @param length
* @return
*/
public static byte[] get0xFFBytes(int length) {
byte[] bytes = new byte[length];
for (int i = 0; i < length; i++) {
bytes[i] = (byte) 0xFF;
}
return bytes;
}

/**
* 字节转ASCII 结束符为 00
* @param bytes
* @param reverse
* @return
*/
public static String toAsciiFromBytes(byte[] bytes, boolean reverse) {
if (bytes.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
int idx = reverse ? bytes.length - 1 - i : i;
if (bytes[idx] == 0) {
break;
}
sb.append((char) bytes[idx]);
}
return sb.toString();
}

/**
* 字节转ASCII
* @param str
* @return
*/
public static byte[] toBytesFromAscii(String str) {
byte[] bytes = new byte[str.length()];
for (int i = 0; i < str.length(); i++) {
bytes[i] = (byte) str.charAt(i);
}
return bytes;
}

public static byte[] mergeBytes(byte[] bytes1, byte[] bytes2) {
byte[] bytes = new byte[bytes1.length + bytes2.length];
System.arraycopy(bytes1, 0, bytes, 0, bytes1.length);
System.arraycopy(bytes2, 0, bytes, bytes1.length, bytes2.length);
return bytes;
}

}

编写信息体中含记录类型和业务数据的信息对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

/**
* 云端计费模型
* [1] 下发记录类型
* [2-9] 充电设备编号 压缩 BCD 码 8Byte 充电设备编号
* [10] 充电接口标识 BIN 码 1Byte 充电设备为一桩多充时用来标记接口号,一桩一充时此项为0。多个接口时顺序对每个接口进行编号
* [11-18] 计费模型 ID BIN 码 8Byte 运营管理系统产生
* [19-25] 生效时间 BIN 码 7Byte CP56Time2a 格式
* [26-32] 失效时间 BIN 码 7Byte CP56Time2a 格式
* [33-34] 执行状态 压缩 BCD 码 2Byte 0001-有效 0002-无效
* [35-36] 计量类型 压缩 BCD 码 2Byte 0001-充电量
* [37] 时段数 N BIN 码 1Byte 取值范围:0—12
* [38-39] 第 1 个时段起始时间点 BIN 码 2Byte 高字节:小时(0-24)低字节:分钟(0-60)
* [40] 第 1 个时段标志 BIN 码 1Byte 1:尖时段;2:峰时段 3:平时段,4:谷时段
* [41-44] 第 1 个尖时段尖电价 BIN 码 4Byte 精确到小数点后五位
* [45-48] 第 1 个峰时段电价 BIN 码 4Byte 精确到小数点后五位
* [49-52] 第 1 个平时段电价 BIN 码 4Byte 精确到小数点后五位
* [53-56] 第 1 个谷时段电价 BIN 码 4Byte 精确到小数点后五位
* [57-60] 第 1 个服务费单价 BIN 码 4Byte 精确到小数点后五位
* [61-64] 第 1 个占位费单价 BIN 码 4Byte 精确到小数点后五位
* [65-68] 第 1 个预约费单价 BIN 码 4Byte 精确到小数点后五位
* @author mafgwo
* @since 2022/10/20
*/
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Data
public class CloudFeeModeVO extends RecordBaseVO {
/**
* 下发数据类型
*/
private Byte recordType;

/**
* 充电设备编号
*/
@ByteType(len = 8)
private String cloudPileSn;

/**
* 充电接口标识
*/
private Byte gunNo;

/**
* 计费模型ID
*/
private Long feeModeId;

/**
* 生效时间
*/
private Date activeTime;
/**
* 失效时间
*/
private Date inActiveTime;

/**
* 执行状态 0001-有效 0002-无效
*/
@ByteType(len = 2)
private String execStatus;

/**
* 计量类型
*/
@ByteType(len = 2)
private String measureType;

/**
* 时段数
*/
private Byte periodNum;

/**
* 时段
*/
@ListType(itemNumField = "periodNum")
private List<PeriodVO> periods;

/**
* [38-39] 第 1 个时段起始时间点 BIN 码 2Byte 高字节:小时(0-24)低字节:分钟(0-60)
* [40] 第 1 个时段标志 BIN 码 1Byte 1:尖时段;2:峰时段 3:平时段,4:谷时段
* [41-44] 第 1 个尖时段尖电价 BIN 码 4Byte 精确到小数点后五位
* [45-48] 第 1 个峰时段电价 BIN 码 4Byte 精确到小数点后五位
* [49-52] 第 1 个平时段电价 BIN 码 4Byte 精确到小数点后五位
* [53-56] 第 1 个谷时段电价 BIN 码 4Byte 精确到小数点后五位
* [57-60] 第 1 个服务费单价 BIN 码 4Byte 精确到小数点后五位
* [61-64] 第 1 个占位费单价 BIN 码 4Byte 精确到小数点后五位
* [65-68] 第 1 个预约费单价 BIN 码 4Byte 精确到小数点后五位
*/
@Data
public static class PeriodVO {

/**
* 时段起始时间点 分钟
*/
private Byte minute;

/**
* 时段起始时间点 小时
*/
private Byte hour;

/**
* 时段标志 BIN 码 1Byte 1:尖时段;2:峰时段 3:平时段,4:谷时段
*/
private Byte periodType;

/**
* 尖时段尖电价
*/
@ByteType(len = 4, decimalNum = 5)
private Float sharpPrice;

/**
* 峰时段电价
*/
@ByteType(len = 4, decimalNum = 5)
private Float peakPrice;

/**
* 平时段电价
*/
@ByteType(len = 4, decimalNum = 5)
private Float flatPrice;

/**
* 谷时段电价
*/
@ByteType(len = 4, decimalNum = 5)
private Float valleyPrice;

/**
* 服务费
*/
@ByteType(len = 4, decimalNum = 5)
private Float serviceFee;

/**
* 占位费
*/
@ByteType(len = 4, decimalNum = 5)
private Float spaceFee;

/**
* 预约费
*/
@ByteType(len = 4, decimalNum = 5)
private Float retainingFee;
}

}

字节数组转对象

1
2
3
4
// 由于上层已经封装好了IEC104规约的应用规约控制信息(APCI)和数据单元标识符 通俗理解即头部信息已经封装好了。只需要对消息体的差异做单独解析即可。
// msgBytes 是从记录类型开始直到结束的所有字节数组
// 以下一行代码即可完成解析
CloudFeeModeVO cloudFeeModeVO = RecordDataUtils.cloudConvertToObj(msgBytes, CloudFeeModeVO.class);

四 总结

作为一名优秀的码农,我们写代码前,一定要思考一下如何实现才是最好的。
也许有些实现方式会让你前期花不少功夫去磨针,但是等针磨好了,后续开发效率和代码质量都会非常高。

如果,你也熟悉充电桩规约,或者正在对接充电桩协议,都非常欢迎关注一下我,然后加我微信沟通交流。