[ESP32+NestJS] 펌웨어 업데이트를 원격으로 해보자 - OTA

임찬혁·2023년 2월 16일
2
post-thumbnail

하드웨어 제품은 펌웨어를 업데이트하려면 유선으로 연결해야합니다. 소프트웨어 제품은 원격으로 붙어서 진행할 수 있지만 하드웨어는 그렇지 못해 번거로운 상황이 많이 생길 수 있습니다.
이런 문제를 해결하기 위한 OTA(Over The Air) 라는 펌웨어 업데이트 방식이 있는데, 이번 포스트에서는 OTA 를 구현해보겠습니다.
기능 특성상 펌웨어와 서버를 왔다갔다 하면서 진행해야하는데, 차례대로 따라하시면 쉽게 하실 수 있습니다.

개발환경

  • 하드웨어 보드: LOLIN D32 (ESP32-WROOM-32E)
  • 하드웨어 IDE: Arduino IDE 2.0.3
  • 개발 피씨: MacBook Pro(16형, 2021년 모델)
  • 서버 프레임워크: NestJS

펌웨어

개발환경 설정

ESP32 보드에 펌웨어를 업로드하기 위해서는 Arduino IDE 가 필요하고, 관련 패키지를 추가해줘야합니다.

  1. Arduino 에 접속해서 각자의 플랫폼에 맞는 설치파일을 받으시면 됩니다. 그냥 다운받으려면 JUST DOWNLOAD 버튼을 누르시면 됩니다.
  2. 설치완료 후 실행하면 다음과 같은 화면이 나옵니다.
  3. Arduino IDE > Preferences 메뉴로 들어가서 Additional boards manager URLshttps://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json 를 적은 후 OK 를 누르면 ESP32 관련 패키지들이 자동으로 추가됩니다.
  4. Tools > Board > Boards Manager 메뉴로 들어가서 ESP32 를 검색하고 설치합니다.
  5. 보드를 피씨에 연결한 후 Arduino IDE 에서 보드와 포트를 선택합니다.

펌웨어 작성

ESP32 패키지에서 제공해주는 httpUpdate 예제코드를 사용하겠습니다.

  1. File > Examples > HTTPUpdate > httpUpdate 를 선택하면 해당 코드가 IDE에 불러와집니다.
  2. 펌웨어 버전을 확인할 수 있도록 setup() 함수 안에 Serial.println("v1"); 을 추가해줍니다.
  3. 와이파이에 연결할 수 있도록 AP의 SSIDPASSWORDWiFiMulti.addAP() 함수에 입력해줍니다.
  4. 펌웨어 업데이트가 완료된 후 보드를 수동으로 재시작해주기 위해 자동리부트 기능을 비활성화합니다. 설정 코드는 loop() 함수 안에 추가하면 됩니다.
  5. OTA 서버 의 주소와 포트, 경로를 입력합니다. 이 부분은 뒤에서 서버구성을 완료한 후에 다시 진행하겠습니다.
  6. 펌웨어 업데이트를 실패하면 재시도를 하는데, 재시도 사이에 딜레이를 넣어주겠습니다.
  7. 펌웨어 업데이트가 완료되면 보드를 재시작해주는 코드를 추가합니다.

여기까지 해두고서 OTA 서버 를 구성하고 다시 돌아오겠습니다.

OTA 서버

서버에서 할 일은 간단합니다. 펌웨어 업데이트 관련 라우터로 요청이 들어오면 요청에 맞는 펌웨어 파일을 전송해주는 역할이 전부입니다.

  1. nest-cli 를 설치하고 새 프로젝트를 만듭니다.
npm i -g @nestjs/cli

nest new ota-server
  1. ota-server > src > main.ts 파일에 서버의 주소를 확인하기위한 코드를 작성합니다. 서버가 실행되면 현재의 주소를 콘솔에 출력해줍니다. 저의 경우에는 192.168.219.115 로 나왔고, 포트는 3000 번을 사용하겠습니다.
// main.ts

import { networkInterfaces } from 'os';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const nets = networkInterfaces();

  await app.listen(3000, () => {
    const results = {};

    for(let name of Object.keys(nets)) {
      for(let net of nets[name]) {
        if(net.family === 'IPv4' && !net.internal) {
          if(!results[name]) {
            results[name] = [];
          }
          results[name].push(net.address);
        }
      }
    }

    console.log(`Listening on port ${3000}\n${JSON.stringify(results, null, '  ')}`);
  });
}
bootstrap();

