PC 프로그램 신규 하드웨어 등록

PC 프로그램 신규 하드웨어 등록

이 문서는 2023년 3월 14일에 작성되었습니다.

여기서 말하는 PC 프로그램이란? 하드웨어를 제어하기 위한 PC용 프로그램입니다. electron 기반으로 구현되었습니다.

신규 하드웨어 작성 및 배포 절차

신규 하드웨어를 추가하는 절차는 다음과 같습니다.

  • PC 프로그램에서 하드웨어 제어 로직을 작성하고
  • 블록 공작소에서 블록을 제작
  • PC 프로그램과 블록 연동 테스트
  • 코디니 사업팀에 승인 요청(단, 승인은 최초 한번만 필요합니다)
  • 배포: 승인이 된 후에, 블록 공작소에서 배포하면 일반 사용자에게 즉시 노출됩니다.
  • 수정 배포: 수정사항이 있으면, 수정후에 다시 배포하시면 됩니다. 승인과정은 필요없습니다.
하드웨어 승인절차하드웨어 승인절차

신규 하드웨어 작성

  • 모든 하드웨어에는 하드웨어 ID(hwId)를 부여합니다.
  • src/custom/hw 폴더에 hwId 폴더를 만드세요.
  • hwId는 원하는 것으로 결정하세요. 중복되지만 않으면 문제되지 않습니다.
  • 만약 hwId가 awesome인 하드웨어를 추가한다면 src/custom/hw/awesome 폴더를 만들면 됩니다.

hwId 규칙

  • hwId는 소문자로 시작해야 하고, 공백이 없습니다.
  • 알파벳만 가능합니다. 하이픈(-)이나 언더바(_)는 사용할 수 없습니다.
  • camel case 표기법을 권장합니다.
  • 예를 들면 아래와 같은 하드웨어 ID들이 있습니다.
    • exMarsCube
    • saeonAltinoLite
    • wiseXboard

작성할 내용

  • hwIdawesome인 하드웨어를 예로 든다면,

  • 구현할 내용은 openDevice.ts, IAwesomeControl.ts , AwesomeControl.ts, index.ts 파일입니다.

    • openDevice.ts 파일은 시리얼 장치를 오픈하는 로직이 포함됩니다. 시리얼포트의 baudRateCOM1 같은 path를 설정합니다.
    • IAwesomeControl.ts 은 하드웨어를 제어하는 함수들의 인터페이스이고
    • AwesomeControl.ts 은 하드웨어 인터페이스의 구현체입니다. 실제로 하드웨어와 통신하는 코드가 포함됩니다.
    • index.ts는 기타 부가적인 정보와 함께 인터페이스와 구현체를 export합니다.

기본적인 샘플 소스코드

src/custom/hw/awesome/openDevice.ts 파일

  • openDevice.ts 파일에는 2개의 함수를 구현해야 합니다. isPortMatch(), openDevice() 함수입니다.
  • isPortMatch()awesome 하드웨어가 주어진 시리얼 포트를 지원하는지 여부를 체크하는 함수입니다.
    • 지원한다면 true를 리턴합니다.
    • 보통 시리얼 포트 드라이버의 제조사명으로 지원여부를 체크합니다.
    • 아래의 예제는 CP210 드라이버 제조사인 silicon labs를 체크하고 있습니다.
    • 개발중에는 무조건 true를 리턴해도 괜찮지만, 릴리즈할 때는 적절하게 작성하는 것이 사용자 편의를 위해 좋습니다.
  • 두번째 구현할 openDevice() 함수는 awesome 하드웨어의 시리얼포트를 open하는 로직을 작성하는 함수입니다.
    • baudRate,parity 같은 시리얼 포트 옵션 정보들을 설정할 수 있습니다.
    • 아래의 예에서는 baudRate38400으로 설정하고 있고, parity는 생략했으므로, 기본값인 none이 사용됩니다.
import { DelimiterParser, SerialPort } from 'serialport'
import { ISerialDeviceOpenParams, ISerialPortInfo } from 'src/custom-types'
import { SerialDevice } from 'src/hw-server/serialport/SerialDevice'

// 디버깅용 로그 태그
const DEBUG_TAG = 'awesome'

/**
 * 지원하는 시리얼포트 여부 체크
 *
 * @param portInfo 포트 정보
 * @returns 지원하는 포트라면 true를 리턴
 */
