[XOLAR] AWS IoT Core + SpringBoot

2한나·2024년 10월 24일
1

XOLAR

목록 보기
2/4
post-thumbnail

목표
SpringBoot와 AWS IoT를 연동하여, SpringBoot에서 REST API 요청을 통해 AWS IoT 사물 생성 및 Device Shadow 업데이트 구현

구현할 아키텍처 구조

AWS IoT 사물 설정

사물 생성

인증서 생성

정책 생성

arn:partition:iot:region:AWS-account-ID:Resource-type/Resource-name

인증서에 정책 연결

사물에 인증서 연결

Spring Boot와 연결

종속성 추가

아래 링크에서 적당한 버전을 가져왔다.
https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-iot

// AWS IoT Core
implementation 'com.amazonaws:aws-java-sdk-iot:1.12.700'

IAM 사용자 생성 및 액세스키 발급

해당 사용자를 사용해 Spring Boot에서 AWS IoT 클라이언트를 생성할 것이다.
필요 권한: AdministratorAccess, AWSIoTDataAccess, AWSIoTFullAccess

applicaion.yml 설정

aws:
  accessKeyId: {IAM 액세스키}
  secretKey: {IAM 시크릿 키}
  certificateArn: {AWS IoT 인증서 ARN} 

Config 설정

AwsConfig.java

@Configuration
public class AwsConfig {
    @Value("${aws.accessKeyId}")
    private String accessKeyId;

    @Value("${aws.secretKey}")
    private String secretKey;

    /**
     * AWS IoT 클라이언트를 빈으로 등록
     */
    @Bean
    public AWSIot getIotClient(){
        // AWS IoT 클라이언트 생성
        return AWSIotClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(
                        // IAM 액세스 키 아이디와 시크릿키를 사용해 인증 정보 제공
                        new BasicAWSCredentials(accessKeyId, secretKey)))
                // AWS 리전을 서울로 설정
                .withRegion(Regions.AP_NORTHEAST_2)
                .build();
    }

    /**
     * AWS IoT Data 클라이언트를 빈으로 등록
     */
    @Bean
    public AWSIotDataClient getIotDataClient(){
        // AWS IoT Data 클라이언트 생성
        return (AWSIotDataClient) AWSIotDataClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(new AWSCredentials() {
                    // IAM 액세스 키 아이디와 시크릿키를 사용해 인증 정보 설정
                    @Override
                    public String getAWSAccessKeyId() {
                        return accessKeyId;
                    }

                    @Override
                    public String getAWSSecretKey() {
                        return secretKey;
                    }
                }))
                // AWS 리전을 서울로 설정
                .withRegion(Regions.AP_NORTHEAST_2)
                .build();
    }
}

MQTTConfig.java

@Configuration
@RequiredArgsConstructor
public class MQTTConfig {
    private final AwsConfig awsConfig;

    /**
     * AWS IoT Device Shadow에 메세지 게시
     * : 패널의 고유 번호를 이름으로하는 사물의 shadow에 어떤 비상 작동 버튼이 눌렸는지 넣어 메세지 게시
     *
     * AWS IoT Device Shadow: 디바이스의 현재 상태를 저장하여 디바이스가 오프라인 상태일 때도 정보를 클라우드에 보존하여
     * 언제든지 상태 정보에 접근할 수 있도록 한다.
     */
    public void publishToShadow(String panelNumber, EmergencyStatus emergencyStatus) throws IOException{
        // 게시할 주제 설정 (패널 고유 번호를 이름으로 하는 사물의 shadow에 메세지를 게시하기 위해)
        String topic = "$aws/things/" + panelNumber + "/shadow/update";

        // EmergencyStatus 객체를 JSON 문자열로 변환 (payload는 JSON 형태여야 하기때문)
        ObjectMapper objectMapper = new ObjectMapper();
        String emergencyStatusJson = objectMapper.writeValueAsString(emergencyStatus);

        // Device Shadow 내용을 업데이트하는 payload 설정 (비상 작동 모드를 status에 넣어 보냄)
        String payload = "{\"state\":{\"reported\":{\"status\":" + emergencyStatusJson +"}}}";
        ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(payload);

        // PublishRequest 객체 생성
        PublishRequest publishRequest = new PublishRequest();
        publishRequest.withPayload(byteBuffer);
        publishRequest.withTopic(topic);
        publishRequest.setQos(0);

        // AWS IoT Data Client를 통해 메세지 게시
        awsConfig.getIotDataClient().publish(publishRequest);
        System.out.println("성공적으로 메세지를 게시했습니다.");
    }
}

태양광패널 등록 기능

포스트맨을 통해 등록을 요청하면, AWS IoT에 해당 이름의 사물을 생성한다.

SolarPanelController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/solar-panels")
public class SolarPanelController {
    private final SolarPanelService solarPanelService;

    // 생략

    // 태양광 패널 등록 <- 수정 필요 dto로 이름 받아오기
    @PostMapping("/register/{thingName}")
    public ResponseEntity<String> createThing(@PathVariable String thingName){
        return ResponseEntity.status(HttpStatus.OK).body(solarPanelService.createThingAutomatically(thingName));
    }
}

SolarPanelService.java

@Service
@RequiredArgsConstructor
public class SolarPanelService {
    private final SolarPanelRepository solarPanelRepository;
    private final ElectronicService electronicService;
    private final BillService billService;
    private final UserService userService;
    private final AwsConfig awsConfig;

    // 생략
    
