package com.mes.s7.enhanced; import com.github.xingshuangs.iot.common.buff.ByteReadBuff; import com.github.xingshuangs.iot.common.buff.ByteWriteBuff; import com.github.xingshuangs.iot.common.enums.EDataType; import com.github.xingshuangs.iot.common.serializer.IPLCSerializable; import com.github.xingshuangs.iot.exceptions.S7CommException; import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType; import com.github.xingshuangs.iot.protocol.s7.model.DataItem; import com.github.xingshuangs.iot.protocol.s7.model.RequestItem; import com.github.xingshuangs.iot.protocol.s7.serializer.S7Parameter; import com.github.xingshuangs.iot.protocol.s7.serializer.S7ParseData; import com.github.xingshuangs.iot.protocol.s7.serializer.S7Variable; import com.github.xingshuangs.iot.protocol.s7.service.S7PLC; import com.github.xingshuangs.iot.protocol.s7.utils.AddressUtil; import java.lang.reflect.Field; import java.nio.charset.Charset; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @Author : zhoush * @Date: 2025/8/1 8:29 * @Description: */ public class EnhancedS7Serializer implements IPLCSerializable { private final S7PLC s7PLC; public EnhancedS7Serializer(S7PLC s7Plc) { this.s7PLC = s7Plc; } public S7PLC getS7PLC() { return this.s7PLC; } /** * Static method for creating instance object. * 静态方法实例对象 * * @param s7Plc plc object * @return serializer object */ public static EnhancedS7Serializer newInstance(S7PLC s7Plc) { return new EnhancedS7Serializer(s7Plc); } /** * Convert to parsed data according to the S7Variable annotation. * 将类根据S7Variable注解转换为解析数据 * * @param targetClass target object class * @return result data list */ private List parseBean(final Class targetClass, String dbArea, int beginIndex) { List s7ParseDataList = new ArrayList<>(); for (final Field field : targetClass.getDeclaredFields()) { final S7Variable s7Variable = field.getAnnotation(S7Variable.class); if (s7Variable == null) { continue; } int index = Integer.parseInt(s7Variable.address()) + beginIndex; String address = dbArea + index; S7Parameter parameter = new S7Parameter(address, s7Variable.type(), s7Variable.count()); S7ParseData s7ParseData = this.createS7ParseData(parameter, field); s7ParseDataList.add(s7ParseData); } return s7ParseDataList; } /** * Convert to parsed data according to the S7Variable annotation. * 将类根据S7Variable注解转换为解析数据 * * @param targetClass target object class * @return result data list */ private List parseBean(final Class targetClass) { List s7ParseDataList = new ArrayList<>(); for (final Field field : targetClass.getDeclaredFields()) { final S7Variable s7Variable = field.getAnnotation(S7Variable.class); if (s7Variable == null) { continue; } S7Parameter parameter = new S7Parameter(s7Variable.address(), s7Variable.type(), s7Variable.count()); S7ParseData s7ParseData = this.createS7ParseData(parameter, field); s7ParseDataList.add(s7ParseData); } return s7ParseDataList; } /** * Create S7ParseData list by S7Parameter list. * (解析参数) * * @param parameters parameter list * @return List */ private List parseBean(final List parameters) { try { List s7ParseDataList = new ArrayList<>(); for (final S7Parameter p : parameters) { if (p == null) { // parameters列表中存在null throw new S7CommException("null exists in the parameters list"); } S7ParseData s7ParseData = this.createS7ParseData(p, p.getClass().getDeclaredField("value")); s7ParseDataList.add(s7ParseData); } return s7ParseDataList; } catch (NoSuchFieldException e) { throw new S7CommException(e); } } /** * Check that the data of the S7Variable meets the rule requirements. * (校验S7Variable的数据是否满足规则要求) * * @param parameter parameter */ private void checkS7Variable(final S7Parameter parameter) { if (parameter.getAddress().isEmpty()) { // S7参数中[address]不能为空 throw new S7CommException("[address] in the S7 parameter cannot be empty"); } if (parameter.getCount() < 0) { // S7参数中[count]不能为负数 throw new S7CommException("[count] in the S7 parameter cannot be negative"); } if (parameter.getDataType() == EDataType.STRING && parameter.getCount() > 254) { // S7参数中字符串类型类型数据的[count]不能大于254 throw new S7CommException("The [count] value of the string type in the S7 parameter cannot be greater than 254"); } if (parameter.getDataType() != EDataType.BYTE && parameter.getDataType() != EDataType.STRING && parameter.getCount() > 1) { // S7参数中只有[type]=字节和字符串类型数据的[count]才能大于1,其他必须等于1 throw new S7CommException("In the S7 parameter, only [type]= bytes and [count] of string type data can be greater than 1, and the rest must be equal to 1"); } } /** * Read the data according to the condition. * (根据条件读取数据) * * @param s7ParseDataList condition */ private void readDataByCondition(List s7ParseDataList) { if (s7ParseDataList.isEmpty()) { // 解析出的注解数据个数为空,无法读取数据 throw new S7CommException("The number of parsed annotation data is empty, and the data cannot be read"); } // 读取PLC数据 List requestItems = s7ParseDataList.stream().map(S7ParseData::getRequestItem).collect(Collectors.toList()); List dataItems = this.s7PLC.readS7Data(requestItems); if (s7ParseDataList.size() != dataItems.size()) { // 所需的字段解析项个数与返回的数据项数量不一致,错误 throw new S7CommException("The number of field parsing items required is inconsistent with the number of data items returned"); } // 提取数据 for (int i = 0; i < dataItems.size(); i++) { s7ParseDataList.get(i).setDataItem(dataItems.get(i)); } } /** * Build the S7ParseData data. * (构建S7ParseData数据) * * @param p S7Parameter data * @param field target field * @return S7ParseData */ private S7ParseData createS7ParseData(S7Parameter p, Field field) { this.checkS7Variable(p); // 组装S7解析数据 S7ParseData s7ParseData = new S7ParseData(); s7ParseData.setDataType(p.getDataType()); s7ParseData.setCount(p.getCount()); s7ParseData.setField(field); if (p.getDataType() == EDataType.BOOL) { s7ParseData.setRequestItem(AddressUtil.parseBit(p.getAddress())); } else if (p.getDataType() == EDataType.STRING) { RequestItem requestItem = AddressUtil.parseByte(p.getAddress(), 1 + p.getCount() * p.getDataType().getByteLength()); // 为什么字节索引+1,为了避免修改PLC中string[60]类型的第一个字节数据,该数据为字符串的允许最大长度 // S1200(非S200Smart):数据类型为 string 的操作数可存储多个字符,最多可包括 254 个字符。字符串中的第一个字节为总长度,第二个字节为有效字符数量。 // S200SMART:字符串由变量存储时,字符串长度为0至254个字符,最长为255个字节,其中第一个字符为长度字节 int offset = this.s7PLC.getPlcType() == EPlcType.S200_SMART ? 0 : 1; requestItem.setByteAddress(requestItem.getByteAddress() + offset); s7ParseData.setRequestItem(requestItem); } else { s7ParseData.setRequestItem(AddressUtil.parseByte(p.getAddress(), p.getCount() * p.getDataType().getByteLength())); } return s7ParseData; } /** * region read * * @param targetClass * @param dbArea * @param beginIndex * @param * @return */ public T read(Class targetClass, String dbArea, int beginIndex) { List s7ParseDataList = this.parseBean(targetClass, dbArea, beginIndex); this.readDataByCondition(s7ParseDataList); return this.fillData(targetClass, s7ParseDataList); } @Override public T read(Class targetClass) { List s7ParseDataList = this.parseBean(targetClass); this.readDataByCondition(s7ParseDataList); return this.fillData(targetClass, s7ParseDataList); } /** * Read data, fill value. * (读取数据) * * @param parameters parameter list * @return S7Parameter list */ public List read(List parameters) { List s7ParseDataList = this.parseBean(parameters); this.readDataByCondition(s7ParseDataList); this.fillData(parameters, s7ParseDataList); return parameters; } /** * Fill value of field. * (提取数据) * * @param targetClass target class * @param s7ParseDataList S7ParseData list * @param type * @return target object. */ private T fillData(Class targetClass, List s7ParseDataList) { try { final T result = targetClass.newInstance(); for (S7ParseData item : s7ParseDataList) { this.fillField(result, item); } return result; } catch (Exception e) { throw new S7CommException("Serialization fetch data error:" + e.getMessage(), e); } } /** * Fill value of field. * (填充数据) * * @param parameters parameter list * @param s7ParseDataList List */ private void fillData(final List parameters, final List s7ParseDataList) { try { for (int i = 0; i < s7ParseDataList.size(); i++) { S7ParseData item = s7ParseDataList.get(i); S7Parameter parameter = parameters.get(i); this.fillField(parameter, item); } } catch (Exception e) { throw new S7CommException("Serialization fetch data error:" + e.getMessage(), e); } } /** * Fill value of field. * 填充字段数据 * * @param result target object * @param item S7ParseData * @param type * @throws IllegalAccessException IllegalAccessException */ private void fillField(T result, S7ParseData item) throws IllegalAccessException { ByteReadBuff buff = new ByteReadBuff(item.getDataItem().getData()); item.getField().setAccessible(true); switch (item.getDataType()) { case BOOL: item.getField().set(result, buff.getBoolean(0)); break; case BYTE: item.getField().set(result, buff.getBytes(item.getCount())); break; case UINT16: item.getField().set(result, buff.getUInt16()); break; case INT16: item.getField().set(result, buff.getInt16()); break; case TIME: case UINT32: item.getField().set(result, buff.getUInt32()); break; case INT32: item.getField().set(result, buff.getInt32()); break; case INT64: item.getField().set(result, buff.getInt64()); break; case FLOAT32: item.getField().set(result, buff.getFloat32()); break; case FLOAT64: item.getField().set(result, buff.getFloat64()); break; case STRING: int length = buff.getByteToInt(0); item.getField().set(result, buff.getString(1, Math.min(length, item.getCount()), Charset.forName("GB2312"))); break; case DATE: LocalDate date = LocalDate.of(1990, 1, 1).plusDays(buff.getUInt16()); item.getField().set(result, date); break; case TIME_OF_DAY: LocalTime time = LocalTime.ofSecondOfDay(buff.getUInt32() / 1000); item.getField().set(result, time); break; case DTL: int year = buff.getUInt16(); int month = buff.getByteToInt(); int dayOfMonth = buff.getByteToInt(); int week = buff.getByteToInt(); int hour = buff.getByteToInt(); int minute = buff.getByteToInt(); int second = buff.getByteToInt(); long nanoOfSecond = buff.getUInt32(); LocalDateTime dateTime = LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, (int) nanoOfSecond); item.getField().set(result, dateTime); break; default: throw new S7CommException("Data type can not be recognized"); } } // endregion /** * region write * * @param targetBean * @param dbArea * @param beginIndex * @param */ public void write(T targetBean, String dbArea, int beginIndex) { // 解析参数 List s7ParseDataList = this.parseBean(targetBean.getClass(), dbArea, beginIndex); if (s7ParseDataList.isEmpty()) { // 解析出的注解数据个数为空,无法读取数据 throw new S7CommException("The number of parsed annotation data is empty, and the data cannot be read"); } // 填充字节数据 s7ParseDataList = this.extractData(targetBean, s7ParseDataList); // 写入PLC List requestItems = s7ParseDataList.stream().map(S7ParseData::getRequestItem).collect(Collectors.toList()); List dataItems = s7ParseDataList.stream().map(S7ParseData::getDataItem).collect(Collectors.toList()); this.s7PLC.writeS7Data(requestItems, dataItems); } // region write @Override public void write(T targetBean) { // 解析参数 List s7ParseDataList = this.parseBean(targetBean.getClass()); if (s7ParseDataList.isEmpty()) { // 解析出的注解数据个数为空,无法读取数据 throw new S7CommException("The number of parsed annotation data is empty, and the data cannot be read"); } // 填充字节数据 s7ParseDataList = this.extractData(targetBean, s7ParseDataList); // 写入PLC List requestItems = s7ParseDataList.stream().map(S7ParseData::getRequestItem).collect(Collectors.toList()); List dataItems = s7ParseDataList.stream().map(S7ParseData::getDataItem).collect(Collectors.toList()); this.s7PLC.writeS7Data(requestItems, dataItems); } /** * Write data to plc by parameter list. * (写入数据) * * @param parameters parameter list */ public void write(List parameters) { // 解析参数 List s7ParseDataList = this.parseBean(parameters); if (s7ParseDataList.size() != parameters.size()) { // 解析出的数据个数与传入的数据个数不一致 throw new S7CommException("The number of parsed data is inconsistent with the number of incoming data"); } // 填充字节数据 s7ParseDataList = this.extractData(parameters, s7ParseDataList); // 写入PLC List requestItems = s7ParseDataList.stream().map(S7ParseData::getRequestItem).collect(Collectors.toList()); List dataItems = s7ParseDataList.stream().map(S7ParseData::getDataItem).collect(Collectors.toList()); this.s7PLC.writeS7Data(requestItems, dataItems); } /** * Extract data. * (提取数据) * * @param targetBean target object * @param s7ParseDataList List * @param type * @return List */ private List extractData(T targetBean, List s7ParseDataList) { try { for (S7ParseData item : s7ParseDataList) { item.getField().setAccessible(true); Object data = item.getField().get(targetBean); if (data == null) { continue; } this.extractField(item, data); } return s7ParseDataList.stream().filter(x -> x.getDataItem() != null).collect(Collectors.toList()); } catch (Exception e) { // 序列化填充字节数据错误 throw new S7CommException("Serialized fill byte data error:" + e.getMessage(), e); } } /** * Extract data. * (提取数据) * * @param parameters parameter list * @param s7ParseDataList List * @return List */ private List extractData(List parameters, List s7ParseDataList) { try { for (int i = 0; i < s7ParseDataList.size(); i++) { S7ParseData item = s7ParseDataList.get(i); S7Parameter parameter = parameters.get(i); if (parameter.getValue() == null) { throw new S7CommException("The value of the parameter cannot be null"); } this.extractField(item, parameter.getValue()); } return s7ParseDataList.stream().filter(x -> x.getDataItem() != null).collect(Collectors.toList()); } catch (Exception e) { // 序列化填充字节数据错误 throw new S7CommException("Serialized fill byte data error:" + e.getMessage(), e); } } /** * Extract value of data to S7ParseData * (提取字段数) * * @param item S7ParseData * @param data data source */ private void extractField(S7ParseData item, Object data) { switch (item.getDataType()) { case BOOL: item.setDataItem(DataItem.createReqByBoolean((Boolean) data)); break; case BYTE: item.setDataItem(DataItem.createReqByByte(ByteReadBuff.newInstance((byte[]) data) .getBytes(item.getCount()))); break; case UINT16: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(2) .putShort((Integer) data).getData())); break; case INT16: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(2) .putShort((Short) data).getData())); break; case TIME: case UINT32: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(4) .putInteger((Long) data).getData())); break; case INT32: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(4) .putInteger((Integer) data).getData())); break; case INT64: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(8) .putLong((Long) data).getData())); break; case FLOAT32: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(4) .putFloat((Float) data).getData())); break; case FLOAT64: item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(8) .putDouble((Double) data).getData())); break; case STRING: byte[] bytes = ((String) data).getBytes(Charset.forName("GB2312")); int actualLength = Math.min(bytes.length, item.getCount()); byte[] targetBytes = new byte[1 + actualLength]; targetBytes[0] = (byte) actualLength; System.arraycopy(bytes, 0, targetBytes, 1, actualLength); item.setDataItem(DataItem.createReqByByte(targetBytes)); // 根据实际情况获取最小字符串长度+1,重新更新待写入的数据长度 item.getRequestItem().setCount(targetBytes.length); break; case DATE: // TODO: 后面时间采用工具类 LocalDate start = LocalDate.of(1990, 1, 1); long date = ((LocalDate) data).toEpochDay() - start.toEpochDay(); item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(2) .putShort((short) date).getData())); break; case TIME_OF_DAY: long timeOfDay = ((LocalTime) data).toSecondOfDay() * 1000L; item.setDataItem(DataItem.createReqByByte(ByteWriteBuff.newInstance(4) .putInteger(timeOfDay).getData())); break; case DTL: LocalDateTime dateTime = (LocalDateTime) data; byte[] dateTimeData = ByteWriteBuff.newInstance(12) .putShort(dateTime.getYear()) .putByte(dateTime.getMonthValue()) .putByte(dateTime.getDayOfMonth()) .putByte(dateTime.getDayOfWeek().getValue()) .putByte(dateTime.getHour()) .putByte(dateTime.getMinute()) .putByte(dateTime.getSecond()) .putInteger(dateTime.getNano()) .getData(); item.setDataItem(DataItem.createReqByByte(dateTimeData)); break; default: // 无法识别数据类型 throw new S7CommException("Data type can not be recognized"); } } }