Typescript로 IoT 개발하기

Sharlotte ·2023년 6월 2일
3

동기 Motivation

저는 중학교 자유학기제때 아두이노를 접하고 고등학교에서 동아리에 들어가며 아두이노 개발을 경험해보고 이 아두이노에 대해서 몇가지 단점을 느꼈습니다.

  1. 본격적인 C 입문이 아니면 그 개발 자체에 근본적인 제약이 걸립니다. 분야 입문에 있어서 C 공부가 강제됩니다.
  2. 즉, 라이트한 목적으로 IoT나 임베디드를 찍먹할 사람에게 C라는 저급 언어를 본격적으로 입문해야 한단 압박감은 비교적 가벼운 이미지를 가진 아두이노에 모순된 태도를 지니고 있습니다.
  3. 가볍게 IoT를 만들 수단 자체가 존재하지 않습니다. 파이썬으로 데스크톱과 연결하는 방법이 있다지만 업로드가 불가하니 개발 환경에 종속적입니다.

많은 교육과 키트들은 모듈과 받아쓰라는듯한 코드만 제공해주고 C에 대한 세부적인 설명은 하지 않아서 뭔갈 응용할려면 C를 따로 배울 필요가 있었습니다. 물론, 아두이노의 C는 완전한 C가 아닙니다. 아두이노 IDE를 통한 임베디드 개발도 극한의 성능과 메모리를 다룰 임베디드 세상에선 꽤나 고급이겠지만 그럼에도 불구하고, 여전히 IoT 입문자들에게 C는 어렵습니다. 러닝 커브가 요구 사항에 비해 너무 비대합니다. Javascript를 줄곧 개발해오던 저는 Microsoft가 하고 있는 새로운 시도를 발견했습니다.

해결: DeviceScript

DeviceScript는 TypeScript의 서브셋 언어로, Microsoft가 작년 12월부터 릴리즈를 시작한 정말 따끈따끈한 반년된 언어입니다. 이제 타입스크립트를 통해 라즈베리파이 Picoesp32 개발 보드에서 IoT 개발을 진행할 수 있습니다.

특징

런타임은 Node.js가 아닙니다.

DeviceScript는 wasm 가상머신에서 실행됩니다. 웹어셈블리어로 되었기 때문에 브라우저와 Node.js 런타임에서 실행할 수도 있습니다. 그래도 DeviceScript 내부에선 이러한 Node.js의 부재 때문에 Node.js에서 fetch를 호출할 수 없듯이, process같은 Node.js API를 활용할 수 없습니다.

그러나 Node.js에서 개발할 수도 있습니다.

물론 Node.js로 돌릴 방법이 아에 없는건 아닙니다. Node.js Simulation 문서에선 src/sim/app.js를 Node.js 진입점으로 삼아 기기 또는 시뮬레이터와 Websocket 연결을 하여 실행을 한다고 설명합니다.

TypeScript와 완전히 동일하진 않습니다.

DeviceScript 언어 참조 문서에 따르면 DeviceScript는 TypeScript와 완전히 같진 않은데, 내부적으로 실행되는 환경이 다르다보니 생긴 차이로 추측됩니다. 이러한 차이는 생각보다 많지는 않은데 개발자의 낭만을 채워주는 부분들이 몇몇 보였습니다.

  • with문, eval함수, 네임스페이스, arguments 키워드와 같이 비권장되고 deprecated된 문법이 없습니다. 레거시에서 해방!
  • 제너레이터 함수가 없습니다. 비동기에 대해선 아래 문단에서 추가로 설명하겠습니다.
  • 속성을 비열거형으로 표시할 수 없습니다.
  • for in 문이 없어지고 for of문만이 남았습니다. 이제 안햇갈린다!
  • ==!= 연산자가 없는 대신 ===, !===로 완전 비교만이 남았습니다. 동치 비교에서의 억까를 근본적으로 퇴치!
  • RAM 절약을 위해 일부 객체(Fiber, Register, Event, 정적 buffer, 함수, 문자열과 숫자)는 임의로 속성을 제어할 수 없습니다.

아래는 향후 개발될 기능들입니다.

  • getter, setter가 없습니다.
  • 템플릿 리터럴은 있는데 태그된 템플릿 리터럴은 없습니다. (styled.div`` 같은...)
  • 클래스의 정적 필드의 초기화가 없습니다.
  • 이넘(enum)을 런타임 배열로 사용할 수 없습니다.

비동기가 약간 다릅니다.

