flutter_aclas_scale_plus 1.0.0
flutter_aclas_scale_plus: ^1.0.0 copied to clipboard
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)),
),
),
],
),
);
}
}