다시 펌웨어로 돌아가 서버 정보를 설정하겠습니다.

펌웨어

OTA 서버 연결 설정

위의 펌웨어 작성 5번 항목에 필요한 정보를 얻었으니, 이제 펌웨어 코드를 완성할 수 있습니다. OTA 서버 를 실행해서 얻은 주소와 포트를 적어줍니다. 경로는 /firmware 로 하겠습니다.
해당 값들을 httpUpdate.update() 함수에 적어주시면 됩니다.

업로드

펌웨어 코드를 다 작성하고나서 Arduino IDE 왼쪽 상단에 있는 Upload 버튼을 눌러 보드에 업로드를 해줍니다.

업로드가 안되면 Tools > Upload Speed 메뉴에서 Baud Rate115200 으로 바꿔보세요.

업로드가 완료되면 현재 펌웨어 버전을 확인할 수 있습니다.

지금은 OTA 서버 가 실행중이 아니기때문에 connection refused 에러가 발생합니다.

시리얼 로그는 Arduino IDE 오른쪽 상단에 있는 Serial Monitor 버튼을 눌러서 확인할 수 있습니다.

v2 펌웨어 파일 생성

OTA 서버 에 업로드해둘 펌웨어 파일을 생성합니다.
1. 기존 펌웨어와 차이점을 두기 위해 펌웨어 버전을 v2 로 수정합니다.

2. Sketch > Export Compiled Binary 메뉴를 선택해서 새로운 펌웨어 파일을 생성합니다.
3. Sketch > Show Sketch Folder 메뉴를 선택해서 펌웨어 파일이 있는 폴더를 열어줍니다.
4. build > esp32.esp32.d32 > httpUpdate.ino.bin 파일을 OTA 서버 프로젝트 > firmware 폴더로 복사합니다.

OTA 서버

펌웨어에서 설정한대로 /firmware 경로로 접속했을 때 펌웨어 파일을 다운받을 수 있도록 코드를 작성해보겠습니다. src/app.controller.ts 파일에서 진행합니다.
구현 방식은 Express 방식과 NestJS 방식으로 두 가지가 있습니다.

  • Express 방식
import * as path from 'path';
import { statSync, createReadStream } from 'fs';
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}
  
  @Get('/firmware')
  getFirmware(@Res() res: Response) {
    console.log('Received the firmware download request');
    const filePath = path.join(process.cwd(), 'firmware/httpUpdate.ino.bin');
    const fileStat = statSync(filePath);
    const fileStream = createReadStream(filePath);

    res.set({
      'Content-Length': fileStat.size,
    });

    fileStream.pipe(res);
  }
}
  • NestJS 방식
import * as path from 'path';
import { statSync, createReadStream } from 'fs';
import { Controller, Get, Res, StreamableFile } from '@nestjs/common';
import { Response } from 'express';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('/firmware')
  getFirmware(@Res({ passthrough: true }) res: Response): StreamableFile {
    console.log('Received the firmware download request');
    const filePath = path.join(process.cwd(), 'firmware/httpUpdate.ino.bin');
    const fileStat = statSync(filePath);
    const fileStream = createReadStream(filePath);

    res.set({
      'Content-Length': fileStat.size,
    });

    return new StreamableFile(fileStream);
  }
}

주의해야할 점은 HeaderContent-Length 를 같이 보내주어야 합니다. 그렇지않으면 premature close 라는 에러가 발생합니다.

브라우저에서 localhost:3000/firmware 로 접속해보면 펌웨어 파일이 다운로드되는 것을 확인할 수 있습니다.

펌웨어

이제 보드를 연결하면 자동으로 펌웨어를 다운로드받고 업데이트를 진행합니다.

펌웨어 버전이 v2 로 올라간 것을 확인할 수 있습니다.

마치며

OTA 에 대한 글들은 대부분 Express 를 기반으로 작성한 경우가 많아서 이번에 NestJS 로 개발을 진행하며 작성해보았습니다. 하드웨어, 펌웨어 개발도 오랜만에 해보니 재밌는 것 같네요.

profile
개발새발자

0개의 댓글