export function isPortMatch(port: ISerialPortInfo): boolean {
    // const { manufacturer, productId, vendorId } = port
    const { manufacturer = '' } = port

    // silicon labs(CP210)만 허용
    const matched = manufacturer.toLowerCase().indexOf('silicon labs') >= 0

    return matched
}

/**
 * 시리얼 디바이스 오픈
 * open 중인 상태의 SerialDevice를 리턴합니다.
 *
 * 연결이 되기를 기다리려면
 * await device.waitUntilOpen()
 *
 * @param serialPortPath 시리얼포트 Path, ex) COM1, /dev/ttyUSB0
 * @param uiLogger UI 콘솔 로거, 사용자 화면에 로깅 메시지를 표시합니다
 * @returns SerialDevice
 */
export function openDevice(params: ISerialDeviceOpenParams): SerialDevice {
    const { serialPortPath, uiLogger } = params
    console.log(DEBUG_TAG, 'openDevice()', serialPortPath)

    // 시리얼 디바이스 생성, 시리얼포트를 감싸는 객체입니다.
    // 실제 serial port의 상태를 관리하고, UI에 로그를 전송합니다.
    const device = new SerialDevice(DEBUG_TAG, uiLogger)

    // 시리얼 포트 생성
    const port = new SerialPort({
        path: serialPortPath,
        baudRate: 38400,
        autoOpen: false, // autoOpen은 반드시 false
    })

    // 시리얼 디바이스 열기
    device.open(port)

    // open 중인 상태의 SerialDevice를 리턴합니다.
    return device
}
  • 위의 코드에서 openDevice() 함수에 uiLogger 변수가 있습니다. uiLogger하드웨어 제어 콘솔에 로그를 남기는 용도입니다.
  • 아래 그림에서 하드웨어 제어 콘솔 예시를 보여주고 있습니다.
하드웨어 제어 콘솔 예시하드웨어 제어 콘솔 예시

src/custom/hw/awesome/IAwesomeControl.ts 파일

  • IAwesomeControl.ts 파일은 하드웨어를 제어하는 함수들의 인터페이스입니다.
  • 함수의 이름과 인자는 목적에 맞게 자유롭게 지정할 수 있습니다. 예를 들어, 아래의 digitalRead(pin)는 인자로 주어진 핀번호에서 디지털 값을 읽는 기능을 합니다.
  • 블록코딩에서 호출할 함수이므로, 모두 Promise를 리턴해야 합니다.
  • 주의할 점: 블록 코딩의 블록에서 인터페이스의 함수를 호출하는 방식으로 동작하는데, 블록 코딩의 한 블록이, 2개의 함수를 호출할 수 없습니다. 한 블록 당 1개의 함수를 호출한다는 점을 유의해서, 함수를 설계해주세요. 즉, 블록과 함수는 일대일 매핑입니다.
// file: src/custom/hw/awesome/IAwesomeControl.ts

/**
 * 컨트롤 인터페이스
 */
export interface IAwesomeControl {
    digitalRead(pin: number): Promise<number>
    digitalWrite(pin: number, value: number): Promise<void>
}

src/custom/hw/awesome/AwesomeControl.ts 파일

  • src/custom/hw/awesome/IAwesomeControl 인터페이스의 구현 부분입니다.
  • IAwesomeControl 인터페이스의 함수 2개를 구현해야 합니다.
// file: src/custom/hw/awesome/AwesomeControl.ts

import { AbstractHwConrtol } from '../AbstractHwControl'
import { IAwesomeControl } from './IAwesomeControl'

const chr = (ch: string): number => ch.charCodeAt(0)

/**
 * 하드웨어 서비스
 */
export class AwesomeControl extends AbstractHwConrtol implements IAwesomeControl {
    /**
     * IAwesomeControl 인터페이스 구현
     */
    digitalRead = async (ctx: any, pin: number): Promise<number> => {
        const values = await this.readNext_(ctx)
        // [pin1 ~ pin5]
        const v = values[pin - 1]
        return v > 100 ? 1 : 0
    }

    /**
     * IAwesomeControl 인터페이스 구현
     */
    digitalWrite = async (ctx: any, pin: number, value: number): Promise<void> => {
        value = value > 0 ? 1 : 0

        const pkt = [chr('X'), chr('R'), 2, 0, 0, 0, 0, 0, chr('S')]
        pkt[2 + pin] = value
        await this.write_(ctx, pkt)
    }
}
  • 위의 코드에서 주목할 부분은 this.readNext_()this.write_()입니다. 이 함수는 부모 클래스인 AbstractHwConrtol에 구현되어 있습니다.
  • readNext_() 함수는 시리얼포트에서 다음 데이터를 읽는 함수입니다.
  • write_() 함수는 시리얼포트에 데이터를 쓰는 함수입니다. Buffer 또는 number[]를 쓸 수 있습니다.

