本文へスキップ
株式会社オープンナレッジ
株式会社オープンナレッジ Open Knowledge Inc.
現場モバイル・エッジ技術 読了目安:約3分(平易な表現解説)

電波の届きにくい倉庫・訪問診療・工場でも0.1秒で動くモバイル基盤: 〜「カメラで撮るだけ・3秒話すだけ」で完結する Flutter × Rust × 暗号化金庫〜

監修:株式会社オープンナレッジ 代表取締役CTO 加藤 明生(上級医療情報技師 / 登録情報処理安全確保支援士)

1. 基本と全体像

この記事の要点と4つの章のご案内

「1行書くたびに本社へ電話する社員」と「鍵付きポケット手帳を持つ社員」に関するイメージ写真
「1行書くたびに本社へ電話する社員」と「鍵付きポケット手帳を持つ社員」
電波がなくても止まらない「3層モバイル基盤」の仕組みに関するイメージ写真
電波がなくても止まらない「3層モバイル基盤」の仕組み
電波の届きにくい倉庫・訪問診療・工場でも0.1秒で動くモバイル基盤: 〜「カメラで撮るだけ・3秒話すだけ」で完結する Flutter × Rust × 暗号化金庫〜 の各章におけるテーマと要点一覧表
章構成 主なテーマ 要点と確認ポイント
1. 基本と全体像 「1行書くたびに本社へ電話する社員」と「鍵付きポケット手帳を持つ社員」 はい、0.1秒で普段通りに動きます。一般的なクラウド専用アプリは毎回インターネットの向こう側へ通信するため電波が弱いと止まってしまいますが、当社のモバイル基盤はスマートフォン内部の暗号化金庫(SQLite + SQLCipher)にまず記録し、電波が戻った瞬間に裏側で自動同期する「ローカル・ファースト設計」を採用しているためです。
2. 現場での進め方と対策 電波がなくても止まらない「3層モバイル基盤」の仕組み 文字打ちはほとんど必要ありません。領収書や点検表を「カメラで撮るだけ」、または作業後に「3秒間声で話すだけ」で、アプリ内のAIとRustエンジンが自動的に整理し、最大3つの大きなボタンから選ぶだけで完了します。
3. 関連する制度とルール 手袋をしたままでも、夕方の疲れた目でも「迷わず押せる」優しさの設計 個人の写真フォルダ(カメラロール)には一切保存されません。撮影した瞬間にアプリ内部の暗号化領域(AES-256暗号化)へ直接封印されるため、万が一端末を紛失した場合や私有スマホの業務利用(BYOD)でも、経済産業省「SCS評価制度(★3)」の基準を満たす高い安全性を保てます。
4. 詳しい技術仕様としくみ 端末内カメラロール分離・SQLCipher暗号化・CRDT同期の検証基準 監修:株式会社オープンナレッジ 代表取締役CTO 加藤 明生(上級医療情報技師 / 登録情報処理安全確保支援士)
第1章〜第4章では、実務チェックリスト・公的制度基準・詳しい技術仕様を確認できます。
この記事の要点

「電波ゼロの場所でも0.1秒で手元の暗号化手帳に記録し、電波が戻ったら自動で本社へ届ける。」

現場で使うスマホアプリが「電波が悪くてずっと読み込み中のまま動かない」「細かい文字入力が大変で使われなくなる」という失敗をなくすため、私たちは滑らかな画面(Flutter)・超高速の共通頭脳(Rust)・端末内の暗号化金庫(SQLite暗号化)を1つに統合しました。医療の訪問先、冷凍倉庫、建設現場でもストレスゼロで動きます。

身近なたとえ話と3つの公的データ

「1行書くたびに本社へ電話する社員」と「鍵付きポケット手帳を持つ社員」

一般的なクラウド型アプリは、たとえるなら「メモを1行書くたびに、毎回本社のパソコンへ電話をかけて許可をもらう仕組み」です。オフィスの中なら問題ありませんが、地下の倉庫やエレベーターの中、山間部の訪問先などで電話(電波)が途切れた瞬間、画面が真っ白になって仕事が止まってしまいます。

