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
 
작성할 내용
- 
hwId가awesome인 하드웨어를 예로 든다면,
- 
구현할 내용은 openDevice.ts,IAwesomeControl.ts,AwesomeControl.ts,index.ts파일입니다.- openDevice.ts파일은 시리얼 장치를 오픈하는 로직이 포함됩니다. 시리얼포트의- baudRate나- COM1같은- 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같은 시리얼 포트 옵션 정보들을 설정할 수 있습니다.
- 아래의 예에서는 baudRate를38400으로 설정하고 있고,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 파일이어야 합니다.
다음 단계
- 간단하게 하드웨어를 추가하는 방법을 살펴보았습니다.
- 하지만 아직 실제로 적용하기에는 부족합니다. 가령 시리얼포트로부터 수신된 데이터를 파싱해야 할 수도 있습니다.
- 하드웨어에 따라 수신하는 방식이 다르지만, 크게 두 가지로 분류할 수 있습니다.
- 블록코딩에서 명령을 수신했을때만 시리얼포트로부터 읽기
- 명령의 수신 여부와 상관없이 계속해서 수신하기
 
- 다음 하드웨어 데이터 송신문서를 읽어주세요.