LoopBack 4とハードウェアインターフェースの連携
Node.jsエコシステムにおけるIoTソリューションの構築において、高レベルなAPIフレームワークと低レベルなハードウェア通信層の橋渡しは重要な課題となります。LoopBack 4はTypeScriptベースの強力なAPIフレームワークであり、その依存性注入(Dependency Injection)システムを活用することで、シリアル通信などの物理インターフェース管理を効率的かつ疎結合に実装できます。本記事では、LoopBack 4を使用して外部デバイスとシリアル通信を行い、REST API経由で遠隔制御を行うアーキテクチャと実装コードを解説します。
ハードウェア通信層の抽象化
LoopBack 4の最大の特徴は、クラスとインターフェースを通じてロジックをモジュール化できる点です。デバイスとの物理的な接続(ここではシリアルポート)を、アプリケーションのコアロジックから分離するために、Providerパターンを使用します。これにより、通信ポートの実装詳細を隠蔽し、テスト容易性を高めることができます。
以下は、serialportライブラリを使用してシリアル接続を管理するプロバイダーの実装例です。このクラスは、起動時にポートをオープンし、接続インスタンスをアプリケーション全体で利用可能にします。
import {Provider, ValueOrPromise} from '@loopback/core';
import {SerialPort} from 'serialport';
export class SerialConnectionProvider implements Provider<SerialPort> {
/**
* コンストラクタで設定値を受け取ることも可能ですが、
* ここでは固定設定として実装しています。
*/
constructor() {}
value(): ValueOrPromise<SerialPort> {
// ボーレートやパスは環境に合わせて調整してください
const connection = new SerialPort({
path: '/dev/ttyUSB0',
baudRate: 9600,
});
return Promise.resolve(connection);
}
}
データモデルの定義
クライアントから送信されるコマンドやデバイスの状態を表現するために、LoopBack 4のモデルを定義します。このモデルは、APIのリクエストボディのバリデーションや、データベースへの永続化(必要な場合)にも利用されます。
import {model, property} from '@loopback/repository';
@model()
export class DeviceInstruction extends Entity {
@property({
type: 'string',
idGenerated: true,
})
id?: string;
@property({
type: 'string',
required: true,
})
payload: string;
@property({
type: 'boolean',
default: false,
})
isExecuted: boolean;
constructor(data?: Partial<DeviceInstruction>) {
super(data);
}
}
通信ロジックのサービス化
実際のデータ送信ロジックは、コントローラーから分離してサービスクラスに実装します。ここで先ほど定義したSerialPortのインスタンスを注入し、デバイスへの書き込み処理を行います。エラーハンドリングや非同期処理を適切に行うことで、APIの応信性を保ちます。
import {injectable, inject} from '@loopback/core';
import {SerialPort} from 'serialport';
import {DeviceInstruction} from '../models';
export const HARDWARE_SERVICE_BINDING = 'services.HardwareService';
@injectable()
export class HardwareService {
constructor(
@inject('services.SerialConnection') private serialConn: SerialPort,
) {}
/**
* デバイスにコマンドを送信するメソッド
*/
async transmitInstruction(instruction: DeviceInstruction): Promise<void> {
return new Promise((resolve, reject) => {
// 改行コードの付加やエンコーディング処理はプロトコルに応じて変更
this.serialConn.write(instruction.payload + '\n', 'utf8', (err) => {
if (err) {
reject(new Error(`Transmission failed: ${err.message}`));
} else {
resolve();
}
});
});
}
}
REST APIエンドポイントの実装
最後に、クライアントからのHTTPリクエストを受け付けるコントローラーを作成します。コントローラーはHardwareServiceに依存しており、リクエストを受け取るとサービス層のメソッドを呼び出します。
import {post, requestBody, RestBindings} from '@loopback/rest';
import {inject} from '@loopback/core';
import {DeviceInstruction} from '../models';
import {HardwareService} from '../services/hardware.service';
export class DeviceCommandController {
constructor(
@inject(HARDWARE_SERVICE_BINDING)
private hardwareService: HardwareService,
) {}
@post('/device/control')
async sendControlSignal(
@requestBody({
content: {
'application/json': {
schema: {
type: 'object',
required: ['payload'],
properties: {
payload: {type: 'string'},
},
},
},
},
})
instruction: DeviceInstruction,
): Promise<object> {
try {
await this.hardwareService.transmitInstruction(instruction);
return {
status: 'success',
message: 'Command transmitted to hardware interface.',
};
} catch (error) {
// LoopBackのエラーハンドリングに委ねる
throw error;
}
}
}
この構成により、LoopBack 4アプリケーションはRESTfulなインターフェースを通じて物理デバイスと安全に通信できます。シリアルポートの設定変更や通信プロトコルの修正が必要になった場合でも、プロバイダーやサービス層のみを変更すれば良く、APIの仕様に影響を与えにくい保守性の高いシステムとなります。