これに対してオープンナレッジの「ローカル・ファースト(手元優先)設計」は、スマホの中に「指紋や暗証番号がないと原則として開かない鍵付きポケット手帳」を持たせる仕組みです。

現場では電波の有無に関係なく0.1秒で手元の手帳にサクサク記録でき、建物の外に出て電波がつながった瞬間に、裏側のプログラムが自動的に本社の金庫(`db01`)へ安全にコピーを届けます。もちろん、業務で撮った写真が個人のアルバムに混ざることもありません。

ポイント 01 画面の反応スピード(オフライン時)
0.1 秒未満

端末内のRustエンジンと暗号化SQLiteが直接処理するため、電波がない場所でも待ち時間ゼロ(120fps描画)で動作します。

ポイント 02 iPhone / Android 共通コード率
95 % 以上共通

iOSとAndroidを別々に2回作る必要がなく、開発・改修の期間と費用を約半分に抑えながら計算ズレをゼロにします。

ポイント 03 個人アルバムへの業務写真残留
0 枚(安全な分離)

撮影した伝票や現場写真はAES-256暗号化領域へ直行するため、個人用カメラロールからのうっかり漏洩を100%防ぎます。

全体構造の図解・比較表

電波がなくても止まらない「3層モバイル基盤」の仕組み

スマートフォン・タブレット内部(電波ゼロでも0.1秒で完結) 01 基本的な画面 (Flutter 120fps) ・カメラで撮る ・3秒で話す ・大きな3ボタン ⇄ 02 超高速の頭脳 (Rust FFIコア) ・書類の傾き補正 ・金額の即時検算 ・個人情報の保護 ⇄ 03 端末内金庫 (暗号化SQLite) ・AES-256暗号化 ・写真フォルダ隔離 ・即座に一時保存 電波回復時 → 自動で差分同期 本社・専用金庫サーバー (db01 / PostgreSQL 19) ・事務所のPCへ即時反映 ・ZFS不変バックアップ保護
比較項目 一般的なブラウザ・Webアプリ iOS / Android 別々の従来開発 推奨設計:Flutter × Rust オフライン基盤
地下・倉庫・移動中 電波が切れるとエラーになり入力内容が消える オフライン対応の追加開発に高額費用が必要 標準で0.1秒オフライン記録+自動再同期
開発・保守の費用 初期費用は安いが操作性が重い 2つのチームが必要で費用・期間が約2倍 1つの共通Rust頭脳でiOS/Android両対応
端末紛失時の安全性 ブラウザのキャッシュや写真フォルダに残る危険 設計次第でバラつきが出やすい 端末内AES-256暗号化+カメラロール全体非保存
背景の仕組みと今日からできる3つの対策

手袋をしたままでも、夕方の疲れた目でも「迷わず押せる」優しさの設計

私たちが現場向けのモバイルアプリを作るとき、一番大切にしているのは「明るいオフィスの机の上」ではなく、「薄暗い倉庫の奥」「雨の日の配送トラックの運転席」「訪問介護を終えた夕方の車内」でどう見えるか、どう触れるかです。

たとえば、軍手や医療用手袋をしたままでも押し間違えないよう、ボタンの大きさは人間工学に基づいた48ピクセル以上のゆとりサイズに設計し、画面の中の入力項目は最大3つ以内に絞っています。

また、当社が研究開発を進める手話・筋電(sEMG)アシスト技術『もしもの話』と同じく、スマートフォンの内部で音声やセンサー情報を瞬時に処理できるため、インターネット回線が混み合う災害時や緊急時にも「普段通りに使える命綱」として機能します。

今日から確認できる「現場スマホ・タブレット活用」3つの安全チェック

※クリックして自社の現場状況をチェックしてみましょう(外部送信されません)

モバイル暗号化領域・オフライン同期検証マトリクスと関連コラム

