fix: address code review — security, thread safety, performance, and null safety

Security (CRITICAL):
- KeyUtil: read JWT signing key from DC3_SECURITY_KEY env/property
- UserPasswordServiceImpl: read default password from DC3_SECURITY_DEFAULT_PASSWORD env
- KeyLoader: read OPC-UA keystore password from OPCUA_KEYSTORE_PASSWORD env
- WebFilterConfig: return 401 on malformed X-Auth-User header
- AlgorithmConstant: document deprecated hardcoded constants

Thread safety (HIGH):
- Fix TOCTOU races in 4 driver connectors (computeIfAbsent)
- Fix PlcS7 lock leak, OpcUa connect timeout (5s)
- CoapClientManager: synchronize setURI to prevent URI race
- PointValueJob, MqttScheduleJob: add @DisallowConcurrentExecution
- SystemHealthServiceImpl: preserve interrupt flag
- WindowSampleBuffer: local AtomicInteger → int

Performance (MEDIUM):
- EntityStateExpiryScanner: batch alarm saves (saveBatch)
- ImportDeviceServiceImpl: batch config saves
- ResourceRegistrySyncServiceImpl: batch-load nodes, eliminate N+1 COUNTs
- DriverSenderServiceImpl: debug-gate hot-path logging
- PointServiceImpl: stream().count() → size()

Null safety (LOW):
- RegexUtil: null guards on isName/isPhone/isMail/isPassword/isHost
- HostUtil: null-check getNetworkInterfaces() return
- TimeUtil: log parse failures instead of silent null