이 이야기는 따로 문서가 있는데, 요약해서 말하자면 DeviceScript의 비동기는 진짜로 멀티스레드(파이버)입니다. 그런데 await async는 비동기가 아닙니다. await/async 동작 자체는 달라지지 않지만 모든 비동기 함수는 await를 사용해야 하는 특징이 있습니다. 비동기로 실행하고 싶다면 Function.start 함수를 사용할 수 있습니다.
또한 Promise는 타입스크립트처럼 사용할 수 있으나 런타임에 없기 때문에 속성을 가질 수 없습니다.

Observables를 기본적으로 지원합니다.

DeviceScript는 특이하게도 Rxjs의 Observables를 내장으로 지니고 있습니다. Rxjs의 이해는 테오님의 포스트를 참고해보세요.
옵저버블은 데이터 흐름을 파이프라인(pipe)의 조합과 연결로 제어할 수 있습니다. 임베디드에선 센서를 통한 데이터 흐름이 많기 때문에 레지스터(Register)read, write 다음으로 subscribe를 두었는데, 이덕분에 observables로 확장할 수 있던 것 같습니다.

이러한 옵저버블의 사용은 아래와 같이

왼쪽의 연속된 데이터들에서 중복을 거르고 싶다 할 때

temperature
    .pipe(threshold(1))
    .subscribe(temp => console.log(temp))

와 같이 pipe를 통해 옵저버블 파이프라인 함수를 연속적으로 사용할 수 있습니다. 이러한 파이프라인은

filter나map이나reduce도 있고
debunce나throttle도 있습니다.

옵저버블의 파이프라인 함수는 그저 주어진 값을 처리하여 반환하는 일개 함수에 불과하므로 직접 만들수도 있습니다. 개인적으로 IoT개발도 새로운데 옵저버블 실전 응용이란 경험까지 얻어 많이 신납니다.

테스트 라이브러리를 기본적으로 지원합니다.

DeviceScript는 Jest같은 테스트 코드도 지원합니다. 간단하게 describe, test, expect, beforeEach, afterEach 등이 있습니다. IoT에서 완전히 통제된 테스트를 하는건 힘들지만 그래도 있으니 좋은 것 같습니다.

버퍼를 직접 만들 수 있습니다.

임베디드에선 극한의 메모리 절약이 중요합니다. 이미 타스를 끌어들인 마당에 뭘 줄이겠냐만은 그래도 Buffer를 통해 직접적인 메모리 컨트롤도 지원하고 있습니다.
DeviceScript의 Buffers 문서에 따르면 Buffer를 통해 메모리를 할당받아 읽고 쓸 수 있다고 합니다. 버퍼를 만드는 받벚은 다양한데, 일반적으로 new Buffer() 생성자 함수를 사용하는 방법이 있고 템플릿 리터럴로 빠르게 읽기 전용 버퍼를 만드는 방법도 있습니다.

const buf: Buffer = hex`00 ab 12 2f 00`

추가로 길이가 가변적인 패킷이 있는데, 극한의 절약에 유용할 것 같습니다.

const lamp = new ds.Led()
ds.packet.setLength(2)
ds.packet.setAt(0, "u0.16", 0.7)
lamp.intensity.write(ds.packet)

이제 시작해봅시다.

이 글에선 DeviceScript의 Lightbulb blinking 예제에서 더 나아가 survo motor의 제어 + 가변 저항을 통한 제어까지 구현한 코드를 보여드리지만 DeviceScript가 워낙에 추상화가 잘된 덕에 혼자 문서를 읽어도 쉽게 적응할 수 있습니다. 공식문서의 Getting Start도 잘 마련되어있고, Github issue의 응답률도 매우 좋은 편입니다. (경험상 약 2시간) 그러므로 이 글에선 모든걸 다루지 않고, 문서 위주로 첨언을 합니다.

VisualStudioCode로 시작하기

Visual Studio Code에는 DeviceScript Extension이 있어서 매우 편리한 명령우 팔레트 도구와 dashboard를 사용할 수 있습니다. 이 확장은 지역 CLI를 기반으로 실행되므로 nnm install @devicescript/cli를 통해 지역적으로 cli를 설치해야 합니다.

cli가 설치되지 않으면, 즉 node_modules/.bin이 없거나 비어있으면 deviceScript에게 뭔갈 실행시킬 때 위와 같은 에러 메시지가 등장합니다.

그 외엔 딱히 할 게 없습니다. 그 다음 문서들을 계속 읽어보는걸 잊지 마세요.
시작은 f5로 디버깅을 하는 방법도 있고, 오른쪽 위 툴바에서 버튼을 눌러도 됩니다.

CommandLineInterface로 시작하기

..왜?
CLI로 시작할 경우 연결과 대쉬보드에 관해선 따로 웹페이지를 재공해줍니다. 기본적으로 http://localhost:8081/ 이며 들어가면 아래와 같이 연결, 대쉬보드가 모두 보입니다.

참고로 @devicescript/cli는 전역 전용이 아닙니다.

