flutter_aclas_scale_plus 1.0.0 copy "flutter_aclas_scale_plus: ^1.0.0" to clipboard
flutter_aclas_scale_plus: ^1.0.0 copied to clipboard

PlatformAndroid

Flutter plugin for ACLAS weight scales over Serial, USB, and BLE. Connect, read weight (live or one-shot), zero, tare, and handle USB attach/detach.

example/lib/main.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_aclas_scale_plus/flutter_aclas_scale_plus.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  try {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    const fatalError = true;

    // Non-async exceptions
    FlutterError.onError = (errorDetails) {
      if (fatalError) {
        // If you want to record a "fatal" exception
        FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
        // ignore: dead_code
      } else {
        // If you want to record a "non-fatal" exception
        FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
      }
    };

    // Async exceptions
    PlatformDispatcher.instance.onError = (error, stack) {
      if (fatalError) {
        // If you want to record a "fatal" exception
        FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
        // ignore: dead_code
      } else {
        // If you want to record a "non-fatal" exception
        FirebaseCrashlytics.instance.recordError(error, stack);
      }
      return true;
    };
  } catch (e, stack) {
    debugPrint('Firebase init failed: $e\n$stack');
  }
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ACLAS Scale Demo',
      theme: ThemeData(useMaterial3: true),
      home: const ScaleDemoPage(),
    );
  }
}

class ScaleDemoPage extends StatefulWidget {
  const ScaleDemoPage({super.key});

  @override
  State<ScaleDemoPage> createState() => _ScaleDemoPageState();
}

