はじめに:現場のタブレットに課せられた役割
ネットワークから切り離されたタブレットの役割は、「未送信の検査データをローカルDBに一時蓄積し、PCと接続できた瞬間にそれらを一括放流すること」です。
今回は、CommunityToolkit.Mvvm を使用したクリーンなViewModelと、Android固有機能(BluetoothAdapter/TcpListener)を組み合わせた実装を解説します。
1. Android固有のBluetooth送信サービス
Bluetooth通信では、標準的なSPP (Serial Port Profile) のUUIDを使用します。これにより、前回のPC側(WPF)の BluetoothListener と通信が可能になります。
実装のポイント
UUIDの固定:00001101-0000-1000-8000-00805f9b34fbを使用します。
RFCOMMソケット: ソケットを作成し、ConnectAsync() でペアリング済みPCに接続します。
終端文字: 受信側の ReadLine() を成立させるため、JSONの末尾に \n を付与します。
#if ANDROID
public async Task<bool> SendDataAsync(BluetoothDeviceDto deviceDto, string json)
{
BluetoothAdapter adapter = BluetoothAdapter.DefaultAdapter;
BluetoothDevice device = adapter.GetRemoteDevice(deviceDto.Address);
BluetoothSocket socket = null;
try {
socket = device.CreateRfcommSocketToServiceRecord(SppUuid);
await socket.ConnectAsync();
using var stream = socket.OutputStream;
byte[] data = Encoding.UTF8.GetBytes(json + "\n"); // 終端文字が肝
await stream.WriteAsync(data, 0, data.Length);
await stream.FlushAsync();
return true;
}
catch (Exception) { return false; }
finally { socket?.Close(); }
}
#endif
2. USB(ADBポートフォワーディング)での送信
Bluetoothが不安定な現場を想定し、USB有線接続もサポートします。ここでは、Android側で TcpListener を立てて待ち受け、PC側(ADB接続)からの要求に対してデータを流し込みます。
public async Task<bool> SendDataAsync(string json)
{
TcpListener server = new TcpListener(IPAddress.Any, 12345);
server.Start();
// PC側(ADB)からの接続を30秒間待機
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
// ... (Pendingを確認しながらAcceptTcpClient)
using var stream = tcpClient.GetStream();
byte[] data = Encoding.UTF8.GetBytes(json + "\n");
await stream.WriteAsync(data, 0, data.Length);
}
3. ViewModelでの同期管理
ViewModelでは、「未送信件数の把握」と「同期成功後のステータス更新」を管理します。
同期フローの制御
データのシリアライズ: DBから取得した未送信レコードを JsonSerializer で一行の文字列に。
送信実行: 選択されたモード(BT/USB)で送信。
DB更新: 送信成功時のみ、ローカルDBの「送信済みフラグ」を更新。これにより二重送信を防止します。
[RelayCommand(CanExecute = nameof(CanSync))]
private async Task SyncNow()
{
IsBusy = true;
var unsentRecords = await _repository.GetUnsentRecordsAsync();
string jsonPayload = JsonSerializer.Serialize(unsentRecords);
bool success = IsBluetoothMode
? await _bluetoothService.SendDataAsync(SelectedDevice, jsonPayload)
: await _usbService.SendDataAsync(jsonPayload);
if (success) {
// 送信成功したレコードのみフラグを立てる
foreach (var record in unsentRecords) {
await _repository.UpdateSyncStatusAsync(record.InspectionId);
}
await RefreshUnsentCount(); // 画面上の「未送信:0件」へ
}
}
4. 現場での運用の注意点
① 権限の壁(Android 12以降)
Bluetoothのデバイス検索や接続には、BLUETOOTH_CONNECT や BLUETOOTH_SCAN 権限が必要です。MAUI側で Permissions.RequestAsync を呼び出し、ユーザーに許可を求めるフローが必須となります。
② ペアリング済みリストの活用
現場でスムーズに接続するため、あらかじめPCとタブレットをOS設定でペアリングしておき、アプリ側では GetPairedDevices() で取得したリストからPCを選択するだけの設計にしています。
③ 送信完了のフィードバック
ネットワークがない環境では、「本当に送れたのか」がユーザーにとって最大の不安要素です。送信成功時に DisplayAlert で件数を明示し、ローカルDBの状態を即座にUIに反映させることが重要です。
まとめ
.NET MAUI側の送信コードが完成したことで、PC側と対になる 「オフライン同期システム」 のパズルが完成しました。
MAUI: 未送信データをJSON化してBT/USBソケットに流し込む。
WPF: 受信したJSONをバラして隔離ネットワーク内のDBへ仲介する。
この構成により、Wi-Fiに一切頼らない、堅牢なデータ収集ソリューションが実現できました。