端末内カメラロール分離・SQLCipher暗号化・CRDT同期の検証基準

現場のスマートフォンやタブレットで伝票や本人確認書類を扱う際、個人用アルバムや個人クラウドへの誤同期を防ぎ、圏外でも業務を止めないための技術検証項目です。

保護対象 実装アーキテクチャ セキュリティ・可用性保証
撮影画像の隔離保管 App-Scoped Sandbox + In-Memory Stream OS標準カメラロールや個人クラウド(iCloud/Googleフォト)へ1バイトも書き出さない
オフラインDB暗号化 SQLCipher AES-256-CBC + OS Keystore Secure Enclave / StrongBox 内のハードウェア保護鍵で4KBページ単位に暗号化
圏外・再接続時の自動統合 HLC (Hybrid Logical Clock) LWW-CRDT 地下や倉庫の圏外環境でも0.1秒でローカル保存し、電波回復時に競合なく差分同期
関連コラム・豆知識:旅客機のコックピットでも使われている「SQLite」

私たちがスマートフォン内部の暗号化金庫として採用しているデータベース「SQLite(エスキューライト)」は、世界で最も多く使われているソフトウェアの一つで、エアバス社の最新旅客機(A350)のフライトソフトにも採用されています。その理由は、開発チームが「プログラム本体よりも1,000倍以上多いテストコード(数億通りの過酷な故障テスト)」をパスさせてから公開しているため、高度1万メートルのオフライン環境でも決して壊れないからです。

参考窓口のご案内

ご質問・公的制度の活用に関するお問い合わせ窓口

本ページの解説内容に関するご質問や、経済産業省「サプライチェーン強化に向けたセキュリティ対策評価制度(SCS評価制度)」・IPA「SECURITY ACTION」等の公的制度活用、地域の防犯・教育講習に関するお問い合わせは、以下の窓口にて承っております。

メール窓口:e@ok.st お電話:072-628-9664(平日 9:00〜17:00)
技術仕様・実装解説:Technical Architecture & Implementation — Hybrid Logical Clocks (HLC) & Join-Semilattice CvRDT Convergence

圏外・災害現場でも原則としてデータが衝突しない Hybrid Logical Clocks (HLC) と状態ベース CvRDT(半束合流)の数理証明

Kulkarni et al. HLC / Shapiro et al. CRDT / Rust FFI
1. 設計仕様・数理モデル(Formal Architecture, Kernel Internals & Mathematical Formulation)

医療訪問看護・冷凍倉庫検品・災害避難所運営などのエッジ現場では、地下や山間部でのモバイル通信途絶(Network Partition)が日常的に発生します。CAP定理(Gilbert & Lynch, 2002)により、分断耐性(Partition Tolerance)の下で即時応答性(Availability: 0ms ローカル書き込み)を保証するには、結果整合性モデルを採用しつつ、再接続時の更新競合(Write-Write Conflict)を人間の手動マージなしで数学的に収束させる必要があります。

当社のモバイルコア(Rust 2024 FFI ライブラリ + SQLite WAL モード)は、物理時計(NTP)のドリフトや逆行に耐性を持つ Hybrid Logical Clock(HLC: Kulkarni et al., 2014) を64ビット整数(上位48-bit 物理ミリ秒 l.j、下位16-bit 論理カウンタ c.j)へパッキングして全レコードに付与します。イベント e → f(Lamport 因果先行関係)に対し、物理時計が多少ずれていても必ず (l.e, c.e, nodee) < (l.f, c.f, nodef) の全順序(Total Order)が保証されます。

さらに各データ構造を結び半束(Join-Semilattice \langle S, \sqcup \rangle)をなす状態ベース CRDT(CvRDT)として定義し、マージ演算子 \sqcup が 01 交換律(A \sqcup B = B \sqcup A)、02 結合律((A \sqcup B) \sqcup C = A \sqcup (B \sqcup C))、03 べき等律(A \sqcup A = A)の3公理を満たすことをコンパイル時テストで検証しています。これにより、パケットの再送・順序入れ替わり・複数端末の同時オフライン編集が起きても、全端末の状態は1ビットの狂いもなく同一の最小上界(Least Upper Bound: \sup(A, B))へ収束します。