class _ScaleDemoPageState extends State<ScaleDemoPage> {
  final FlutterAclasScalePlus _scale = FlutterAclasScalePlus();
  StreamSubscription<AclasScaleEvent>? _eventSub;
  final List<String> _log = [];
  final List<String> _devices = [];
  AclasConnectionType _type = AclasConnectionType.serial;
  String _selectedPath = '';
  bool _connected = false;
  String _weight = '--';
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _listenEvents();
    _init();
  }

  void _listenEvents() {
    _eventSub?.cancel();
    _eventSub = _scale.eventStream.listen((e) {
      if (!mounted) return;
      setState(() {
        switch (e.type) {
          case AclasScaleEventType.connected:
            _log.add('Connected');
            _connected = true;
            break;
          case AclasScaleEventType.disconnected:
            _log.add('Disconnected');
            _connected = false;
            break;
          case AclasScaleEventType.usbAttached:
            _log.add('USB attached');
            _onUsbAttached();
            break;
          case AclasScaleEventType.usbDetached:
            _log.add('USB detached');
            _connected = false;
            break;
          case AclasScaleEventType.weight:
            if (e.weightInfo != null) _weight = e.weightInfo!.weight;
            break;
          case AclasScaleEventType.error:
            _log.add('Error: ${e.errorCode} ${e.errorMessage}');
            break;
          case AclasScaleEventType.crash:
            _log.add('Crash: ${e.errorMessage}');
            _reportToCrashlytics(e);
            break;
        }
      });
    });
  }

  /// Report plugin crash to Firebase Crashlytics so it appears in the dashboard.
  Future<void> _reportToCrashlytics(AclasScaleEvent e) async {
    try {
      final msg = e.errorMessage ?? 'Unknown';
      final stack = e.stackTrace ?? '';
      final type = e.exceptionType ?? '';
      await FirebaseCrashlytics.instance.recordError(
        Exception('AclasScale [$type]: $msg'),
        null,
        fatal: false,
        information: stack.isNotEmpty ? ['Native stack:\n$stack'] : [],
      );
      await FirebaseCrashlytics.instance.sendUnsentReports();
    } catch (_) {}
  }

  Future<void> _init() async {
    setState(() => _loading = true);
    try {
      final ok = await _scale.initDevice(_type);
      if (!mounted) return;
      setState(() {
        _log.add('Init ${_type.name}: ${ok ? "OK" : "fail"}');
        _loading = false;
      });
      if (ok) _loadDevices();
    } catch (e) {
      if (mounted)
        setState(() {
          _log.add('Init error: $e');
          _loading = false;
        });
    }
  }

  Future<void> _loadDevices() async {
    try {
      final list = await _scale.getDeviceList();
      if (mounted)
        setState(() {
          _devices.clear();
          _devices.addAll(list);
          if (_devices.isNotEmpty && _selectedPath.isEmpty)
            _selectedPath = _devices.first;
        });
    } catch (e) {
      if (mounted) setState(() => _log.add('getDeviceList: $e'));
    }
  }

  /// On USB attached: re-init scaler (required after detach), refresh device list, then auto-connect to first device.
  Future<void> _onUsbAttached() async {
    await Future.delayed(const Duration(milliseconds: 300));
    if (!mounted) return;
    // Re-initialize the scaler after USB attach (demo calls InitDevice before open to avoid IllegalThreadStateException).
    bool initOk = false;
    for (int attempt = 0; attempt < 3 && !initOk; attempt++) {
      initOk = await _scale.initDevice(_type);
      if (!mounted) return;
      if (!initOk && attempt < 2) {
        await Future.delayed(const Duration(milliseconds: 300));
        if (!mounted) return;
      }
    }
    if (!initOk) {
      setState(() => _log.add('Reconnect: init failed'));
      return;
    }
    await _loadDevices();
    if (!mounted) return;
    if (_devices.isEmpty) {
      setState(() => _log.add('Reconnect: no devices'));
      return;
    }
    setState(() => _log.add('Reconnecting…'));
    try {
      int ret;
      if (_type == AclasConnectionType.serial) {
        ret = await _scale.openScale(path: _devices.first);
      } else if (_type == AclasConnectionType.usb) {
        ret = await _scale.openScale(index: 0);
      } else {
        ret = await _scale.openScale(address: _devices.first);
      }
      if (!mounted) return;
      setState(() {
        if (ret == 0) {
          _log.add('Reconnected');
          _selectedPath = _devices.first;
        } else if (ret == -4) {
          _log.add('Reconnect: USB permission needed');
        } else {
          _log.add('Reconnect failed: $ret');
        }
      });
    } catch (e) {
      if (mounted) setState(() => _log.add('Reconnect error: $e'));
    }
  }

  Future<void> _open() async {
    if (_loading) return;
    setState(() => _loading = true);
    try {
      int ret;
      if (_type == AclasConnectionType.serial) {
        ret = await _scale.openScale(
          path: _selectedPath.isNotEmpty ? _selectedPath : null,
        );
      } else if (_type == AclasConnectionType.usb) {
        final idx = _devices.indexOf(_selectedPath);
        ret = await _scale.openScale(index: idx >= 0 ? idx : 0);
      } else {
        ret = await _scale.openScale(
          address: _selectedPath.isNotEmpty ? _selectedPath : null,
        );
      }
      if (!mounted) return;
      setState(() {
        _loading = false;
        if (ret == 0)
          _log.add('Open OK');
        else if (ret == -4)
          _log.add('USB permission needed');
        else
          _log.add('Open failed: $ret');
      });
    } on PlatformException catch (e) {
      if (mounted)
        setState(() {
          _loading = false;
          _log.add('Open error: ${e.code} ${e.message}');
        });
    }
  }

  Future<void> _close() async {
    await _scale.closeScale();
    if (mounted)
      setState(() {
        _weight = '--';
        _log.add('Closed');
      });
  }

  Future<void> _startLive() async {
    await _scale.startLiveWeightEnsuringConnection();
    if (mounted) setState(() => _log.add('Live weight started'));
  }

  Future<void> _stopLive() async {
    await _scale.stopLiveWeight();
    if (mounted) setState(() => _log.add('Live weight stopped'));
  }

  Future<void> _zero() async {
    try {
      final ok = await _scale.zero();
      if (mounted) setState(() => _log.add('Zero: ${ok ? "OK" : "fail"}'));
    } catch (e) {
      if (mounted) setState(() => _log.add('Zero error: $e'));
    }
  }

  Future<void> _tare() async {
    try {
      final ok = await _scale.tare();
      if (mounted) setState(() => _log.add('Tare: ${ok ? "OK" : "fail"}'));
    } catch (e) {
      if (mounted) setState(() => _log.add('Tare error: $e'));
    }
  }

  Future<void> _getWeight() async {
    try {
      final w = await _scale.getWeightEnsuringConnection();
      if (mounted)
        setState(() {
          _weight = w?.weight ?? '--';
          _log.add('GetWeight: ${w?.weight ?? "null"}');
        });
    } catch (e) {
      if (mounted) setState(() => _log.add('GetWeight error: $e'));
    }
  }

  @override
  void dispose() {
    _eventSub?.cancel();
    _scale.closeScale();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ACLAS Scale Plus')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text(
            'Connection type',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          Row(
            children: [
              Radio<AclasConnectionType>(
                value: AclasConnectionType.serial,
                groupValue: _type,
                onChanged: _loading
                    ? null
                    : (v) {
                        if (v != null) {
                          setState(() {
                            _type = v;
                            _selectedPath = '';
                            _devices.clear();
                            _init();
                          });
                        }
                      },
              ),
              const Text('Serial'),
              Radio<AclasConnectionType>(
                value: AclasConnectionType.usb,
                groupValue: _type,
                onChanged: _loading
                    ? null
                    : (v) {
                        if (v != null) {
                          setState(() {
                            _type = v;
                            _selectedPath = '';
                            _devices.clear();
                            _init();
                          });
                        }
                      },
              ),
              const Text('USB'),
              Radio<AclasConnectionType>(
                value: AclasConnectionType.ble,
                groupValue: _type,
                onChanged: _loading
                    ? null
                    : (v) {
                        if (v != null) {
                          setState(() {
                            _type = v;
                            _selectedPath = '';
                            _devices.clear();
                            _init();
                          });
                        }
                      },
              ),
              const Text('BLE'),
            ],
          ),
          if (_devices.isNotEmpty) ...[
            const SizedBox(height: 8),
            const Text('Device', style: TextStyle(fontWeight: FontWeight.bold)),
            Builder(
              builder: (context) {
                final uniqueDevices = _devices.toSet().toList();
                final value = uniqueDevices.contains(_selectedPath)
                    ? _selectedPath
                    : uniqueDevices.first;
                return DropdownButton<String>(
                  value: value,
                  items: uniqueDevices
                      .map((d) => DropdownMenuItem(value: d, child: Text(d)))
                      .toList(),
                  onChanged: _loading
                      ? null
                      : (v) => setState(() => _selectedPath = v ?? ''),
                );
              },
            ),
          ],
          const SizedBox(height: 12),
          Row(
            children: [
              ElevatedButton(
                onPressed: _loading || _connected ? null : _open,
                child: const Text('Open'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: !_connected ? null : _close,
                child: const Text('Close'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Text(
            'Weight: $_weight',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              ElevatedButton(
                onPressed: _startLive,
                child: const Text('Start live'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: _connected ? _stopLive : null,
                child: const Text('Stop live'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: _connected ? _getWeight : null,
                child: const Text('Get weight'),
              ),
            ],
          ),
          Row(
            children: [
              ElevatedButton(
                onPressed: _connected ? _zero : null,
                child: const Text('Zero'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: _connected ? _tare : null,
                child: const Text('Tare'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          const Text('Log', style: TextStyle(fontWeight: FontWeight.bold)),
          ..._log.reversed
              .take(20)
              .map(
                (s) => Padding(
                  padding: const EdgeInsets.symmetric(vertical: 2),
                  child: Text(s, style: const TextStyle(fontSize: 12)),
                ),
              ),
        ],
      ),
    );
  }
}
0
likes
160
points
57
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for ACLAS weight scales over Serial, USB, and BLE. Connect, read weight (live or one-shot), zero, tare, and handle USB attach/detach.

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_aclas_scale_plus

Packages that implement flutter_aclas_scale_plus