ドローンデモ
Lambda C は ドローンを始めとする自律ロボット制御 を主要なターゲットとして設計されました。このページでは、リポジトリに同梱されている demos/raylib_drone/ を題材に、組み込み制御を Lambda C でどう構造化するかを示します。

なぜドローンが Lambda C の最初のターゲットなのか
自律ドローンのソフトウェアは古典的に2層に分かれます。
- エアフレーム / HAL 層 — モータ・センサ・電源・armed 状態などを管理する低レベル C。安全性検証の対象であり、変更は慎重に行う。
- オートパイロット / ミッション層 — 「離陸して B 点まで飛び、荷物を降ろし、A に戻る」といった意図を表現する。検証は容易だが、現場ごとにルートやポリシーが変わるため頻繁に書き換わる。
これは Lambda C のコア設計と一致します:
- 下層 (HAL) は C のホスト側 に置く — 直接 MCU を叩き、calibrated/armed 限界を判定し、サチュレートや拒否を行う。
- 上層 (ミッション) は Lambda C スクリプト に置く —
.lcbcバイトコードとして配備し、署名済み OTA で差し替える。
スクリプトは「やりたいこと」を表明し、ホストの FFI が「物理的に可能か」を判定する。これは航空宇宙・自動車制御で標準的な階層分離そのものです。Lambda C はこの境界を 約 100 ns のロード時リンク済 FFI で一直線に通す目的で作られています。
同梱デモが行うこと
地点 A で荷物を受け取り、B に配達し、A に戻る (ENABLE_POINT_C = 1 で A → B → C → A の三角ルートに拡張)。
IDLE → TAKEOFF → FLY_TO_B → HOVER_B → DESCEND_B → DROP
│
▼
TAKEOFF_B → (FLY_TO_C → HOVER_C → DESCEND_C → WAIT_C → TAKEOFF_C)?
│
▼
FLY_TO_A → HOVER_A → DESCEND_A → LANDED
実装されているもの:
- ミッションステートマシン — 単純往復
scripts/delivery.cと巡回scripts/patrol.cの 2 本 (11 状態、三角ルートで最大 17 状態) - 高度/位置制御 —
altitude_hold()とfly_toward()による比例制御 - raylib による 3D 可視化 — リアルタイムにドローン挙動を確認
- スクリプト切替 —
Rキーで 2 本の挙動スクリプトをホットスワップ (VM 状態は維持)。編集して保存すれば自動リロード - トレースモード —
Tキーで命令単位の実行ログ - Watchdog — フレームあたり 16ms で実行中断 (
lcvm_set_watchdog_frame(16000))
画面左上にはテレメトリ HUD (高度・位置・スロットル・姿勢・ARMED 状態・ミッション状態・荷物状態・選択中ルート) が、右上にはスクリプト状態と FPS が表示されます。
操作キー:
| キー | 動作 |
|---|---|
R | スクリプトを切替 — 単純往復 delivery.c (A→B→A) ⇔ 巡回 patrol.c (A→B→C→A) |
1 / 2 / 3 | 巡航高度を 3m / 5m / 10m に変更 |
Space | ドローン位置をリセット |
T | トレースモードのトグル |
ESC | 終了 |
R を押すと、ホストが SDK のスクリプトパスを差し替えて再ロードし、まったく別の挙動スクリプトに切り替わります (main_sdk.c の swap_script())。2 本とも荷物を A で積み B で降ろす配送ミッションで、違いは経路だけ — 直行 (A→B→A) か C 地点を経由する三角ルート (A→B→C→A) か。スクリプトをエディタで直接編集して保存すれば自動でリロードされ、1/2/3 は巡航高度をその場で書き換える便利キーです (script_patcher.h 参照)。
スクリプト層: ミッションの「意図」を書く
delivery.c は 物理を知らない。drone_get_altitude() や drone_set_throttle() といった FFI を通じて意図を表明するだけです。
/* FFI 宣言: ホストが何を提供してくれるか */
double drone_get_time();
double drone_get_altitude();
double drone_get_x();
double drone_get_z();
void drone_set_arm(int armed);
void drone_set_throttle(double throttle);
void drone_set_pitch(double pitch);
void drone_set_roll(double roll);
void drone_set_led(int r, int g, int b);
/* ミッションステートマシン */
switch (g_mission_state) {
case 1: /* TAKEOFF — 巡航高度まで上昇 */
drone_set_throttle(altitude_hold(CRUISE_ALT));
if (at_altitude(CRUISE_ALT)) g_mission_state = 2;
break;
case 2: /* FLY_TO_B — 配達地点へ */
drone_set_throttle(altitude_hold(CRUISE_ALT));
fly_toward(POINT_B_X, POINT_B_Z);
if (at_position(POINT_B_X, POINT_B_Z)) {
g_hover_start_time = drone_get_time();
g_mission_state = 3;
}
break;
/* ... */
}
このスクリプトは:
- 約 5KB のバイトコード にコンパイルされる
- GC を持たない ため一定時間内で実行が完了する
- クロージャや動的型がない ため挙動が予測可能
- 書き換えてもホストの C コードを再コンパイルする必要がない
ホスト層: 物理と「実行可能性」を所有する
ホスト側 (main_sdk.c と drone_api.c) が物理シミュレーションと FFI 実装を持ちます。実機では同じ位置にモータドライバ、IMU、armed 検証ロジックが入る想定です。
/* SDK が要求する 2 つのコールバックだけ */
static void on_init(LcvmState *vm, void *user) {
/* フレーム watchdog: 60fps ≒ 16ms */
lcvm_set_watchdog_frame(16000);
/* FFI を一括登録 (drone_ffi.h から自動生成) */
lcvm_register_ffi_all();
}
static void on_frame(LcvmState *vm, float dt, void *user) {
UserState *state = (UserState *)user;
state->sim_time += dt;
drone_set_time(state->sim_time);
/* スクリプトが書き込んだ throttle/pitch/roll を物理に反映 */
drone_physics_update(dt);
}
ホスト側 FFI 実装の中で実行可能性を判定します。例:
drone_set_throttle(double)はスクリプトからの値を [0.0, 1.0] にクランプするdrone_set_arm(int)は GPS / バッテリ状態を検証してから armed フラグを変える (実機の場合)- センサ読み取り FFI はキャリブレーション済みの物理量を返す
スクリプトが「スロットル 2.0」と要求しても、HAL が拒否ないし飽和させる。スクリプトに無制限の権限はない — これが Lambda C の安全境界です。
FFI: 意図と実行を繋ぐ薄い境界
ドローンデモは わずか 12 個の FFI で完結します:
/* drone_ffi.h - 12 関数だけ */
double drone_get_time();
double drone_get_altitude();
double drone_get_x();
double drone_get_z();
void drone_set_arm(int armed);
void drone_set_mission_state(int state);
void drone_set_has_package(int has);
void drone_set_throttle(double throttle);
void drone_set_pitch(double pitch);
void drone_set_roll(double roll);
void drone_set_yaw(double yaw);
void drone_set_led(int r, int g, int b);
これだけで配達ミッション全体が記述できる、というのが要点です。lcvmc --gen-ffi drone_ffi.h で登録関数 lcvm_register_ffi_all() が自動生成されるため、手書きの FFI ボイラープレートはほぼ不要です。
実行時は ロード時に名前→ID へ解決済み であり、毎フレーム数百回呼ばれても約 100 ns/呼び出しの定数時間です。
ホットリロード: 飛行中のドローンを止めずにミッションを書き換える
デモを実行中に scripts/delivery.c を開き、巡航高度などの値を編集して保存します:
double CRUISE_ALT = 5.0; /* 8.0 に変えて保存してみる */
保存すると SDK がファイル変更を検知し、lcvmc が自動的に走って新しい .lcbc がロードされ、飛行中のドローンが次のフレームから新しい高度で動く。VM 状態 (現在の位置・速度・armed フラグ) はそのままです。さらに R キーを押せば、delivery.c (単純往復) と patrol.c (巡回) の別スクリプトへ丸ごと切り替わります。
これがどう実機に効くのか:
- 試作機の現場検証で、コーディング → ビルド → 再起動 → アーミング → 再現待ち、のサイクルを潰せる
- 製品段階では同じ仕組みが 署名済み
.lcbcの OTA 配信 に直結する - ホスト C コードは認証済みのまま、ミッション層だけを更新する運用が可能
Simulator SDK: 同じ設計を自分のドメインへ
ドローンデモは Lambda C Simulator SDK (lib/lcvm_sim.h) のリファレンス実装です。SDK は次を提供します:
- スクリプトのロード / 再リンク
- 自動コンパイル付きのホットリロード (
lcvm_sim_reload()) - エラー時の安全な復帰 (
setjmp/longjmp) - ファイル監視によるバックグラウンド再ロード
顧客が実装するのは2つのコールバックだけ:
lcvm_sim_config_t cfg = {
.script_path = "mission.c",
.enable_hot_reload = 1,
.on_init = on_init, /* FFI 登録 */
.on_frame = on_frame, /* 物理 + 描画 */
.user_data = &state,
};
lcvm_sim_ctx_t *sim = lcvm_sim_create(&cfg);
while (running) {
lcvm_sim_update(sim, dt);
}
ドローンは一例にすぎず、同じパターンは次のような領域にそのまま展開できます:
- 産業ロボット — 関節角と把持状態をスクリプトで指令、ホストが IK と干渉チェックを担当
- HVAC / 給湯器 — 温度設定とバルブ制御をスクリプトで、熱モデルと安全インターロックをホストで
- 自動倉庫 / AGV — ルーティングをスクリプトで、SLAM とモータ制御をホストで
各ドメインのコールバック例は サンプル を参照してください。
試してみる
cd demos/raylib_drone
make
./drone_emu_sdk # Windows なら drone_emu_sdk.exe
# またはリポジトリルートから: .\run_delivery.ps1
走らせたら:
Rで 単純往復 (delivery.c) と巡回 (patrol.c) を切替 — ホストが別スクリプトをロードし、飛行中のドローンが次フレームから新ルートで動く1/2/3で巡航高度を 3 / 5 / 10 m に変更scripts/delivery.cをエディタで直接編集して保存 — 自動でリロードされ飛行中に反映Tでトレースモード切替 — コンソールに命令ストリームが流れるSpaceでドローン位置リセット — ミッションは継続ESCで終了
scripts/delivery.c / scripts/patrol.c をベースに自分のミッションを書いてみるのが、Lambda C を理解する最短ルートです。