src/custom/hw/awesome/index.ts 파일

  • 이제, 지금까지 작성한 정보를 index.ts 파일에 정리할 단계입니다.
  • 하드웨어 ID(hwId)와 하드웨어 이름(hwName)을 설정해주세요.
  • 하드웨어 종류(hwKind)는 serial 고정값입니다.
  • 필요한 USB 드라이버가 다르다면, public/drivders 폴더에 설치파일을 복사한 후에 적어주세요.
// file: src/custom/hw/awesome/index.ts

import { HwKindKey, IHwDescriptor, IHwInfo } from 'src/custom-types/basic-types'
import { isPortMatch, openDevice } from './openDevice'
import { AwesomeControl } from './AwesomeControl'

const hwId = 'awesome'
const hwKind: HwKindKey = 'serial'

const info: IHwInfo = {
    hwId,
    hwKind,
    hwName: '어썸보드',
    category: 'module',
    supportPlatforms: ['win32'],
    pcDrivers: [
        {
            name: 'USB 드라이버',
            'win32-ia32': 'CP210x_Universal_Windows_Driver/CP210xVCPInstaller_x86.exe',
            'win32-x64': 'CP210x_Universal_Windows_Driver/CP210xVCPInstaller_x64.exe',
        },
    ],
}

export const awesome: IHwDescriptor = {
    hwId, // 하드웨어 ID
    hwKind, // 하드웨어 종류
    info, // 하드웨어 정보
    hw: {
        hwId,
        hwKind,
        isPortMatch, // 시리얼포트 지원 함수
        createControl: () => new AwesomeControl(),
        openDevice,
    },
}

하드웨어 익스포트

  • 여기까지 커스텀 하드웨어 추가하는 과정을 살펴보았습니다. 하지만 아직 PC 프로그램의 하드웨어 목록에서는 조회되지 않습니다.
  • 추가한 awesome 하드웨어를 하드웨어 목록에 등록해야 합니다.
  • 아래와 같이 src/custom/hw/index.ts 파일에 등록하면 됩니다.
// file: src/custom/hw/index.ts

import { wiseXboard } from './hw/wiseXboard'
import { wiseXboardPremium } from './hw/wiseXboardPremium'
import { exMarsCube } from './hw/exMarsCube'
import { saeonAltinoLite } from './hw/saeonAltinoLite'
import { awesome } from './hw/awesome'
import { IHwDescriptor } from 'src/custom-types'

export const HardwareDescriptors: Record<string, IHwDescriptor> = {
    [wiseXboardPremium.hwId]: wiseXboardPremium,
    [wiseXboard.hwId]: wiseXboard,
    [exMarsCube.hwId]: exMarsCube,
    [saeonAltinoLite.hwId]: saeonAltinoLite,

    // 추가한 awesome 하드웨어
    [awesome.hwId]: awesome,
}

하드웨어의 조회 및 이미지

  • 이제 PC 프로그램을 실행해서 하드웨어가 조회되는지 확인해보세요.
하드웨어 목록 화면 예시하드웨어 목록 화면 예시
  • 만약 하드웨어의 이미지가 표시되지 않는다면, 이미지를 등록해주세요.
  • 하드웨어 ID가 awesome이라면 public/static/images/devices/awesome.png 경로에 하드웨어의 이미지를 저장하시면 됩니다. 하드웨어의 ID와 이름이 같은 png 파일이어야 합니다.

다음 단계

  • 간단하게 하드웨어를 추가하는 방법을 살펴보았습니다.
  • 하지만 아직 실제로 적용하기에는 부족합니다. 가령 시리얼포트로부터 수신된 데이터를 파싱해야 할 수도 있습니다.
  • 하드웨어에 따라 수신하는 방식이 다르지만, 크게 두 가지로 분류할 수 있습니다.
    • 블록코딩에서 명령을 수신했을때만 시리얼포트로부터 읽기
    • 명령의 수신 여부와 상관없이 계속해서 수신하기
  • 다음 하드웨어 데이터 송신 문서를 읽어주세요.