DeviceScript vscode 확장과의 상호작용을 상시로 하는데, 만약 전역으로 둔다면 언제 어디서든지 vscode의 devicescript가 멋대로 실행하는 대참사가 발생할 수도 있습니다. 메인테이너도 비권장하고, 그 외에 여러 문제가 있어서 개인적으로 vscode를 권장드립니다.

예제

LED 깜빡이기

import { pins } from "@dsboard/adafruit_qt_py_c3"
import { startLightBulb } from "@devicescript/servers"
const lightBulb = startLightBulb({
    pin: pins.A1, //보드에 따라 알아서 pins 가져오세요.
})
setInterval(async () => {
    await lightBulb.toggle()
}, 500)

DeviceScript 공식문서에서 알려주는 것과 별달리 차이가 없지만 여기서 마이크로소프트를 믿고 interval를 1로 때려박다가 불상사가 터질지도 모르니 궁금하면 한번 해보세요. 전 아래 "loopback-rx-ovf"가 터지며 거의 4일을 날렸습니다.

Servo motor 돌리기

import { startServo } from "@devicescript/servers"
import { pins } from "@dsboard/adafruit_qt_py_c3"

const servo = startServo({
    pin: pins.A2,
})
await servo.enabled.write(true)

let i = 0
setInterval(async () => {
    i++
    await servo.angle.write((i % 180) - 90)
}, 100)

서보모터는 기본적으로 꺼져있고 모터의 회전각은 60분법의 "도"입니다.

가변저항으로 servo motor 돌리기

import { debounceTime } from "@devicescript/observables"
import { startPotentiometer, startServo } from "@devicescript/servers"
import { pins } from "@dsboard/adafruit_qt_py_c3"

// 오차범위
const Max_Error_Range = 5

const servo = startServo({
    pin: pins.A2,
})
await servo.enabled.write(true)

const potentio = startPotentiometer({
    pin: pins.A3,
})

potentio.reading
    .pipe(
        debounceTime(10) // 0.01초마다
    )
    .subscribe(async rot => {
        const prev = await servo.angle.read()
        const curr = rot * 180 - 90
        await servo.angle.write(
            prev + Math.clamp(-Max_Error_Range, curr - prev, Max_Error_Range)
        )
    })

대부분의 부품들은 저마다의 Register를 지니고 있습니다. 기본적으로 reading가 그러한데, @devicescript/observables 모듈을 가져오면 모듈에 들어있는 타입 정의에 따라 pipe 메서드를 추가적으로 활용할 수 있게 됩니다. 없었는데 있었어요
이번 예제에선 가변 저항이 매순간 튀는 값이 많아서 0.01초마다 오차범위 내로만 받도록 이중 처리 방식을 사용했습니다.

ThroubleShooting

DeviceScript는 이미 자체적인 throuble shooting 문서를 지니고 있으나 이 레포지토리의 나이가 겨우 반년인 점을 보았을 때 아직 등장하지 않은 엣지 케이스가 수두룩할 것입니다. 실제로 제가 겪었습니다.

"loopback-rx-ovf"

위 메시지가 반복되면서 어느순간 아래와 같이 로그가 뜨며 터미널이 강제 종료됩니다.

무엇을 재부팅하든 해결되지 않습니다.
관련 Github Issue에 따르면 board를 다시 flash하지 않고 먼저 clean를 한 뒤 flash를 해야 한다고 알려줍니다.

CLI 명령어를 찾을 수 없어요.

6.7 제가 겪고 있는 문제인데, 일시적인 해결책으로서 npm run devsyarn devs로 CLI 명령어에 접근할 수 있습니다. bash나 cmd가 명령어를 찾을 수 없으니 yarn이 대신 찾게 해주는거라 결국 설치는 해야 합니다. 설치 확인은 node_modules/.bin/ 에서 직접 확인할 수 있습니다.

더 알아야 할 것

DeviceScript는 공식적으로 npm을 사용합니다.

Github Issue 코멘트에 따르면 가끔 등장할 yarn는 outdated된 것이라고 합니다. npm 사용을 권장드립니다.

모든 하드웨어 모듈을 지원하진 않습니다.

아두이노처럼 pin digital/analog read/write만을 생각해오신 분들에겐 이상한 말일 수 있을겁니다. DeviceScript는 기본적으로 지원하는 모듈들이 몇가지 있습니다. DeviceScript의 Servers API 문서에서 지원하는 모든 모듈들을 확인할 수 있습니다. 이들은 아두이노에 있던 라이브러리를 대체할 수 있을지도 모릅니다.

profile
샤르르르

1개의 댓글

comment-user-thumbnail
2024년 1월 12일

로직에만 집중할 수 있는 개발을 원했는데 환상적이네요

답글 달기