2. パラメータ・構成仕様(Byte & Parameter Layout) Primary Spec
Algebraic Property / BitfieldMathematical Definition64-Bit / Memory RepresentationField Operational Guarantee
HLC Physical ll' = max(l, ptwall, lmsg)Bits 63..16 (48-bit Unix ms)Monotonic even if device clock skews backward
HLC Logical cIncremented when l' == lBits 15..0 (16-bit counter, 65535/ms)Captures sub-millisecond causal ordering
Commutativitys1 \sqcup s2 = s2 \sqcup s1Lexicographic (HLC, NodeId) tie-breakOrder of sync packet arrival never matters
Idempotences1 \sqcup s1 = s1Pure functional state transitionsSafe retry over lossy 4G/5G/Wi-Fi links
3. 実装コード・設定仕様(src/mobile_core/hlc_cvrdt_lattice.rs)
Rust Edition 2024 (#![no_std] 64-Bit Packed HLC & Semilattice CvRDT)
#![no_std]
#![forbid(unsafe_code)]

/// 64-bit packed Hybrid Logical Clock: [48-bit physical_ms | 16-bit logical_counter]
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct PackedHlc64(pub u64);

impl PackedHlc64 {
    #[inline]
    pub const fn new(physical_ms: u64, logical: u16) -> Self {
        Self(((physical_ms & 0xFFFF_FFFF_FFFF) << 16) | (logical as u64))
    }

    #[inline]
    pub const fn physical_ms(self) -> u64 {
        self.0 >> 16
    }

    #[inline]
    pub const fn logical(self) -> u16 {
        (self.0 & 0xFFFF) as u16
    }

    /// Local event tick: advances monotonically even if wall clock regresses.
    pub const fn tick_local(self, wall_ms: u64) -> Self {
        let old_l = self.physical_ms();
        if wall_ms > old_l {
            Self::new(wall_ms, 0)
        } else {
            Self::new(old_l, self.logical().saturating_add(1))
        }
    }

    /// Receive remote message HLC: enforces causal ordering e -> f => HLC(e) < HLC(f).
    pub const fn update_remote(self, remote: Self, wall_ms: u64) -> Self {
        let l_self = self.physical_ms();
        let l_rem = remote.physical_ms();
        let max_l = if wall_ms >= l_self && wall_ms >= l_rem {
            wall_ms
        } else if l_self >= l_rem {
            l_self
        } else {
            l_rem
        };

        let next_c = if max_l == l_self && max_l == l_rem {
            let m = if self.logical() >= remote.logical() { self.logical() } else { remote.logical() };
            m.saturating_add(1)
        } else if max_l == l_self {
            self.logical().saturating_add(1)
        } else if max_l == l_rem {
            remote.logical().saturating_add(1)
        } else {
            0
        };
        Self::new(max_l, next_c)
    }
}

/// Join-Semilattice LWW-Register CRDT with deterministic (HLC, NodeId) total order.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct LwwRegister<T: Copy + PartialEq + Eq> {
    pub value: T,
    pub hlc: PackedHlc64,
    pub node_id: u32,
}

impl<T: Copy + PartialEq + Eq> LwwRegister<T> {
    /// Least Upper Bound (Join operator `sqcup`): associative, commutative, idempotent.
    #[inline]
    pub const fn merge(self, other: Self) -> Self {
        if self.hlc.0 > other.hlc.0 {
            self
        } else if self.hlc.0 < other.hlc.0 {
            other
        } else if self.node_id >= other.node_id {
            self
        } else {
            other
        }
    }
}

図解・ビジュアルの拡大表示

図表内のテキストもそのまま選択・検索可能です(ベクターSVG描画) 枠外クリックまたは ESC キーで閉じます