    /**
     * 자동으로 AWS IoT에 사물을 등록하는 메서드
     */
    public String createThingAutomatically(String thingName) {
        // 해당 이름의 사물이 이미 존재하는지 확인
        if(!describeThing(thingName)){
            // 사물이 존재하지 않는 경우, 사물 생성
            CreateThingResult response = awsConfig.getIotClient()
                    .createThing(new CreateThingRequest().withThingName(thingName));

            // 사물에 인증서 연결
            AttachThingPrincipalRequest attachThingPrincipalRequest = new AttachThingPrincipalRequest()
                    .withPrincipal(certificateArn)
                            .withThingName(thingName);
            awsConfig.getIotClient().attachThingPrincipal(attachThingPrincipalRequest);

            System.out.print("사물이 성공적으로 생성되었습니다.");
            return "사물이 성공적으로 생성되었습니다.";
        }
        // 사물이 이미 존재하는 경우, 아래 메세지 반환
        return "해당 이름의 사물이 이미 존재합니다.";
    }

    /**
     * 해당 이름의 사물이 존재하는지 확인하는 메서드
     */
    private boolean describeThing(String thingName) {
        // 사물 이름이 null이라면 사물이 없다는 의미이므로 false 반환
        if(thingName == null){
            return false;
        }
        try {
            // 사물이 AWS IoT에 존재하는지 확인 -> 예외가 발생하지 않는다면 사물이 존재함을 의미하므로 true 반환
            DescribeThingResponse(thingName);
            return true;
        } catch (ResourceNotFoundException e){
            // 사물이 존재하지 않는경우, 예외가 발생하으로 false 반환
            return false;
        }
    }

    /**
     * AWS IoT에 해당 이름으로 이미 등록된 사물이 있는지 확인하는 메서드
     */
    private DescribeThingResult DescribeThingResponse(String thingName) {
        // 해당 사물의 이름으로 DescribeThingRequest 객체를 생성
        DescribeThingRequest describeThingRequest = new DescribeThingRequest();
        describeThingRequest.setThingName(thingName);

        // AWS IoT 클라이언트를 사용하여 describeThing 요청을 보내고, 결과 반환
        return awsConfig.getIotClient().describeThing(describeThingRequest);
    }


  // 생략


}

비상버튼 작동 기능

포스트맨을 통해 비상버튼 기능을 요청하면, 해당 태양광패널의 고유번호를 가진 사물의 Device Shadow를 업데이트 한다.

EmergencyController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/solar-panels")
public class EmergencyController {
    private final EmergencyService emergencyService;

    // 강풍 비상 버튼
    @PostMapping("{panelId}/strong-wind")
    public ResponseEntity<String> publishStrongWindMessage(@PathVariable Long panelId) throws IOException {
        EmergencyStatus emergencyStatus = EmergencyStatus.STRONG_WIND;
        return ResponseEntity.status(HttpStatus.OK).body(emergencyService.publishToShadow(panelId, emergencyStatus));
    }

    // 폭설 비상 버튼
    @PostMapping("{panelId}/heavy-snow")
    public ResponseEntity<String> publishHeavySnowMessage(@PathVariable Long panelId) throws IOException {
        EmergencyStatus emergencyStatus = EmergencyStatus.HEAVY_SNOW;
        return ResponseEntity.status(HttpStatus.OK).body(emergencyService.publishToShadow(panelId, emergencyStatus));
    }

    // 비상 작동 해제 버튼
    @PostMapping("{panelId}/normal")
    public ResponseEntity<String> publishNormalMessage(@PathVariable Long panelId) throws IOException {
        EmergencyStatus emergencyStatus = EmergencyStatus.NORMAL;
        return ResponseEntity.status(HttpStatus.OK).body(emergencyService.publishToShadow(panelId, emergencyStatus));
    }

}

EmergencyService.java

@Service
@RequiredArgsConstructor
public class EmergencyService {
    private final MQTTConfig mqttConfig;
    private final SolarPanelService solarPanelService;

    /**
     * 비상 버튼을 눌렀을 때, 해당 패널의 Device Shadow로 메세지를 게시하는 메서드
     */
    public String publishToShadow(Long panelId, EmergencyStatus emergencyStatus) throws IOException {
        // 패널 Id를 이용해 해당 패널의 정보 조회 -> 패널의 고유 번호 가져오기
        SolarPanel panel = solarPanelService.findById(panelId);
        String panelNumber = panel.getPanelNumber();

        // MQTTConfig를 이용해 패널 고유 번호와 비상 상태를 Device Shadow에 게시
        mqttConfig.publishToShadow(panelNumber, emergencyStatus);

        // 패널의 현재 모양을 알려주는 imageNumber를 비상모드 모양에 따라 변경
        if(emergencyStatus == EmergencyStatus.STRONG_WIND){
            panel.setImageNumber(3);
        } else if (emergencyStatus == EmergencyStatus.HEAVY_SNOW) {
            panel.setImageNumber(1);
        } else {
            // 비상작동 해제 버튼을 눌렀을 경우: 현재 태양 위치를 바라보도록 수정해야함
            panel.setImageNumber(2);
        }

        return emergencyStatus.getTitle() + "버튼이 눌렸습니다.";
    }


}

테스트

사물 생성

PoatMan으로 사물 생성 요청 보내기 (사물이름: aaa)

AWS IoT에 사물 생성됨

비상버튼 요청

PostMan으로 비상버튼 요청 보내기 (강풍모드)

AWS IoT 해당 사물의 Device Shadow가 해당 모드에 따라 업데이트됨

PostMan으로 다른 비상버튼 요청 보내기 (비상작동해제)

AWS IoT 해당 사물의 Device Shadow가 해당 모드에 따라 업데이트됨

0개의 댓글