bluetooth_thermometer 0.9.3 copy "bluetooth_thermometer: ^0.9.3" to clipboard
bluetooth_thermometer: ^0.9.3 copied to clipboard

discontinued
retracted[pending analysis]

A Flutter package for connecting to and reading temperature from ThermoWorks Bluetooth thermometers (Thermapen Blue, TempTest Blue).

example/lib/main.dart

// @dart=3.10
import 'package:flutter/material.dart';
import 'package:bluetooth_thermometer/bluetooth_thermometer.dart';
import 'info_screen.dart';

void main() => runApp(const MaterialApp(home: ThermometerExample()));

class ThermometerExample extends StatefulWidget {
  final ThermometerClient? client;

  const ThermometerExample({this.client, super.key});

  @override
  State<ThermometerExample> createState() => _ThermometerExampleState();
}

class _ThermometerExampleState extends State<ThermometerExample> {
  late ThermometerClient _client;
  List<ThermometerDevice> _devices = [];
  bool _isScanning = false;

  @override
  void initState() {
    super.initState();
    _client = widget.client ?? ThermometerClient();

    _client.devices.listen((list) => _safeSetState(() => _devices = list));

    _client.errorStream.listen((error) {
      if (!mounted) return;
      error.isPermissionError
          ? context.showPermissionDialog()
          : ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Error: ${error.message}'), backgroundColor: Colors.red),
            );
    });
  }

  Future<void> _toggleScan() async {
    if (_isScanning) {
      await _client.stopScan();
      _safeSetState(() => _isScanning = false);

      return;
    }

    _safeSetState(() {
      _isScanning = true;
      _devices = [];
    });

    try {
      await _client.scanForDevices();
      if (mounted && _devices.isEmpty) context.showNoDevicesDialog();
    } catch (e) {
      if (mounted && e.isPermissionError) context.showPermissionDialog();
    } finally {
      _safeSetState(() => _isScanning = false);
    }
  }

  Future<void> _autoConnect() async {
    if (_isScanning) return;

    _safeSetState(() {
      _isScanning = true;
      _devices = [];
    });

    try {
      final success = await _client.autoConnect();
      if (!success && mounted) {
        context.showInfoDialog(
          title: 'Auto Connect Failed',
          content: 'No known devices found nearby.\nTry scanning manually first.',
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Auto-connect error: $e'), backgroundColor: Colors.red),
        );
      }
    } finally {
      _safeSetState(() => _isScanning = false);
    }
  }

  Future<void> _connectToDevice(ThermometerDevice device) async {
    await _client.stopScan();
    _safeSetState(() => _isScanning = false);
    try {
      _client.connect(device);
    } catch (_) {}
  }

  @override
  Widget build(BuildContext context) => ThermometerLifecycleManager(
    client: _client,
    onAutoConnectSuccess: () {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Auto-connected successfully!')));
      }
    },
    onIdleDisconnect: () {
      if (mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(const SnackBar(content: Text('Disconnected due to inactivity')));
      }
    },
    child: Scaffold(
      appBar: AppBar(
        title: const Text('Thermapen Blue Example'),
        actions: [
          ThermometerSelector(client: _client),
          IconButton(
            icon: const Icon(Icons.info_outline),
            onPressed: () => Navigator.of(
              context,
            ).push<void>(MaterialPageRoute<void>(builder: (_) => const InfoScreen())),
            tooltip: 'Help & Permissions',
          ),
        ],
      ),
      body: StreamBuilder<ThermometerConnectionState>(
        stream: _client.connectionStateStream,
        builder: (_, snapshot) {
          final state = snapshot.data ?? .disconnected;

          return switch (state) {
            .connected => _ConnectedView(client: _client),
            .connecting => const _ConnectingView(),
            _ => _ScanView(
              isScanning: _isScanning,
              devices: _devices,
              onToggleScan: _toggleScan,
              onAutoConnect: _autoConnect,
              onDeviceSelected: _connectToDevice,
              client: _client, // Pass client
            ),
          };
        },
      ),
    ),
  );

  @override
  void dispose() {
    _client.dispose();
    super.dispose();
  }

  void _safeSetState(VoidCallback fn) {
    if (mounted) setState(fn);
  }
}