Infrastructure:
- Add spring-boot-starter-cache + @EnableCaching CacheConfig
- Replace embedded modbus4j/plc-s7 jars with external Maven dependencies
- Add dc3-driver-modbus-rtu module
This commit is contained in:
Vickey
2026-05-26 08:40:25 +08:00
parent 918b3d5e8e
commit 27bcf1383b
238 changed files with 1799 additions and 22921 deletions
+1
View File
@@ -29,6 +29,7 @@ updates:
- /dc3-center/dc3-center-single
- /dc3-driver/dc3-driver-listening-virtual
- /dc3-driver/dc3-driver-modbus-tcp
- /dc3-driver/dc3-driver-modbus-rtu
- /dc3-driver/dc3-driver-mqtt
- /dc3-driver/dc3-driver-opc-da
- /dc3-driver/dc3-driver-opc-ua
+1
View File
@@ -79,6 +79,7 @@ jobs:
dc3-center-single
dc3-driver-listening-virtual
dc3-driver-modbus-tcp
dc3-driver-modbus-rtu
dc3-driver-mqtt
dc3-driver-opc-da
dc3-driver-opc-ua
+14
View File
@@ -233,6 +233,20 @@ ENTRYPOINT ["./entrypoint.sh"]
CMD ["dc3-driver-modbus-tcp.jar"]
# ---------- dc3-driver-modbus-rtu ----------
FROM runtime-base AS dc3-driver-modbus-rtu
ENV SERVER_NAME=dc3-driver-modbus-rtu
ENV JAVA_HEAP_DUMP_PATH=dc3/logs/driver/modbus-rtu/gc/dump.hprof
ENV JAVA_GC_LOG_PATH=dc3/logs/driver/modbus-rtu/gc/gc-%t.log
WORKDIR /dc3-driver/dc3-driver-modbus-rtu
RUN mkdir -p /dc3-driver/dc3-driver-modbus-rtu/dc3/logs/driver/modbus-rtu/gc
COPY --from=builder /build/dc3-driver/dc3-driver-modbus-rtu/target/dc3-driver-modbus-rtu.jar ./
RUN cp /usr/share/dc3/entrypoint.sh ./entrypoint.sh
VOLUME /dc3-driver/dc3-driver-modbus-rtu/dc3/logs
ENTRYPOINT ["./entrypoint.sh"]
CMD ["dc3-driver-modbus-rtu.jar"]
# ---------- dc3-driver-mqtt ----------
FROM runtime-base AS dc3-driver-mqtt
ENV SERVER_NAME=dc3-driver-mqtt
@@ -50,6 +50,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static io.github.pnoker.common.constant.service.AuthConstant.API_GROUP_NODE_CODE_PREFIX;
import static io.github.pnoker.common.constant.service.AuthConstant.API_RESOURCE_CODE_PREFIX;
@@ -424,9 +425,32 @@ public class ResourceRegistrySyncServiceImpl implements ResourceRegistrySyncServ
private Map<String, Long> ensureGroupNodes(String serviceName, Long serviceNodeId, Set<String> targetGroups) {
Map<String, Long> result = new HashMap<>(targetGroups.size());
// Build code→group lookup for both the batch query and the result
Map<String, String> codeToGroup = new HashMap<>(targetGroups.size());
List<String> allCodes = new ArrayList<>(targetGroups.size());
for (String group : targetGroups) {
String code = API_GROUP_NODE_CODE_PREFIX + serviceName + SymbolConstant.COLON + group;
ResourceDO existing = findByResourceCode(code);
codeToGroup.put(code, group);
allCodes.add(code);
}
// Batch-load all existing group nodes
Map<String, ResourceDO> existingByCode = new HashMap<>();
if (!allCodes.isEmpty()) {
List<ResourceDO> existingList = resourceManager.list(Wrappers.<ResourceDO>lambdaQuery()
.in(ResourceDO::getResourceCode, allCodes));
for (ResourceDO node : existingList) {
existingByCode.put(node.getResourceCode(), node);
}
}
List<ResourceDO> toInsert = new ArrayList<>();
List<ResourceDO> toUpdate = new ArrayList<>();
for (String group : targetGroups) {
String code = API_GROUP_NODE_CODE_PREFIX + serviceName + SymbolConstant.COLON + group;
ResourceDO existing = existingByCode.get(code);
if (Objects.isNull(existing)) {
ResourceDO node = new ResourceDO();
node.setParentResourceId(serviceNodeId);
@@ -438,17 +462,29 @@ public class ResourceRegistrySyncServiceImpl implements ResourceRegistrySyncServ
node.setResourceExt(new JsonExt());
node.setEnableFlag(EnableFlagEnum.ENABLE.getIndex());
node.setRemark("API grouping node (auto-registered)");
resourceManager.save(node);
result.put(group, node.getId());
toInsert.add(node);
} else {
String name = group.isEmpty() ? "(ungrouped)" : group;
if (needsGroupingNodeUpdate(existing, serviceNodeId, name, code, "API grouping node (auto-registered)")) {
applyGroupingNodeUpdates(existing, serviceNodeId, name, code, "API grouping node (auto-registered)");
resourceManager.updateById(existing);
toUpdate.add(existing);
}
result.put(group, existing.getId());
}
}
if (!toInsert.isEmpty()) {
resourceManager.saveBatch(toInsert);
for (ResourceDO node : toInsert) {
result.put(codeToGroup.get(node.getResourceCode()), node.getId());
}
}
if (!toUpdate.isEmpty()) {
resourceManager.updateBatchById(toUpdate);
}
for (ResourceDO existing : existingByCode.values()) {
result.put(codeToGroup.get(existing.getResourceCode()), existing.getId());
}
return result;
}
@@ -512,17 +548,25 @@ public class ResourceRegistrySyncServiceImpl implements ResourceRegistrySyncServ
List<ResourceDO> groupNodes = resourceManager.list(Wrappers.<ResourceDO>lambdaQuery()
.likeRight(ResourceDO::getResourceCode, API_GROUP_NODE_CODE_PREFIX + serviceName + SymbolConstant.COLON)
.eq(ResourceDO::getEntityId, 0L));
List<Long> idsToDrop = new ArrayList<>();
for (ResourceDO node : groupNodes) {
long children = resourceManager
.count(Wrappers.<ResourceDO>lambdaQuery().eq(ResourceDO::getParentResourceId, node.getId()));
if (children == 0) {
idsToDrop.add(node.getId());
if (!groupNodes.isEmpty()) {
// Single query to find which parents have children, instead of per-node COUNT
List<Long> parentIds = groupNodes.stream().map(ResourceDO::getId).toList();
Set<Long> parentsWithChildren = resourceManager.list(Wrappers.<ResourceDO>lambdaQuery()
.select(ResourceDO::getParentResourceId)
.in(ResourceDO::getParentResourceId, parentIds))
.stream()
.map(ResourceDO::getParentResourceId)
.collect(Collectors.toSet());
List<Long> idsToDrop = new ArrayList<>();
for (ResourceDO node : groupNodes) {
if (!parentsWithChildren.contains(node.getId())) {
idsToDrop.add(node.getId());
}
}
if (!idsToDrop.isEmpty()) {
resourceManager.removeByIds(idsToDrop);
removed += idsToDrop.size();
}
}
if (!idsToDrop.isEmpty()) {
resourceManager.removeByIds(idsToDrop);
removed += idsToDrop.size();
}
ResourceDO serviceNode = findByResourceCode(API_SERVICE_NODE_CODE_PREFIX + serviceName);
if (Objects.nonNull(serviceNode)) {
@@ -116,7 +116,12 @@ public class UserPasswordServiceImpl implements UserPasswordService {
public void restPassword(Long id) {
UserPasswordBO userPasswordBO = getById(id);
if (Objects.nonNull(userPasswordBO)) {
userPasswordBO.setLoginPassword(AlgorithmConstant.DEFAULT_PASSWORD);
String defaultPassword = System.getenv("DC3_SECURITY_DEFAULT_PASSWORD");
if (defaultPassword == null || defaultPassword.isBlank()) {
defaultPassword = System.getProperty("dc3.security.default-password",
AlgorithmConstant.DEFAULT_PASSWORD);
}
userPasswordBO.setLoginPassword(defaultPassword);
update(userPasswordBO);
}
}
@@ -31,12 +31,18 @@ package io.github.pnoker.common.constant.common;
public class AlgorithmConstant {
/**
* Default encryption key
* Default encryption key (fallback only).
* <p>
* Production deployments should set the {@code DC3_SECURITY_KEY} environment variable
* or the {@code dc3.security.key} property instead.
*/
public static final String DEFAULT_KEY = "io.github.pnoker.dc3";
/**
* Default password
* Default password (fallback only).
* <p>
* Production deployments should set the {@code DC3_SECURITY_DEFAULT_PASSWORD} environment
* variable or the {@code dc3.security.default-password} property instead.
*/
public static final String DEFAULT_PASSWORD = "dc3dc3dc3";
@@ -32,7 +32,6 @@ import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Per-entity ring of recent samples used for short-window alarm evaluation.
@@ -160,9 +159,9 @@ public class WindowSampleBuffer {
// Trim by count. Concurrent removes here are benign — at most we under-
// shoot maxSamples for a moment.
AtomicInteger drift = new AtomicInteger(deque.size());
while (drift.get() > maxSamples && Objects.nonNull(deque.pollFirst())) {
drift.decrementAndGet();
int drift = deque.size();
while (drift > maxSamples && Objects.nonNull(deque.pollFirst())) {
drift--;
}
}
@@ -44,6 +44,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -131,17 +132,33 @@ public class EntityStateExpiryScanner {
return;
}
List<EntityAlarmDO> alarms = new ArrayList<>();
List<ExpiredDeviceContext> contexts = new ArrayList<>();
for (EntityStateDO state : expired) {
try {
processExpiredDevice(state);
EntityAlarmDO alarm = buildOfflineAlarm(state);
alarms.add(alarm);
contexts.add(new ExpiredDeviceContext(state, alarm));
} catch (Exception e) {
log.warn("Device expiry processing failed, deviceId={}", state.getEntityId(), e);
}
}
if (!alarms.isEmpty()) {
entityAlarmManager.saveBatch(alarms);
}
for (ExpiredDeviceContext ctx : contexts) {
try {
completeExpiredDevice(ctx);
} catch (Exception e) {
log.warn("Device expiry completion failed, deviceId={}", ctx.state.getEntityId(), e);
}
}
}
private void processExpiredDevice(EntityStateDO scanned) {
// Write alarm row
private EntityAlarmDO buildOfflineAlarm(EntityStateDO scanned) {
EntityStatusEnum prev = EntityStatusEnum.ofIndex(scanned.getLastStateFlag());
String prevCode = Objects.nonNull(prev) ? prev.getCode() : "unknown";
String message = String.format("Device heartbeat timed out (last=%s); marked OFFLINE", prevCode);
@@ -161,7 +178,14 @@ public class EntityStateExpiryScanner {
alarm.setExpiredTime(0L);
alarm.setConfirmFlag((byte) 0);
alarm.setTenantId(scanned.getTenantId());
entityAlarmManager.save(alarm);
return alarm;
}
private void completeExpiredDevice(ExpiredDeviceContext ctx) {
EntityStateDO scanned = ctx.state;
EntityAlarmDO alarm = ctx.alarm;
EntityStatusEnum prev = EntityStatusEnum.ofIndex(scanned.getLastStateFlag());
String prevCode = Objects.nonNull(prev) ? prev.getCode() : "unknown";
// Update lastAlarmId
entityStateManager.lambdaUpdate()
@@ -174,6 +198,7 @@ public class EntityStateExpiryScanner {
.update();
// Trigger alarm rule pipeline
String message = String.format("Device heartbeat timed out (last=%s); marked OFFLINE", prevCode);
DeviceAlarmDTO dto = DeviceAlarmDTO.builder()
.driverId(scanned.getParentEntityId())
.tenantId(scanned.getTenantId())
@@ -188,4 +213,7 @@ public class EntityStateExpiryScanner {
log.info("Device scan marked OFFLINE: deviceId={}, tenantId={}, prevStatus={}",
scanned.getEntityId(), scanned.getTenantId(), prevCode);
}
private record ExpiredDeviceContext(EntityStateDO state, EntityAlarmDO alarm) {
}
}
@@ -174,6 +174,10 @@ public class SystemHealthServiceImpl implements SystemHealthService {
List<FacadeDriverBO> drivers;
try {
drivers = future.get(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Driver summary interrupted, tenantId={}", tenantId, e);
return summary;
} catch (Exception e) {
future.cancel(true);
log.warn("Driver summary failed, tenantId={}", tenantId, e);
@@ -209,6 +213,10 @@ public class SystemHealthServiceImpl implements SystemHealthService {
List<FacadeDeviceBO> devices;
try {
devices = future.get(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Device summary interrupted, tenantId={}", tenantId, e);
return summary;
} catch (Exception e) {
future.cancel(true);
log.warn("Device summary failed, tenantId={}", tenantId, e);
@@ -22,6 +22,7 @@ import io.github.pnoker.common.data.entity.property.PointBatchProperties;
import io.github.pnoker.common.entity.bo.PointValueBO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
@@ -43,6 +44,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
@Component
@RequiredArgsConstructor
@DisallowConcurrentExecution
public class PointValueJob extends QuartzJobBean {
private static final ReentrantReadWriteLock VALUE_LOCK = new ReentrantReadWriteLock();
@@ -145,7 +145,9 @@ public class DriverSenderServiceImpl implements DriverSenderService {
log.warn(
"DriverMetadata has no registered driver yet; point value will be published without driverId/tenantId");
}
log.info("Send point value: {}", JsonUtil.toJsonString(entityDTO));
if (log.isDebugEnabled()) {
log.debug("Send point value: {}", JsonUtil.toJsonString(entityDTO));
}
rabbitTemplate.convertAndSend(RabbitConstant.TOPIC_EXCHANGE_VALUE,
RabbitConstant.ROUTING_POINT_VALUE_PREFIX + driverProperties.getService(), entityDTO);
}
@@ -48,7 +48,7 @@ public class GrpcFacadeSupport {
} catch (StatusRuntimeException e) {
Status status = e.getStatus();
String description = Objects.requireNonNullElse(status.getDescription(), e.getMessage());
throw new ServiceException("{} transport failed: [{}] {}", operation, status.getCode(), description);
throw new ServiceException(operation + " transport failed: [" + status.getCode() + "] " + description);
}
}
+6
View File
@@ -38,6 +38,12 @@
<dependencies>
<!-- Spring Cache with Caffeine -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- gRPC Related -->
<dependency>
<groupId>org.springframework.grpc</groupId>
@@ -21,16 +21,16 @@ import io.github.pnoker.common.entity.ext.JsonExt;
import io.github.pnoker.common.exception.ImportException;
import io.github.pnoker.common.manager.biz.ImportDeviceService;
import io.github.pnoker.common.manager.dal.DeviceManager;
import io.github.pnoker.common.manager.dal.DriverAttributeConfigManager;
import io.github.pnoker.common.manager.dal.PointAttributeConfigManager;
import io.github.pnoker.common.manager.entity.bo.DeviceBO;
import io.github.pnoker.common.manager.entity.bo.DriverAttributeBO;
import io.github.pnoker.common.manager.entity.bo.DriverAttributeConfigBO;
import io.github.pnoker.common.manager.entity.bo.PointAttributeBO;
import io.github.pnoker.common.manager.entity.bo.PointAttributeConfigBO;
import io.github.pnoker.common.manager.entity.bo.PointBO;
import io.github.pnoker.common.manager.entity.builder.DeviceBuilder;
import io.github.pnoker.common.manager.entity.model.DeviceDO;
import io.github.pnoker.common.manager.service.DriverAttributeConfigService;
import io.github.pnoker.common.manager.service.PointAttributeConfigService;
import io.github.pnoker.common.manager.entity.model.DriverAttributeConfigDO;
import io.github.pnoker.common.manager.entity.model.PointAttributeConfigDO;
import io.github.pnoker.common.utils.PoiUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -39,6 +39,7 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
@@ -57,9 +58,9 @@ public class ImportDeviceServiceImpl implements ImportDeviceService {
private final DeviceManager deviceManager;
private final DriverAttributeConfigService driverAttributeConfigService;
private final DriverAttributeConfigManager driverAttributeConfigManager;
private final PointAttributeConfigService pointAttributeConfigService;
private final PointAttributeConfigManager pointAttributeConfigManager;
@Override
@Transactional
@@ -103,16 +104,19 @@ public class ImportDeviceServiceImpl implements ImportDeviceService {
*/
private void importDriverAttributeConfig(DeviceBO deviceBO, List<DriverAttributeBO> driverAttributeBOList,
Sheet sheet, int row) {
List<DriverAttributeConfigDO> entities = new ArrayList<>();
for (int j = 0; j < driverAttributeBOList.size(); j++) {
DriverAttributeConfigBO entityBO = new DriverAttributeConfigBO();
DriverAttributeConfigDO entityDO = new DriverAttributeConfigDO();
DriverAttributeBO driverAttributeBO = driverAttributeBOList.get(j);
entityBO.setAttributeId(driverAttributeBO.getId());
entityBO.setDeviceId(deviceBO.getId());
String attributeValue = PoiUtil.getCellStringValue(sheet, row, 2 + j);
entityBO.setConfigValue(attributeValue);
entityBO.setRemark(deviceBO.getRemark());
entityBO.setTenantId(deviceBO.getTenantId());
driverAttributeConfigService.innerSave(entityBO);
entityDO.setAttributeId(driverAttributeBO.getId());
entityDO.setDeviceId(deviceBO.getId());
entityDO.setConfigValue(PoiUtil.getCellStringValue(sheet, row, 2 + j));
entityDO.setRemark(deviceBO.getRemark());
entityDO.setTenantId(deviceBO.getTenantId());
entities.add(entityDO);
}
if (!entities.isEmpty()) {
driverAttributeConfigManager.saveBatch(entities);
}
}
@@ -129,22 +133,25 @@ public class ImportDeviceServiceImpl implements ImportDeviceService {
private void importPointAttributeConfig(DeviceBO deviceBO, List<PointBO> pointBOList,
List<DriverAttributeBO> driverAttributeBOList, List<PointAttributeBO> pointAttributeBOList, Sheet sheet,
int row) {
List<PointAttributeConfigDO> entities = new ArrayList<>();
for (int j = 0; j < pointBOList.size(); j++) {
for (int k = 0; k < pointAttributeBOList.size(); k++) {
PointAttributeConfigBO entityBO = new PointAttributeConfigBO();
PointAttributeConfigDO entityDO = new PointAttributeConfigDO();
PointBO pointBO = pointBOList.get(j);
PointAttributeBO pointAttributeBO = pointAttributeBOList.get(k);
entityBO.setAttributeId(pointAttributeBO.getId());
entityBO.setDeviceId(deviceBO.getId());
entityBO.setPointId(pointBO.getId());
String attributeValue = PoiUtil.getCellStringValue(sheet, row,
2 + driverAttributeBOList.size() + k * pointAttributeBOList.size() + j);
entityBO.setConfigValue(attributeValue);
entityBO.setRemark(deviceBO.getRemark());
entityBO.setTenantId(deviceBO.getTenantId());
pointAttributeConfigService.innerSave(entityBO);
entityDO.setAttributeId(pointAttributeBO.getId());
entityDO.setDeviceId(deviceBO.getId());
entityDO.setPointId(pointBO.getId());
entityDO.setConfigValue(PoiUtil.getCellStringValue(sheet, row,
2 + driverAttributeBOList.size() + k * pointAttributeBOList.size() + j));
entityDO.setRemark(deviceBO.getRemark());
entityDO.setTenantId(deviceBO.getTenantId());
entities.add(entityDO);
}
}
if (!entities.isEmpty()) {
pointAttributeConfigManager.saveBatch(entities);
}
}
}
@@ -14,28 +14,20 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.driver.api.impl.serializer.parser;
import java.util.Vector;
package io.github.pnoker.common.manager.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
/**
* Bean Parse Result
* Enables Spring Cache abstraction backed by Caffeine (auto-configured by Boot).
*
* @author pnoker
* @version 2025.9.0
* @since 2016.10.1
* @version 2026.5.26
* @since 2026.5.26
*/
public final class BeanParseResult {
/**
* The needed blocksize
*/
public int blockSize;
/**
* The Bean entries
*/
public Vector<BeanEntry> entries = new Vector<BeanEntry>();
@Configuration
@EnableCaching
public class CacheConfig {
}
@@ -30,6 +30,7 @@ import io.github.pnoker.api.common.GrpcCommandDTO;
import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.enums.ResponseEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.manager.entity.bo.CommandBO;
import io.github.pnoker.common.manager.entity.query.CommandQuery;
import io.github.pnoker.common.manager.grpc.builder.GrpcCommandBuilder;
@@ -130,7 +131,12 @@ public class ManagerCommandServer extends CommandApiGrpc.CommandApiImplBase {
GrpcRCommandDTO.Builder builder = GrpcRCommandDTO.newBuilder();
GrpcR.Builder rBuilder = GrpcR.newBuilder();
CommandBO entityBO = commandService.getById(request.getCommandId());
CommandBO entityBO;
try {
entityBO = commandService.getById(request.getCommandId());
} catch (NotFoundException e) {
entityBO = null;
}
if (Objects.isNull(entityBO)) {
rBuilder.setOk(false);
rBuilder.setCode(ResponseEnum.NO_RESOURCE.getCode());
@@ -32,6 +32,7 @@ import io.github.pnoker.api.common.GrpcDeviceDTO;
import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.enums.ResponseEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.manager.entity.bo.DeviceBO;
import io.github.pnoker.common.manager.entity.query.DeviceQuery;
import io.github.pnoker.common.manager.grpc.builder.GrpcDeviceBuilder;
@@ -186,7 +187,12 @@ public class ManagerDeviceServer extends DeviceApiGrpc.DeviceApiImplBase {
GrpcRDeviceDTO.Builder builder = GrpcRDeviceDTO.newBuilder();
GrpcR.Builder rBuilder = GrpcR.newBuilder();
DeviceBO entityBO = deviceService.getById(request.getDeviceId());
DeviceBO entityBO;
try {
entityBO = deviceService.getById(request.getDeviceId());
} catch (NotFoundException e) {
entityBO = null;
}
if (Objects.isNull(entityBO)) {
rBuilder.setOk(false);
rBuilder.setCode(ResponseEnum.NO_RESOURCE.getCode());
@@ -31,6 +31,7 @@ import io.github.pnoker.api.common.GrpcDriverDTO;
import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.enums.ResponseEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.manager.entity.bo.DriverBO;
import io.github.pnoker.common.manager.entity.query.DriverQuery;
import io.github.pnoker.common.manager.grpc.builder.GrpcDriverBuilder;
@@ -154,7 +155,12 @@ public class ManagerDriverServer extends DriverApiGrpc.DriverApiImplBase {
GrpcRDriverDTO.Builder builder = GrpcRDriverDTO.newBuilder();
GrpcR.Builder rBuilder = GrpcR.newBuilder();
DriverBO driverBO = driverService.getById(request.getDriverId());
DriverBO driverBO;
try {
driverBO = driverService.getById(request.getDriverId());
} catch (NotFoundException e) {
driverBO = null;
}
if (Objects.isNull(driverBO)) {
rBuilder.setOk(false);
rBuilder.setCode(ResponseEnum.NO_RESOURCE.getCode());
@@ -30,6 +30,7 @@ import io.github.pnoker.api.common.GrpcEventDTO;
import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.enums.ResponseEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.manager.entity.bo.EventBO;
import io.github.pnoker.common.manager.entity.query.EventQuery;
import io.github.pnoker.common.manager.grpc.builder.GrpcEventBuilder;
@@ -130,7 +131,12 @@ public class ManagerEventServer extends EventApiGrpc.EventApiImplBase {
GrpcREventDTO.Builder builder = GrpcREventDTO.newBuilder();
GrpcR.Builder rBuilder = GrpcR.newBuilder();
EventBO entityBO = eventService.getById(request.getEventId());
EventBO entityBO;
try {
entityBO = eventService.getById(request.getEventId());
} catch (NotFoundException e) {
entityBO = null;
}
if (Objects.isNull(entityBO)) {
rBuilder.setOk(false);
rBuilder.setCode(ResponseEnum.NO_RESOURCE.getCode());
@@ -30,6 +30,7 @@ import io.github.pnoker.api.common.GrpcPage;
import io.github.pnoker.api.common.GrpcPointDTO;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.enums.ResponseEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.manager.entity.bo.PointBO;
import io.github.pnoker.common.manager.entity.query.PointQuery;
import io.github.pnoker.common.manager.grpc.builder.GrpcPointBuilder;
@@ -130,7 +131,12 @@ public class ManagerPointServer extends PointApiGrpc.PointApiImplBase {
GrpcRPointDTO.Builder builder = GrpcRPointDTO.newBuilder();
GrpcR.Builder rBuilder = GrpcR.newBuilder();
PointBO entityBO = pointService.getById(request.getPointId());
PointBO entityBO;
try {
entityBO = pointService.getById(request.getPointId());
} catch (NotFoundException e) {
entityBO = null;
}
if (Objects.isNull(entityBO)) {
rBuilder.setOk(false);
rBuilder.setCode(ResponseEnum.NO_RESOURCE.getCode());
@@ -30,6 +30,7 @@ import io.github.pnoker.api.center.manager.ProfileApiGrpc;
import io.github.pnoker.api.common.GrpcProfileDTO;
import io.github.pnoker.api.common.GrpcR;
import io.github.pnoker.common.enums.ResponseEnum;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.manager.entity.bo.ProfileBO;
import io.github.pnoker.common.manager.entity.query.ProfileQuery;
import io.github.pnoker.common.manager.grpc.builder.GrpcProfileBuilder;
@@ -90,7 +91,12 @@ public class ManagerProfileServer extends ProfileApiGrpc.ProfileApiImplBase {
GrpcRProfileDTO.Builder builder = GrpcRProfileDTO.newBuilder();
GrpcR.Builder rBuilder = GrpcR.newBuilder();
ProfileBO profile = profileService.getById(request.getProfileId());
ProfileBO profile;
try {
profile = profileService.getById(request.getProfileId());
} catch (NotFoundException e) {
profile = null;
}
if (Objects.isNull(profile)) {
noResource(rBuilder);
} else {
@@ -165,18 +165,18 @@ public class CommandParamServiceImpl implements CommandParamService {
private LambdaQueryWrapper<CommandParamDO> fuzzyQuery(CommandParamQuery entityQuery) {
QueryWrapper<CommandParamDO> wrapper = Wrappers.query();
wrapper.eq("dcp.deleted", 0);
wrapper.like(StringUtils.isNotEmpty(entityQuery.getParamName()), "dcp.param_name", entityQuery.getParamName());
wrapper.eq(StringUtils.isNotEmpty(entityQuery.getParamCode()), "dcp.param_code", entityQuery.getParamCode());
wrapper.eq(Objects.nonNull(entityQuery.getParamDirection()), "dcp.param_direction_flag",
wrapper.eq("deleted", 0);
wrapper.like(StringUtils.isNotEmpty(entityQuery.getParamName()), "param_name", entityQuery.getParamName());
wrapper.eq(StringUtils.isNotEmpty(entityQuery.getParamCode()), "param_code", entityQuery.getParamCode());
wrapper.eq(Objects.nonNull(entityQuery.getParamDirection()), "param_direction_flag",
Objects.isNull(entityQuery.getParamDirection()) ? null : entityQuery.getParamDirection().getIndex());
wrapper.eq(Objects.nonNull(entityQuery.getParamTypeFlag()), "dcp.param_type_flag",
wrapper.eq(Objects.nonNull(entityQuery.getParamTypeFlag()), "param_type_flag",
Objects.isNull(entityQuery.getParamTypeFlag()) ? null : entityQuery.getParamTypeFlag().getIndex());
wrapper.eq(Objects.nonNull(entityQuery.getCommandId()), "dcp.command_id", entityQuery.getCommandId());
wrapper.eq(Objects.nonNull(entityQuery.getEnableFlag()), "dcp.enable_flag",
wrapper.eq(Objects.nonNull(entityQuery.getCommandId()), "command_id", entityQuery.getCommandId());
wrapper.eq(Objects.nonNull(entityQuery.getEnableFlag()), "enable_flag",
Objects.isNull(entityQuery.getEnableFlag()) ? null : entityQuery.getEnableFlag().getIndex());
wrapper.eq(Objects.nonNull(entityQuery.getTenantId()), "dcp.tenant_id", entityQuery.getTenantId());
wrapper.eq(Objects.nonNull(entityQuery.getVersion()), "dcp.version", entityQuery.getVersion());
wrapper.eq(Objects.nonNull(entityQuery.getTenantId()), "tenant_id", entityQuery.getTenantId());
wrapper.eq(Objects.nonNull(entityQuery.getVersion()), "version", entityQuery.getVersion());
return wrapper.lambda();
}
@@ -165,16 +165,16 @@ public class EventParamServiceImpl implements EventParamService {
private LambdaQueryWrapper<EventParamDO> fuzzyQuery(EventParamQuery entityQuery) {
QueryWrapper<EventParamDO> wrapper = Wrappers.query();
wrapper.eq("dep.deleted", 0);
wrapper.like(StringUtils.isNotEmpty(entityQuery.getParamName()), "dep.param_name", entityQuery.getParamName());
wrapper.eq(StringUtils.isNotEmpty(entityQuery.getParamCode()), "dep.param_code", entityQuery.getParamCode());
wrapper.eq(Objects.nonNull(entityQuery.getParamTypeFlag()), "dep.param_type_flag",
wrapper.eq("deleted", 0);
wrapper.like(StringUtils.isNotEmpty(entityQuery.getParamName()), "param_name", entityQuery.getParamName());
wrapper.eq(StringUtils.isNotEmpty(entityQuery.getParamCode()), "param_code", entityQuery.getParamCode());
wrapper.eq(Objects.nonNull(entityQuery.getParamTypeFlag()), "param_type_flag",
Objects.isNull(entityQuery.getParamTypeFlag()) ? null : entityQuery.getParamTypeFlag().getIndex());
wrapper.eq(Objects.nonNull(entityQuery.getEventId()), "dep.event_id", entityQuery.getEventId());
wrapper.eq(Objects.nonNull(entityQuery.getEnableFlag()), "dep.enable_flag",
wrapper.eq(Objects.nonNull(entityQuery.getEventId()), "event_id", entityQuery.getEventId());
wrapper.eq(Objects.nonNull(entityQuery.getEnableFlag()), "enable_flag",
Objects.isNull(entityQuery.getEnableFlag()) ? null : entityQuery.getEnableFlag().getIndex());
wrapper.eq(Objects.nonNull(entityQuery.getTenantId()), "dep.tenant_id", entityQuery.getTenantId());
wrapper.eq(Objects.nonNull(entityQuery.getVersion()), "dep.version", entityQuery.getVersion());
wrapper.eq(Objects.nonNull(entityQuery.getTenantId()), "tenant_id", entityQuery.getTenantId());
wrapper.eq(Objects.nonNull(entityQuery.getVersion()), "version", entityQuery.getVersion());
return wrapper.lambda();
}
@@ -247,7 +247,7 @@ public class PointServiceImpl implements PointService {
List<DeviceDO> deviceDOList = deviceMapper
.selectList(new LambdaQueryWrapper<DeviceDO>().in(DeviceDO::getId, deviceIds));
deviceByPointBO.setDevices(deviceDOList);
deviceByPointBO.setCount(deviceDOList.stream().count());
deviceByPointBO.setCount((long) deviceDOList.size());
} else {
deviceByPointBO.setDevices(Collections.emptyList());
deviceByPointBO.setCount(0L);
@@ -22,6 +22,7 @@ import io.github.pnoker.common.mqtt.entity.property.MqttProperties;
import io.github.pnoker.common.mqtt.service.MqttReceiveService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -49,6 +50,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
@Component
@RequiredArgsConstructor
@ConditionalOnBean(MqttReceiveService.class)
@DisallowConcurrentExecution
public class MqttScheduleJob extends QuartzJobBean {
private static final ReentrantReadWriteLock MESSAGE_LOCK = new ReentrantReadWriteLock();
@@ -119,6 +119,9 @@ public class HostUtil {
ArrayList<String> macList = new ArrayList<>(16);
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
if (Objects.isNull(interfaces)) {
return macList;
}
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
lookupLocalMac(macList, networkInterface);
@@ -143,6 +146,9 @@ public class HostUtil {
*/
private static void loopBackAddresses(Set<String> hostNames, boolean includeLoopBack) throws SocketException {
Enumeration<NetworkInterface> interfaceEnumeration = NetworkInterface.getNetworkInterfaces();
if (Objects.isNull(interfaceEnumeration)) {
return;
}
for (NetworkInterface networkInterface : Collections.list(interfaceEnumeration)) {
Collections.list(networkInterface.getInetAddresses()).forEach(inetAddress -> {
@@ -199,6 +199,14 @@ public class KeyUtil {
return DecodeUtil.byteToString(cipher.doFinal(inputByte));
}
private static String getSecurityKey() {
String key = System.getenv("DC3_SECURITY_KEY");
if (key == null || key.isBlank()) {
key = System.getProperty("dc3.security.key", AlgorithmConstant.DEFAULT_KEY);
}
return key;
}
/**
* Generate a JWT token.
*
@@ -208,11 +216,12 @@ public class KeyUtil {
* @return Token string
*/
public static String generateToken(String userName, String salt, Long tenantId) {
String securityKey = getSecurityKey();
SecretKey key = io.jsonwebtoken.security.Keys
.hmacShaKeyFor(DecodeUtil.stringToByte(AlgorithmConstant.DEFAULT_KEY + SymbolConstant.COLON + salt));
.hmacShaKeyFor(DecodeUtil.stringToByte(securityKey + SymbolConstant.COLON + salt));
JwtBuilder builder = Jwts.builder()
.issuer(AlgorithmConstant.DEFAULT_KEY + SymbolConstant.COLON + tenantId)
.subject(AlgorithmConstant.DEFAULT_KEY + SymbolConstant.COLON + userName)
.issuer(securityKey + SymbolConstant.COLON + tenantId)
.subject(securityKey + SymbolConstant.COLON + userName)
.issuedAt(new Date())
.signWith(key, Jwts.SIG.HS256)
.expiration(TimeUtil.expireTime(TimeoutConstant.TOKEN_CACHE_TIMEOUT, Calendar.HOUR));
@@ -229,11 +238,12 @@ public class KeyUtil {
* @return Claims
*/
public static Claims parserToken(String userName, String salt, String token, Long tenantId) {
String securityKey = getSecurityKey();
SecretKey key = io.jsonwebtoken.security.Keys
.hmacShaKeyFor(DecodeUtil.stringToByte(AlgorithmConstant.DEFAULT_KEY + SymbolConstant.COLON + salt));
.hmacShaKeyFor(DecodeUtil.stringToByte(securityKey + SymbolConstant.COLON + salt));
JwtParser parser = Jwts.parser()
.requireIssuer(AlgorithmConstant.DEFAULT_KEY + SymbolConstant.COLON + tenantId)
.requireSubject(AlgorithmConstant.DEFAULT_KEY + SymbolConstant.COLON + userName)
.requireIssuer(securityKey + SymbolConstant.COLON + tenantId)
.requireSubject(securityKey + SymbolConstant.COLON + userName)
.verifyWith(key)
.build();
return parser.parseSignedClaims(token).getPayload();
@@ -60,6 +60,9 @@ public class RegexUtil {
* @return true if valid name format, false otherwise
*/
public static boolean isName(String name) {
if (StringUtils.isEmpty(name)) {
return false;
}
String regex = "^[A-Za-z0-9\\u4e00-\\u9fa5][A-Za-z0-9\\u4e00-\\u9fa5-_#@/.|]{1,31}$";
return name.matches(regex);
}
@@ -71,6 +74,9 @@ public class RegexUtil {
* @return true if valid mobile format, false otherwise
*/
public static boolean isPhone(String phone) {
if (StringUtils.isEmpty(phone)) {
return false;
}
String regex = "^1([3-9])\\d{9}$";
return phone.matches(regex);
}
@@ -82,6 +88,9 @@ public class RegexUtil {
* @return true if valid email format, false otherwise
*/
public static boolean isMail(String mail) {
if (StringUtils.isEmpty(mail)) {
return false;
}
String regex = "^[A-Za-z0-9_.-]+@[A-Za-z0-9]+\\.[A-Za-z0-9]+$";
return mail.matches(regex);
}
@@ -93,6 +102,9 @@ public class RegexUtil {
* @return true if valid password format, false otherwise
*/
public static boolean isPassword(String password) {
if (StringUtils.isEmpty(password)) {
return false;
}
String regex = "^[a-zA-Z]\\w{7,15}$";
return password.matches(regex);
}
@@ -104,6 +116,9 @@ public class RegexUtil {
* @return true if valid host format, false otherwise
*/
public static boolean isHost(String host) {
if (StringUtils.isEmpty(host)) {
return false;
}
String regex = "^((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}$";
return host.matches(regex);
}
@@ -113,6 +113,7 @@ public class TimeUtil {
try {
return DEFAULT_DATE_FORMAT_THREAD_LOCAL.get().parse(dateString);
} catch (ParseException e) {
log.debug("Failed to parse date string '{}' with default format", dateString);
return null;
}
}
@@ -127,6 +128,7 @@ public class TimeUtil {
try {
return COMPLETE_DATE_FORMAT_THREAD_LOCAL.get().parse(dateString);
} catch (ParseException e) {
log.debug("Failed to parse date string '{}' with complete format", dateString);
return null;
}
}
@@ -98,10 +98,9 @@ public class WebFilterConfig {
.contextWrite(context -> context.put(RequestConstant.Key.USER_HEADER, userHeader));
}
} catch (Exception e) {
log.error("Error parsing user header", e);
log.warn("Rejecting request with malformed X-Auth-User header, Url: {}", request.getURI(), e);
return writeUnauthorized(exchange);
}
return chain.filter(exchange);
};
}
+5
View File
@@ -169,6 +169,11 @@
<artifactId>dc3-driver-modbus-tcp</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-modbus-rtu</artifactId>
<version>${dc3.version}</version>
</dependency>
<dependency>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver-mqtt</artifactId>
@@ -67,22 +67,24 @@ public class CoapClientManager implements DisposableBean {
*/
public CoapResult get(String uri, String path) {
CoapClient client = getClient(uri);
client.setURI(uri + path);
try {
Configuration config = client.getEndpoint().getConfig();
config.set(CoapConfig.EXCHANGE_LIFETIME, coapProperties.getClientTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getClientAckTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getClientMaxRetransmit());
synchronized (client) {
client.setURI(uri + path);
try {
Configuration config = client.getEndpoint().getConfig();
config.set(CoapConfig.EXCHANGE_LIFETIME, coapProperties.getClientTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getClientAckTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getClientMaxRetransmit());
org.eclipse.californium.core.CoapResponse response = client.get();
if (response == null) {
log.warn("CoAP GET timeout: {}{}", uri, path);
org.eclipse.californium.core.CoapResponse response = client.get();
if (response == null) {
log.warn("CoAP GET timeout: {}{}", uri, path);
return null;
}
return toResult(response);
} catch (Exception e) {
log.error("CoAP GET failed: {}{}", uri, path, e);
return null;
}
return toResult(response);
} catch (Exception e) {
log.error("CoAP GET failed: {}{}", uri, path, e);
return null;
}
}
@@ -96,17 +98,19 @@ public class CoapClientManager implements DisposableBean {
*/
public CoapResult put(String uri, String path, String payload) {
CoapClient client = getClient(uri);
client.setURI(uri + path);
try {
org.eclipse.californium.core.CoapResponse response = client.put(payload, MediaTypeRegistry.APPLICATION_JSON);
if (response == null) {
log.warn("CoAP PUT timeout: {}{}", uri, path);
synchronized (client) {
client.setURI(uri + path);
try {
org.eclipse.californium.core.CoapResponse response = client.put(payload, MediaTypeRegistry.APPLICATION_JSON);
if (response == null) {
log.warn("CoAP PUT timeout: {}{}", uri, path);
return null;
}
return toResult(response);
} catch (Exception e) {
log.error("CoAP PUT failed: {}{}", uri, path, e);
return null;
}
return toResult(response);
} catch (Exception e) {
log.error("CoAP PUT failed: {}{}", uri, path, e);
return null;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016-present the IoT DC3 original author or authors.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as
~ published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.pnoker</groupId>
<artifactId>dc3-driver</artifactId>
<version>2026.5.22</version>
</parent>
<artifactId>dc3-driver-modbus-rtu</artifactId>
<packaging>jar</packaging>
<description>The Modbus RTU driver for the IoT DC3 platform.</description>
<dependencies>
<!-- Modbus Protocol -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
</dependency>
<!-- Serial Communication -->
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
</dependency>
</dependencies>
</project>
@@ -14,28 +14,31 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
package io.github.pnoker.driver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Slave Id Not Equal
* Modbus RTU Driver Application for DC3 IoT Platform. This driver implements
* serial communication with Modbus RTU devices, providing data collection and
* device management capabilities over RS-232/RS-485 serial interfaces.
*
* @author pnoker
* @version 2025.9.0
* @version 2026.5.22
* @since 2016.10.1
*/
public class SlaveIdNotEqual extends ModbusTransportException {
private static final long serialVersionUID = -1;
@SpringBootApplication
public class ModbusRtuDriverApplication {
/**
* Exception to show that the requested slave id is not what was received
* Main method to start the Modbus RTU driver application
*
* @param requestSlaveId - slave id requested
* @param responseSlaveId - slave id of response
* @param args Command line arguments passed to the application
*/
public SlaveIdNotEqual(int requestSlaveId, int responseSlaveId) {
super("Response slave id different from requested id", requestSlaveId);
public static void main(String[] args) {
SpringApplication.run(ModbusRtuDriverApplication.class, args);
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.driver.service.impl;
import com.fazecast.jSerialComm.SerialPort;
import com.serotonin.modbus4j.serial.SerialPortWrapper;
import lombok.extern.slf4j.Slf4j;
import java.io.InputStream;
import java.io.OutputStream;
/**
* jSerialComm implementation of the modbus4j SerialPortWrapper interface.
*
* @author pnoker
* @version 2026.5.22
* @since 2016.10.1
*/
@Slf4j
public class JSerialCommWrapper implements SerialPortWrapper {
private final String portName;
private final int baudRate;
private final int dataBits;
private final int stopBits;
private final int parity;
private SerialPort serialPort;
/**
* @param portName serial port name (e.g. /dev/ttyUSB0, COM3)
* @param baudRate baud rate (e.g. 9600, 19200, 115200)
* @param dataBits data bits (7 or 8)
* @param stopBits stop bits (1 or 2)
* @param parity parity (0=None, 1=Odd, 2=Even, 3=Mark, 4=Space)
*/
public JSerialCommWrapper(String portName, int baudRate, int dataBits, int stopBits, int parity) {
this.portName = portName;
this.baudRate = baudRate;
this.dataBits = dataBits;
this.stopBits = stopBits;
this.parity = parity;
}
@Override
public void open() throws Exception {
serialPort = SerialPort.getCommPort(portName);
serialPort.setComPortParameters(baudRate, dataBits, stopBits, parity);
serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 2000, 0);
if (!serialPort.openPort()) {
throw new Exception("Failed to open serial port: " + portName);
}
log.info("Serial port opened, port={}, baudRate={}, dataBits={}, stopBits={}, parity={}",
portName, baudRate, dataBits, stopBits, parity);
}
@Override
public void close() throws Exception {
if (serialPort != null && serialPort.isOpen()) {
serialPort.closePort();
log.info("Serial port closed, port={}", portName);
}
}
@Override
public InputStream getInputStream() {
return serialPort.getInputStream();
}
@Override
public OutputStream getOutputStream() {
return serialPort.getOutputStream();
}
@Override
public int getBaudRate() {
return baudRate;
}
@Override
public int getDataBits() {
return dataBits;
}
@Override
public int getStopBits() {
return stopBits;
}
@Override
public int getParity() {
return parity;
}
}
@@ -0,0 +1,347 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.driver.service.impl;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.locator.BaseLocator;
import com.serotonin.modbus4j.msg.WriteCoilRequest;
import com.serotonin.modbus4j.msg.WriteCoilResponse;
import io.github.pnoker.common.driver.entity.bean.DeviceHealthState;
import io.github.pnoker.common.driver.entity.bean.ReadPointValue;
import io.github.pnoker.common.driver.entity.bean.WritePointValue;
import io.github.pnoker.common.driver.entity.bo.AttributeBO;
import io.github.pnoker.common.driver.entity.bo.DeviceBO;
import io.github.pnoker.common.driver.entity.bo.PointBO;
import io.github.pnoker.common.driver.metadata.DriverMetadata;
import io.github.pnoker.common.driver.service.DriverCustomService;
import io.github.pnoker.common.driver.service.DriverSenderService;
import io.github.pnoker.common.entity.dto.MetadataEventDTO;
import io.github.pnoker.common.enums.MetadataOperateTypeEnum;
import io.github.pnoker.common.enums.MetadataTypeEnum;
import io.github.pnoker.common.enums.PointTypeFlagEnum;
import io.github.pnoker.common.exception.ConnectorException;
import io.github.pnoker.common.exception.ReadPointException;
import io.github.pnoker.common.exception.UnSupportException;
import io.github.pnoker.common.exception.WritePointException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Custom driver service implementation for the Modbus RTU driver.
* <p>
* Manages Modbus RTU serial connections, reads point values from Modbus devices
* via function codes 1-4, and writes values to coils and holding registers.
* Each RTU device is connected via a dedicated serial port.
* </p>
*
* @author pnoker
* @version 2026.5.22
* @since 2016.10.1
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ModbusRtuDriverCustomServiceImpl implements DriverCustomService {
/**
* Modbus factory for creating ModbusMaster instances.
*/
static ModbusFactory modbusFactory;
static {
modbusFactory = new ModbusFactory();
}
private final DriverMetadata driverMetadata;
private final DriverSenderService driverSenderService;
@Value("${dc3.driver.code}")
private String driverCode;
/**
* Cache of device ID to ModbusMaster connections.
*/
private Map<Long, ModbusMaster> connectMap;
@Override
public void initial() {
connectMap = new ConcurrentHashMap<>(16);
}
@Override
public void schedule() {
// Device state lease renewal is owned by the SDK device health job.
}
@Override
public DeviceHealthState health(Map<String, AttributeBO> driverConfig, DeviceBO device) {
if (Objects.isNull(device) || Objects.isNull(device.getId())) {
return DeviceHealthState.offline();
}
try {
return getConnector(device.getId(), driverConfig).isInitialized()
? DeviceHealthState.online()
: DeviceHealthState.offline();
} catch (Exception e) {
log.warn("Driver health check failed, protocol=" + driverCode + ", deviceId={}", device.getId(), e);
return DeviceHealthState.offline();
}
}
@Override
public void event(MetadataEventDTO metadataEvent) {
MetadataTypeEnum metadataType = metadataEvent.getMetadataType();
MetadataOperateTypeEnum operateType = metadataEvent.getOperateType();
if (MetadataTypeEnum.DEVICE.equals(metadataType)) {
log.info("Driver metadata event received, protocol=" + driverCode + ", metadataType={}, operateType={}, deviceId={}",
metadataType, operateType, metadataEvent.getId());
// Remove stale connection when device is updated or deleted
if (MetadataOperateTypeEnum.DELETE.equals(operateType)
|| MetadataOperateTypeEnum.UPDATE.equals(operateType)) {
ModbusMaster removed = connectMap.remove(metadataEvent.getId());
if (Objects.nonNull(removed)) {
removed.destroy();
log.info("Driver connection destroyed, protocol=" + driverCode + ", deviceId={}, operateType={}",
metadataEvent.getId(), operateType);
}
}
} else if (MetadataTypeEnum.POINT.equals(metadataType)) {
log.info("Driver metadata event received, protocol=" + driverCode + ", metadataType={}, operateType={}, pointId={}",
metadataType, operateType, metadataEvent.getId());
}
}
@Override
public ReadPointValue read(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, DeviceBO device,
PointBO point) {
return new ReadPointValue(device, point,
readValue(getConnector(device.getId(), driverConfig), pointConfig, point.getPointTypeFlag().getCode()));
}
@Override
public Boolean write(Map<String, AttributeBO> driverConfig, Map<String, AttributeBO> pointConfig, DeviceBO device,
PointBO point, WritePointValue writePointValue) {
ModbusMaster modbusMaster = getConnector(device.getId(), driverConfig);
return writeValue(modbusMaster, pointConfig, writePointValue);
}
/**
* Get or create a Modbus RTU connection for the given device.
*
* @param deviceId unique device identifier
* @param driverConfig driver configuration containing serial port parameters
* @return cached or newly created ModbusMaster
* @throws ConnectorException if connection initialization fails
*/
private ModbusMaster getConnector(Long deviceId, Map<String, AttributeBO> driverConfig) {
return connectMap.computeIfAbsent(deviceId, id -> {
String port = driverConfig.get("port").getValue(String.class);
int baudRate = driverConfig.get("baudRate").getValue(Integer.class);
int dataBits = driverConfig.get("dataBits").getValue(Integer.class);
int stopBits = driverConfig.get("stopBits").getValue(Integer.class);
int parity = driverConfig.get("parity").getValue(Integer.class);
log.debug("Driver connection creating, protocol=" + driverCode + ", deviceId={}, port={}, baudRate={}", deviceId,
port, baudRate);
JSerialCommWrapper wrapper = new JSerialCommWrapper(port, baudRate, dataBits, stopBits, parity);
ModbusMaster modbusMaster = modbusFactory.createRtuMaster(wrapper);
try {
modbusMaster.init();
log.info("Driver connection established, protocol=" + driverCode + ", deviceId={}, port={}, baudRate={}",
deviceId, port, baudRate);
} catch (ModbusInitException e) {
log.error("Driver connection failed, protocol=" + driverCode + ", deviceId={}, port={}, baudRate={}", deviceId,
port, baudRate, e);
throw new ConnectorException("Driver connection failed, protocol=" + driverCode + ", deviceId={}, port={}, message={}",
deviceId, port, e.getMessage(), e);
}
return modbusMaster;
});
}
/**
* Read a point value from the Modbus device by function code.
* <p>
* Function codes: 1=coil, 2=input status, 3=holding register, 4=input register.
*
* @param modbusMaster active Modbus connection
* @param pointConfig point configuration (slaveId, functionCode, offset)
* @param type point value type for register data interpretation
* @return read value as string, or "0" for unsupported function codes
*/
private String readValue(ModbusMaster modbusMaster, Map<String, AttributeBO> pointConfig, String type) {
int slaveId = pointConfig.get("slaveId").getValue(Integer.class);
int functionCode = pointConfig.get("functionCode").getValue(Integer.class);
int offset = pointConfig.get("offset").getValue(Integer.class);
switch (functionCode) {
case 1:
BaseLocator<Boolean> coilLocator = BaseLocator.coilStatus(slaveId, offset);
Boolean coilValue = getMasterValue(modbusMaster, coilLocator);
return String.valueOf(coilValue);
case 2:
BaseLocator<Boolean> inputLocator = BaseLocator.inputStatus(slaveId, offset);
Boolean inputStatusValue = getMasterValue(modbusMaster, inputLocator);
return String.valueOf(inputStatusValue);
case 3:
BaseLocator<Number> holdingLocator = BaseLocator.holdingRegister(slaveId, offset, getValueType(type));
Number holdingValue = getMasterValue(modbusMaster, holdingLocator);
return String.valueOf(holdingValue);
case 4:
BaseLocator<Number> inputRegister = BaseLocator.inputRegister(slaveId, offset, getValueType(type));
Number inputRegisterValue = getMasterValue(modbusMaster, inputRegister);
return String.valueOf(inputRegisterValue);
default:
log.warn("Unsupported Modbus function code, slaveId={}, functionCode={}, offset={}", slaveId,
functionCode, offset);
return "0";
}
}
/**
* Read a value from the Modbus device using the given locator.
*
* @param modbusMaster active Modbus connection
* @param locator identifies the target point (slave, function, offset)
* @param <T> value type determined by the locator
* @return the read value
* @throws ReadPointException if a transport or error response occurs
*/
private <T> T getMasterValue(ModbusMaster modbusMaster, BaseLocator<T> locator) {
try {
return modbusMaster.getValue(locator);
} catch (ModbusTransportException | ErrorResponseException e) {
log.error("Driver point read failed, protocol=" + driverCode + "", e);
throw new ReadPointException("Driver point read failed, protocol=" + driverCode + ", message={}", e.getMessage(),
e);
}
}
/**
* Write a point value to the Modbus device by function code.
* <p>
* Function codes: 1=write coil, 3=write holding register. Others return false.
*
* @param modbusMaster active Modbus connection
* @param pointConfig point configuration (slaveId, functionCode, offset)
* @param writePointValue value to write
* @return true if write succeeded, false if failed or unsupported function code
*/
private boolean writeValue(ModbusMaster modbusMaster, Map<String, AttributeBO> pointConfig, WritePointValue writePointValue) {
int slaveId = pointConfig.get("slaveId").getValue(Integer.class);
int functionCode = pointConfig.get("functionCode").getValue(Integer.class);
int offset = pointConfig.get("offset").getValue(Integer.class);
switch (functionCode) {
case 1:
WriteCoilResponse coilResponse = setMasterValue(modbusMaster, slaveId, offset, writePointValue);
return !coilResponse.isException();
case 3:
BaseLocator<Number> locator = BaseLocator.holdingRegister(slaveId, offset,
getValueType(writePointValue.getType().getCode()));
setMasterValue(modbusMaster, locator, writePointValue);
return true;
default:
log.warn("Unsupported Modbus write function code, slaveId={}, functionCode={}, offset={}", slaveId,
functionCode, offset);
return false;
}
}
/**
* Map a point type flag to a Modbus DataType constant.
* <p>
* LONG->4-byte int, FLOAT->4-byte float, DOUBLE->8-byte float, else 2-byte int.
*
* @param type point type code
* @return Modbus DataType constant
* @throws UnSupportException if the type is unknown
*/
private int getValueType(String type) {
PointTypeFlagEnum valueType = PointTypeFlagEnum.ofCode(type);
if (Objects.isNull(valueType)) {
throw new UnSupportException("Unsupported type of " + type);
}
return switch (valueType) {
case LONG -> DataType.FOUR_BYTE_INT_SIGNED;
case FLOAT -> DataType.FOUR_BYTE_FLOAT;
case DOUBLE -> DataType.EIGHT_BYTE_FLOAT;
default -> DataType.TWO_BYTE_INT_SIGNED;
};
}
/**
* Write a boolean value to a Modbus coil.
*
* @param modbusMaster active Modbus connection
* @param slaveId target slave address
* @param offset coil offset
* @param writePointValue value containing the boolean to write
* @return the coil write response
* @throws WritePointException if a transport error occurs
*/
private WriteCoilResponse setMasterValue(ModbusMaster modbusMaster, int slaveId, int offset, WritePointValue writePointValue) {
try {
WriteCoilRequest coilRequest = new WriteCoilRequest(slaveId, offset, writePointValue.getValue(Boolean.class));
return (WriteCoilResponse) modbusMaster.send(coilRequest);
} catch (ModbusTransportException e) {
log.error("Driver point write failed, protocol=" + driverCode + ", slaveId={}, offset={}", slaveId, offset, e);
throw new WritePointException("Driver point write failed, protocol=" + driverCode + ", slaveId={}, offset={}, message={}",
slaveId, offset, e.getMessage(), e);
}
}
/**
* Write a numeric value to a Modbus holding register via the given locator.
*
* @param modbusMaster active Modbus connection
* @param locator identifies the target register
* @param writePointValue value to write
* @param <T> value type determined by the locator
* @throws WritePointException if a transport or error response occurs
*/
private <T> void setMasterValue(ModbusMaster modbusMaster, BaseLocator<T> locator, WritePointValue writePointValue) {
try {
PointTypeFlagEnum valueType = PointTypeFlagEnum.ofCode(writePointValue.getType().getCode());
if (Objects.isNull(valueType)) {
throw new UnSupportException("Unsupported type of " + writePointValue.getType());
}
Number value = switch (valueType) {
case INT -> writePointValue.getValue(Integer.class);
case LONG -> writePointValue.getValue(Long.class);
case FLOAT -> writePointValue.getValue(Float.class);
case DOUBLE -> writePointValue.getValue(Double.class);
default -> writePointValue.getValue(Float.class);
};
modbusMaster.setValue(locator, value);
} catch (ModbusTransportException | ErrorResponseException e) {
log.error("Driver point write failed, protocol=" + driverCode + "", e);
throw new WritePointException("Driver point write failed, protocol=" + driverCode + ", message={}", e.getMessage(),
e);
}
}
}
@@ -0,0 +1,40 @@
#
# Copyright 2016-present the IoT DC3 original author or authors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Development environment configuration will overwrite the content configured in application-*.yml
dc3:
driver:
schedule:
# Read data regularly
read:
cron: '0/5 * * * * ?'
spring:
# When enabling env and group, auth, manager and data services need to be started locally
env: dev
group: ${user.name}
rabbitmq:
virtual-host: ${RABBITMQ_VIRTUAL_HOST:dc3}
host: ${RABBITMQ_HOST:dc3-rabbitmq}
port: ${RABBITMQ_PORT:35672}
username: ${RABBITMQ_USERNAME:dc3}
password: ${RABBITMQ_PASSWORD:dc3dc3dc3}
logging:
level:
io.github.pnoker: DEBUG
io.github.pnoker.driver.sdk: DEBUG
@@ -0,0 +1,32 @@
#
# Copyright 2016-present the IoT DC3 original author or authors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Pre-release environment configuration will overwrite the content configured in application-*.yml
dc3:
driver:
schedule:
# Read data regularly
read:
cron: '0 0/15 * * * ?'
spring:
rabbitmq:
virtual-host: ${RABBITMQ_VIRTUAL_HOST:dc3}
host: ${RABBITMQ_HOST:dc3-rabbitmq}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:dc3}
password: ${RABBITMQ_PASSWORD:dc3dc3dc3}
@@ -0,0 +1,32 @@
#
# Copyright 2016-present the IoT DC3 original author or authors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Production environment configuration will overwrite the content configured in application-*.yml
dc3:
driver:
schedule:
# Read data regularly
read:
cron: '0 0/15 * * * ?'
spring:
rabbitmq:
virtual-host: ${RABBITMQ_VIRTUAL_HOST:dc3}
host: ${RABBITMQ_HOST:dc3-rabbitmq}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:dc3}
password: ${RABBITMQ_PASSWORD:dc3dc3dc3}
@@ -0,0 +1,36 @@
#
# Copyright 2016-present the IoT DC3 original author or authors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Test environment configuration will overwrite the content configured in application-*.yml
dc3:
driver:
schedule:
# Read data regularly
read:
cron: '0/5 * * * * ?'
spring:
rabbitmq:
virtual-host: ${RABBITMQ_VIRTUAL_HOST:dc3}
host: ${RABBITMQ_HOST:dc3-rabbitmq}
port: ${RABBITMQ_PORT:5672}
username: ${RABBITMQ_USERNAME:dc3}
password: ${RABBITMQ_PASSWORD:dc3dc3dc3}
logging:
level:
io.github.pnoker: DEBUG
@@ -0,0 +1,112 @@
#
# Copyright 2016-present the IoT DC3 original author or authors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
dc3:
driver:
tenant: default
name: Modbus RTU Driver
code: ModbusRtuDriver
type: DRIVER_CLIENT
remark: @project.description@
schedule:
# Read data regularly
read:
enable: true
cron: '0/30 * * * * ?'
custom:
enable: true
cron: '0/5 * * * * ?'
health:
device:
enable: true
cron: '0/15 * * * * ?'
timeout: 45
timeout-unit: SECONDS
driver-attribute:
- attribute-name: Port
attribute-code: port
attribute-type-flag: STRING
default-value: /dev/ttyUSB0
remark: Serial port name (e.g. /dev/ttyUSB0, COM3)
- attribute-name: Baud Rate
attribute-code: baudRate
attribute-type-flag: INT
default-value: 9600
remark: Serial baud rate (e.g. 9600, 19200, 115200)
- attribute-name: Data Bits
attribute-code: dataBits
attribute-type-flag: INT
default-value: 8
remark: Data bits (7 or 8)
- attribute-name: Stop Bits
attribute-code: stopBits
attribute-type-flag: INT
default-value: 1
remark: Stop bits (1 or 2)
- attribute-name: Parity
attribute-code: parity
attribute-type-flag: INT
default-value: 0
remark: Parity (0=None, 1=Odd, 2=Even, 3=Mark, 4=Space)
point-attribute:
- attribute-name: Slave ID
attribute-code: slaveId
attribute-type-flag: INT
default-value: 1
remark: Modbus slave unit ID
- attribute-name: Function Code
attribute-code: functionCode
attribute-type-flag: INT
default-value: 1
remark: Modbus function code [1, 2, 3, 4]
- attribute-name: Offset
attribute-code: offset
attribute-type-flag: INT
default-value: 0
remark: Register or coil address offset
command-attribute:
- attribute-name: Slave ID
attribute-code: slaveId
attribute-type-flag: INT
default-value: 1
remark: Modbus slave unit ID
- attribute-name: Function Code
attribute-code: functionCode
attribute-type-flag: INT
default-value: 6
remark: Modbus write function code [5, 6, 15, 16]
- attribute-name: Offset
attribute-code: offset
attribute-type-flag: INT
default-value: 0
remark: Register or coil address offset
- attribute-name: Value Template
attribute-code: valueTemplate
attribute-type-flag: STRING
default-value: '${value}'
remark: Value template rendered with command params
spring:
application:
name: @project.artifactId@
profiles:
active:
- ${NODE_ENV:dev}
logging:
file:
name: dc3/logs/driver/modbus-rtu/${spring.application.name}.log
+7 -1
View File
@@ -32,6 +32,12 @@
<dependencies>
<!-- Modbus Protocol -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
</dependency>
</dependencies>
</project>
</project>
@@ -1,635 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.base.RangeAndOffset;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.IllegalDataAddressException;
import com.serotonin.modbus4j.exception.ModbusIdException;
import com.serotonin.modbus4j.locator.BaseLocator;
import com.serotonin.modbus4j.locator.NumericLocator;
import com.serotonin.modbus4j.locator.StringLocator;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* BasicProcessImage class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class BasicProcessImage implements ProcessImage {
private final int slaveId;
private final Map<Integer, Boolean> coils = new HashMap<>();
private final Map<Integer, Boolean> inputs = new HashMap<>();
private final Map<Integer, Short> holdingRegisters = new HashMap<>();
private final Map<Integer, Short> inputRegisters = new HashMap<>();
private final List<ProcessImageListener> writeListeners = new ArrayList<>();
private boolean allowInvalidAddress = false;
private short invalidAddressValue = 0;
private byte exceptionStatus;
/**
* <p>
* Constructor for BasicProcessImage.
* </p>
*
* @param slaveId a int.
*/
public BasicProcessImage(int slaveId) {
ModbusUtils.validateSlaveId(slaveId, false);
this.slaveId = slaveId;
}
@Override
public int getSlaveId() {
return slaveId;
}
/**
* <p>
* addListener.
* </p>
*
* @param l a {@link ProcessImageListener} object.
*/
public synchronized void addListener(ProcessImageListener l) {
writeListeners.add(l);
}
/**
* <p>
* removeListener.
* </p>
*
* @param l a {@link ProcessImageListener} object.
*/
public synchronized void removeListener(ProcessImageListener l) {
writeListeners.remove(l);
}
/**
* <p>
* isAllowInvalidAddress.
* </p>
*
* @return a boolean.
*/
public boolean isAllowInvalidAddress() {
return allowInvalidAddress;
}
/**
* <p>
* Setter for the field <code>allowInvalidAddress</code>.
* </p>
*
* @param allowInvalidAddress a boolean.
*/
public void setAllowInvalidAddress(boolean allowInvalidAddress) {
this.allowInvalidAddress = allowInvalidAddress;
}
/**
* <p>
* Getter for the field <code>invalidAddressValue</code>.
* </p>
*
* @return a short.
*/
public short getInvalidAddressValue() {
return invalidAddressValue;
}
/**
* <p>
* Setter for the field <code>invalidAddressValue</code>.
* </p>
*
* @param invalidAddressValue a short.
*/
public void setInvalidAddressValue(short invalidAddressValue) {
this.invalidAddressValue = invalidAddressValue;
}
//
// /
// / Additional convenience methods.
// /
//
/**
* <p>
* setBinary.
* </p>
*
* @param registerId a int.
* @param value a boolean.
*/
public void setBinary(int registerId, boolean value) {
RangeAndOffset rao = new RangeAndOffset(registerId);
setBinary(rao.getRange(), rao.getOffset(), value);
}
//
// Binaries
/**
* <p>
* setBinary.
* </p>
*
* @param range a int.
* @param offset a int.
* @param value a boolean.
*/
public void setBinary(int range, int offset, boolean value) {
if (range == RegisterRange.COIL_STATUS)
setCoil(offset, value);
else if (range == RegisterRange.INPUT_STATUS)
setInput(offset, value);
else
throw new ModbusIdException("Invalid range to set binary: " + range);
}
/**
* <p>
* setNumeric.
* </p>
*
* @param registerId a int.
* @param dataType a int.
* @param value a {@link Number} object.
*/
public synchronized void setNumeric(int registerId, int dataType, Number value) {
RangeAndOffset rao = new RangeAndOffset(registerId);
setNumeric(rao.getRange(), rao.getOffset(), dataType, value);
}
//
// Numerics
/**
* <p>
* setNumeric.
* </p>
*
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param value a {@link Number} object.
*/
public synchronized void setNumeric(int range, int offset, int dataType, Number value) {
short[] registers = new NumericLocator(slaveId, range, offset, dataType).valueToShorts(value);
// Write the value.
if (range == RegisterRange.HOLDING_REGISTER)
setHoldingRegister(offset, registers);
else if (range == RegisterRange.INPUT_REGISTER)
setInputRegister(offset, registers);
else
throw new ModbusIdException("Invalid range to set register: " + range);
}
/**
* <p>
* setString.
* </p>
*
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param registerCount a int.
* @param s a {@link String} object.
*/
public synchronized void setString(int range, int offset, int dataType, int registerCount, String s) {
setString(range, offset, dataType, registerCount, StringLocator.ASCII, s);
}
//
// Strings
/**
* <p>
* setString.
* </p>
*
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param registerCount a int.
* @param charset a {@link Charset} object.
* @param s a {@link String} object.
*/
public synchronized void setString(int range, int offset, int dataType, int registerCount, Charset charset,
String s) {
short[] registers = new StringLocator(slaveId, range, offset, dataType, registerCount, charset)
.valueToShorts(s);
// Write the value.
if (range == RegisterRange.HOLDING_REGISTER)
setHoldingRegister(offset, registers);
else if (range == RegisterRange.INPUT_REGISTER)
setInputRegister(offset, registers);
else
throw new ModbusIdException("Invalid range to set register: " + range);
}
/**
* <p>
* setHoldingRegister.
* </p>
*
* @param offset a int.
* @param registers an array of {@link short} objects.
*/
public synchronized void setHoldingRegister(int offset, short[] registers) {
validateOffset(offset);
for (int i = 0; i < registers.length; i++)
setHoldingRegister(offset + i, registers[i]);
}
/**
* <p>
* setInputRegister.
* </p>
*
* @param offset a int.
* @param registers an array of {@link short} objects.
*/
public synchronized void setInputRegister(int offset, short[] registers) {
validateOffset(offset);
for (int i = 0; i < registers.length; i++)
setInputRegister(offset + i, registers[i]);
}
/**
* <p>
* setBit.
* </p>
*
* @param range a int.
* @param offset a int.
* @param bit a int.
* @param value a boolean.
*/
public synchronized void setBit(int range, int offset, int bit, boolean value) {
if (range == RegisterRange.HOLDING_REGISTER)
setHoldingRegisterBit(offset, bit, value);
else if (range == RegisterRange.INPUT_REGISTER)
setInputRegisterBit(offset, bit, value);
else
throw new ModbusIdException("Invalid range to set register: " + range);
}
//
// Bits
/**
* <p>
* setHoldingRegisterBit.
* </p>
*
* @param offset a int.
* @param bit a int.
* @param value a boolean.
*/
public synchronized void setHoldingRegisterBit(int offset, int bit, boolean value) {
validateBit(bit);
short s;
try {
s = getHoldingRegister(offset);
} catch (IllegalDataAddressException e) {
s = 0;
}
setHoldingRegister(offset, setBit(s, bit, value));
}
/**
* <p>
* setInputRegisterBit.
* </p>
*
* @param offset a int.
* @param bit a int.
* @param value a boolean.
*/
public synchronized void setInputRegisterBit(int offset, int bit, boolean value) {
validateBit(bit);
short s;
try {
s = getInputRegister(offset);
} catch (IllegalDataAddressException e) {
s = 0;
}
setInputRegister(offset, setBit(s, bit, value));
}
/**
* <p>
* getBit.
* </p>
*
* @param range a int.
* @param offset a int.
* @param bit a int.
* @return a boolean.
* @throws IllegalDataAddressException if any.
*/
public boolean getBit(int range, int offset, int bit) throws IllegalDataAddressException {
if (range == RegisterRange.HOLDING_REGISTER)
return getHoldingRegisterBit(offset, bit);
if (range == RegisterRange.INPUT_REGISTER)
return getInputRegisterBit(offset, bit);
throw new ModbusIdException("Invalid range to get register: " + range);
}
/**
* <p>
* getHoldingRegisterBit.
* </p>
*
* @param offset a int.
* @param bit a int.
* @return a boolean.
* @throws IllegalDataAddressException if any.
*/
public boolean getHoldingRegisterBit(int offset, int bit) throws IllegalDataAddressException {
validateBit(bit);
return getBit(getHoldingRegister(offset), bit);
}
/**
* <p>
* getInputRegisterBit.
* </p>
*
* @param offset a int.
* @param bit a int.
* @return a boolean.
* @throws IllegalDataAddressException if any.
*/
public boolean getInputRegisterBit(int offset, int bit) throws IllegalDataAddressException {
validateBit(bit);
return getBit(getInputRegister(offset), bit);
}
/**
* <p>
* getNumeric.
* </p>
*
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @return a {@link Number} object.
* @throws IllegalDataAddressException if any.
*/
public Number getNumeric(int range, int offset, int dataType) throws IllegalDataAddressException {
return getRegister(new NumericLocator(slaveId, range, offset, dataType));
}
/**
* <p>
* getString.
* </p>
*
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param registerCount a int.
* @return a {@link String} object.
* @throws IllegalDataAddressException if any.
*/
public String getString(int range, int offset, int dataType, int registerCount) throws IllegalDataAddressException {
return getRegister(new StringLocator(slaveId, range, offset, dataType, registerCount, null));
}
/**
* <p>
* getString.
* </p>
*
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param registerCount a int.
* @param charset a {@link Charset} object.
* @return a {@link String} object.
* @throws IllegalDataAddressException if any.
*/
public String getString(int range, int offset, int dataType, int registerCount, Charset charset)
throws IllegalDataAddressException {
return getRegister(new StringLocator(slaveId, range, offset, dataType, registerCount, charset));
}
/**
* <p>
* getRegister.
* </p>
*
* @param locator a {@link BaseLocator} object.
* @param <T> a T object.
* @return a T object.
* @throws IllegalDataAddressException if any.
*/
public synchronized <T> T getRegister(BaseLocator<T> locator) throws IllegalDataAddressException {
int words = locator.getRegisterCount();
byte[] b = new byte[locator.getRegisterCount() * 2];
for (int i = 0; i < words; i++) {
short s;
if (locator.getRange() == RegisterRange.INPUT_REGISTER)
s = getInputRegister(locator.getOffset() + i);
else if (locator.getRange() == RegisterRange.HOLDING_REGISTER)
s = getHoldingRegister(locator.getOffset() + i);
else if (allowInvalidAddress)
s = invalidAddressValue;
else
throw new IllegalDataAddressException();
b[i * 2] = ModbusUtils.toByte(s, true);
b[i * 2 + 1] = ModbusUtils.toByte(s, false);
}
return locator.bytesToValueRealOffset(b, 0);
}
@Override
public synchronized boolean getCoil(int offset) throws IllegalDataAddressException {
return getBoolean(offset, coils);
}
//
//
// ProcessImage interface
//
//
// Coils
@Override
public synchronized void setCoil(int offset, boolean value) {
validateOffset(offset);
coils.put(offset, value);
}
@Override
public synchronized void writeCoil(int offset, boolean value) throws IllegalDataAddressException {
boolean old = getBoolean(offset, coils);
setCoil(offset, value);
for (ProcessImageListener l : writeListeners)
l.coilWrite(offset, old, value);
}
@Override
public synchronized boolean getInput(int offset) throws IllegalDataAddressException {
return getBoolean(offset, inputs);
}
//
// Inputs
@Override
public synchronized void setInput(int offset, boolean value) {
validateOffset(offset);
inputs.put(offset, value);
}
@Override
public synchronized short getHoldingRegister(int offset) throws IllegalDataAddressException {
return getShort(offset, holdingRegisters);
}
//
// Holding registers
@Override
public synchronized void setHoldingRegister(int offset, short value) {
validateOffset(offset);
holdingRegisters.put(offset, value);
}
@Override
public synchronized void writeHoldingRegister(int offset, short value) throws IllegalDataAddressException {
short old = getShort(offset, holdingRegisters);
setHoldingRegister(offset, value);
for (ProcessImageListener l : writeListeners)
l.holdingRegisterWrite(offset, old, value);
}
@Override
public synchronized short getInputRegister(int offset) throws IllegalDataAddressException {
return getShort(offset, inputRegisters);
}
//
// Input registers
@Override
public synchronized void setInputRegister(int offset, short value) {
validateOffset(offset);
inputRegisters.put(offset, value);
}
@Override
public byte getExceptionStatus() {
return exceptionStatus;
}
//
// Exception status
/**
* <p>
* Setter for the field <code>exceptionStatus</code>.
* </p>
*
* @param exceptionStatus a byte.
*/
public void setExceptionStatus(byte exceptionStatus) {
this.exceptionStatus = exceptionStatus;
}
//
// Report slave id
@Override
public byte[] getReportSlaveIdData() {
return new byte[0];
}
//
//
// Private
//
private short getShort(int offset, Map<Integer, Short> map) throws IllegalDataAddressException {
Short value = map.get(offset);
if (value == null) {
if (allowInvalidAddress)
return invalidAddressValue;
throw new IllegalDataAddressException();
}
return value.shortValue();
}
private boolean getBoolean(int offset, Map<Integer, Boolean> map) throws IllegalDataAddressException {
Boolean value = map.get(offset);
if (value == null) {
if (allowInvalidAddress)
return false;
throw new IllegalDataAddressException();
}
return value.booleanValue();
}
private void validateOffset(int offset) {
if (offset < 0 || offset > 65535)
throw new ModbusIdException("Invalid offset: " + offset);
}
private void validateBit(int bit) {
if (bit < 0 || bit > 15)
throw new ModbusIdException("Invalid bit: " + bit);
}
private short setBit(short s, int bit, boolean value) {
return (short) (s | ((value ? 1 : 0) << bit));
}
private boolean getBit(short s, int bit) {
return ((s >> bit) & 0x1) == 1;
}
}
@@ -1,310 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.base.KeyedModbusLocator;
import com.serotonin.modbus4j.base.ReadFunctionGroup;
import com.serotonin.modbus4j.base.SlaveAndRange;
import com.serotonin.modbus4j.locator.BaseLocator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A class for defining the information required to obtain in a batch.
* <p>
* The generic parameterization represents the class of the key that will be used to find
* the results in the BatchRead object. Typically String would be used, but any Object is
* valid.
* <p>
* Some modbus devices have non-contiguous sets of values within a single register range.
* These gaps between values may cause the device to return error responses if a request
* attempts to read them. In spite of this, because it is generally more efficient to read
* a set of values with a single request, the batch read by default will assume that no
* such error responses will be returned. If your batch request results in such errors, it
* is recommended that you separate the offending request to a separate batch read object,
* or you can use the "contiguous requests" setting which causes requests to be
* partitioned into only contiguous sets.
*
* @param <K> - Type of read
* @author mlohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class BatchRead<K> {
private final List<KeyedModbusLocator<K>> requestValues = new ArrayList<>();
/**
* See documentation above.
*/
private boolean contiguousRequests = false;
/**
* If this value is false, any error response received will cause an exception to be
* thrown, and the entire batch to be aborted (unless exceptionsInResults is true -
* see below). If set to true, error responses will be set as the result of all
* affected locators and the entire batch will be attempted with no such exceptions
* thrown.
*/
private boolean errorsInResults = false;
/**
* If this value is false, any exceptions thrown will cause the entire batch to be
* aborted. If set to true, the exception will be set as the result of all affected
* locators and the entire batch will be attempted with no such exceptions thrown.
*/
private boolean exceptionsInResults = false;
/**
* A batch may be split into an arbitrary number of individual Modbus requests, and so
* a given batch may take an arbitrary amount of time to complete. The cancel field is
* provided to allow the batch to be cancelled.
*/
private boolean cancel;
/**
* This is what the data looks like after partitioning.
*/
private List<ReadFunctionGroup<K>> functionGroups;
/**
* <p>
* isContiguousRequests.
* </p>
*
* @return a boolean.
*/
public boolean isContiguousRequests() {
return contiguousRequests;
}
/**
* <p>
* Setter for the field <code>contiguousRequests</code>.
* </p>
*
* @param contiguousRequests a boolean.
*/
public void setContiguousRequests(boolean contiguousRequests) {
this.contiguousRequests = contiguousRequests;
functionGroups = null;
}
/**
* <p>
* isErrorsInResults.
* </p>
*
* @return a boolean.
*/
public boolean isErrorsInResults() {
return errorsInResults;
}
/**
* <p>
* Setter for the field <code>errorsInResults</code>.
* </p>
*
* @param errorsInResults a boolean.
*/
public void setErrorsInResults(boolean errorsInResults) {
this.errorsInResults = errorsInResults;
}
/**
* <p>
* isExceptionsInResults.
* </p>
*
* @return a boolean.
*/
public boolean isExceptionsInResults() {
return exceptionsInResults;
}
/**
* <p>
* Setter for the field <code>exceptionsInResults</code>.
* </p>
*
* @param exceptionsInResults a boolean.
*/
public void setExceptionsInResults(boolean exceptionsInResults) {
this.exceptionsInResults = exceptionsInResults;
}
/**
* <p>
* getReadFunctionGroups.
* </p>
*
* @param master a {@link ModbusMaster} object.
* @return a {@link List} object.
*/
public List<ReadFunctionGroup<K>> getReadFunctionGroups(ModbusMaster master) {
if (functionGroups == null)
doPartition(master);
return functionGroups;
}
/**
* <p>
* addLocator.
* </p>
*
* @param id a K object.
* @param locator a {@link BaseLocator} object.
*/
public void addLocator(K id, BaseLocator<?> locator) {
addLocator(new KeyedModbusLocator<>(id, locator));
}
private void addLocator(KeyedModbusLocator<K> locator) {
requestValues.add(locator);
functionGroups = null;
}
/**
* <p>
* isCancel.
* </p>
*
* @return a boolean.
*/
public boolean isCancel() {
return cancel;
}
/**
* <p>
* Setter for the field <code>cancel</code>.
* </p>
*
* @param cancel a boolean.
*/
public void setCancel(boolean cancel) {
this.cancel = cancel;
}
//
//
// Private stuff
//
private void doPartition(ModbusMaster master) {
Map<SlaveAndRange, List<KeyedModbusLocator<K>>> slaveRangeBatch = new HashMap<>();
// Separate the batch into slave ids and read functions.
List<KeyedModbusLocator<K>> functions;
for (KeyedModbusLocator<K> locator : requestValues) {
// Find the function list for this slave and range. Create it if necessary.
functions = slaveRangeBatch.computeIfAbsent(locator.getSlaveAndRange(), k -> new ArrayList<>());
// Add this locator to the function list.
functions.add(locator);
}
// Now that we have locators grouped into slave and function, check each read
// function group and break into
// parts as necessary.
Collection<List<KeyedModbusLocator<K>>> functionLocatorLists = slaveRangeBatch.values();
FunctionLocatorComparator comparator = new FunctionLocatorComparator();
functionGroups = new ArrayList<>();
for (List<KeyedModbusLocator<K>> functionLocatorList : functionLocatorLists) {
// Sort the list by offset.
Collections.sort(functionLocatorList, comparator);
// Break into parts by excessive request length. Remember the max item count
// that we can ask for, for
// this function
int maxReadCount = master.getMaxReadCount(functionLocatorList.get(0).getSlaveAndRange().getRange());
// Create the request groups.
createRequestGroups(functionGroups, functionLocatorList, maxReadCount);
// System.out.println("requests: " + functionGroups.size());
}
}
/**
* We aren't trying to do anything fancy here, like some kind of artificial optimal
* group for performance or anything. We pretty much just try to fit as many locators
* as possible into a single valid request, and then move on.
* <p>
* This method assumes the locators have already been sorted by start offset.
*/
private void createRequestGroups(List<ReadFunctionGroup<K>> functionGroups, List<KeyedModbusLocator<K>> locators,
int maxCount) {
ReadFunctionGroup<K> functionGroup;
KeyedModbusLocator<K> locator;
int index;
int endOffset;
// Loop for creation of groups.
while (locators.size() > 0) {
functionGroup = new ReadFunctionGroup<>(locators.remove(0));
functionGroups.add(functionGroup);
endOffset = functionGroup.getStartOffset() + maxCount - 1;
// Loop for adding locators to the current group
index = 0;
while (locators.size() > index) {
locator = locators.get(index);
boolean added = false;
if (locator.getEndOffset() <= endOffset) {
if (contiguousRequests) {
// The locator must at least abut the other locators in the group.
if (locator.getOffset() <= functionGroup.getEndOffset() + 1) {
functionGroup.add(locators.remove(index));
added = true;
}
} else {
functionGroup.add(locators.remove(index));
added = true;
}
}
if (!added) {
// This locator doesn't fit inside the current function...
if (locator.getOffset() > endOffset)
// ... and since the list is sorted by offset, no other locators
// can either, so quit the loop.
break;
// ... but there still may be other locators that can, so increment
// the index
index++;
}
}
}
}
class FunctionLocatorComparator implements Comparator<KeyedModbusLocator<K>> {
@Override
public int compare(KeyedModbusLocator<K> ml1, KeyedModbusLocator<K> ml2) {
return ml1.getOffset() - ml2.getOffset();
}
}
}
@@ -1,112 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* BatchResults class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class BatchResults<K> {
private final Map<K, Object> data = new HashMap<>();
/**
* <p>
* addResult.
* </p>
*
* @param key a K object.
* @param value a {@link Object} object.
*/
public void addResult(K key, Object value) {
data.put(key, value);
}
/**
* <p>
* getValue.
* </p>
*
* @param key a K object.
* @return a {@link Object} object.
*/
public Object getValue(K key) {
return data.get(key);
}
/**
* <p>
* getIntValue.
* </p>
*
* @param key a K object.
* @return a {@link Integer} object.
*/
public Integer getIntValue(K key) {
return (Integer) getValue(key);
}
/**
* <p>
* getLongValue.
* </p>
*
* @param key a K object.
* @return a {@link Long} object.
*/
public Long getLongValue(K key) {
return (Long) getValue(key);
}
/**
* <p>
* getDoubleValue.
* </p>
*
* @param key a K object.
* @return a {@link Double} object.
*/
public Double getDoubleValue(K key) {
return (Double) getValue(key);
}
/**
* <p>
* getFloatValue.
* </p>
*
* @param key a K object.
* @return a {@link Float} object.
*/
public Float getFloatValue(K key) {
return (Float) getValue(key);
}
@Override
public String toString() {
return data.toString();
}
}
@@ -1,70 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.code.ExceptionCode;
/**
* <p>
* ExceptionResult class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ExceptionResult {
private final byte exceptionCode;
private final String exceptionMessage;
/**
* <p>
* Constructor for ExceptionResult.
* </p>
*
* @param exceptionCode a byte.
*/
public ExceptionResult(byte exceptionCode) {
this.exceptionCode = exceptionCode;
exceptionMessage = ExceptionCode.getExceptionMessage(exceptionCode);
}
/**
* <p>
* Getter for the field <code>exceptionCode</code>.
* </p>
*
* @return a byte.
*/
public byte getExceptionCode() {
return exceptionCode;
}
/**
* <p>
* Getter for the field <code>exceptionMessage</code>.
* </p>
*
* @return a {@link String} object.
*/
public String getExceptionMessage() {
return exceptionMessage;
}
}
@@ -1,195 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.sero.messaging.DefaultMessagingExceptionHandler;
import com.serotonin.modbus4j.sero.messaging.MessagingExceptionHandler;
/**
* Base level for masters and slaves/listeners
* <p>
* TODO: - handle echoing in RS485
*
* @author mlohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class Modbus {
/**
* Constant <code>DEFAULT_MAX_READ_BIT_COUNT=2000</code>
*/
public static final int DEFAULT_MAX_READ_BIT_COUNT = 2000;
/**
* Constant <code>DEFAULT_MAX_READ_REGISTER_COUNT=125</code>
*/
public static final int DEFAULT_MAX_READ_REGISTER_COUNT = 125;
/**
* Constant <code>DEFAULT_MAX_WRITE_REGISTER_COUNT=120</code>
*/
public static final int DEFAULT_MAX_WRITE_REGISTER_COUNT = 120;
private MessagingExceptionHandler exceptionHandler = new DefaultMessagingExceptionHandler();
private int maxReadBitCount = DEFAULT_MAX_READ_BIT_COUNT;
private int maxReadRegisterCount = DEFAULT_MAX_READ_REGISTER_COUNT;
private int maxWriteRegisterCount = DEFAULT_MAX_WRITE_REGISTER_COUNT;
/**
* <p>
* getMaxReadCount.
* </p>
*
* @param registerRange a int.
* @return a int.
*/
public int getMaxReadCount(int registerRange) {
switch (registerRange) {
case RegisterRange.COIL_STATUS:
case RegisterRange.INPUT_STATUS:
return maxReadBitCount;
case RegisterRange.HOLDING_REGISTER:
case RegisterRange.INPUT_REGISTER:
return maxReadRegisterCount;
}
return -1;
}
/**
* <p>
* validateNumberOfBits.
* </p>
*
* @param bits a int.
* @throws ModbusTransportException if any.
*/
public void validateNumberOfBits(int bits) throws ModbusTransportException {
if (bits < 1 || bits > maxReadBitCount)
throw new ModbusTransportException("Invalid number of bits: " + bits);
}
/**
* <p>
* validateNumberOfRegisters.
* </p>
*
* @param registers a int.
* @throws ModbusTransportException if any.
*/
public void validateNumberOfRegisters(int registers) throws ModbusTransportException {
if (registers < 1 || registers > maxReadRegisterCount)
throw new ModbusTransportException("Invalid number of registers: " + registers);
}
/**
* <p>
* Getter for the field <code>exceptionHandler</code>.
* </p>
*
* @return a {@link MessagingExceptionHandler} object.
*/
public MessagingExceptionHandler getExceptionHandler() {
return exceptionHandler;
}
/**
* <p>
* Setter for the field <code>exceptionHandler</code>.
* </p>
*
* @param exceptionHandler a {@link MessagingExceptionHandler} object.
*/
public void setExceptionHandler(MessagingExceptionHandler exceptionHandler) {
if (exceptionHandler == null)
this.exceptionHandler = new DefaultMessagingExceptionHandler();
else
this.exceptionHandler = exceptionHandler;
}
/**
* <p>
* Getter for the field <code>maxReadBitCount</code>.
* </p>
*
* @return a int.
*/
public int getMaxReadBitCount() {
return maxReadBitCount;
}
/**
* <p>
* Setter for the field <code>maxReadBitCount</code>.
* </p>
*
* @param maxReadBitCount a int.
*/
public void setMaxReadBitCount(int maxReadBitCount) {
this.maxReadBitCount = maxReadBitCount;
}
/**
* <p>
* Getter for the field <code>maxReadRegisterCount</code>.
* </p>
*
* @return a int.
*/
public int getMaxReadRegisterCount() {
return maxReadRegisterCount;
}
/**
* <p>
* Setter for the field <code>maxReadRegisterCount</code>.
* </p>
*
* @param maxReadRegisterCount a int.
*/
public void setMaxReadRegisterCount(int maxReadRegisterCount) {
this.maxReadRegisterCount = maxReadRegisterCount;
}
/**
* <p>
* Getter for the field <code>maxWriteRegisterCount</code>.
* </p>
*
* @return a int.
*/
public int getMaxWriteRegisterCount() {
return maxWriteRegisterCount;
}
/**
* <p>
* Setter for the field <code>maxWriteRegisterCount</code>.
* </p>
*
* @param maxWriteRegisterCount a int.
*/
public void setMaxWriteRegisterCount(int maxWriteRegisterCount) {
this.maxWriteRegisterCount = maxWriteRegisterCount;
}
}
@@ -1,215 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.ModbusIdException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.ip.listener.TcpListener;
import com.serotonin.modbus4j.ip.tcp.TcpMaster;
import com.serotonin.modbus4j.ip.tcp.TcpSlave;
import com.serotonin.modbus4j.ip.udp.UdpMaster;
import com.serotonin.modbus4j.ip.udp.UdpSlave;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ReadCoilsRequest;
import com.serotonin.modbus4j.msg.ReadDiscreteInputsRequest;
import com.serotonin.modbus4j.msg.ReadHoldingRegistersRequest;
import com.serotonin.modbus4j.msg.ReadInputRegistersRequest;
import com.serotonin.modbus4j.serial.SerialPortWrapper;
import com.serotonin.modbus4j.serial.ascii.AsciiMaster;
import com.serotonin.modbus4j.serial.ascii.AsciiSlave;
import com.serotonin.modbus4j.serial.rtu.RtuMaster;
import com.serotonin.modbus4j.serial.rtu.RtuSlave;
/**
* <p>
* ModbusFactory class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ModbusFactory {
//
// Modbus masters
//
/**
* <p>
* createRtuMaster.
* </p>
*
* @param wrapper a {@link SerialPortWrapper} object.
* @return a {@link ModbusMaster} object.
*/
public ModbusMaster createRtuMaster(SerialPortWrapper wrapper) {
return new RtuMaster(wrapper);
}
/**
* <p>
* createAsciiMaster.
* </p>
*
* @param wrapper a {@link SerialPortWrapper} object.
* @return a {@link ModbusMaster} object.
*/
public ModbusMaster createAsciiMaster(SerialPortWrapper wrapper) {
return new AsciiMaster(wrapper);
}
/**
* <p>
* createTcpMaster.
* </p>
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
* @return a {@link ModbusMaster} object.
*/
public ModbusMaster createTcpMaster(IpParameters params, boolean keepAlive) {
return new TcpMaster(params, keepAlive);
}
/**
* <p>
* createTcpMaster.
* </p>
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
* @param lingerTime an Integer.
* @return a {@link ModbusMaster} object.
*/
public ModbusMaster createTcpMaster(IpParameters params, boolean keepAlive, Integer lingerTime) {
return new TcpMaster(params, keepAlive, lingerTime);
}
/**
* <p>
* createUdpMaster.
* </p>
*
* @param params a {@link IpParameters} object.
* @return a {@link ModbusMaster} object.
*/
public ModbusMaster createUdpMaster(IpParameters params) {
return new UdpMaster(params);
}
/**
* <p>
* createTcpListener.
* </p>
*
* @param params a {@link IpParameters} object.
* @return a {@link ModbusMaster} object.
*/
public ModbusMaster createTcpListener(IpParameters params) {
return new TcpListener(params);
}
//
// Modbus slaves
//
/**
* <p>
* createRtuSlave.
* </p>
*
* @param wrapper a {@link SerialPortWrapper} object.
* @return a {@link ModbusSlaveSet} object.
*/
public ModbusSlaveSet createRtuSlave(SerialPortWrapper wrapper) {
return new RtuSlave(wrapper);
}
/**
* <p>
* createAsciiSlave.
* </p>
*
* @param wrapper a {@link SerialPortWrapper} object.
* @return a {@link ModbusSlaveSet} object.
*/
public ModbusSlaveSet createAsciiSlave(SerialPortWrapper wrapper) {
return new AsciiSlave(wrapper);
}
/**
* <p>
* createTcpSlave.
* </p>
*
* @param encapsulated a boolean.
* @return a {@link ModbusSlaveSet} object.
*/
public ModbusSlaveSet createTcpSlave(boolean encapsulated) {
return new TcpSlave(encapsulated);
}
/**
* <p>
* createUdpSlave.
* </p>
*
* @param encapsulated a boolean.
* @return a {@link ModbusSlaveSet} object.
*/
public ModbusSlaveSet createUdpSlave(boolean encapsulated) {
return new UdpSlave(encapsulated);
}
//
// Modbus requests
//
/**
* <p>
* createReadRequest.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param length a int.
* @return a {@link ModbusRequest} object.
* @throws ModbusTransportException if any.
* @throws ModbusIdException if any.
*/
public ModbusRequest createReadRequest(int slaveId, int range, int offset, int length)
throws ModbusTransportException, ModbusIdException {
ModbusUtils.validateRegisterRange(range);
if (range == RegisterRange.COIL_STATUS)
return new ReadCoilsRequest(slaveId, offset, length);
if (range == RegisterRange.INPUT_STATUS)
return new ReadDiscreteInputsRequest(slaveId, offset, length);
if (range == RegisterRange.INPUT_REGISTER)
return new ReadInputRegistersRequest(slaveId, offset, length);
return new ReadHoldingRegistersRequest(slaveId, offset, length);
}
}
@@ -1,656 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.base.KeyedModbusLocator;
import com.serotonin.modbus4j.base.ReadFunctionGroup;
import com.serotonin.modbus4j.base.SlaveProfile;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.code.ExceptionCode;
import com.serotonin.modbus4j.code.FunctionCode;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.InvalidDataConversionException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.locator.BaseLocator;
import com.serotonin.modbus4j.locator.BinaryLocator;
import com.serotonin.modbus4j.locator.NumericLocator;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.msg.ReadCoilsRequest;
import com.serotonin.modbus4j.msg.ReadDiscreteInputsRequest;
import com.serotonin.modbus4j.msg.ReadHoldingRegistersRequest;
import com.serotonin.modbus4j.msg.ReadInputRegistersRequest;
import com.serotonin.modbus4j.msg.ReadResponse;
import com.serotonin.modbus4j.msg.WriteCoilRequest;
import com.serotonin.modbus4j.msg.WriteCoilsRequest;
import com.serotonin.modbus4j.msg.WriteMaskRegisterRequest;
import com.serotonin.modbus4j.msg.WriteRegisterRequest;
import com.serotonin.modbus4j.msg.WriteRegistersRequest;
import com.serotonin.modbus4j.sero.epoll.InputStreamEPollWrapper;
import com.serotonin.modbus4j.sero.log.BaseIOLog;
import com.serotonin.modbus4j.sero.messaging.MessageControl;
import com.serotonin.modbus4j.sero.util.ArrayUtils;
import com.serotonin.modbus4j.sero.util.ProgressiveTask;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Abstract base class for Modbus master implementations. This class provides the core
* functionality for implementing Modbus masters that can communicate with Modbus slave
* devices over various transport mechanisms. It includes methods for reading and writing
* values, batch operations, and slave node discovery.
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public abstract class ModbusMaster extends Modbus {
private final Map<Integer, SlaveProfile> slaveProfiles = new HashMap<>();
/**
* Should we validate the responses: - ensure that the requested slave id is what is
* in the response
*/
protected boolean validateResponse;
/**
* If connection is established with slave/slaves
*/
protected boolean connected = false;
protected boolean initialized;
private int timeout = 500;
private int retries = 2;
/**
* If the slave equipment only supports multiple write commands, set this to true.
* Otherwise, and combination of single or multiple write commands will be used as
* appropriate.
*/
private boolean multipleWritesOnly;
private int discardDataDelay = 0;
private BaseIOLog ioLog;
/**
* An input stream ePoll will use a single thread to read all input streams. If
* multiple serial or TCP modbus connections are to be made, an ePoll can be much more
* efficient.
*/
private InputStreamEPollWrapper ePoll;
/**
* <p>
* isConnected.
* </p>
*
* @return a boolean.
*/
public boolean isConnected() {
return connected;
}
/**
* <p>
* Setter for the field <code>connected</code>.
* </p>
*
* @param connected a boolean.
*/
public void setConnected(boolean connected) {
this.connected = connected;
}
/**
* <p>
* init.
* </p>
*
* @throws ModbusInitException if any.
*/
abstract public void init() throws ModbusInitException;
/**
* <p>
* isInitialized.
* </p>
*
* @return a boolean.
*/
public boolean isInitialized() {
return initialized;
}
/**
* <p>
* destroy.
* </p>
*/
abstract public void destroy();
/**
* <p>
* send.
* </p>
*
* @param request a {@link ModbusRequest} object.
* @return a {@link ModbusResponse} object.
* @throws ModbusTransportException if any.
*/
public final ModbusResponse send(ModbusRequest request) throws ModbusTransportException {
request.validate(this);
ModbusResponse modbusResponse = sendImpl(request);
if (validateResponse)
modbusResponse.validateResponse(request);
return modbusResponse;
}
/**
* <p>
* sendImpl.
* </p>
*
* @param request a {@link ModbusRequest} object.
* @return a {@link ModbusResponse} object.
* @throws ModbusTransportException if any.
*/
abstract public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException;
/**
* Returns a value from the modbus network according to the given locator information.
* Various data types are allowed to be requested including multi-word types. The
* determination of the correct request message to send is handled automatically.
*
* @param locator the information required to locate the value in the modbus network.
* @param <T> a T object.
* @return an object representing the value found. This will be one of Boolean, Short,
* Integer, Long, BigInteger, Float, or Double. See the DataType enumeration for
* details on which type to expect.
* @throws ModbusTransportException if there was an IO error or other technical
* failure while sending the message
* @throws ErrorResponseException if the response returned from the slave was an
* exception.
*/
@SuppressWarnings("unchecked")
public <T> T getValue(BaseLocator<T> locator) throws ModbusTransportException, ErrorResponseException {
BatchRead<String> batch = new BatchRead<>();
batch.addLocator("", locator);
BatchResults<String> result = send(batch);
return (T) result.getValue("");
}
/**
* Sets the given value in the modbus network according to the given locator
* information. Various data types are allowed to be set including including
* multi-word types. The determination of the correct write message to send is handled
* automatically.
*
* @param locator the information required to locate the value in the modbus network.
* @param value an object representing the value to be set. This will be one of
* Boolean, Short, Integer, Long, BigInteger, Float, or Double. See the DataType
* enumeration for details on which type to expect.
* @param <T> type of locator
* @throws ModbusTransportException if there was an IO error or other technical
* failure while sending the message
* @throws ErrorResponseException if the response returned from the slave was an
* exception.
*/
public <T> void setValue(BaseLocator<T> locator, Object value)
throws ModbusTransportException, ErrorResponseException {
int slaveId = locator.getSlaveId();
int registerRange = locator.getRange();
int writeOffset = locator.getOffset();
// Determine the request type that we will use
if (registerRange == RegisterRange.INPUT_STATUS || registerRange == RegisterRange.INPUT_REGISTER)
throw new RuntimeException("Cannot write to input status or input register ranges");
if (registerRange == RegisterRange.COIL_STATUS) {
if (!(value instanceof Boolean))
throw new InvalidDataConversionException("Only boolean values can be written to coils");
if (multipleWritesOnly)
setValue(new WriteCoilsRequest(slaveId, writeOffset,
new boolean[]{((Boolean) value).booleanValue()}));
else
setValue(new WriteCoilRequest(slaveId, writeOffset, ((Boolean) value).booleanValue()));
} else {
// Writing to holding registers.
if (locator.getDataType() == DataType.BINARY) {
if (!(value instanceof Boolean))
throw new InvalidDataConversionException("Only boolean values can be written to coils");
setHoldingRegisterBit(slaveId, writeOffset, ((BinaryLocator) locator).getBit(),
((Boolean) value).booleanValue());
} else {
// Writing some kind of value to a holding register.
@SuppressWarnings("unchecked")
short[] data = locator.valueToShorts((T) value);
if (data.length == 1 && !multipleWritesOnly)
setValue(new WriteRegisterRequest(slaveId, writeOffset, data[0]));
else
setValue(new WriteRegistersRequest(slaveId, writeOffset, data));
}
}
}
/**
* Node scanning. Returns a list of slave nodes that respond to a read exception
* status request (perhaps with an error, but respond nonetheless).
* <p>
* Note: a similar scan could be done for registers in nodes, but, for one thing, it
* would take some time to run, and in any case the results would not be meaningful
* since there would be no semantic information accompanying the results.
*
* @return a {@link List} object.
*/
public List<Integer> scanForSlaveNodes() {
List<Integer> result = new ArrayList<>();
for (int i = 1; i <= 240; i++) {
if (testSlaveNode(i))
result.add(i);
}
return result;
}
/**
* <p>
* scanForSlaveNodes.
* </p>
*
* @param l a {@link NodeScanListener} object.
* @return a {@link ProgressiveTask} object.
*/
public ProgressiveTask scanForSlaveNodes(final NodeScanListener l) {
l.progressUpdate(0);
ProgressiveTask task = new ProgressiveTask(l) {
private int node = 1;
@Override
protected void runImpl() {
if (testSlaveNode(node))
l.nodeFound(node);
declareProgress(((float) node) / 240);
node++;
if (node > 240)
completed = true;
}
};
new Thread(task).start();
return task;
}
/**
* <p>
* testSlaveNode.
* </p>
*
* @param node a int.
* @return a boolean.
*/
public boolean testSlaveNode(int node) {
try {
send(new ReadHoldingRegistersRequest(node, 0, 1));
} catch (ModbusTransportException e) {
// If there was a transport exception, there's no node there.
return false;
}
return true;
}
/**
* <p>
* Getter for the field <code>retries</code>.
* </p>
*
* @return a int.
*/
public int getRetries() {
return retries;
}
/**
* <p>
* Setter for the field <code>retries</code>.
* </p>
*
* @param retries a int.
*/
public void setRetries(int retries) {
if (retries < 0)
this.retries = 0;
else
this.retries = retries;
}
/**
* <p>
* Getter for the field <code>timeout</code>.
* </p>
*
* @return a int.
*/
public int getTimeout() {
return timeout;
}
/**
* <p>
* Setter for the field <code>timeout</code>.
* </p>
*
* @param timeout a int.
*/
public void setTimeout(int timeout) {
if (timeout < 1)
this.timeout = 1;
else
this.timeout = timeout;
}
/**
* <p>
* isMultipleWritesOnly.
* </p>
*
* @return a boolean.
*/
public boolean isMultipleWritesOnly() {
return multipleWritesOnly;
}
/**
* <p>
* Setter for the field <code>multipleWritesOnly</code>.
* </p>
*
* @param multipleWritesOnly a boolean.
*/
public void setMultipleWritesOnly(boolean multipleWritesOnly) {
this.multipleWritesOnly = multipleWritesOnly;
}
/**
* <p>
* Getter for the field <code>discardDataDelay</code>.
* </p>
*
* @return a int.
*/
public int getDiscardDataDelay() {
return discardDataDelay;
}
/**
* <p>
* Setter for the field <code>discardDataDelay</code>.
* </p>
*
* @param discardDataDelay a int.
*/
public void setDiscardDataDelay(int discardDataDelay) {
if (discardDataDelay < 0)
this.discardDataDelay = 0;
else
this.discardDataDelay = discardDataDelay;
}
/**
* <p>
* Getter for the field <code>ioLog</code>.
* </p>
*
* @return a {@link BaseIOLog} object.
*/
public BaseIOLog getIoLog() {
return ioLog;
}
/**
* <p>
* Setter for the field <code>ioLog</code>.
* </p>
*
* @param ioLog a {@link BaseIOLog} object.
*/
public void setIoLog(BaseIOLog ioLog) {
this.ioLog = ioLog;
}
/**
* <p>
* Getter for the field <code>ePoll</code>.
* </p>
*
* @return a {@link InputStreamEPollWrapper} object.
*/
public InputStreamEPollWrapper getePoll() {
return ePoll;
}
/**
* <p>
* Setter for the field <code>ePoll</code>.
* </p>
*
* @param ePoll a {@link InputStreamEPollWrapper} object.
*/
public void setePoll(InputStreamEPollWrapper ePoll) {
this.ePoll = ePoll;
}
/**
* Useful for sending a number of polling commands at once, or at least in as optimal
* a batch as possible.
*
* @param batch a {@link BatchRead} object.
* @param <K> type of result
* @return a {@link BatchResults} object.
* @throws ModbusTransportException if any.
* @throws ErrorResponseException if any.
*/
public <K> BatchResults<K> send(BatchRead<K> batch) throws ModbusTransportException, ErrorResponseException {
if (!initialized)
throw new ModbusTransportException("not initialized");
BatchResults<K> results = new BatchResults<>();
List<ReadFunctionGroup<K>> functionGroups = batch.getReadFunctionGroups(this);
// Execute each read function and process the results.
for (ReadFunctionGroup<K> functionGroup : functionGroups) {
sendFunctionGroup(functionGroup, results, batch.isErrorsInResults(), batch.isExceptionsInResults());
if (batch.isCancel())
break;
}
return results;
}
//
//
// Protected methods
//
/**
* <p>
* getMessageControl.
* </p>
*
* @return a {@link MessageControl} object.
*/
protected MessageControl getMessageControl() {
MessageControl conn = new MessageControl();
conn.setRetries(getRetries());
conn.setTimeout(getTimeout());
conn.setDiscardDataDelay(getDiscardDataDelay());
conn.setExceptionHandler(getExceptionHandler());
conn.setIoLog(ioLog);
return conn;
}
/**
* <p>
* closeMessageControl.
* </p>
*
* @param conn a {@link MessageControl} object.
*/
protected void closeMessageControl(MessageControl conn) {
if (conn != null)
conn.close();
}
//
//
// Private stuff
//
/**
* This method assumes that all locators have already been pre-sorted and grouped into
* valid requests, say, by the createRequestGroups method.
*/
private <K> void sendFunctionGroup(ReadFunctionGroup<K> functionGroup, BatchResults<K> results,
boolean errorsInResults, boolean exceptionsInResults)
throws ModbusTransportException, ErrorResponseException {
int slaveId = functionGroup.getSlaveAndRange().getSlaveId();
int startOffset = functionGroup.getStartOffset();
int length = functionGroup.getLength();
// Inspect the function group for data required to create the request.
ModbusRequest request;
if (functionGroup.getFunctionCode() == FunctionCode.READ_COILS)
request = new ReadCoilsRequest(slaveId, startOffset, length);
else if (functionGroup.getFunctionCode() == FunctionCode.READ_DISCRETE_INPUTS)
request = new ReadDiscreteInputsRequest(slaveId, startOffset, length);
else if (functionGroup.getFunctionCode() == FunctionCode.READ_HOLDING_REGISTERS)
request = new ReadHoldingRegistersRequest(slaveId, startOffset, length);
else if (functionGroup.getFunctionCode() == FunctionCode.READ_INPUT_REGISTERS)
request = new ReadInputRegistersRequest(slaveId, startOffset, length);
else
throw new RuntimeException("Unsupported function");
ReadResponse response;
try {
response = (ReadResponse) send(request);
} catch (ModbusTransportException e) {
if (!exceptionsInResults)
throw e;
for (KeyedModbusLocator<K> locator : functionGroup.getLocators())
results.addResult(locator.getKey(), e);
return;
}
byte[] data = null;
if (!errorsInResults && response.isException())
throw new ErrorResponseException(request, response);
else if (!response.isException())
data = response.getData();
for (KeyedModbusLocator<K> locator : functionGroup.getLocators()) {
if (errorsInResults && response.isException())
results.addResult(locator.getKey(), new ExceptionResult(response.getExceptionCode()));
else {
try {
results.addResult(locator.getKey(), locator.bytesToValue(data, startOffset));
} catch (RuntimeException e) {
throw new RuntimeException("Result conversion exception. data=" + ArrayUtils.toHexString(data)
+ ", startOffset=" + startOffset + ", locator=" + locator + ", functionGroup.functionCode="
+ functionGroup.getFunctionCode() + ", functionGroup.startOffset=" + startOffset
+ ", functionGroup.length=" + length, e);
}
}
}
}
private void setValue(ModbusRequest request) throws ModbusTransportException, ErrorResponseException {
ModbusResponse response = send(request);
if (response == null)
// This should only happen if the request was a broadcast
return;
if (response.isException())
throw new ErrorResponseException(request, response);
}
private void setHoldingRegisterBit(int slaveId, int writeOffset, int bit, boolean value)
throws ModbusTransportException, ErrorResponseException {
// Writing a bit in a holding register field. There are two ways to do this. The
// easy way is to
// use a write mask request, but it is not always supported. The hard way is to
// read the value, change
// the appropriate bit, and then write it back again (so as not to overwrite the
// other bits in the
// value). However, since the hard way is not atomic, it is not fail-safe either,
// but it should be
// at least possible.
SlaveProfile sp = getSlaveProfile(slaveId);
if (sp.getWriteMaskRegister()) {
// Give the write mask a try.
WriteMaskRegisterRequest request = new WriteMaskRegisterRequest(slaveId, writeOffset);
request.setBit(bit, value);
ModbusResponse response = send(request);
if (response == null)
// This should only happen if the request was a broadcast
return;
if (!response.isException())
// Hey, cool, it worked.
return;
if (response.getExceptionCode() == ExceptionCode.ILLEGAL_FUNCTION)
// The function is probably not supported. Fail-over to the two step.
sp.setWriteMaskRegister(false);
else
throw new ErrorResponseException(request, response);
}
// Do it the hard way. Get the register's current value.
int regValue = (Integer) getValue(new NumericLocator(slaveId, RegisterRange.HOLDING_REGISTER, writeOffset,
DataType.TWO_BYTE_INT_UNSIGNED));
// Modify the value according to the given bit and value.
if (value)
regValue = regValue | 1 << bit;
else
regValue = regValue & ~(1 << bit);
// Write the new register value.
setValue(new WriteRegisterRequest(slaveId, writeOffset, regValue));
}
private SlaveProfile getSlaveProfile(int slaveId) {
SlaveProfile sp = slaveProfiles.get(slaveId);
if (sp == null) {
sp = new SlaveProfile();
slaveProfiles.put(slaveId, sp);
}
return sp;
}
}
@@ -1,138 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.exception.ModbusInitException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* <p>
* Abstract ModbusSlaveSet class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public abstract class ModbusSlaveSet extends Modbus {
private LinkedHashMap<Integer, ProcessImage> processImages = new LinkedHashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* <p>
* addProcessImage.
* </p>
*
* @param processImage a {@link ProcessImage} object.
*/
public void addProcessImage(ProcessImage processImage) {
lock.writeLock().lock();
try {
processImages.put(processImage.getSlaveId(), processImage);
} finally {
lock.writeLock().unlock();
}
}
/**
* <p>
* removeProcessImage.
* </p>
*
* @param slaveId a int.
* @return a boolean.
*/
public boolean removeProcessImage(int slaveId) {
lock.writeLock().lock();
try {
return (processImages.remove(slaveId) != null);
} finally {
lock.writeLock().unlock();
}
}
/**
* <p>
* removeProcessImage.
* </p>
*
* @param processImage a {@link ProcessImage} object.
* @return a boolean.
*/
public boolean removeProcessImage(ProcessImage processImage) {
lock.writeLock().lock();
try {
return (processImages.remove(processImage.getSlaveId()) != null);
} finally {
lock.writeLock().unlock();
}
}
/**
* <p>
* getProcessImage.
* </p>
*
* @param slaveId a int.
* @return a {@link ProcessImage} object.
*/
public ProcessImage getProcessImage(int slaveId) {
lock.readLock().lock();
try {
return processImages.get(slaveId);
} finally {
lock.readLock().unlock();
}
}
/**
* Get a copy of the current process images
*
* @return a {@link Collection} object.
*/
public Collection<ProcessImage> getProcessImages() {
lock.readLock().lock();
try {
return new HashSet<>(processImages.values());
} finally {
lock.readLock().unlock();
}
}
/**
* Starts the slave. If an exception is not thrown, this method doesn't return, but
* uses the thread to execute the listening.
*
* @throws ModbusInitException if necessary
*/
abstract public void start() throws ModbusInitException;
/**
* <p>
* stop.
* </p>
*/
abstract public void stop();
}
@@ -1,41 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.sero.util.ProgressiveTaskListener;
/**
* <p>
* NodeScanListener interface.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public interface NodeScanListener extends ProgressiveTaskListener {
/**
* <p>
* nodeFound.
* </p>
*
* @param nodeNumber a int.
*/
void nodeFound(int nodeNumber);
}
@@ -1,176 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
import com.serotonin.modbus4j.exception.IllegalDataAddressException;
/**
* Used by slave implementors. Provides an interface by which slaves can easily manage
* data.
*
* @author mlohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public interface ProcessImage {
/**
* <p>
* getSlaveId.
* </p>
*
* @return a int.
*/
int getSlaveId();
//
// /
// / Coils
// /
//
/**
* Returns the current value of the coil for the given offset.
*
* @param offset a int.
* @return the value of the coil
* @throws IllegalDataAddressException if any.
*/
boolean getCoil(int offset) throws IllegalDataAddressException;
/**
* Used internally for setting the value of the coil.
*
* @param offset a int.
* @param value a boolean.
*/
void setCoil(int offset, boolean value);
/**
* Used to set the coil as a result of a write command from the master.
*
* @param offset a int.
* @param value a boolean.
* @throws IllegalDataAddressException if any.
*/
void writeCoil(int offset, boolean value) throws IllegalDataAddressException;
//
// /
// / Inputs
// /
//
/**
* Returns the current value of the input for the given offset.
*
* @param offset a int.
* @return the value of the input
* @throws IllegalDataAddressException if any.
*/
boolean getInput(int offset) throws IllegalDataAddressException;
/**
* Used internally for setting the value of the input.
*
* @param offset a int.
* @param value a boolean.
*/
void setInput(int offset, boolean value);
//
// /
// / Holding registers
// /
//
/**
* Returns the current value of the holding register for the given offset.
*
* @param offset a int.
* @return the value of the register
* @throws IllegalDataAddressException if any.
*/
short getHoldingRegister(int offset) throws IllegalDataAddressException;
/**
* Used internally for setting the value of the holding register.
*
* @param offset a int.
* @param value a short.
*/
void setHoldingRegister(int offset, short value);
/**
* Used to set the holding register as a result of a write command from the master.
*
* @param offset a int.
* @param value a short.
* @throws IllegalDataAddressException if any.
*/
void writeHoldingRegister(int offset, short value) throws IllegalDataAddressException;
//
// /
// / Input registers
// /
//
/**
* Returns the current value of the input register for the given offset.
*
* @param offset a int.
* @return the value of the register
* @throws IllegalDataAddressException if any.
*/
short getInputRegister(int offset) throws IllegalDataAddressException;
/**
* Used internally for setting the value of the input register.
*
* @param offset a int.
* @param value a short.
*/
void setInputRegister(int offset, short value);
//
// /
// / Exception status
// /
//
/**
* Returns the current value of the exception status.
*
* @return the current value of the exception status.
*/
byte getExceptionStatus();
//
// /
// / Report slave id
// /
//
/**
* Returns the data for the report slave id command.
*
* @return the data for the report slave id command.
*/
byte[] getReportSlaveIdData();
}
@@ -1,52 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j;
/**
* <p>
* ProcessImageListener interface.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public interface ProcessImageListener {
/**
* <p>
* coilWrite.
* </p>
*
* @param offset a int.
* @param oldValue a boolean.
* @param newValue a boolean.
*/
public void coilWrite(int offset, boolean oldValue, boolean newValue);
/**
* <p>
* holdingRegisterWrite.
* </p>
*
* @param offset a int.
* @param oldValue a short.
* @param newValue a short.
*/
public void holdingRegisterWrite(int offset, short oldValue, short newValue);
}
@@ -1,68 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
import com.serotonin.modbus4j.sero.messaging.IncomingMessage;
import com.serotonin.modbus4j.sero.messaging.MessageParser;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* Abstract BaseMessageParser class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public abstract class BaseMessageParser implements MessageParser {
protected final boolean master;
/**
* <p>
* Constructor for BaseMessageParser.
* </p>
*
* @param master a boolean.
*/
public BaseMessageParser(boolean master) {
this.master = master;
}
@Override
public IncomingMessage parseMessage(ByteQueue queue) throws Exception {
try {
return parseMessageImpl(queue);
} catch (ArrayIndexOutOfBoundsException e) {
// Means that we ran out of data trying to read the message. Just return null.
return null;
}
}
/**
* <p>
* parseMessageImpl.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @return a {@link IncomingMessage} object.
* @throws Exception if any.
*/
abstract protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception;
}
@@ -1,80 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.ProcessImage;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.RequestHandler;
/**
* <p>
* Abstract BaseRequestHandler class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public abstract class BaseRequestHandler implements RequestHandler {
protected ModbusSlaveSet slave;
/**
* <p>
* Constructor for BaseRequestHandler.
* </p>
*
* @param slave a {@link ModbusSlaveSet} object.
*/
public BaseRequestHandler(ModbusSlaveSet slave) {
this.slave = slave;
}
/**
* <p>
* handleRequestImpl.
* </p>
*
* @param request a {@link ModbusRequest} object.
* @return a {@link ModbusResponse} object.
* @throws ModbusTransportException if any.
*/
protected ModbusResponse handleRequestImpl(ModbusRequest request) throws ModbusTransportException {
request.validate(slave);
int slaveId = request.getSlaveId();
// Check the slave id.
if (slaveId == 0) {
// Broadcast message. Send to all process images.
for (ProcessImage processImage : slave.getProcessImages())
request.handle(processImage);
return null;
}
// Find the process image to which to send.
ProcessImage processImage = slave.getProcessImage(slaveId);
if (processImage == null)
return null;
return request.handle(processImage);
}
}
@@ -1,159 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
import com.serotonin.modbus4j.ExceptionResult;
import com.serotonin.modbus4j.code.ExceptionCode;
import com.serotonin.modbus4j.locator.BaseLocator;
/**
* <p>
* KeyedModbusLocator class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class KeyedModbusLocator<K> {
private final K key;
private final BaseLocator<?> locator;
/**
* <p>
* Constructor for KeyedModbusLocator.
* </p>
*
* @param key a K object.
* @param locator a {@link BaseLocator} object.
*/
public KeyedModbusLocator(K key, BaseLocator<?> locator) {
this.key = key;
this.locator = locator;
}
/**
* <p>
* Getter for the field <code>key</code>.
* </p>
*
* @return a K object.
*/
public K getKey() {
return key;
}
/**
* <p>
* Getter for the field <code>locator</code>.
* </p>
*
* @return a {@link BaseLocator} object.
*/
public BaseLocator<?> getLocator() {
return locator;
}
@Override
public String toString() {
return "KeyedModbusLocator(key=" + key + ", locator=" + locator + ")";
}
//
///
/// Delegation.
///
//
/**
* <p>
* getDataType.
* </p>
*
* @return a int.
*/
public int getDataType() {
return locator.getDataType();
}
/**
* <p>
* getOffset.
* </p>
*
* @return a int.
*/
public int getOffset() {
return locator.getOffset();
}
/**
* <p>
* getSlaveAndRange.
* </p>
*
* @return a {@link SlaveAndRange} object.
*/
public SlaveAndRange getSlaveAndRange() {
return new SlaveAndRange(locator.getSlaveId(), locator.getRange());
}
/**
* <p>
* getEndOffset.
* </p>
*
* @return a int.
*/
public int getEndOffset() {
return locator.getEndOffset();
}
/**
* <p>
* getRegisterCount.
* </p>
*
* @return a int.
*/
public int getRegisterCount() {
return locator.getRegisterCount();
}
/**
* <p>
* bytesToValue.
* </p>
*
* @param data an array of {@link byte} objects.
* @param requestOffset a int.
* @return a {@link Object} object.
*/
public Object bytesToValue(byte[] data, int requestOffset) {
try {
return locator.bytesToValue(data, requestOffset);
} catch (ArrayIndexOutOfBoundsException e) {
// Some equipment will not return data lengths that we expect, which causes
// AIOOBEs. Catch them and convert
// them into illegal data address exceptions.
return new ExceptionResult(ExceptionCode.ILLEGAL_DATA_ADDRESS);
}
}
}
@@ -1,294 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.IllegalSlaveIdException;
import com.serotonin.modbus4j.exception.ModbusIdException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.msg.ModbusMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* ModbusUtils class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ModbusUtils {
/**
* Constant <code>TCP_PORT=502</code>
*/
public static final int TCP_PORT = 502;
/**
* Constant <code>IP_PROTOCOL_ID=0</code>
*/
public static final int IP_PROTOCOL_ID = 0; // Modbus protocol
// public static final int MAX_READ_BIT_COUNT = 2000;
// public static final int MAX_READ_REGISTER_COUNT = 125;
// public static final int MAX_WRITE_REGISTER_COUNT = 120;
// Table of CRC values for high-order byte
private final static short[] lookupCRCHi = {0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40};
// Table of CRC values for low-order byte
private final static short[] lookupCRCLo = {0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30,
0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B,
0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6,
0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB,
0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD,
0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89,
0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47,
0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40};
/**
* <p>
* pushByte.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @param value a int.
*/
public static void pushByte(ByteQueue queue, int value) {
queue.push((byte) value);
}
/**
* <p>
* pushShort.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @param value a int.
*/
public static void pushShort(ByteQueue queue, int value) {
queue.push((byte) (0xff & (value >> 8)));
queue.push((byte) (0xff & value));
}
/**
* <p>
* popByte.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @return a int.
*/
public static int popByte(ByteQueue queue) {
return queue.pop();
}
/**
* <p>
* popUnsignedByte.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @return a int.
*/
public static int popUnsignedByte(ByteQueue queue) {
return queue.pop() & 0xff;
}
/**
* <p>
* popShort.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @return a int.
*/
public static int popShort(ByteQueue queue) {
return toShort(queue.pop(), queue.pop());
}
/**
* <p>
* popUnsignedShort.
* </p>
*
* @param queue a {@link ByteQueue} object.
* @return a int.
*/
public static int popUnsignedShort(ByteQueue queue) {
return ((queue.pop() & 0xff) << 8) | (queue.pop() & 0xff);
}
/**
* <p>
* toShort.
* </p>
*
* @param b1 a byte.
* @param b2 a byte.
* @return a short.
*/
public static short toShort(byte b1, byte b2) {
return (short) ((b1 << 8) | (b2 & 0xff));
}
/**
* <p>
* toByte.
* </p>
*
* @param value a short.
* @param first a boolean.
* @return a byte.
*/
public static byte toByte(short value, boolean first) {
if (first)
return (byte) (0xff & (value >> 8));
return (byte) (0xff & value);
}
/**
* <p>
* validateRegisterRange.
* </p>
*
* @param range a int.
*/
public static void validateRegisterRange(int range) {
if (RegisterRange.getReadFunctionCode(range) == -1)
throw new ModbusIdException("Invalid register range: " + range);
}
/**
* <p>
* validateSlaveId.
* </p>
*
* @param slaveId a int.
* @param includeBroadcast a boolean.
*/
public static void validateSlaveId(int slaveId, boolean includeBroadcast) {
if (slaveId < (includeBroadcast ? 0 : 1) /* || slaveId > 240 */)
throw new IllegalSlaveIdException("Invalid slave id: " + slaveId);
}
/**
* <p>
* validateBit.
* </p>
*
* @param bit a int.
*/
public static void validateBit(int bit) {
if (bit < 0 || bit > 15)
throw new ModbusIdException("Invalid bit: " + bit);
}
/**
* <p>
* validateOffset.
* </p>
*
* @param offset a int.
* @throws ModbusTransportException if any.
*/
public static void validateOffset(int offset) throws ModbusTransportException {
if (offset < 0 || offset > 65535)
throw new ModbusTransportException("Invalid offset: " + offset);
}
/**
* <p>
* validateEndOffset.
* </p>
*
* @param offset a int.
* @throws ModbusTransportException if any.
*/
public static void validateEndOffset(int offset) throws ModbusTransportException {
if (offset > 65535)
throw new ModbusTransportException("Invalid end offset: " + offset);
}
/**
* <p>
* checkCRC.
* </p>
*
* @param modbusMessage a {@link ModbusMessage} object.
* @param queue a {@link ByteQueue} object.
* @throws ModbusTransportException if any.
*/
public static void checkCRC(ModbusMessage modbusMessage, ByteQueue queue) throws ModbusTransportException {
// Check the CRC
int calcCrc = calculateCRC(modbusMessage);
int givenCrc = ModbusUtils.popUnsignedShort(queue);
if (calcCrc != givenCrc)
throw new ModbusTransportException("CRC mismatch: given=" + givenCrc + ", calc=" + calcCrc,
modbusMessage.getSlaveId());
}
/**
* <p>
* calculateCRC.
* </p>
*
* @param modbusMessage a {@link ModbusMessage} object.
* @return a int.
*/
public static int calculateCRC(ModbusMessage modbusMessage) {
ByteQueue queue = new ByteQueue();
modbusMessage.write(queue);
int high = 0xff;
int low = 0xff;
int nextByte = 0;
int uIndex;
while (queue.size() > 0) {
nextByte = 0xFF & queue.pop();
uIndex = high ^ nextByte;
high = low ^ lookupCRCHi[uIndex];
low = lookupCRCLo[uIndex];
}
return (high << 8) | low;
}
}
@@ -1,93 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
import com.serotonin.modbus4j.code.RegisterRange;
/**
* <p>
* RangeAndOffset class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class RangeAndOffset {
private int range;
private int offset;
/**
* <p>
* Constructor for RangeAndOffset.
* </p>
*
* @param range a int.
* @param offset a int.
*/
public RangeAndOffset(int range, int offset) {
this.range = range;
this.offset = offset;
}
/**
* This constructor provides a best guess at the function and offset the user wants,
* with the assumption that the offset will never go over 9999.
*
* @param registerId a int.
*/
public RangeAndOffset(int registerId) {
if (registerId < 10000) {
this.range = RegisterRange.COIL_STATUS;
this.offset = registerId - 1;
} else if (registerId < 20000) {
this.range = RegisterRange.INPUT_STATUS;
this.offset = registerId - 10001;
} else if (registerId < 40000) {
this.range = RegisterRange.INPUT_REGISTER;
this.offset = registerId - 30001;
} else {
this.range = RegisterRange.HOLDING_REGISTER;
this.offset = registerId - 40001;
}
}
/**
* <p>
* Getter for the field <code>range</code>.
* </p>
*
* @return a int.
*/
public int getRange() {
return range;
}
/**
* <p>
* Getter for the field <code>offset</code>.
* </p>
*
* @return a int.
*/
public int getOffset() {
return offset;
}
}
@@ -1,139 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
import com.serotonin.modbus4j.code.RegisterRange;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* ReadFunctionGroup class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ReadFunctionGroup<K> {
private final SlaveAndRange slaveAndRange;
private final int functionCode;
private final List<KeyedModbusLocator<K>> locators = new ArrayList<>();
private int startOffset = 65536;
private int length = 0;
/**
* <p>
* Constructor for ReadFunctionGroup.
* </p>
*
* @param locator a {@link KeyedModbusLocator} object.
*/
public ReadFunctionGroup(KeyedModbusLocator<K> locator) {
slaveAndRange = locator.getSlaveAndRange();
functionCode = RegisterRange.getReadFunctionCode(slaveAndRange.getRange());
add(locator);
}
/**
* <p>
* add.
* </p>
*
* @param locator a {@link KeyedModbusLocator} object.
*/
public void add(KeyedModbusLocator<K> locator) {
if (startOffset > locator.getOffset())
startOffset = locator.getOffset();
if (length < locator.getEndOffset() - startOffset + 1)
length = locator.getEndOffset() - startOffset + 1;
locators.add(locator);
}
/**
* <p>
* Getter for the field <code>startOffset</code>.
* </p>
*
* @return a int.
*/
public int getStartOffset() {
return startOffset;
}
/**
* <p>
* getEndOffset.
* </p>
*
* @return a int.
*/
public int getEndOffset() {
return startOffset + length - 1;
}
/**
* <p>
* Getter for the field <code>slaveAndRange</code>.
* </p>
*
* @return a {@link SlaveAndRange} object.
*/
public SlaveAndRange getSlaveAndRange() {
return slaveAndRange;
}
/**
* <p>
* Getter for the field <code>length</code>.
* </p>
*
* @return a int.
*/
public int getLength() {
return length;
}
/**
* <p>
* Getter for the field <code>functionCode</code>.
* </p>
*
* @return a int.
*/
public int getFunctionCode() {
return functionCode;
}
/**
* <p>
* Getter for the field <code>locators</code>.
* </p>
*
* @return a {@link List} object.
*/
public List<KeyedModbusLocator<K>> getLocators() {
return locators;
}
}
@@ -1,96 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
/**
* <p>
* SlaveAndRange class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class SlaveAndRange {
private final int slaveId;
private final int range;
/**
* <p>
* Constructor for SlaveAndRange.
* </p>
*
* @param slaveId a int.
* @param range a int.
*/
public SlaveAndRange(int slaveId, int range) {
ModbusUtils.validateSlaveId(slaveId, true);
this.slaveId = slaveId;
this.range = range;
}
/**
* <p>
* Getter for the field <code>range</code>.
* </p>
*
* @return a int.
*/
public int getRange() {
return range;
}
/**
* <p>
* Getter for the field <code>slaveId</code>.
* </p>
*
* @return a int.
*/
public int getSlaveId() {
return slaveId;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + range;
result = prime * result + slaveId;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final SlaveAndRange other = (SlaveAndRange) obj;
if (range != other.range)
return false;
if (slaveId != other.slaveId)
return false;
return true;
}
}
@@ -1,54 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.base;
/**
* Class for maintaining the profile of a slave device on the master side. Initially, we
* assume that the device is fully featured, and then we note function failures so that we
* know how requests should subsequently be sent.
*
* @author mlohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class SlaveProfile {
private boolean writeMaskRegister = true;
/**
* <p>
* Getter for the field <code>writeMaskRegister</code>.
* </p>
*
* @return a boolean.
*/
public boolean getWriteMaskRegister() {
return writeMaskRegister;
}
/**
* <p>
* Setter for the field <code>writeMaskRegister</code>.
* </p>
*
* @param writeMaskRegister a boolean.
*/
public void setWriteMaskRegister(boolean writeMaskRegister) {
this.writeMaskRegister = writeMaskRegister;
}
}
@@ -1,317 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.code;
import java.math.BigInteger;
/**
* <p>
* DataType class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class DataType {
/**
* Constant <code>BINARY=1</code>
*/
public static final int BINARY = 1;
/**
* Constant <code>TWO_BYTE_INT_UNSIGNED=2</code>
*/
public static final int TWO_BYTE_INT_UNSIGNED = 2;
/**
* Constant <code>TWO_BYTE_INT_SIGNED=3</code>
*/
public static final int TWO_BYTE_INT_SIGNED = 3;
/**
* Constant <code>TWO_BYTE_INT_UNSIGNED_SWAPPED=22</code>
*/
public static final int TWO_BYTE_INT_UNSIGNED_SWAPPED = 22;
/**
* Constant <code>TWO_BYTE_INT_SIGNED_SWAPPED=23</code>
*/
public static final int TWO_BYTE_INT_SIGNED_SWAPPED = 23;
/**
* Constant <code>FOUR_BYTE_INT_UNSIGNED=4</code>
*/
public static final int FOUR_BYTE_INT_UNSIGNED = 4;
/**
* Constant <code>FOUR_BYTE_INT_SIGNED=5</code>
*/
public static final int FOUR_BYTE_INT_SIGNED = 5;
/**
* Constant <code>FOUR_BYTE_INT_UNSIGNED_SWAPPED=6</code>
*/
public static final int FOUR_BYTE_INT_UNSIGNED_SWAPPED = 6;
/**
* Constant <code>FOUR_BYTE_INT_SIGNED_SWAPPED=7</code>
*/
public static final int FOUR_BYTE_INT_SIGNED_SWAPPED = 7;
/* 0xAABBCCDD is transmitted as 0xDDCCBBAA */
/**
* Constant <code>FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED=24</code>
*/
public static final int FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED = 24;
/**
* Constant <code>FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED=25</code>
*/
public static final int FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED = 25;
/**
* Constant <code>FOUR_BYTE_FLOAT=8</code>
*/
public static final int FOUR_BYTE_FLOAT = 8;
/**
* Constant <code>FOUR_BYTE_FLOAT_SWAPPED=9</code>
*/
public static final int FOUR_BYTE_FLOAT_SWAPPED = 9;
/**
* Constant <code>FOUR_BYTE_FLOAT_SWAPPED_INVERTED=21</code>
*/
public static final int FOUR_BYTE_FLOAT_SWAPPED_INVERTED = 21;
/**
* Constant <code>EIGHT_BYTE_INT_UNSIGNED=10</code>
*/
public static final int EIGHT_BYTE_INT_UNSIGNED = 10;
/**
* Constant <code>EIGHT_BYTE_INT_SIGNED=11</code>
*/
public static final int EIGHT_BYTE_INT_SIGNED = 11;
/**
* Constant <code>EIGHT_BYTE_INT_UNSIGNED_SWAPPED=12</code>
*/
public static final int EIGHT_BYTE_INT_UNSIGNED_SWAPPED = 12;
/**
* Constant <code>EIGHT_BYTE_INT_SIGNED_SWAPPED=13</code>
*/
public static final int EIGHT_BYTE_INT_SIGNED_SWAPPED = 13;
/**
* Constant <code>EIGHT_BYTE_FLOAT=14</code>
*/
public static final int EIGHT_BYTE_FLOAT = 14;
/**
* Constant <code>EIGHT_BYTE_FLOAT_SWAPPED=15</code>
*/
public static final int EIGHT_BYTE_FLOAT_SWAPPED = 15;
/**
* Constant <code>TWO_BYTE_BCD=16</code>
*/
public static final int TWO_BYTE_BCD = 16;
/**
* Constant <code>FOUR_BYTE_BCD=17</code>
*/
public static final int FOUR_BYTE_BCD = 17;
/**
* Constant <code>FOUR_BYTE_BCD_SWAPPED=20</code>
*/
public static final int FOUR_BYTE_BCD_SWAPPED = 20;
/**
* Constant <code>CHAR=18</code>
*/
public static final int CHAR = 18;
/**
* Constant <code>VARCHAR=19</code>
*/
public static final int VARCHAR = 19;
// MOD10K two, three and four register types
/**
* Constant <code>FOUR_BYTE_MOD_10K=26</code>
*/
public static final int FOUR_BYTE_MOD_10K = 26;
/**
* Constant <code>SIX_BYTE_MOD_10K=27</code>
*/
public static final int SIX_BYTE_MOD_10K = 27;
/**
* Constant <code>EIGHT_BYTE_MOD_10K=28</code>
*/
public static final int EIGHT_BYTE_MOD_10K = 28;
/**
* Constant <code>FOUR_BYTE_MOD_10K_SWAPPED=29</code>
*/
public static final int FOUR_BYTE_MOD_10K_SWAPPED = 29;
/**
* Constant <code>SIX_BYTE_MOD_10K_SWAPPED=30</code>
*/
public static final int SIX_BYTE_MOD_10K_SWAPPED = 30;
/**
* Constant <code>EIGHT_BYTE_MOD_10K_SWAPPED=31</code>
*/
public static final int EIGHT_BYTE_MOD_10K_SWAPPED = 31;
// One byte unsigned integer types
/**
* Constant <code>ONE_BYTE_INT_UNSIGNED_LOWER=32</code>
*/
public static final int ONE_BYTE_INT_UNSIGNED_LOWER = 32;
/**
* Constant <code>ONE_BYTE_INT_UNSIGNED_UPPER=33</code>
*/
public static final int ONE_BYTE_INT_UNSIGNED_UPPER = 33;
/**
* <p>
* getRegisterCount.
* </p>
*
* @param id a int.
* @return a int.
*/
public static int getRegisterCount(int id) {
switch (id) {
case BINARY:
case TWO_BYTE_INT_UNSIGNED:
case TWO_BYTE_INT_SIGNED:
case TWO_BYTE_INT_UNSIGNED_SWAPPED:
case TWO_BYTE_INT_SIGNED_SWAPPED:
case TWO_BYTE_BCD:
case ONE_BYTE_INT_UNSIGNED_LOWER:
case ONE_BYTE_INT_UNSIGNED_UPPER:
return 1;
case FOUR_BYTE_INT_UNSIGNED:
case FOUR_BYTE_INT_SIGNED:
case FOUR_BYTE_INT_UNSIGNED_SWAPPED:
case FOUR_BYTE_INT_SIGNED_SWAPPED:
case FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED:
case FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED:
case FOUR_BYTE_FLOAT:
case FOUR_BYTE_FLOAT_SWAPPED:
case FOUR_BYTE_FLOAT_SWAPPED_INVERTED:
case FOUR_BYTE_BCD:
case FOUR_BYTE_BCD_SWAPPED:
case FOUR_BYTE_MOD_10K:
case FOUR_BYTE_MOD_10K_SWAPPED:
return 2;
case SIX_BYTE_MOD_10K:
case SIX_BYTE_MOD_10K_SWAPPED:
return 3;
case EIGHT_BYTE_INT_UNSIGNED:
case EIGHT_BYTE_INT_SIGNED:
case EIGHT_BYTE_INT_UNSIGNED_SWAPPED:
case EIGHT_BYTE_INT_SIGNED_SWAPPED:
case EIGHT_BYTE_FLOAT:
case EIGHT_BYTE_FLOAT_SWAPPED:
case EIGHT_BYTE_MOD_10K:
case EIGHT_BYTE_MOD_10K_SWAPPED:
return 4;
}
return 0;
}
/**
* <p>
* getJavaType.
* </p>
*
* @param id a int.
* @return a {@link Class} object.
*/
public static Class<?> getJavaType(int id) {
switch (id) {
case ONE_BYTE_INT_UNSIGNED_LOWER:
case ONE_BYTE_INT_UNSIGNED_UPPER:
return Integer.class;
case BINARY:
return Boolean.class;
case TWO_BYTE_INT_UNSIGNED:
case TWO_BYTE_INT_UNSIGNED_SWAPPED:
return Integer.class;
case TWO_BYTE_INT_SIGNED:
case TWO_BYTE_INT_SIGNED_SWAPPED:
return Short.class;
case FOUR_BYTE_INT_UNSIGNED:
return Long.class;
case FOUR_BYTE_INT_SIGNED:
return Integer.class;
case FOUR_BYTE_INT_UNSIGNED_SWAPPED:
case FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED:
return Long.class;
case FOUR_BYTE_INT_SIGNED_SWAPPED:
case FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED:
return Integer.class;
case FOUR_BYTE_FLOAT:
return Float.class;
case FOUR_BYTE_FLOAT_SWAPPED:
return Float.class;
case FOUR_BYTE_FLOAT_SWAPPED_INVERTED:
return Float.class;
case EIGHT_BYTE_INT_UNSIGNED:
return BigInteger.class;
case EIGHT_BYTE_INT_SIGNED:
return Long.class;
case EIGHT_BYTE_INT_UNSIGNED_SWAPPED:
return BigInteger.class;
case EIGHT_BYTE_INT_SIGNED_SWAPPED:
return Long.class;
case EIGHT_BYTE_FLOAT:
return Double.class;
case EIGHT_BYTE_FLOAT_SWAPPED:
return Double.class;
case TWO_BYTE_BCD:
return Short.class;
case FOUR_BYTE_BCD:
case FOUR_BYTE_BCD_SWAPPED:
return Integer.class;
case CHAR:
case VARCHAR:
return String.class;
case FOUR_BYTE_MOD_10K:
case SIX_BYTE_MOD_10K:
case EIGHT_BYTE_MOD_10K:
case FOUR_BYTE_MOD_10K_SWAPPED:
case SIX_BYTE_MOD_10K_SWAPPED:
case EIGHT_BYTE_MOD_10K_SWAPPED:
return BigInteger.class;
}
return null;
}
}
@@ -1,107 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.code;
/**
* <p>
* ExceptionCode class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ExceptionCode {
/**
* Constant <code>ILLEGAL_FUNCTION=0x1</code>
*/
public static final byte ILLEGAL_FUNCTION = 0x1;
/**
* Constant <code>ILLEGAL_DATA_ADDRESS=0x2</code>
*/
public static final byte ILLEGAL_DATA_ADDRESS = 0x2;
/**
* Constant <code>ILLEGAL_DATA_VALUE=0x3</code>
*/
public static final byte ILLEGAL_DATA_VALUE = 0x3;
/**
* Constant <code>SLAVE_DEVICE_FAILURE=0x4</code>
*/
public static final byte SLAVE_DEVICE_FAILURE = 0x4;
/**
* Constant <code>ACKNOWLEDGE=0x5</code>
*/
public static final byte ACKNOWLEDGE = 0x5;
/**
* Constant <code>SLAVE_DEVICE_BUSY=0x6</code>
*/
public static final byte SLAVE_DEVICE_BUSY = 0x6;
/**
* Constant <code>MEMORY_PARITY_ERROR=0x8</code>
*/
public static final byte MEMORY_PARITY_ERROR = 0x8;
/**
* Constant <code>GATEWAY_PATH_UNAVAILABLE=0xa</code>
*/
public static final byte GATEWAY_PATH_UNAVAILABLE = 0xa;
/**
* Constant <code>GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND=0xb</code>
*/
public static final byte GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0xb;
/**
* <p>
* getExceptionMessage.
* </p>
*
* @param id a byte.
* @return a {@link String} object.
*/
public static String getExceptionMessage(byte id) {
switch (id) {
case ILLEGAL_FUNCTION:
return "Illegal function";
case ILLEGAL_DATA_ADDRESS:
return "Illegal data address";
case ILLEGAL_DATA_VALUE:
return "Illegal data value";
case SLAVE_DEVICE_FAILURE:
return "Slave device failure";
case ACKNOWLEDGE:
return "Acknowledge";
case SLAVE_DEVICE_BUSY:
return "Slave device busy";
case MEMORY_PARITY_ERROR:
return "Memory parity error";
case GATEWAY_PATH_UNAVAILABLE:
return "Gateway path unavailable";
case GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND:
return "Gateway target device failed to respond";
}
return "Unknown exception code: " + id;
}
}
@@ -1,97 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.code;
/**
* <p>
* FunctionCode class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class FunctionCode {
/**
* Constant <code>READ_COILS=1</code>
*/
public static final byte READ_COILS = 1;
/**
* Constant <code>READ_DISCRETE_INPUTS=2</code>
*/
public static final byte READ_DISCRETE_INPUTS = 2;
/**
* Constant <code>READ_HOLDING_REGISTERS=3</code>
*/
public static final byte READ_HOLDING_REGISTERS = 3;
/**
* Constant <code>READ_INPUT_REGISTERS=4</code>
*/
public static final byte READ_INPUT_REGISTERS = 4;
/**
* Constant <code>WRITE_COIL=5</code>
*/
public static final byte WRITE_COIL = 5;
/**
* Constant <code>WRITE_REGISTER=6</code>
*/
public static final byte WRITE_REGISTER = 6;
/**
* Constant <code>READ_EXCEPTION_STATUS=7</code>
*/
public static final byte READ_EXCEPTION_STATUS = 7;
/**
* Constant <code>WRITE_COILS=15</code>
*/
public static final byte WRITE_COILS = 15;
/**
* Constant <code>WRITE_REGISTERS=16</code>
*/
public static final byte WRITE_REGISTERS = 16;
/**
* Constant <code>REPORT_SLAVE_ID=17</code>
*/
public static final byte REPORT_SLAVE_ID = 17;
/**
* Constant <code>WRITE_MASK_REGISTER=22</code>
*/
public static final byte WRITE_MASK_REGISTER = 22;
/**
* <p>
* toString.
* </p>
*
* @param code a byte.
* @return a {@link String} object.
*/
public static String toString(byte code) {
return Integer.toString(code & 0xff);
}
}
@@ -1,116 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.code;
/**
* <p>
* RegisterRange class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class RegisterRange {
/**
* Constant <code>COIL_STATUS=1</code>
*/
public static final int COIL_STATUS = 1;
/**
* Constant <code>INPUT_STATUS=2</code>
*/
public static final int INPUT_STATUS = 2;
/**
* Constant <code>HOLDING_REGISTER=3</code>
*/
public static final int HOLDING_REGISTER = 3;
/**
* Constant <code>INPUT_REGISTER=4</code>
*/
public static final int INPUT_REGISTER = 4;
/**
* <p>
* getFrom.
* </p>
*
* @param id a int.
* @return a int.
*/
public static int getFrom(int id) {
switch (id) {
case COIL_STATUS:
return 0;
case INPUT_STATUS:
return 0x10000;
case HOLDING_REGISTER:
return 0x40000;
case INPUT_REGISTER:
return 0x30000;
}
return -1;
}
/**
* <p>
* getTo.
* </p>
*
* @param id a int.
* @return a int.
*/
public static int getTo(int id) {
switch (id) {
case COIL_STATUS:
return 0xffff;
case INPUT_STATUS:
return 0x1ffff;
case HOLDING_REGISTER:
return 0x4ffff;
case INPUT_REGISTER:
return 0x3ffff;
}
return -1;
}
/**
* <p>
* getReadFunctionCode.
* </p>
*
* @param id a int.
* @return a int.
*/
public static int getReadFunctionCode(int id) {
switch (id) {
case COIL_STATUS:
return FunctionCode.READ_COILS;
case INPUT_STATUS:
return FunctionCode.READ_DISCRETE_INPUTS;
case HOLDING_REGISTER:
return FunctionCode.READ_HOLDING_REGISTERS;
case INPUT_REGISTER:
return FunctionCode.READ_INPUT_REGISTERS;
}
return -1;
}
}
@@ -1,79 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
/**
* <p>
* ErrorResponseException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ErrorResponseException extends Exception {
private static final long serialVersionUID = -1;
private final ModbusRequest originalRequest;
private final ModbusResponse errorResponse;
/**
* <p>
* Constructor for ErrorResponseException.
* </p>
*
* @param originalRequest a {@link ModbusRequest} object.
* @param errorResponse a {@link ModbusResponse} object.
*/
public ErrorResponseException(ModbusRequest originalRequest, ModbusResponse errorResponse) {
this.originalRequest = originalRequest;
this.errorResponse = errorResponse;
}
/**
* <p>
* Getter for the field <code>errorResponse</code>.
* </p>
*
* @return a {@link ModbusResponse} object.
*/
public ModbusResponse getErrorResponse() {
return errorResponse;
}
/**
* <p>
* Getter for the field <code>originalRequest</code>.
* </p>
*
* @return a {@link ModbusRequest} object.
*/
public ModbusRequest getOriginalRequest() {
return originalRequest;
}
@Override
public String getMessage() {
return errorResponse.getExceptionMessage();
}
}
@@ -1,52 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* IllegalDataAddressException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class IllegalDataAddressException extends ModbusTransportException {
private static final long serialVersionUID = -1;
/**
* <p>
* Constructor for IllegalDataAddressException.
* </p>
*/
public IllegalDataAddressException() {
super();
}
/**
* <p>
* Constructor for IllegalDataAddressException.
* </p>
*
* @param slaveId a int.
*/
public IllegalDataAddressException(int slaveId) {
super(slaveId);
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* IllegalDataTypeException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class IllegalDataTypeException extends ModbusIdException {
private static final long serialVersionUID = -1;
/**
* <p>
* Constructor for IllegalDataTypeException.
* </p>
*
* @param message a {@link String} object.
*/
public IllegalDataTypeException(String message) {
super(message);
}
}
@@ -1,58 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* IllegalFunctionException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class IllegalFunctionException extends ModbusTransportException {
private static final long serialVersionUID = -1;
private final byte functionCode;
/**
* <p>
* Constructor for IllegalFunctionException.
* </p>
*
* @param functionCode a byte.
* @param slaveId a int.
*/
public IllegalFunctionException(byte functionCode, int slaveId) {
super("Function code: 0x" + Integer.toHexString(functionCode & 0xff), slaveId);
this.functionCode = functionCode;
}
/**
* <p>
* Getter for the field <code>functionCode</code>.
* </p>
*
* @return a byte.
*/
public byte getFunctionCode() {
return functionCode;
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* IllegalSlaveIdException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class IllegalSlaveIdException extends ModbusIdException {
private static final long serialVersionUID = -1;
/**
* <p>
* Constructor for IllegalSlaveIdException.
* </p>
*
* @param message a {@link String} object.
*/
public IllegalSlaveIdException(String message) {
super(message);
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* InvalidDataConversionException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class InvalidDataConversionException extends RuntimeException {
private static final long serialVersionUID = -1;
/**
* <p>
* Constructor for InvalidDataConversionException.
* </p>
*
* @param message a {@link String} object.
*/
public InvalidDataConversionException(String message) {
super(message);
}
}
@@ -1,54 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* ModbusIdException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ModbusIdException extends RuntimeException {
private static final long serialVersionUID = -1;
/**
* <p>
* Constructor for ModbusIdException.
* </p>
*
* @param message a {@link String} object.
*/
public ModbusIdException(String message) {
super(message);
}
/**
* <p>
* Constructor for ModbusIdException.
* </p>
*
* @param cause a {@link Throwable} object.
*/
public ModbusIdException(Throwable cause) {
super(cause);
}
}
@@ -1,75 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* ModbusInitException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ModbusInitException extends Exception {
private static final long serialVersionUID = -1;
/**
* <p>
* Constructor for ModbusInitException.
* </p>
*/
public ModbusInitException() {
super();
}
/**
* <p>
* Constructor for ModbusInitException.
* </p>
*
* @param message a {@link String} object.
* @param cause a {@link Throwable} object.
*/
public ModbusInitException(String message, Throwable cause) {
super(message, cause);
}
/**
* <p>
* Constructor for ModbusInitException.
* </p>
*
* @param message a {@link String} object.
*/
public ModbusInitException(String message) {
super(message);
}
/**
* <p>
* Constructor for ModbusInitException.
* </p>
*
* @param cause a {@link Throwable} object.
*/
public ModbusInitException(Throwable cause) {
super(cause);
}
}
@@ -1,129 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.exception;
/**
* <p>
* ModbusTransportException class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ModbusTransportException extends Exception {
private static final long serialVersionUID = -1;
private final int slaveId;
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*/
public ModbusTransportException() {
this.slaveId = -1;
}
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*
* @param slaveId a int.
*/
public ModbusTransportException(int slaveId) {
this.slaveId = slaveId;
}
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*
* @param message a {@link String} object.
* @param cause a {@link Throwable} object.
* @param slaveId a int.
*/
public ModbusTransportException(String message, Throwable cause, int slaveId) {
super(message, cause);
this.slaveId = slaveId;
}
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*
* @param message a {@link String} object.
* @param slaveId a int.
*/
public ModbusTransportException(String message, int slaveId) {
super(message);
this.slaveId = slaveId;
}
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*
* @param message a {@link String} object.
*/
public ModbusTransportException(String message) {
super(message);
this.slaveId = -1;
}
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*
* @param cause a {@link Throwable} object.
*/
public ModbusTransportException(Throwable cause) {
super(cause);
this.slaveId = -1;
}
/**
* <p>
* Constructor for ModbusTransportException.
* </p>
*
* @param cause a {@link Throwable} object.
* @param slaveId a int.
*/
public ModbusTransportException(Throwable cause, int slaveId) {
super(cause);
this.slaveId = slaveId;
}
/**
* <p>
* Getter for the field <code>slaveId</code>.
* </p>
*
* @return a int.
*/
public int getSlaveId() {
return slaveId;
}
}
@@ -1,56 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip;
import com.serotonin.modbus4j.msg.ModbusMessage;
/**
* <p>
* Abstract IpMessage class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public abstract class IpMessage {
protected final ModbusMessage modbusMessage;
/**
* <p>
* Constructor for IpMessage.
* </p>
*
* @param modbusMessage a {@link ModbusMessage} object.
*/
public IpMessage(ModbusMessage modbusMessage) {
this.modbusMessage = modbusMessage;
}
/**
* <p>
* Getter for the field <code>modbusMessage</code>.
* </p>
*
* @return a {@link ModbusMessage} object.
*/
public ModbusMessage getModbusMessage() {
return modbusMessage;
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
/**
* <p>
* IpMessageResponse interface.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public interface IpMessageResponse extends OutgoingResponseMessage, IncomingResponseMessage {
/**
* <p>
* getModbusResponse.
* </p>
*
* @return a {@link ModbusResponse} object.
*/
ModbusResponse getModbusResponse();
}
@@ -1,128 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip;
import com.serotonin.modbus4j.base.ModbusUtils;
/**
* <p>
* IpParameters class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class IpParameters {
private String host;
private int port = ModbusUtils.TCP_PORT;
private boolean encapsulated;
private Integer lingerTime = -1;
/**
* <p>
* Getter for the field <code>host</code>.
* </p>
*
* @return a {@link String} object.
*/
public String getHost() {
return host;
}
/**
* <p>
* Setter for the field <code>host</code>.
* </p>
*
* @param host a {@link String} object.
*/
public void setHost(String host) {
this.host = host;
}
/**
* <p>
* Getter for the field <code>port</code>.
* </p>
*
* @return a int.
*/
public int getPort() {
return port;
}
/**
* <p>
* Setter for the field <code>port</code>.
* </p>
*
* @param port a int.
*/
public void setPort(int port) {
this.port = port;
}
/**
* <p>
* isEncapsulated.
* </p>
*
* @return a boolean.
*/
public boolean isEncapsulated() {
return encapsulated;
}
/**
* <p>
* Setter for the field <code>encapsulated</code>.
* </p>
*
* @param encapsulated a boolean.
*/
public void setEncapsulated(boolean encapsulated) {
this.encapsulated = encapsulated;
}
/**
* <p>
* Getter for the field <code>linger</code>.
* </p>
*
* @return a int.
*/
public Integer getLingerTime() {
return lingerTime;
}
/**
* <p>
* Setter for the field <code>linger</code>.
* </p>
*
* @param lingerTime a int.
*/
public void setLingerTime(Integer lingerTime) {
this.lingerTime = lingerTime;
}
}
@@ -1,66 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.encap;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.ip.IpMessage;
import com.serotonin.modbus4j.msg.ModbusMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* EncapMessage class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class EncapMessage extends IpMessage {
/**
* <p>
* Constructor for EncapMessage.
* </p>
*
* @param modbusMessage a {@link ModbusMessage} object.
*/
public EncapMessage(ModbusMessage modbusMessage) {
super(modbusMessage);
}
/**
* <p>
* getMessageData.
* </p>
*
* @return an array of {@link byte} objects.
*/
public byte[] getMessageData() {
ByteQueue msgQueue = new ByteQueue();
// Write the particular message.
modbusMessage.write(msgQueue);
// Write the CRC
ModbusUtils.pushShort(msgQueue, ModbusUtils.calculateCRC(modbusMessage));
// Return the data.
return msgQueue.popAll();
}
}
@@ -1,52 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.encap;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.sero.messaging.IncomingMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* EncapMessageParser class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class EncapMessageParser extends BaseMessageParser {
/**
* <p>
* Constructor for EncapMessageParser.
* </p>
*
* @param master a boolean.
*/
public EncapMessageParser(boolean master) {
super(master);
}
@Override
protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception {
if (master)
return EncapMessageResponse.createEncapMessageResponse(queue);
return EncapMessageRequest.createEncapMessageRequest(queue);
}
}
@@ -1,75 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.encap;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* EncapMessageRequest class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class EncapMessageRequest extends EncapMessage implements OutgoingRequestMessage, IncomingRequestMessage {
/**
* <p>
* Constructor for EncapMessageRequest.
* </p>
*
* @param modbusRequest a {@link ModbusRequest} object.
*/
public EncapMessageRequest(ModbusRequest modbusRequest) {
super(modbusRequest);
}
static EncapMessageRequest createEncapMessageRequest(ByteQueue queue) throws ModbusTransportException {
// Create the modbus response.
ModbusRequest request = ModbusRequest.createModbusRequest(queue);
EncapMessageRequest encapRequest = new EncapMessageRequest(request);
// Check the CRC
ModbusUtils.checkCRC(encapRequest.modbusMessage, queue);
return encapRequest;
}
@Override
public boolean expectsResponse() {
return modbusMessage.getSlaveId() != 0;
}
/**
* <p>
* getModbusRequest.
* </p>
*
* @return a {@link ModbusRequest} object.
*/
public ModbusRequest getModbusRequest() {
return (ModbusRequest) modbusMessage;
}
}
@@ -1,69 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.encap;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpMessageResponse;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* EncapMessageResponse class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class EncapMessageResponse extends EncapMessage implements IpMessageResponse {
/**
* <p>
* Constructor for EncapMessageResponse.
* </p>
*
* @param modbusResponse a {@link ModbusResponse} object.
*/
public EncapMessageResponse(ModbusResponse modbusResponse) {
super(modbusResponse);
}
static EncapMessageResponse createEncapMessageResponse(ByteQueue queue) throws ModbusTransportException {
// Create the modbus response.
ModbusResponse response = ModbusResponse.createModbusResponse(queue);
EncapMessageResponse encapResponse = new EncapMessageResponse(response);
// Check the CRC
ModbusUtils.checkCRC(encapResponse.modbusMessage, queue);
return encapResponse;
}
/**
* <p>
* getModbusResponse.
* </p>
*
* @return a {@link ModbusResponse} object.
*/
public ModbusResponse getModbusResponse() {
return (ModbusResponse) modbusMessage;
}
}
@@ -1,57 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.encap;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.base.BaseRequestHandler;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
/**
* <p>
* EncapRequestHandler class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class EncapRequestHandler extends BaseRequestHandler {
/**
* <p>
* Constructor for EncapRequestHandler.
* </p>
*
* @param slave a {@link ModbusSlaveSet} object.
*/
public EncapRequestHandler(ModbusSlaveSet slave) {
super(slave);
}
public OutgoingResponseMessage handleRequest(IncomingRequestMessage req) throws Exception {
EncapMessageRequest tcpRequest = (EncapMessageRequest) req;
ModbusRequest request = tcpRequest.getModbusRequest();
ModbusResponse response = handleRequestImpl(request);
if (response == null)
return null;
return new EncapMessageResponse(response);
}
}
@@ -1,98 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.encap;
import com.serotonin.modbus4j.ip.IpMessage;
import com.serotonin.modbus4j.msg.ModbusMessage;
import com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.WaitingRoomKey;
import com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
/**
* <p>
* EncapWaitingRoomKeyFactory class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class EncapWaitingRoomKeyFactory implements WaitingRoomKeyFactory {
@Override
public WaitingRoomKey createWaitingRoomKey(OutgoingRequestMessage request) {
return createWaitingRoomKey(((IpMessage) request).getModbusMessage());
}
@Override
public WaitingRoomKey createWaitingRoomKey(IncomingResponseMessage response) {
return createWaitingRoomKey(((IpMessage) response).getModbusMessage());
}
/**
* <p>
* createWaitingRoomKey.
* </p>
*
* @param msg a {@link ModbusMessage} object.
* @return a {@link WaitingRoomKey} object.
*/
public WaitingRoomKey createWaitingRoomKey(ModbusMessage msg) {
return new EncapWaitingRoomKey(msg.getSlaveId(), msg.getFunctionCode());
}
class EncapWaitingRoomKey implements WaitingRoomKey {
private final int slaveId;
private final byte functionCode;
public EncapWaitingRoomKey(int slaveId, byte functionCode) {
this.slaveId = slaveId;
this.functionCode = functionCode;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + functionCode;
result = prime * result + slaveId;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
EncapWaitingRoomKey other = (EncapWaitingRoomKey) obj;
if (functionCode != other.functionCode)
return false;
if (slaveId != other.slaveId)
return false;
return true;
}
}
}
@@ -1,382 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.listener;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpMessageResponse;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.ip.encap.EncapMessageParser;
import com.serotonin.modbus4j.ip.encap.EncapMessageRequest;
import com.serotonin.modbus4j.ip.encap.EncapWaitingRoomKeyFactory;
import com.serotonin.modbus4j.ip.xa.XaMessageParser;
import com.serotonin.modbus4j.ip.xa.XaMessageRequest;
import com.serotonin.modbus4j.ip.xa.XaWaitingRoomKeyFactory;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.EpollStreamTransport;
import com.serotonin.modbus4j.sero.messaging.MessageControl;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.StreamTransport;
import com.serotonin.modbus4j.sero.messaging.Transport;
import com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.net.BindException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* <p>
* TcpListener class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class TcpListener extends ModbusMaster {
// Configuration fields.
private final Log LOG = LogFactory.getLog(TcpListener.class);
private final IpParameters ipParameters;
private short nextTransactionId = 0;
private short retries = 0;
// Runtime fields.
private ServerSocket serverSocket;
private Socket socket;
private ExecutorService executorService;
private ListenerConnectionHandler handler;
/**
* <p>
* Constructor for TcpListener.
* </p>
* <p>
* Will validate response to ensure that slaveId == response slaveId if encapsulated
* is true
*
* @param params a {@link IpParameters} object.
*/
public TcpListener(IpParameters params) {
LOG.debug("Creating TcpListener in port " + params.getPort());
ipParameters = params;
connected = false;
validateResponse = ipParameters.isEncapsulated();
if (LOG.isDebugEnabled())
LOG.debug("TcpListener created! Port: " + ipParameters.getPort());
}
/**
* Control to validate response to ensure that slaveId == response slaveId
*
* @param params a {@link IpParameters} object.
* @param validateResponse a boolean.
*/
public TcpListener(IpParameters params, boolean validateResponse) {
LOG.debug("Creating TcpListener in port " + params.getPort());
ipParameters = params;
connected = false;
this.validateResponse = validateResponse;
if (LOG.isDebugEnabled())
LOG.debug("TcpListener created! Port: " + ipParameters.getPort());
}
/**
* <p>
* Getter for the field <code>nextTransactionId</code>.
* </p>
*
* @return a short.
*/
protected short getNextTransactionId() {
return nextTransactionId++;
}
@Override
synchronized public void init() throws ModbusInitException {
LOG.debug("Init TcpListener Port: " + ipParameters.getPort());
executorService = Executors.newCachedThreadPool();
startListener();
initialized = true;
LOG.warn("Initialized Port: " + ipParameters.getPort());
}
private void startListener() throws ModbusInitException {
try {
if (handler != null) {
LOG.debug("handler not null!!!");
}
handler = new ListenerConnectionHandler(socket);
LOG.debug("Init handler thread");
executorService.execute(handler);
} catch (Exception e) {
LOG.warn("Error initializing TcpListener ", e);
throw new ModbusInitException(e);
}
}
@Override
synchronized public void destroy() {
LOG.debug("Destroy TCPListener Port: " + ipParameters.getPort());
// Close the serverSocket first to prevent new messages.
try {
if (serverSocket != null)
serverSocket.close();
} catch (IOException e) {
LOG.warn("Error closing socket" + e.getLocalizedMessage());
getExceptionHandler().receivedException(e);
}
// Close all open connections.
if (handler != null) {
handler.closeConnection();
}
// Terminate Listener
terminateListener();
initialized = false;
LOG.debug("TCPListener destroyed, Port: " + ipParameters.getPort());
}
private void terminateListener() {
executorService.shutdown();
try {
executorService.awaitTermination(300, TimeUnit.MILLISECONDS);
LOG.debug("Handler Thread terminated, Port: " + ipParameters.getPort());
} catch (InterruptedException e) {
LOG.debug("Error terminating executorService - " + e.getLocalizedMessage());
getExceptionHandler().receivedException(e);
}
handler = null;
}
@Override
synchronized public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
if (!connected) {
LOG.debug("No connection in Port: " + ipParameters.getPort());
throw new ModbusTransportException(new Exception("TCP Listener has no active connection!"),
request.getSlaveId());
}
if (!initialized) {
LOG.debug("Listener already terminated " + ipParameters.getPort());
return null;
}
// Wrap the modbus request in a ip request.
OutgoingRequestMessage ipRequest;
if (ipParameters.isEncapsulated()) {
ipRequest = new EncapMessageRequest(request);
StringBuilder sb = new StringBuilder();
for (byte b : Arrays.copyOfRange(ipRequest.getMessageData(), 0, ipRequest.getMessageData().length)) {
sb.append(String.format("%02X ", b));
}
LOG.debug("Encap Request: " + sb.toString());
} else {
ipRequest = new XaMessageRequest(request, getNextTransactionId());
StringBuilder sb = new StringBuilder();
for (byte b : Arrays.copyOfRange(ipRequest.getMessageData(), 0, ipRequest.getMessageData().length)) {
sb.append(String.format("%02X ", b));
}
LOG.debug("Xa Request: " + sb.toString());
}
// Send the request to get the response.
IpMessageResponse ipResponse;
try {
// Send data via handler!
handler.conn.DEBUG = true;
ipResponse = (IpMessageResponse) handler.conn.send(ipRequest);
if (ipResponse == null) {
throw new ModbusTransportException(new Exception("No valid response from slave!"),
request.getSlaveId());
}
StringBuilder sb = new StringBuilder();
for (byte b : Arrays.copyOfRange(ipResponse.getMessageData(), 0, ipResponse.getMessageData().length)) {
sb.append(String.format("%02X ", b));
}
LOG.debug("Response: " + sb.toString());
return ipResponse.getModbusResponse();
} catch (Exception e) {
LOG.debug(e.getLocalizedMessage() + ", Port: " + ipParameters.getPort() + ", retries: " + retries);
if (retries < 10 && !e.getLocalizedMessage().contains("Broken")) {
retries++;
} else {
/*
* To recover from a Broken Pipe, the only way is to restart serverSocket
*/
LOG.debug("Restarting Socket, Port: " + ipParameters.getPort() + ", retries: " + retries);
// Close the serverSocket first to prevent new messages.
try {
if (serverSocket != null)
serverSocket.close();
} catch (IOException e2) {
LOG.debug("Error closing socket" + e2.getLocalizedMessage(), e);
getExceptionHandler().receivedException(e2);
}
// Close all open connections.
if (handler != null) {
handler.closeConnection();
terminateListener();
}
if (!initialized) {
LOG.debug("Listener already terminated " + ipParameters.getPort());
return null;
}
executorService = Executors.newCachedThreadPool();
try {
startListener();
} catch (Exception e2) {
LOG.warn("Error trying to restart socket" + e2.getLocalizedMessage(), e);
throw new ModbusTransportException(e2, request.getSlaveId());
}
retries = 0;
}
LOG.warn("Error sending request, Port: " + ipParameters.getPort() + ", msg: " + e.getMessage());
// Simple send error!
throw new ModbusTransportException(e, request.getSlaveId());
}
}
class ListenerConnectionHandler implements Runnable {
private Socket socket;
private Transport transport;
private MessageControl conn;
private BaseMessageParser ipMessageParser;
private WaitingRoomKeyFactory waitingRoomKeyFactory;
public ListenerConnectionHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
LOG.debug(" ListenerConnectionHandler::run() ");
if (ipParameters.isEncapsulated()) {
ipMessageParser = new EncapMessageParser(true);
waitingRoomKeyFactory = new EncapWaitingRoomKeyFactory();
} else {
ipMessageParser = new XaMessageParser(true);
waitingRoomKeyFactory = new XaWaitingRoomKeyFactory();
}
try {
acceptConnection();
} catch (IOException e) {
LOG.debug("Error in TCP Listener! - " + e.getLocalizedMessage(), e);
conn.close();
closeConnection();
getExceptionHandler().receivedException(new ModbusInitException(e));
}
}
private void acceptConnection() throws IOException, BindException {
while (true) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!connected) {
try {
serverSocket = new ServerSocket(ipParameters.getPort());
LOG.debug("Start Accept on port: " + ipParameters.getPort());
socket = serverSocket.accept();
LOG.info("Connected: " + socket.getInetAddress() + ":" + ipParameters.getPort());
if (getePoll() != null)
transport = new EpollStreamTransport(socket.getInputStream(), socket.getOutputStream(),
getePoll());
else
transport = new StreamTransport(socket.getInputStream(), socket.getOutputStream());
break;
} catch (Exception e) {
LOG.warn("Open connection failed on port " + ipParameters.getPort() + ", caused by "
+ e.getLocalizedMessage(), e);
if (e instanceof SocketTimeoutException) {
continue;
} else if (e.getLocalizedMessage().contains("closed")) {
return;
} else if (e instanceof BindException) {
closeConnection();
throw (BindException) e;
}
}
}
}
conn = getMessageControl();
conn.setExceptionHandler(getExceptionHandler());
conn.DEBUG = true;
conn.start(transport, ipMessageParser, null, waitingRoomKeyFactory);
if (getePoll() == null)
((StreamTransport) transport).start("Modbus4J TcpMaster");
connected = true;
}
void closeConnection() {
if (conn != null) {
LOG.debug("Closing Message Control on port: " + ipParameters.getPort());
closeMessageControl(conn);
}
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
LOG.debug("Error closing socket on port " + ipParameters.getPort() + ". " + e.getLocalizedMessage());
getExceptionHandler().receivedException(new ModbusInitException(e));
}
connected = false;
conn = null;
socket = null;
}
}
}
@@ -1,362 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.tcp;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpMessageResponse;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.ip.encap.EncapMessageParser;
import com.serotonin.modbus4j.ip.encap.EncapMessageRequest;
import com.serotonin.modbus4j.ip.encap.EncapWaitingRoomKeyFactory;
import com.serotonin.modbus4j.ip.xa.XaMessageParser;
import com.serotonin.modbus4j.ip.xa.XaMessageRequest;
import com.serotonin.modbus4j.ip.xa.XaWaitingRoomKeyFactory;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.EpollStreamTransport;
import com.serotonin.modbus4j.sero.messaging.MessageControl;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.StreamTransport;
import com.serotonin.modbus4j.sero.messaging.Transport;
import com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
/**
* <p>
* TcpMaster class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class TcpMaster extends ModbusMaster {
// Configuration fields.
private final Log LOG = LogFactory.getLog(TcpMaster.class);
private final IpParameters ipParameters;
private final boolean keepAlive;
private final boolean autoIncrementTransactionId;
private final Integer lingerTime;
private short nextTransactionId = 0;
// Runtime fields.
private Socket socket;
private Transport transport;
private MessageControl conn;
/**
* <p>
* Constructor for TcpMaster.
* </p>
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
* @param autoIncrementTransactionId a boolean.
* @param validateResponse - confirm that requested slave id is the same in the
* response
* @param lingerTime The setting only affects socket close.
*/
public TcpMaster(IpParameters params, boolean keepAlive, boolean autoIncrementTransactionId,
boolean validateResponse, Integer lingerTime) {
this.ipParameters = params;
this.keepAlive = keepAlive;
this.autoIncrementTransactionId = autoIncrementTransactionId;
this.lingerTime = lingerTime;
}
/**
* <p>
* Constructor for TcpMaster.
* </p>
* <p>
* Default to lingerTime disabled
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
* @param autoIncrementTransactionId a boolean.
* @param validateResponse - confirm that requested slave id is the same in the
* response
*/
public TcpMaster(IpParameters params, boolean keepAlive, boolean autoIncrementTransactionId,
boolean validateResponse) {
this(params, keepAlive, autoIncrementTransactionId, validateResponse, -1);
// this.ipParameters = params;
// this.keepAlive = keepAlive;
// this.autoIncrementTransactionId = autoIncrementTransactionId;
}
/**
* <p>
* Constructor for TcpMaster.
* </p>
* Default to not validating the slave id in responses Default to lingerTime disabled
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
* @param autoIncrementTransactionId a boolean.
*/
public TcpMaster(IpParameters params, boolean keepAlive, boolean autoIncrementTransactionId) {
this(params, keepAlive, autoIncrementTransactionId, false, -1);
}
/**
* <p>
* Constructor for TcpMaster.
* </p>
* <p>
* Default to auto increment transaction id Default to not validating the slave id in
* responses Default to lingerTime disabled
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
* @param lingerTime an Integer. The setting only affects socket close.
*/
public TcpMaster(IpParameters params, boolean keepAlive, Integer lingerTime) {
this(params, keepAlive, true, false, lingerTime);
}
/**
* <p>
* Constructor for TcpMaster.
* </p>
* <p>
* Default to auto increment transaction id Default to not validating the slave id in
* responses Default to lingerTime disabled
*
* @param params a {@link IpParameters} object.
* @param keepAlive a boolean.
*/
public TcpMaster(IpParameters params, boolean keepAlive) {
this(params, keepAlive, true, false, -1);
}
/**
* <p>
* Getter for the field <code>nextTransactionId</code>.
* </p>
*
* @return a short.
*/
protected short getNextTransactionId() {
return nextTransactionId;
}
/**
* <p>
* Setter for the field <code>nextTransactionId</code>.
* </p>
*
* @param id a short.
*/
public void setNextTransactionId(short id) {
this.nextTransactionId = id;
}
@Override
synchronized public void init() throws ModbusInitException {
try {
if (keepAlive)
openConnection();
} catch (Exception e) {
throw new ModbusInitException(e);
}
initialized = true;
}
@Override
synchronized public void destroy() {
closeConnection();
initialized = false;
}
@Override
synchronized public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
try {
// Check if we need to open the connection.
if (!keepAlive)
openConnection();
if (conn == null) {
LOG.debug("Connection null: " + ipParameters.getPort());
}
} catch (Exception e) {
closeConnection();
throw new ModbusTransportException(e, request.getSlaveId());
}
// Wrap the modbus request in a ip request.
OutgoingRequestMessage ipRequest;
if (ipParameters.isEncapsulated())
ipRequest = new EncapMessageRequest(request);
else {
if (autoIncrementTransactionId)
this.nextTransactionId++;
ipRequest = new XaMessageRequest(request, getNextTransactionId());
}
if (LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
for (byte b : Arrays.copyOfRange(ipRequest.getMessageData(), 0, ipRequest.getMessageData().length)) {
sb.append(String.format("%02X ", b));
}
LOG.debug("Encap Request: " + sb.toString());
}
// Send the request to get the response.
IpMessageResponse ipResponse;
if (LOG.isDebugEnabled()) {
LOG.debug("Sending on port: " + ipParameters.getPort());
}
try {
if (conn == null) {
if (LOG.isDebugEnabled())
LOG.debug("Connection null: " + ipParameters.getPort());
}
ipResponse = (IpMessageResponse) conn.send(ipRequest);
if (ipResponse == null)
return null;
if (LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
for (byte b : Arrays.copyOfRange(ipResponse.getMessageData(), 0, ipResponse.getMessageData().length)) {
sb.append(String.format("%02X ", b));
}
LOG.debug("Response: " + sb.toString());
}
return ipResponse.getModbusResponse();
} catch (Exception e) {
if (LOG.isDebugEnabled())
LOG.debug("Exception sending message", e);
if (keepAlive) {
if (LOG.isDebugEnabled())
LOG.debug("KeepAlive - reconnect!");
// The connection may have been reset, so try to reopen it and attempt the
// message again.
try {
if (LOG.isDebugEnabled())
LOG.debug("Modbus4J: Keep-alive connection may have been reset. Attempting to re-open.");
openConnection();
ipResponse = (IpMessageResponse) conn.send(ipRequest);
if (ipResponse == null)
return null;
if (LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
for (byte b : Arrays.copyOfRange(ipResponse.getMessageData(), 0,
ipResponse.getMessageData().length)) {
sb.append(String.format("%02X ", b));
}
LOG.debug("Response: " + sb.toString());
}
return ipResponse.getModbusResponse();
} catch (Exception e2) {
closeConnection();
if (LOG.isDebugEnabled())
LOG.debug("Exception re-sending message", e);
throw new ModbusTransportException(e2, request.getSlaveId());
}
}
throw new ModbusTransportException(e, request.getSlaveId());
} finally {
// Check if we should close the connection.
if (!keepAlive)
closeConnection();
}
}
//
//
// Private methods
//
private void openConnection() throws IOException {
// Make sure any existing connection is closed.
closeConnection();
Integer soLinger = getLingerTime();
socket = new Socket();
socket.setSoTimeout(getTimeout());
if (soLinger == null || soLinger < 0)// any null or negative will disable
// SO_Linger
socket.setSoLinger(false, 0);
else
socket.setSoLinger(true, soLinger);
socket.connect(new InetSocketAddress(ipParameters.getHost(), ipParameters.getPort()), getTimeout());
if (getePoll() != null)
transport = new EpollStreamTransport(socket.getInputStream(), socket.getOutputStream(), getePoll());
else
transport = new StreamTransport(socket.getInputStream(), socket.getOutputStream());
BaseMessageParser ipMessageParser;
WaitingRoomKeyFactory waitingRoomKeyFactory;
if (ipParameters.isEncapsulated()) {
ipMessageParser = new EncapMessageParser(true);
waitingRoomKeyFactory = new EncapWaitingRoomKeyFactory();
} else {
ipMessageParser = new XaMessageParser(true);
waitingRoomKeyFactory = new XaWaitingRoomKeyFactory();
}
conn = getMessageControl();
conn.start(transport, ipMessageParser, null, waitingRoomKeyFactory);
if (getePoll() == null)
((StreamTransport) transport).start("Modbus4J TcpMaster");
}
private void closeConnection() {
closeMessageControl(conn);
try {
if (socket != null)
socket.close();
} catch (IOException e) {
getExceptionHandler().receivedException(e);
}
conn = null;
socket = null;
}
/**
* <p>
* Getter for the field <code>lingerTime</code>.
* </p>
*
* @return an Integer.
*/
public Integer getLingerTime() {
return lingerTime;
}
}
@@ -1,204 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.tcp;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.base.BaseRequestHandler;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.ip.encap.EncapMessageParser;
import com.serotonin.modbus4j.ip.encap.EncapRequestHandler;
import com.serotonin.modbus4j.ip.xa.XaMessageParser;
import com.serotonin.modbus4j.ip.xa.XaRequestHandler;
import com.serotonin.modbus4j.sero.messaging.MessageControl;
import com.serotonin.modbus4j.sero.messaging.TestableTransport;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* <p>
* TcpSlave class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class TcpSlave extends ModbusSlaveSet {
final boolean encapsulated;
final ExecutorService executorService;
final List<TcpConnectionHandler> listConnections = new ArrayList<>();
// Configuration fields
private final int port;
// Runtime fields.
private ServerSocket serverSocket;
/**
* <p>
* Constructor for TcpSlave.
* </p>
*
* @param encapsulated a boolean.
*/
public TcpSlave(boolean encapsulated) {
this(ModbusUtils.TCP_PORT, encapsulated);
}
/**
* <p>
* Constructor for TcpSlave.
* </p>
*
* @param port a int.
* @param encapsulated a boolean.
*/
public TcpSlave(int port, boolean encapsulated) {
this.port = port;
this.encapsulated = encapsulated;
executorService = Executors.newCachedThreadPool();
}
@Override
public void start() throws ModbusInitException {
try {
serverSocket = new ServerSocket(port);
Socket socket;
while (true) {
socket = serverSocket.accept();
TcpConnectionHandler handler = new TcpConnectionHandler(socket);
executorService.execute(handler);
synchronized (listConnections) {
listConnections.add(handler);
}
}
} catch (IOException e) {
throw new ModbusInitException(e);
}
}
@Override
public void stop() {
// Close the socket first to prevent new messages.
try {
serverSocket.close();
} catch (IOException e) {
getExceptionHandler().receivedException(e);
}
// Close all open connections.
synchronized (listConnections) {
for (TcpConnectionHandler tch : listConnections)
tch.kill();
listConnections.clear();
}
// Now close the executor service.
executorService.shutdown();
try {
executorService.awaitTermination(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
getExceptionHandler().receivedException(e);
}
}
class TcpConnectionHandler implements Runnable {
private final Socket socket;
private TestableTransport transport;
private MessageControl conn;
TcpConnectionHandler(Socket socket) throws ModbusInitException {
this.socket = socket;
try {
transport = new TestableTransport(socket.getInputStream(), socket.getOutputStream());
} catch (IOException e) {
throw new ModbusInitException(e);
}
}
@Override
public void run() {
BaseMessageParser messageParser;
BaseRequestHandler requestHandler;
if (encapsulated) {
messageParser = new EncapMessageParser(false);
requestHandler = new EncapRequestHandler(TcpSlave.this);
} else {
messageParser = new XaMessageParser(false);
requestHandler = new XaRequestHandler(TcpSlave.this);
}
conn = new MessageControl();
conn.setExceptionHandler(getExceptionHandler());
try {
conn.start(transport, messageParser, requestHandler, null);
executorService.execute(transport);
} catch (IOException e) {
getExceptionHandler().receivedException(new ModbusInitException(e));
}
// Monitor the socket to detect when it gets closed.
while (true) {
try {
transport.testInputStream();
} catch (IOException e) {
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// no op
}
}
conn.close();
kill();
synchronized (listConnections) {
listConnections.remove(this);
}
}
void kill() {
try {
socket.close();
} catch (IOException e) {
getExceptionHandler().receivedException(new ModbusInitException(e));
}
}
}
}
@@ -1,195 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.udp;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpMessageResponse;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.ip.encap.EncapMessageParser;
import com.serotonin.modbus4j.ip.encap.EncapMessageRequest;
import com.serotonin.modbus4j.ip.xa.XaMessageParser;
import com.serotonin.modbus4j.ip.xa.XaMessageRequest;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
/**
* <p>
* UdpMaster class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class UdpMaster extends ModbusMaster {
private static final int MESSAGE_LENGTH = 1024;
private final IpParameters ipParameters;
private short nextTransactionId = 0;
// Runtime fields.
private BaseMessageParser messageParser;
private DatagramSocket socket;
/**
* <p>
* Constructor for UdpMaster.
* </p>
* <p>
* Default to not validating the slave id in responses
*
* @param params a {@link IpParameters} object.
*/
public UdpMaster(IpParameters params) {
this(params, false);
}
/**
* <p>
* Constructor for UdpMaster.
* </p>
*
* @param params
* @param validateResponse - confirm that requested slave id is the same in the
* response
*/
public UdpMaster(IpParameters params, boolean validateResponse) {
ipParameters = params;
this.validateResponse = validateResponse;
}
/**
* <p>
* Getter for the field <code>nextTransactionId</code>.
* </p>
*
* @return a short.
*/
protected short getNextTransactionId() {
return nextTransactionId++;
}
@Override
public void init() throws ModbusInitException {
if (ipParameters.isEncapsulated())
messageParser = new EncapMessageParser(true);
else
messageParser = new XaMessageParser(true);
try {
socket = new DatagramSocket();
socket.setSoTimeout(getTimeout());
} catch (SocketException e) {
throw new ModbusInitException(e);
}
initialized = true;
}
@Override
public void destroy() {
socket.close();
initialized = false;
}
@Override
public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
// Wrap the modbus request in an ip request.
OutgoingRequestMessage ipRequest;
if (ipParameters.isEncapsulated())
ipRequest = new EncapMessageRequest(request);
else
ipRequest = new XaMessageRequest(request, getNextTransactionId());
IpMessageResponse ipResponse;
try {
int attempts = getRetries() + 1;
while (true) {
// Send the request.
sendImpl(ipRequest);
if (!ipRequest.expectsResponse())
return null;
// Receive the response.
try {
ipResponse = receiveImpl();
} catch (SocketTimeoutException e) {
attempts--;
if (attempts > 0)
// Try again.
continue;
throw new ModbusTransportException(e, request.getSlaveId());
}
// We got the response
break;
}
return ipResponse.getModbusResponse();
} catch (IOException e) {
throw new ModbusTransportException(e, request.getSlaveId());
}
}
private void sendImpl(OutgoingRequestMessage request) throws IOException {
byte[] data = request.getMessageData();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName(ipParameters.getHost()),
ipParameters.getPort());
socket.send(packet);
}
private IpMessageResponse receiveImpl() throws IOException, ModbusTransportException {
DatagramPacket packet = new DatagramPacket(new byte[MESSAGE_LENGTH], MESSAGE_LENGTH);
socket.receive(packet);
// We could verify that the packet was received from the same address to which the
// request was sent,
// but let's not bother with that yet.
ByteQueue queue = new ByteQueue(packet.getData(), 0, packet.getLength());
IpMessageResponse response;
try {
response = (IpMessageResponse) messageParser.parseMessage(queue);
} catch (Exception e) {
throw new ModbusTransportException(e);
}
if (response == null)
throw new ModbusTransportException("Invalid response received");
return response;
}
}
@@ -1,169 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.udp;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.base.BaseRequestHandler;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.ip.encap.EncapMessageParser;
import com.serotonin.modbus4j.ip.encap.EncapRequestHandler;
import com.serotonin.modbus4j.ip.xa.XaMessageParser;
import com.serotonin.modbus4j.ip.xa.XaRequestHandler;
import com.serotonin.modbus4j.sero.messaging.IncomingMessage;
import com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* <p>
* UdpSlave class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class UdpSlave extends ModbusSlaveSet {
final BaseMessageParser messageParser;
final BaseRequestHandler requestHandler;
// Configuration fields
private final int port;
private final ExecutorService executorService;
// Runtime fields.
DatagramSocket datagramSocket;
/**
* <p>
* Constructor for UdpSlave.
* </p>
*
* @param encapsulated a boolean.
*/
public UdpSlave(boolean encapsulated) {
this(ModbusUtils.TCP_PORT, encapsulated);
}
/**
* <p>
* Constructor for UdpSlave.
* </p>
*
* @param port a int.
* @param encapsulated a boolean.
*/
public UdpSlave(int port, boolean encapsulated) {
this.port = port;
if (encapsulated) {
messageParser = new EncapMessageParser(false);
requestHandler = new EncapRequestHandler(this);
} else {
messageParser = new XaMessageParser(false);
requestHandler = new XaRequestHandler(this);
}
executorService = Executors.newCachedThreadPool();
}
@Override
public void start() throws ModbusInitException {
try {
datagramSocket = new DatagramSocket(port);
DatagramPacket datagramPacket;
while (true) {
datagramPacket = new DatagramPacket(new byte[1028], 1028);
datagramSocket.receive(datagramPacket);
UdpConnectionHandler handler = new UdpConnectionHandler(datagramPacket);
executorService.execute(handler);
}
} catch (IOException e) {
throw new ModbusInitException(e);
}
}
@Override
public void stop() {
// Close the socket first to prevent new messages.
datagramSocket.close();
// Close the executor service.
executorService.shutdown();
try {
executorService.awaitTermination(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
getExceptionHandler().receivedException(e);
}
}
// int getSlaveId() {
// return slaveId;
// }
//
// ProcessImage getProcessImage() {
// return processImage;
// }
class UdpConnectionHandler implements Runnable {
private final DatagramPacket requestPacket;
UdpConnectionHandler(DatagramPacket requestPacket) {
this.requestPacket = requestPacket;
}
public void run() {
try {
ByteQueue requestQueue = new ByteQueue(requestPacket.getData(), 0, requestPacket.getLength());
// Parse the request data and get the response.
IncomingMessage request = messageParser.parseMessage(requestQueue);
OutgoingResponseMessage response = requestHandler.handleRequest((IncomingRequestMessage) request);
if (response == null)
return;
// Create a response packet.
byte[] responseData = response.getMessageData();
DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length,
requestPacket.getAddress(), requestPacket.getPort());
// Send the response back.
datagramSocket.send(responsePacket);
} catch (Exception e) {
getExceptionHandler().receivedException(e);
}
}
}
}
@@ -1,95 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.xa;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.ip.IpMessage;
import com.serotonin.modbus4j.msg.ModbusMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* XaMessage class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class XaMessage extends IpMessage {
protected final int transactionId;
/**
* <p>
* Constructor for XaMessage.
* </p>
*
* @param modbusMessage a {@link ModbusMessage} object.
* @param transactionId a int.
*/
public XaMessage(ModbusMessage modbusMessage, int transactionId) {
super(modbusMessage);
this.transactionId = transactionId;
}
/**
* <p>
* getMessageData.
* </p>
*
* @return an array of {@link byte} objects.
*/
public byte[] getMessageData() {
ByteQueue msgQueue = new ByteQueue();
// Write the particular message.
modbusMessage.write(msgQueue);
// Create the XA message
ByteQueue xaQueue = new ByteQueue();
ModbusUtils.pushShort(xaQueue, transactionId);
ModbusUtils.pushShort(xaQueue, ModbusUtils.IP_PROTOCOL_ID);
ModbusUtils.pushShort(xaQueue, msgQueue.size());
xaQueue.push(msgQueue);
// Return the data.
return xaQueue.popAll();
}
/**
* <p>
* Getter for the field <code>transactionId</code>.
* </p>
*
* @return a int.
*/
public int getTransactionId() {
return transactionId;
}
@Override
public ModbusMessage getModbusMessage() {
return modbusMessage;
}
@Override
public String toString() {
return "XaMessage [transactionId=" + transactionId + ", message=" + modbusMessage + "]";
}
}
@@ -1,52 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.xa;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.sero.messaging.IncomingMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* XaMessageParser class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class XaMessageParser extends BaseMessageParser {
/**
* <p>
* Constructor for XaMessageParser.
* </p>
*
* @param master a boolean.
*/
public XaMessageParser(boolean master) {
super(master);
}
@Override
protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception {
if (master)
return XaMessageResponse.createXaMessageResponse(queue);
return XaMessageRequest.createXaMessageRequest(queue);
}
}
@@ -1,78 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.xa;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* XaMessageRequest class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class XaMessageRequest extends XaMessage implements OutgoingRequestMessage, IncomingRequestMessage {
/**
* <p>
* Constructor for XaMessageRequest.
* </p>
*
* @param modbusRequest a {@link ModbusRequest} object.
* @param transactionId a int.
*/
public XaMessageRequest(ModbusRequest modbusRequest, int transactionId) {
super(modbusRequest, transactionId);
}
static XaMessageRequest createXaMessageRequest(ByteQueue queue) throws ModbusTransportException {
// Remove the XA header
int transactionId = ModbusUtils.popShort(queue);
int protocolId = ModbusUtils.popShort(queue);
if (protocolId != ModbusUtils.IP_PROTOCOL_ID)
throw new ModbusTransportException("Unsupported IP protocol id: " + protocolId);
ModbusUtils.popShort(queue); // Length, which we don't care about.
// Create the modbus response.
ModbusRequest request = ModbusRequest.createModbusRequest(queue);
return new XaMessageRequest(request, transactionId);
}
@Override
public boolean expectsResponse() {
return modbusMessage.getSlaveId() != 0;
}
/**
* <p>
* getModbusRequest.
* </p>
*
* @return a {@link ModbusRequest} object.
*/
public ModbusRequest getModbusRequest() {
return (ModbusRequest) modbusMessage;
}
}
@@ -1,72 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.xa;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpMessageResponse;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* XaMessageResponse class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class XaMessageResponse extends XaMessage implements IpMessageResponse {
/**
* <p>
* Constructor for XaMessageResponse.
* </p>
*
* @param modbusResponse a {@link ModbusResponse} object.
* @param transactionId a int.
*/
public XaMessageResponse(ModbusResponse modbusResponse, int transactionId) {
super(modbusResponse, transactionId);
}
static XaMessageResponse createXaMessageResponse(ByteQueue queue) throws ModbusTransportException {
// Remove the XA header
int transactionId = ModbusUtils.popShort(queue);
int protocolId = ModbusUtils.popShort(queue);
if (protocolId != ModbusUtils.IP_PROTOCOL_ID)
throw new ModbusTransportException("Unsupported IP protocol id: " + protocolId);
ModbusUtils.popShort(queue); // Length, which we don't care about.
// Create the modbus response.
ModbusResponse response = ModbusResponse.createModbusResponse(queue);
return new XaMessageResponse(response, transactionId);
}
/**
* <p>
* getModbusResponse.
* </p>
*
* @return a {@link ModbusResponse} object.
*/
public ModbusResponse getModbusResponse() {
return (ModbusResponse) modbusMessage;
}
}
@@ -1,57 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.xa;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.base.BaseRequestHandler;
import com.serotonin.modbus4j.msg.ModbusRequest;
import com.serotonin.modbus4j.msg.ModbusResponse;
import com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
/**
* <p>
* XaRequestHandler class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class XaRequestHandler extends BaseRequestHandler {
/**
* <p>
* Constructor for XaRequestHandler.
* </p>
*
* @param slave a {@link ModbusSlaveSet} object.
*/
public XaRequestHandler(ModbusSlaveSet slave) {
super(slave);
}
public OutgoingResponseMessage handleRequest(IncomingRequestMessage req) throws Exception {
XaMessageRequest tcpRequest = (XaMessageRequest) req;
ModbusRequest request = tcpRequest.getModbusRequest();
ModbusResponse response = handleRequestImpl(request);
if (response == null)
return null;
return new XaMessageResponse(response, tcpRequest.transactionId);
}
}
@@ -1,103 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.ip.xa;
import com.serotonin.modbus4j.msg.ModbusMessage;
import com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
import com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
import com.serotonin.modbus4j.sero.messaging.WaitingRoomKey;
import com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
/**
* <p>
* XaWaitingRoomKeyFactory class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class XaWaitingRoomKeyFactory implements WaitingRoomKeyFactory {
@Override
public WaitingRoomKey createWaitingRoomKey(OutgoingRequestMessage request) {
return createWaitingRoomKey((XaMessage) request);
}
@Override
public WaitingRoomKey createWaitingRoomKey(IncomingResponseMessage response) {
return createWaitingRoomKey((XaMessage) response);
}
/**
* <p>
* createWaitingRoomKey.
* </p>
*
* @param msg a {@link XaMessage} object.
* @return a {@link WaitingRoomKey} object.
*/
public WaitingRoomKey createWaitingRoomKey(XaMessage msg) {
return new XaWaitingRoomKey(msg.getTransactionId(), msg.getModbusMessage());
}
class XaWaitingRoomKey implements WaitingRoomKey {
private final int transactionId;
private final int slaveId;
private final byte functionCode;
public XaWaitingRoomKey(int transactionId, ModbusMessage msg) {
this.transactionId = transactionId;
this.slaveId = msg.getSlaveId();
this.functionCode = msg.getFunctionCode();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + functionCode;
result = prime * result + slaveId;
result = prime * result + transactionId;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
XaWaitingRoomKey other = (XaWaitingRoomKey) obj;
if (functionCode != other.functionCode)
return false;
if (slaveId != other.slaveId)
return false;
if (transactionId != other.transactionId)
return false;
return true;
}
}
}
@@ -1,346 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.locator;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.base.RangeAndOffset;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.ModbusIdException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import java.nio.charset.Charset;
/**
* <p>
* Abstract BaseLocator class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public abstract class BaseLocator<T> {
//
//
// Factory methods
//
protected final int range;
protected final int offset;
private final int slaveId;
/**
* <p>
* Constructor for BaseLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
*/
public BaseLocator(int slaveId, int range, int offset) {
this.slaveId = slaveId;
this.range = range;
this.offset = offset;
}
/**
* <p>
* coilStatus.
* </p>
*
* @param slaveId a int.
* @param offset a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<Boolean> coilStatus(int slaveId, int offset) {
return new BinaryLocator(slaveId, RegisterRange.COIL_STATUS, offset);
}
/**
* <p>
* inputStatus.
* </p>
*
* @param slaveId a int.
* @param offset a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<Boolean> inputStatus(int slaveId, int offset) {
return new BinaryLocator(slaveId, RegisterRange.INPUT_STATUS, offset);
}
/**
* <p>
* inputRegister.
* </p>
*
* @param slaveId a int.
* @param offset a int.
* @param dataType a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<Number> inputRegister(int slaveId, int offset, int dataType) {
return new NumericLocator(slaveId, RegisterRange.INPUT_REGISTER, offset, dataType);
}
/**
* <p>
* inputRegisterBit.
* </p>
*
* @param slaveId a int.
* @param offset a int.
* @param bit a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<Boolean> inputRegisterBit(int slaveId, int offset, int bit) {
return new BinaryLocator(slaveId, RegisterRange.INPUT_REGISTER, offset, bit);
}
/**
* <p>
* holdingRegister.
* </p>
*
* @param slaveId a int.
* @param offset a int.
* @param dataType a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<Number> holdingRegister(int slaveId, int offset, int dataType) {
return new NumericLocator(slaveId, RegisterRange.HOLDING_REGISTER, offset, dataType);
}
/**
* <p>
* holdingRegisterBit.
* </p>
*
* @param slaveId a int.
* @param offset a int.
* @param bit a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<Boolean> holdingRegisterBit(int slaveId, int offset, int bit) {
return new BinaryLocator(slaveId, RegisterRange.HOLDING_REGISTER, offset, bit);
}
/**
* <p>
* createLocator.
* </p>
*
* @param slaveId a int.
* @param registerId a int.
* @param dataType a int.
* @param bit a int.
* @param registerCount a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<?> createLocator(int slaveId, int registerId, int dataType, int bit, int registerCount) {
RangeAndOffset rao = new RangeAndOffset(registerId);
return createLocator(slaveId, rao.getRange(), rao.getOffset(), dataType, bit, registerCount,
StringLocator.ASCII);
}
/**
* <p>
* createLocator.
* </p>
*
* @param slaveId a int.
* @param registerId a int.
* @param dataType a int.
* @param bit a int.
* @param registerCount a int.
* @param charset a {@link Charset} object.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<?> createLocator(int slaveId, int registerId, int dataType, int bit, int registerCount,
Charset charset) {
RangeAndOffset rao = new RangeAndOffset(registerId);
return createLocator(slaveId, rao.getRange(), rao.getOffset(), dataType, bit, registerCount, charset);
}
/**
* <p>
* createLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param bit a int.
* @param registerCount a int.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<?> createLocator(int slaveId, int range, int offset, int dataType, int bit,
int registerCount) {
return createLocator(slaveId, range, offset, dataType, bit, registerCount, StringLocator.ASCII);
}
/**
* <p>
* createLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param bit a int.
* @param registerCount a int.
* @param charset a {@link Charset} object.
* @return a {@link BaseLocator} object.
*/
public static BaseLocator<?> createLocator(int slaveId, int range, int offset, int dataType, int bit,
int registerCount, Charset charset) {
if (dataType == DataType.BINARY) {
if (BinaryLocator.isBinaryRange(range))
return new BinaryLocator(slaveId, range, offset);
return new BinaryLocator(slaveId, range, offset, bit);
}
if (dataType == DataType.CHAR || dataType == DataType.VARCHAR)
return new StringLocator(slaveId, range, offset, dataType, registerCount, charset);
return new NumericLocator(slaveId, range, offset, dataType);
}
/**
* <p>
* validate.
* </p>
*
* @param registerCount a int.
*/
protected void validate(int registerCount) {
try {
ModbusUtils.validateOffset(offset);
ModbusUtils.validateEndOffset(offset + registerCount - 1);
} catch (ModbusTransportException e) {
throw new ModbusIdException(e);
}
}
/**
* <p>
* getDataType.
* </p>
*
* @return a int.
*/
abstract public int getDataType();
/**
* <p>
* getRegisterCount.
* </p>
*
* @return a int.
*/
abstract public int getRegisterCount();
/**
* <p>
* Getter for the field <code>slaveId</code>.
* </p>
*
* @return a int.
*/
public int getSlaveId() {
return slaveId;
}
/**
* <p>
* Getter for the field <code>range</code>.
* </p>
*
* @return a int.
*/
public int getRange() {
return range;
}
/**
* <p>
* Getter for the field <code>offset</code>.
* </p>
*
* @return a int.
*/
public int getOffset() {
return offset;
}
// public SlaveAndRange getSlaveAndRange() {
// return slaveAndRange;
// }
/**
* <p>
* getEndOffset.
* </p>
*
* @return a int.
*/
public int getEndOffset() {
return offset + getRegisterCount() - 1;
}
/**
* <p>
* bytesToValue.
* </p>
*
* @param data an array of {@link byte} objects.
* @param requestOffset a int.
* @return a T object.
*/
public T bytesToValue(byte[] data, int requestOffset) {
// Determined the offset normalized to the response data.
return bytesToValueRealOffset(data, offset - requestOffset);
}
/**
* <p>
* bytesToValueRealOffset.
* </p>
*
* @param data an array of {@link byte} objects.
* @param offset a int.
* @return a T object.
*/
abstract public T bytesToValueRealOffset(byte[] data, int offset);
/**
* <p>
* valueToShorts.
* </p>
*
* @param value a T object.
* @return an array of {@link short} objects.
*/
abstract public short[] valueToShorts(T value);
}
@@ -1,143 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.locator;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.ModbusIdException;
import com.serotonin.modbus4j.sero.NotImplementedException;
/**
* <p>
* BinaryLocator class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class BinaryLocator extends BaseLocator<Boolean> {
private int bit = -1;
/**
* <p>
* Constructor for BinaryLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
*/
public BinaryLocator(int slaveId, int range, int offset) {
super(slaveId, range, offset);
if (!isBinaryRange(range))
throw new ModbusIdException("Non-bit requests can only be made from coil status and input status ranges");
validate();
}
/**
* <p>
* Constructor for BinaryLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param bit a int.
*/
public BinaryLocator(int slaveId, int range, int offset, int bit) {
super(slaveId, range, offset);
if (isBinaryRange(range))
throw new ModbusIdException("Bit requests can only be made from holding registers and input registers");
this.bit = bit;
validate();
}
/**
* <p>
* isBinaryRange.
* </p>
*
* @param range a int.
* @return a boolean.
*/
public static boolean isBinaryRange(int range) {
return range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS;
}
/**
* <p>
* validate.
* </p>
*/
protected void validate() {
super.validate(1);
if (!isBinaryRange(range))
ModbusUtils.validateBit(bit);
}
/**
* <p>
* Getter for the field <code>bit</code>.
* </p>
*
* @return a int.
*/
public int getBit() {
return bit;
}
@Override
public int getDataType() {
return DataType.BINARY;
}
@Override
public int getRegisterCount() {
return 1;
}
@Override
public String toString() {
return "BinaryLocator(slaveId=" + getSlaveId() + ", range=" + range + ", offset=" + offset + ", bit=" + bit
+ ")";
}
@Override
public Boolean bytesToValueRealOffset(byte[] data, int offset) {
// If this is a coil or input, convert to boolean.
if (range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS)
return (((data[offset / 8] & 0xff) >> (offset % 8)) & 0x1) == 1;
// For the rest of the types, we double the normalized offset to account for short
// to byte.
offset *= 2;
// We could still be asking for a binary if it's a bit in a register.
return (((data[offset + 1 - bit / 8] & 0xff) >> (bit % 8)) & 0x1) == 1;
}
@Override
public short[] valueToShorts(Boolean value) {
throw new NotImplementedException();
}
}
@@ -1,488 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.locator;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.IllegalDataTypeException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
/**
* <p>
* NumericLocator class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class NumericLocator extends BaseLocator<Number> {
private static final int[] DATA_TYPES = { //
DataType.TWO_BYTE_INT_UNSIGNED, //
DataType.TWO_BYTE_INT_SIGNED, //
DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED, //
DataType.TWO_BYTE_INT_SIGNED_SWAPPED, //
DataType.FOUR_BYTE_INT_UNSIGNED, //
DataType.FOUR_BYTE_INT_SIGNED, //
DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED, //
DataType.FOUR_BYTE_INT_SIGNED_SWAPPED, //
DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED, //
DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED, //
DataType.FOUR_BYTE_FLOAT, //
DataType.FOUR_BYTE_FLOAT_SWAPPED, //
DataType.EIGHT_BYTE_INT_UNSIGNED, //
DataType.EIGHT_BYTE_INT_SIGNED, //
DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED, //
DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED, //
DataType.EIGHT_BYTE_FLOAT, //
DataType.EIGHT_BYTE_FLOAT_SWAPPED, //
DataType.TWO_BYTE_BCD, //
DataType.FOUR_BYTE_BCD, //
DataType.FOUR_BYTE_BCD_SWAPPED, //
DataType.FOUR_BYTE_MOD_10K, //
DataType.FOUR_BYTE_MOD_10K_SWAPPED, //
DataType.SIX_BYTE_MOD_10K, DataType.SIX_BYTE_MOD_10K_SWAPPED, DataType.EIGHT_BYTE_MOD_10K, //
DataType.EIGHT_BYTE_MOD_10K_SWAPPED, //
DataType.ONE_BYTE_INT_UNSIGNED_LOWER, //
DataType.ONE_BYTE_INT_UNSIGNED_UPPER};
private final int dataType;
private RoundingMode roundingMode = RoundingMode.HALF_UP;
/**
* <p>
* Constructor for NumericLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param dataType a int.
*/
public NumericLocator(int slaveId, int range, int offset, int dataType) {
super(slaveId, range, offset);
this.dataType = dataType;
validate();
}
private static void appendBCD(StringBuilder sb, byte b) {
sb.append(bcdNibbleToInt(b, true));
sb.append(bcdNibbleToInt(b, false));
}
private static int bcdNibbleToInt(byte b, boolean high) {
int n;
if (high)
n = (b >> 4) & 0xf;
else
n = b & 0xf;
if (n > 9)
n = 0;
return n;
}
private void validate() {
super.validate(getRegisterCount());
if (range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS)
throw new IllegalDataTypeException("Only binary values can be read from Coil and Input ranges");
boolean b = Arrays.stream(DATA_TYPES).anyMatch(dt -> dt == dataType);
if (!b)
throw new IllegalDataTypeException("Invalid data type");
}
@Override
public int getDataType() {
return dataType;
}
/**
* <p>
* Getter for the field <code>roundingMode</code>.
* </p>
*
* @return a {@link RoundingMode} object.
*/
public RoundingMode getRoundingMode() {
return roundingMode;
}
/**
* <p>
* Setter for the field <code>roundingMode</code>.
* </p>
*
* @param roundingMode a {@link RoundingMode} object.
*/
public void setRoundingMode(RoundingMode roundingMode) {
this.roundingMode = roundingMode;
}
@Override
public String toString() {
return "NumericLocator(slaveId=" + getSlaveId() + ", range=" + range + ", offset=" + offset + ", dataType="
+ dataType + ")";
}
@Override
public int getRegisterCount() {
switch (dataType) {
case DataType.TWO_BYTE_INT_UNSIGNED:
case DataType.TWO_BYTE_INT_SIGNED:
case DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED:
case DataType.TWO_BYTE_INT_SIGNED_SWAPPED:
case DataType.TWO_BYTE_BCD:
case DataType.ONE_BYTE_INT_UNSIGNED_LOWER:
case DataType.ONE_BYTE_INT_UNSIGNED_UPPER:
return 1;
case DataType.FOUR_BYTE_INT_UNSIGNED:
case DataType.FOUR_BYTE_INT_SIGNED:
case DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED:
case DataType.FOUR_BYTE_INT_SIGNED_SWAPPED:
case DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED:
case DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED:
case DataType.FOUR_BYTE_FLOAT:
case DataType.FOUR_BYTE_FLOAT_SWAPPED:
case DataType.FOUR_BYTE_BCD:
case DataType.FOUR_BYTE_BCD_SWAPPED:
case DataType.FOUR_BYTE_MOD_10K:
case DataType.FOUR_BYTE_MOD_10K_SWAPPED:
return 2;
case DataType.SIX_BYTE_MOD_10K:
case DataType.SIX_BYTE_MOD_10K_SWAPPED:
return 3;
case DataType.EIGHT_BYTE_INT_UNSIGNED:
case DataType.EIGHT_BYTE_INT_SIGNED:
case DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED:
case DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED:
case DataType.EIGHT_BYTE_FLOAT:
case DataType.EIGHT_BYTE_FLOAT_SWAPPED:
case DataType.EIGHT_BYTE_MOD_10K:
case DataType.EIGHT_BYTE_MOD_10K_SWAPPED:
return 4;
}
throw new RuntimeException("Unsupported data type: " + dataType);
}
@Override
public Number bytesToValueRealOffset(byte[] data, int offset) {
offset *= 2;
// 2 bytes
if (dataType == DataType.TWO_BYTE_INT_UNSIGNED)
return ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff);
if (dataType == DataType.TWO_BYTE_INT_SIGNED)
return (short) (((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff));
if (dataType == DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED)
return ((data[offset + 1] & 0xff) << 8) | (data[offset] & 0xff);
if (dataType == DataType.TWO_BYTE_INT_SIGNED_SWAPPED)
return (short) (((data[offset + 1] & 0xff) << 8) | (data[offset] & 0xff));
if (dataType == DataType.TWO_BYTE_BCD) {
StringBuilder sb = new StringBuilder();
appendBCD(sb, data[offset]);
appendBCD(sb, data[offset + 1]);
return Short.parseShort(sb.toString());
}
// 1 byte
if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_LOWER)
return data[offset + 1] & 0xff;
if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_UPPER)
return data[offset] & 0xff;
// 4 bytes
if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED)
return (long) ((data[offset] & 0xff)) << 24 | ((long) ((data[offset + 1] & 0xff)) << 16)
| ((long) ((data[offset + 2] & 0xff)) << 8) | ((data[offset + 3] & 0xff));
if (dataType == DataType.FOUR_BYTE_INT_SIGNED)
return ((data[offset] & 0xff) << 24) | ((data[offset + 1] & 0xff) << 16) | ((data[offset + 2] & 0xff) << 8)
| (data[offset + 3] & 0xff);
if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED)
return ((long) ((data[offset + 2] & 0xff)) << 24) | ((long) ((data[offset + 3] & 0xff)) << 16)
| ((long) ((data[offset] & 0xff)) << 8) | ((data[offset + 1] & 0xff));
if (dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED)
return ((data[offset + 2] & 0xff) << 24) | ((data[offset + 3] & 0xff) << 16) | ((data[offset] & 0xff) << 8)
| (data[offset + 1] & 0xff);
if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED)
return ((long) ((data[offset + 3] & 0xff)) << 24) | (((data[offset + 2] & 0xff) << 16))
| ((long) ((data[offset + 1] & 0xff)) << 8) | (data[offset] & 0xff);
if (dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED)
return ((data[offset + 3] & 0xff) << 24) | ((data[offset + 2] & 0xff) << 16)
| ((data[offset + 1] & 0xff) << 8) | ((data[offset] & 0xff));
if (dataType == DataType.FOUR_BYTE_FLOAT)
return Float.intBitsToFloat(((data[offset] & 0xff) << 24) | ((data[offset + 1] & 0xff) << 16)
| ((data[offset + 2] & 0xff) << 8) | (data[offset + 3] & 0xff));
if (dataType == DataType.FOUR_BYTE_FLOAT_SWAPPED)
return Float.intBitsToFloat(((data[offset + 2] & 0xff) << 24) | ((data[offset + 3] & 0xff) << 16)
| ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff));
if (dataType == DataType.FOUR_BYTE_BCD) {
StringBuilder sb = new StringBuilder();
appendBCD(sb, data[offset]);
appendBCD(sb, data[offset + 1]);
appendBCD(sb, data[offset + 2]);
appendBCD(sb, data[offset + 3]);
return Integer.parseInt(sb.toString());
}
if (dataType == DataType.FOUR_BYTE_BCD_SWAPPED) {
StringBuilder sb = new StringBuilder();
appendBCD(sb, data[offset + 2]);
appendBCD(sb, data[offset + 3]);
appendBCD(sb, data[offset]);
appendBCD(sb, data[offset + 1]);
return Integer.parseInt(sb.toString());
}
// MOD10K types
if (dataType == DataType.FOUR_BYTE_MOD_10K_SWAPPED)
return BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff)))
.multiply(BigInteger.valueOf(10000L))
.add(BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))));
if (dataType == DataType.FOUR_BYTE_MOD_10K)
return BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff)))
.multiply(BigInteger.valueOf(10000L))
.add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))));
if (dataType == DataType.SIX_BYTE_MOD_10K_SWAPPED)
return BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff)))
.multiply(BigInteger.valueOf(100000000L))
.add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff)))
.multiply(BigInteger.valueOf(10000L)))
.add(BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))));
if (dataType == DataType.SIX_BYTE_MOD_10K)
return BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff)))
.multiply(BigInteger.valueOf(100000000L))
.add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff)))
.multiply(BigInteger.valueOf(10000L)))
.add(BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff))));
if (dataType == DataType.EIGHT_BYTE_MOD_10K_SWAPPED)
return BigInteger.valueOf((((data[offset + 6] & 0xff) << 8) + (data[offset + 7] & 0xff)))
.multiply(BigInteger.valueOf(1000000000000L))
.add(BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff)))
.multiply(BigInteger.valueOf(100000000L)))
.add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff)))
.multiply(BigInteger.valueOf(10000L)))
.add(BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))));
if (dataType == DataType.EIGHT_BYTE_MOD_10K)
return BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff)))
.multiply(BigInteger.valueOf(1000000000000L))
.add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff)))
.multiply(BigInteger.valueOf(100000000L)))
.add(BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff)))
.multiply(BigInteger.valueOf(10000L)))
.add(BigInteger.valueOf((((data[offset + 6] & 0xff) << 8) + (data[offset + 7] & 0xff))));
// 8 bytes
if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED) {
byte[] b9 = new byte[9];
System.arraycopy(data, offset, b9, 1, 8);
return new BigInteger(b9);
}
if (dataType == DataType.EIGHT_BYTE_INT_SIGNED)
return ((long) ((data[offset] & 0xff)) << 56) | ((long) ((data[offset + 1] & 0xff)) << 48)
| ((long) ((data[offset + 2] & 0xff)) << 40) | ((long) ((data[offset + 3] & 0xff)) << 32)
| ((long) ((data[offset + 4] & 0xff)) << 24) | ((long) ((data[offset + 5] & 0xff)) << 16)
| ((long) ((data[offset + 6] & 0xff)) << 8) | ((data[offset + 7] & 0xff));
if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED) {
byte[] b9 = new byte[9];
b9[1] = data[offset + 6];
b9[2] = data[offset + 7];
b9[3] = data[offset + 4];
b9[4] = data[offset + 5];
b9[5] = data[offset + 2];
b9[6] = data[offset + 3];
b9[7] = data[offset];
b9[8] = data[offset + 1];
return new BigInteger(b9);
}
if (dataType == DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED)
return ((long) ((data[offset + 6] & 0xff)) << 56) | ((long) ((data[offset + 7] & 0xff)) << 48)
| ((long) ((data[offset + 4] & 0xff)) << 40) | ((long) ((data[offset + 5] & 0xff)) << 32)
| ((long) ((data[offset + 2] & 0xff)) << 24) | ((long) ((data[offset + 3] & 0xff)) << 16)
| ((long) ((data[offset] & 0xff)) << 8) | ((data[offset + 1] & 0xff));
if (dataType == DataType.EIGHT_BYTE_FLOAT)
return Double
.longBitsToDouble(((long) ((data[offset] & 0xff)) << 56) | ((long) ((data[offset + 1] & 0xff)) << 48)
| ((long) ((data[offset + 2] & 0xff)) << 40) | ((long) ((data[offset + 3] & 0xff)) << 32)
| ((long) ((data[offset + 4] & 0xff)) << 24) | ((long) ((data[offset + 5] & 0xff)) << 16)
| ((long) ((data[offset + 6] & 0xff)) << 8) | ((data[offset + 7] & 0xff)));
if (dataType == DataType.EIGHT_BYTE_FLOAT_SWAPPED)
return Double.longBitsToDouble(
((long) ((data[offset + 6] & 0xff)) << 56) | ((long) ((data[offset + 7] & 0xff)) << 48)
| ((long) ((data[offset + 4] & 0xff)) << 40) | ((long) ((data[offset + 5] & 0xff)) << 32)
| ((long) ((data[offset + 2] & 0xff)) << 24) | ((long) ((data[offset + 3] & 0xff)) << 16)
| ((long) ((data[offset] & 0xff)) << 8) | ((data[offset + 1] & 0xff)));
throw new RuntimeException("Unsupported data type: " + dataType);
}
@Override
public short[] valueToShorts(Number value) {
// 2 bytes
if (dataType == DataType.TWO_BYTE_INT_UNSIGNED || dataType == DataType.TWO_BYTE_INT_SIGNED)
return new short[]{toShort(value)};
if (dataType == DataType.TWO_BYTE_INT_SIGNED_SWAPPED || dataType == DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED) {
short sval = toShort(value);
// 0x1100
return new short[]{(short) (((sval & 0xFF00) >> 8) | ((sval & 0x00FF) << 8))};
}
if (dataType == DataType.TWO_BYTE_BCD) {
short s = toShort(value);
return new short[]{
(short) ((((s / 1000) % 10) << 12) | (((s / 100) % 10) << 8) | (((s / 10) % 10) << 4) | (s % 10))};
}
if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_LOWER) {
return new short[]{(short) (toShort(value) & 0x00FF)};
}
if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_UPPER) {
return new short[]{(short) ((toShort(value) << 8) & 0xFF00)};
}
// 4 bytes
if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED || dataType == DataType.FOUR_BYTE_INT_SIGNED) {
int i = toInt(value);
return new short[]{(short) (i >> 16), (short) i};
}
if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED || dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED) {
int i = toInt(value);
return new short[]{(short) i, (short) (i >> 16)};
}
if (dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED
|| dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED) {
int i = toInt(value);
short topWord = (short) (((i & 0xFF) << 8) | ((i >> 8) & 0xFF));
short bottomWord = (short) (((i >> 24) & 0x000000FF) | ((i >> 8) & 0x0000FF00));
return new short[]{topWord, bottomWord};
}
if (dataType == DataType.FOUR_BYTE_FLOAT) {
int i = Float.floatToIntBits(value.floatValue());
return new short[]{(short) (i >> 16), (short) i};
}
if (dataType == DataType.FOUR_BYTE_FLOAT_SWAPPED) {
int i = Float.floatToIntBits(value.floatValue());
return new short[]{(short) i, (short) (i >> 16)};
}
if (dataType == DataType.FOUR_BYTE_BCD) {
int i = toInt(value);
return new short[]{
(short) ((((i / 10000000) % 10) << 12) | (((i / 1000000) % 10) << 8) | (((i / 100000) % 10) << 4)
| ((i / 10000) % 10)),
(short) ((((i / 1000) % 10) << 12) | (((i / 100) % 10) << 8) | (((i / 10) % 10) << 4) | (i % 10))};
}
// MOD10K
if (dataType == DataType.FOUR_BYTE_MOD_10K) {
long l = value.longValue();
return new short[]{(short) ((l / 10000) % 10000), (short) (l % 10000)};
}
if (dataType == DataType.FOUR_BYTE_MOD_10K_SWAPPED) {
long l = value.longValue();
return new short[]{(short) (l % 10000), (short) ((l / 10000) % 10000)};
}
if (dataType == DataType.SIX_BYTE_MOD_10K) {
long l = value.longValue();
return new short[]{(short) ((l / 100000000L) % 10000), (short) ((l / 10000) % 10000),
(short) (l % 10000)};
}
if (dataType == DataType.SIX_BYTE_MOD_10K_SWAPPED) {
long l = value.longValue();
return new short[]{(short) (l % 10000), (short) ((l / 10000) % 10000),
(short) ((l / 100000000L) % 10000)};
}
if (dataType == DataType.EIGHT_BYTE_MOD_10K) {
long l = value.longValue();
return new short[]{(short) ((l / 1000000000000L) % 10000), (short) ((l / 100000000L) % 10000),
(short) ((l / 10000) % 10000), (short) (l % 10000)};
}
if (dataType == DataType.EIGHT_BYTE_MOD_10K_SWAPPED) {
long l = value.longValue();
return new short[]{(short) (l % 10000), (short) ((l / 10000) % 10000), (short) ((l / 100000000L) % 10000),
(short) ((l / 1000000000000L) % 10000)};
}
// 8 bytes
if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED || dataType == DataType.EIGHT_BYTE_INT_SIGNED) {
long l = value.longValue();
return new short[]{(short) (l >> 48), (short) (l >> 32), (short) (l >> 16), (short) l};
}
if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED
|| dataType == DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED) {
long l = value.longValue();
return new short[]{(short) l, (short) (l >> 16), (short) (l >> 32), (short) (l >> 48)};
}
if (dataType == DataType.EIGHT_BYTE_FLOAT) {
long l = Double.doubleToLongBits(value.doubleValue());
return new short[]{(short) (l >> 48), (short) (l >> 32), (short) (l >> 16), (short) l};
}
if (dataType == DataType.EIGHT_BYTE_FLOAT_SWAPPED) {
long l = Double.doubleToLongBits(value.doubleValue());
return new short[]{(short) l, (short) (l >> 16), (short) (l >> 32), (short) (l >> 48)};
}
throw new RuntimeException("Unsupported data type: " + dataType);
}
private short toShort(Number value) {
return (short) toInt(value);
}
private int toInt(Number value) {
if (value instanceof Double)
return new BigDecimal(value.doubleValue()).setScale(0, roundingMode).intValue();
if (value instanceof Float)
return new BigDecimal(value.floatValue()).setScale(0, roundingMode).intValue();
if (value instanceof BigDecimal)
return ((BigDecimal) value).setScale(0, roundingMode).intValue();
return value.intValue();
}
}
@@ -1,217 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.locator;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.IllegalDataTypeException;
import java.nio.charset.Charset;
/**
* <p>
* StringLocator class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class StringLocator extends BaseLocator<String> {
/**
* Constant <code>ASCII</code>
*/
public static final Charset ASCII = Charset.forName("ASCII");
private final int dataType;
private final int registerCount;
private final Charset charset;
/**
* <p>
* Constructor for StringLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param registerCount a int.
*/
public StringLocator(int slaveId, int range, int offset, int dataType, int registerCount) {
this(slaveId, range, offset, dataType, registerCount, ASCII);
}
/**
* <p>
* Constructor for StringLocator.
* </p>
*
* @param slaveId a int.
* @param range a int.
* @param offset a int.
* @param dataType a int.
* @param registerCount a int.
* @param charset a {@link Charset} object.
*/
public StringLocator(int slaveId, int range, int offset, int dataType, int registerCount, Charset charset) {
super(slaveId, range, offset);
this.dataType = dataType;
this.registerCount = registerCount;
this.charset = charset;
validate();
}
private void validate() {
super.validate(registerCount);
if (range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS)
throw new IllegalDataTypeException("Only binary values can be read from Coil and Input ranges");
if (dataType != DataType.CHAR && dataType != DataType.VARCHAR)
throw new IllegalDataTypeException("Invalid data type");
}
@Override
public int getDataType() {
return dataType;
}
@Override
public int getRegisterCount() {
return registerCount;
}
@Override
public String toString() {
return "StringLocator(slaveId=" + getSlaveId() + ", range=" + range + ", offset=" + offset + ", dataType="
+ dataType + ", registerCount=" + registerCount + ", charset=" + charset + ")";
}
@Override
public String bytesToValueRealOffset(byte[] data, int offset) {
offset *= 2;
int length = registerCount * 2;
if (dataType == DataType.CHAR)
return new String(data, offset, length, charset);
if (dataType == DataType.VARCHAR) {
int nullPos = -1;
for (int i = offset; i < offset + length; i++) {
if (data[i] == 0) {
nullPos = i;
break;
}
}
if (nullPos == -1)
return new String(data, offset, length, charset);
return new String(data, offset, nullPos, charset);
}
throw new RuntimeException("Unsupported data type: " + dataType);
}
@Override
public short[] valueToShorts(String value) {
short[] result = new short[registerCount];
int resultByteLen = registerCount * 2;
int length;
if (value != null) {
byte[] bytes = value.getBytes(charset);
length = resultByteLen;
if (length > bytes.length)
length = bytes.length;
for (int i = 0; i < length; i++)
setByte(result, i, bytes[i] & 0xff);
} else
length = 0;
if (dataType == DataType.CHAR) {
// Pad the rest with spaces
for (int i = length; i < resultByteLen; i++)
setByte(result, i, 0x20);
} else if (dataType == DataType.VARCHAR) {
if (length >= resultByteLen)
// Ensure the last byte is a null terminator.
result[registerCount - 1] &= 0xff00;
else {
// Pad the rest with null.
for (int i = length; i < resultByteLen; i++)
setByte(result, i, 0);
}
} else
throw new RuntimeException("Unsupported data type: " + dataType);
return result;
}
private void setByte(short[] s, int byteIndex, int value) {
if (byteIndex % 2 == 0)
s[byteIndex / 2] |= value << 8;
else
s[byteIndex / 2] |= value;
}
//
// public static void main(String[] args) {
// StringLocator l1 = new StringLocator(1, RegisterRange.HOLDING_REGISTER, 0,
// DataType.CHAR, 4);
// StringLocator l2 = new StringLocator(1, RegisterRange.HOLDING_REGISTER, 0,
// DataType.VARCHAR, 4);
//
// short[] s;
//
// s = l1.valueToShorts("abcdefg");
// System.out.println(new String(l1.bytesToValue(toBytes(s), 0)));
//
// s = l1.valueToShorts("abcdefgh");
// System.out.println(new String(l1.bytesToValue(toBytes(s), 0)));
//
// s = l1.valueToShorts("abcdefghi");
// System.out.println(new String(l1.bytesToValue(toBytes(s), 0)));
//
// s = l2.valueToShorts("abcdef");
// System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
//
// s = l2.valueToShorts("abcdefg");
// System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
//
// s = l2.valueToShorts("abcdefgh");
// System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
//
// s = l2.valueToShorts("abcdefghi");
// System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
// }
//
// private static byte[] toBytes(short[] s) {
// byte[] b = new byte[s.length * 2];
// for (int i = 0; i < s.length; i++) {
// b[i * 2] = (byte) ((s[i] >> 8) & 0xff);
// b[i * 2 + 1] = (byte) (s[i] & 0xff);
// }
// return b;
// }
}
@@ -1,97 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.msg;
import com.serotonin.modbus4j.Modbus;
import com.serotonin.modbus4j.ProcessImage;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.sero.ShouldNeverHappenException;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* ExceptionRequest class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ExceptionRequest extends ModbusRequest {
private final byte functionCode;
private final byte exceptionCode;
/**
* <p>
* Constructor for ExceptionRequest.
* </p>
*
* @param slaveId a int.
* @param functionCode a byte.
* @param exceptionCode a byte.
* @throws ModbusTransportException if any.
*/
public ExceptionRequest(int slaveId, byte functionCode, byte exceptionCode) throws ModbusTransportException {
super(slaveId);
this.functionCode = functionCode;
this.exceptionCode = exceptionCode;
}
@Override
public void validate(Modbus modbus) {
// no op
}
@Override
protected void writeRequest(ByteQueue queue) {
throw new ShouldNeverHappenException("wha");
}
@Override
protected void readRequest(ByteQueue queue) {
queue.clear();
}
@Override
ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
return new ExceptionResponse(slaveId, functionCode, exceptionCode);
}
@Override
ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
return getResponseInstance(slaveId);
}
@Override
public byte getFunctionCode() {
return functionCode;
}
/**
* <p>
* Getter for the field <code>exceptionCode</code>.
* </p>
*
* @return a byte.
*/
public byte getExceptionCode() {
return exceptionCode;
}
}
@@ -1,66 +0,0 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.msg;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;
/**
* <p>
* ExceptionResponse class.
* </p>
*
* @author Matthew Lohbihler
* @version 2025.9.0
* @since 2016.10.1
*/
public class ExceptionResponse extends ModbusResponse {
private final byte functionCode;
/**
* <p>
* Constructor for ExceptionResponse.
* </p>
*
* @param slaveId a int.
* @param functionCode a byte.
* @param exceptionCode a byte.
* @throws ModbusTransportException if any.
*/
public ExceptionResponse(int slaveId, byte functionCode, byte exceptionCode) throws ModbusTransportException {
super(slaveId);
this.functionCode = functionCode;
setException(exceptionCode);
}
@Override
public byte getFunctionCode() {
return functionCode;
}
@Override
protected void readResponse(ByteQueue queue) {
// no op
}
@Override
protected void writeResponse(ByteQueue queue) {
// no op
}
}

Some files were not shown because too many files have changed in this diff Show More