mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Show self sensor default
This commit is contained in:
3
.github/workflows/build-artifacts.yml
vendored
3
.github/workflows/build-artifacts.yml
vendored
@@ -193,6 +193,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Install macOS native build tools
|
||||||
|
run: brew install automake libtool
|
||||||
|
|
||||||
- name: Build macOS release
|
- name: Build macOS release
|
||||||
run: flutter build macos --release
|
run: flutter build macos --release
|
||||||
|
|
||||||
|
|||||||
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000814">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000217">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.353323">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.503497">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="115.148883">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="103.12875">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="204.902574">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="58490.472877">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -474,6 +474,21 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
bool isWatched(String publicKeyHex) =>
|
bool isWatched(String publicKeyHex) =>
|
||||||
_watchedSensorKeys.contains(publicKeyHex);
|
_watchedSensorKeys.contains(publicKeyHex);
|
||||||
|
|
||||||
|
Contact? selfContact(
|
||||||
|
ContactsProvider contactsProvider,
|
||||||
|
ConnectionProvider connectionProvider,
|
||||||
|
) {
|
||||||
|
final selfKey = connectionProvider.deviceInfo.publicKey;
|
||||||
|
if (selfKey == null || selfKey.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existing = contactsProvider.findContactByKey(
|
||||||
|
Uint8List.fromList(selfKey),
|
||||||
|
);
|
||||||
|
return existing ?? _buildSelfCandidate(connectionProvider);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> addSensor(Contact contact) async {
|
Future<void> addSensor(Contact contact) async {
|
||||||
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
|
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
|
||||||
return;
|
return;
|
||||||
@@ -549,17 +564,65 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Contact> availableCandidates(ContactsProvider contactsProvider) {
|
List<Contact> availableCandidates(
|
||||||
|
ContactsProvider contactsProvider, {
|
||||||
|
ConnectionProvider? connectionProvider,
|
||||||
|
}) {
|
||||||
final candidates = <Contact>[
|
final candidates = <Contact>[
|
||||||
...contactsProvider.chatContacts,
|
...contactsProvider.chatContacts,
|
||||||
...contactsProvider.repeaters,
|
...contactsProvider.repeaters,
|
||||||
...contactsProvider.sensorContacts,
|
...contactsProvider.sensorContacts,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add "myself" (own device) as a candidate if it has a public key
|
||||||
|
if (connectionProvider != null) {
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
if (self != null &&
|
||||||
|
!candidates.any((c) => c.publicKeyHex == self.publicKeyHex)) {
|
||||||
|
candidates.insert(0, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
candidates.removeWhere((contact) => isWatched(contact.publicKeyHex));
|
candidates.removeWhere((contact) => isWatched(contact.publicKeyHex));
|
||||||
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> displaySensorKeys({
|
||||||
|
required ContactsProvider contactsProvider,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
}) {
|
||||||
|
if (_watchedSensorKeys.isNotEmpty) {
|
||||||
|
return List<String>.unmodifiable(_watchedSensorKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
if (self == null) {
|
||||||
|
return const <String>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <String>[self.publicKeyHex];
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact? contactForDisplay(
|
||||||
|
String publicKeyHex, {
|
||||||
|
required ContactsProvider contactsProvider,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
}) {
|
||||||
|
for (final entry in contactsProvider.contacts) {
|
||||||
|
if (entry.publicKeyHex == publicKeyHex) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
if (self?.publicKeyHex == publicKeyHex) {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> refreshAll({
|
Future<void> refreshAll({
|
||||||
required ContactsProvider contactsProvider,
|
required ContactsProvider contactsProvider,
|
||||||
required ConnectionProvider connectionProvider,
|
required ConnectionProvider connectionProvider,
|
||||||
@@ -597,13 +660,11 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
||||||
|
|
||||||
Contact? contact;
|
final contact = contactForDisplay(
|
||||||
for (final entry in contactsProvider.contacts) {
|
publicKeyHex,
|
||||||
if (entry.publicKeyHex == publicKeyHex) {
|
contactsProvider: contactsProvider,
|
||||||
contact = entry;
|
connectionProvider: connectionProvider,
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
||||||
@@ -686,4 +747,25 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Contact? _buildSelfCandidate(ConnectionProvider connectionProvider) {
|
||||||
|
final deviceInfo = connectionProvider.deviceInfo;
|
||||||
|
final selfKey = deviceInfo.publicKey;
|
||||||
|
if (selfKey == null || selfKey.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Contact(
|
||||||
|
publicKey: Uint8List.fromList(selfKey),
|
||||||
|
type: ContactType.chat,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: deviceInfo.selfName ?? 'My Device',
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: deviceInfo.advLat ?? 0,
|
||||||
|
advLon: deviceInfo.advLon ?? 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,10 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
Future<void> _showAddSensorSheet(BuildContext context) async {
|
Future<void> _showAddSensorSheet(BuildContext context) async {
|
||||||
final sensorsProvider = context.read<SensorsProvider>();
|
final sensorsProvider = context.read<SensorsProvider>();
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
final candidates = sensorsProvider.availableCandidates(contactsProvider);
|
final candidates = sensorsProvider.availableCandidates(
|
||||||
|
contactsProvider,
|
||||||
|
connectionProvider: context.read<ConnectionProvider>(),
|
||||||
|
);
|
||||||
|
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -127,7 +130,11 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
'Add sensor node',
|
'Add sensor node',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(AppLocalizations.of(context)!.pickARelayOrNodeToWatchInSensors),
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.pickARelayOrNodeToWatchInSensors,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
...candidates.map(
|
...candidates.map(
|
||||||
(contact) => ListTile(
|
(contact) => ListTile(
|
||||||
@@ -275,58 +282,72 @@ class _SensorsTabState extends State<SensorsTab> {
|
|||||||
onPressed: () => _showAddSensorSheet(context),
|
onPressed: () => _showAddSensorSheet(context),
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
body: Consumer2<SensorsProvider, ContactsProvider>(
|
body: Consumer3<SensorsProvider, ContactsProvider, ConnectionProvider>(
|
||||||
builder: (context, sensorsProvider, contactsProvider, child) {
|
builder:
|
||||||
final watchedKeys = sensorsProvider.watchedSensorKeys;
|
(
|
||||||
|
context,
|
||||||
|
sensorsProvider,
|
||||||
|
contactsProvider,
|
||||||
|
connectionProvider,
|
||||||
|
child,
|
||||||
|
) {
|
||||||
|
final displayKeys = sensorsProvider.displaySensorKeys(
|
||||||
|
contactsProvider: contactsProvider,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
|
final hasPersistedSensors =
|
||||||
|
sensorsProvider.watchedSensorKeys.isNotEmpty;
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () => _refreshAll(context),
|
onRefresh: () => _refreshAll(context),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 96),
|
||||||
children: [
|
children: [
|
||||||
if (watchedKeys.isEmpty)
|
if (displayKeys.isEmpty)
|
||||||
const _EmptySensorsState()
|
const _EmptySensorsState()
|
||||||
else
|
else
|
||||||
...watchedKeys.map((key) {
|
...displayKeys.map((key) {
|
||||||
Contact? contact;
|
final contact = sensorsProvider.contactForDisplay(
|
||||||
for (final entry in contactsProvider.contacts) {
|
key,
|
||||||
if (entry.publicKeyHex == key) {
|
contactsProvider: contactsProvider,
|
||||||
contact = entry;
|
connectionProvider: connectionProvider,
|
||||||
break;
|
);
|
||||||
}
|
final availableFieldKeys = sensorMetricKeysFor(contact);
|
||||||
}
|
final visibleFields = sensorsProvider
|
||||||
final availableFieldKeys = sensorMetricKeysFor(contact);
|
.effectiveVisibleFieldsFor(key, availableFieldKeys);
|
||||||
final visibleFields = sensorsProvider
|
return SensorTelemetryCard(
|
||||||
.effectiveVisibleFieldsFor(key, availableFieldKeys);
|
contact: contact,
|
||||||
return SensorTelemetryCard(
|
state: sensorsProvider.stateFor(key),
|
||||||
contact: contact,
|
visibleFields: visibleFields,
|
||||||
state: sensorsProvider.stateFor(key),
|
fieldOrder: sensorsProvider.metricOrderFor(
|
||||||
visibleFields: visibleFields,
|
key,
|
||||||
fieldOrder: sensorsProvider.metricOrderFor(
|
availableFieldKeys,
|
||||||
key,
|
),
|
||||||
availableFieldKeys,
|
labelOverrides: sensorsProvider.labelOverridesFor(
|
||||||
),
|
key,
|
||||||
labelOverrides: sensorsProvider.labelOverridesFor(key),
|
),
|
||||||
fieldSpans: {
|
fieldSpans: {
|
||||||
for (final field in visibleFields)
|
for (final field in visibleFields)
|
||||||
field: sensorsProvider.fieldSpanFor(key, field),
|
field: sensorsProvider.fieldSpanFor(key, field),
|
||||||
},
|
},
|
||||||
onRemove: () async {
|
onRemove: hasPersistedSensors
|
||||||
await sensorsProvider.removeSensor(key);
|
? () async {
|
||||||
},
|
await sensorsProvider.removeSensor(key);
|
||||||
onCustomize: () =>
|
}
|
||||||
_showMetricSelector(context, key, contact),
|
: null,
|
||||||
onRefresh: () => sensorsProvider.refreshSensor(
|
onCustomize: () =>
|
||||||
publicKeyHex: key,
|
_showMetricSelector(context, key, contact),
|
||||||
contactsProvider: contactsProvider,
|
onRefresh: () => sensorsProvider.refreshSensor(
|
||||||
connectionProvider: context.read<ConnectionProvider>(),
|
publicKeyHex: key,
|
||||||
),
|
contactsProvider: contactsProvider,
|
||||||
);
|
connectionProvider: connectionProvider,
|
||||||
}),
|
),
|
||||||
],
|
);
|
||||||
),
|
}),
|
||||||
);
|
],
|
||||||
},
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -876,55 +897,8 @@ class _EmptySensorsState extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EmptySensorsStateState extends State<_EmptySensorsState> {
|
class _EmptySensorsStateState extends State<_EmptySensorsState> {
|
||||||
bool _discoveryTriggered = false;
|
|
||||||
bool _discoveryInProgress = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// Auto-trigger sensor discovery when the empty state is shown
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_autoDiscover();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _autoDiscover() async {
|
|
||||||
if (_discoveryTriggered || !mounted) return;
|
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
|
||||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
|
||||||
|
|
||||||
_discoveryTriggered = true;
|
|
||||||
setState(() => _discoveryInProgress = true);
|
|
||||||
try {
|
|
||||||
await connectionProvider.discoverNodeType(advertType: 4);
|
|
||||||
} finally {
|
|
||||||
if (mounted) setState(() => _discoveryInProgress = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _discoverSensors() async {
|
|
||||||
if (_discoveryInProgress || !mounted) return;
|
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
|
||||||
if (!connectionProvider.deviceInfo.isConnected) return;
|
|
||||||
|
|
||||||
setState(() => _discoveryInProgress = true);
|
|
||||||
try {
|
|
||||||
await connectionProvider.discoverNodeType(advertType: 4);
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(AppLocalizations.of(context)!.sensorDiscoverySent)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (mounted) setState(() => _discoveryInProgress = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isConnected =
|
|
||||||
context.watch<ConnectionProvider>().deviceInfo.isConnected;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 64),
|
padding: const EdgeInsets.symmetric(vertical: 64),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -951,29 +925,10 @@ class _EmptySensorsStateState extends State<_EmptySensorsState> {
|
|||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Use + to add discovered relays or nodes. Pull down to refresh telemetry after adding them.',
|
'Use + to add discovered relays or nodes. Your device will appear here automatically when available.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: isConnected && !_discoveryInProgress
|
|
||||||
? _discoverSensors
|
|
||||||
: null,
|
|
||||||
icon: _discoveryInProgress
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.sensors_outlined),
|
|
||||||
label: Text(_discoveryInProgress
|
|
||||||
? 'Discovering...'
|
|
||||||
: 'Discover Sensors'),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2026.0319.1+35
|
version: 2026.0320.1+36
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
Reference in New Issue
Block a user