class _ScanView extends StatelessWidget {
  final bool isScanning;
  final List<ThermometerDevice> devices;
  final VoidCallback onToggleScan;
  final VoidCallback onAutoConnect;
  final ValueChanged<ThermometerDevice> onDeviceSelected;
  final ThermometerClient? client; // Added client

  const _ScanView({
    required this.devices,
    required this.isScanning,
    required this.onAutoConnect,
    required this.onDeviceSelected,
    required this.onToggleScan,
    this.client, // Added client
  });

  @override
  Widget build(BuildContext context) => Column(
    children: [
      Padding(
        padding: const .all(16),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: .spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: onToggleScan,
                  child: Text(isScanning ? 'Scanning...' : 'Scan'),
                ),
                OutlinedButton(
                  onPressed: isScanning ? null : onAutoConnect,
                  child: const Text('Auto Connect'),
                ),
              ],
            ),
            if (client != null) ...[
              const SizedBox(height: 16),
              ThermometerSelector(
                client: client!,
                builder: (context, showModal) => TextButton.icon(
                  onPressed: showModal,
                  icon: const Icon(Icons.list),
                  label: const Text('Open Device List (Bottom Sheet)'),
                ),
              ),
            ],
          ],
        ),
      ),
      Expanded(
        child: devices.isEmpty
            ? Center(
                child: Text(
                  isScanning
                      ? 'Scanning for thermometers...'
                      : 'Tap "Scan" to find devices.\nSee (i) for help.',
                  textAlign: .center,
                  style: const TextStyle(color: Colors.grey),
                ),
              )
            : ListView.builder(
                itemCount: devices.length,
                itemBuilder: (_, i) => _DeviceListTile(devices[i], onTap: onDeviceSelected),
              ),
      ),
    ],
  );
}

class _DeviceListTile extends StatelessWidget {
  final ThermometerDevice device;
  final ValueChanged<ThermometerDevice> onTap;

  const _DeviceListTile(this.device, {required this.onTap});

  @override
  Widget build(BuildContext context) => ListTile(
    title: Text(device.name),
    subtitle: Text('ID: ${device.id}\nSignal: ${device.signalStrength.label}'),
    trailing: const Icon(Icons.chevron_right),
    onTap: () => onTap(device),
  );
}

class _ConnectedView extends StatelessWidget {
  final ThermometerClient client;

  const _ConnectedView({required this.client});

  @override
  Widget build(BuildContext context) => Center(
    child: Column(
      mainAxisAlignment: .center,
      children: [
        const Text('Connected to Thermapen Blue', style: TextStyle(fontSize: 18)),
        const SizedBox(height: 32),
        ThermometerCaptureDisplay(client: client),
      ],
    ),
  );
}

class _ConnectingView extends StatelessWidget {
  const _ConnectingView();

  @override
  Widget build(BuildContext context) => const Center(
    child: Column(
      mainAxisAlignment: .center,
      children: [CircularProgressIndicator(), SizedBox(height: 16), Text('Connecting...')],
    ),
  );
}

extension on Object {
  bool get isPermissionError {
    final msg = toString().toLowerCase();

    return msg.contains('permission') || msg.contains('unauthorized');
  }
}

extension on BuildContext {
  void showPermissionDialog() => _showInfoDialog(
    title: 'Permissions Required',
    content:
        'Bluetooth permissions are missing or denied.\n\n'
        'Please go to your system settings and manually grant Bluetooth '
        'and Location permissions for this app.',
  );

  void showNoDevicesDialog() => _showInfoDialog(
    title: 'No Devices Found',
    content:
        'No Thermapen Blue devices were found.\n\n'
        '1. Ensure the device is powered ON (probe open).\n'
        '2. Ensure it is within range.\n'
        '3. Check permissions in the Info screen.',
  );

  void showInfoDialog({required String content, required String title}) =>
      _showInfoDialog(title: title, content: content);

  void _showInfoDialog({required String content, required String title}) => showDialog<void>(
    context: this,
    builder: (context) => AlertDialog(
      title: Text(title),
      content: Text(content),
      actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('OK'))],
    ),
  );
}
0
likes
0
points
--
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package for connecting to and reading temperature from ThermoWorks Bluetooth thermometers (Thermapen Blue, TempTest Blue).

Repository (GitHub)
View/report issues

License

(pending) (license)

Dependencies

clock, flutter, flutter_reactive_ble, logger, permission_handler, shared_preferences, smart_permission

More

Packages that depend on bluetooth_thermometer