Skip to content

fix: prevent LA pin crash in multimeter (#2955)#3053

Open
moksha-hub wants to merge 5 commits intofossasia:flutterfrom
moksha-hub:fix/2955-la-pin-crash
Open

fix: prevent LA pin crash in multimeter (#2955)#3053
moksha-hub wants to merge 5 commits intofossasia:flutterfrom
moksha-hub:fix/2955-la-pin-crash

Conversation

@moksha-hub
Copy link

@moksha-hub moksha-hub commented Feb 7, 2026

Fixes #2955

Changes

Screenshots / Recordings

Checklist:

  • No hard coding: I have used values from constants.dart or localization files instead of hard-coded values.
  • No end of file edits: No modifications done at end of resource files.
  • Code reformatting: I have formatted the code using dart format or the IDE formatter.
  • Code analysis: My code passes checks run in flutter analyze and tests run in flutter test.

Summary by Sourcery

Handle invalid or null logic analyzer data when measuring frequency to prevent multimeter crashes and report unmeasurable states to the UI.

Bug Fixes:

  • Guard frequency measurement against null logic analyzer state and invalid digital channel values to avoid runtime crashes when using LA pins.
  • Return a sentinel value for failed frequency measurements and surface a user-friendly "Cannot measure" message in the multimeter display instead of crashing.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 7, 2026

Reviewer's Guide

Adds null-safety and error-handling around logic analyzer frequency measurement to prevent crashes, and propagates error states to the multimeter UI with a user-friendly message instead of crashing.

Sequence diagram for multimeter frequency measurement with null-safety

sequenceDiagram
    actor User
    participant MultimeterUI
    participant MultimeterStateProvider
    participant ScienceLab

    User->>MultimeterUI: Select frequency mode and channel
    MultimeterUI->>MultimeterStateProvider: requestFrequencyMeasurement()
    MultimeterStateProvider->>ScienceLab: getFrequency(selectedChannel)
    activate ScienceLab
    ScienceLab->>ScienceLab: getLAInitialStates()
    alt getLAInitialStates throws
        ScienceLab->>ScienceLab: log error
        ScienceLab-->>MultimeterStateProvider: -1
    else getLAInitialStates returns null
        ScienceLab->>ScienceLab: log error (null data)
        ScienceLab-->>MultimeterStateProvider: -1
    else getLAInitialStates ok
        ScienceLab->>ScienceLab: calculateDigitalChannel(channel)
        alt channelNumber is null
            ScienceLab->>ScienceLab: log error (invalid channel)
            ScienceLab-->>MultimeterStateProvider: -1
        else channelNumber valid
            ScienceLab->>ScienceLab: fetchLAChannelFrequency(channelNumber, data)
            ScienceLab-->>MultimeterStateProvider: frequencyHz
        end
    end
    deactivate ScienceLab

    alt frequency < 0
        MultimeterStateProvider->>MultimeterStateProvider: value = Cannot measure!, unit = empty
    else frequency >= 0
        MultimeterStateProvider->>MultimeterStateProvider: value = formatted frequency, unit = Hz
    end

    MultimeterStateProvider-->>MultimeterUI: updated value and unit
    MultimeterUI-->>User: Show result or Cannot measure!
Loading

Updated class diagram for ScienceLab and MultimeterStateProvider frequency logic

classDiagram
    class ScienceLab {
        +Future~double~ getFrequency(String channel)
        +Future~void~ startOneChannelLA(String channel, int channelMode, int deviceMode)
        -Future~List~ getLAInitialStates()
        -int~?~ calculateDigitalChannel(String channel)
        -Future~double~ fetchLAChannelFrequency(int channelNumber, List data)
    }

    class MultimeterStateProvider {
        -ScienceLab _scienceLab
        -bool isSwitchChecked
        -int _selectedIndex
        -List knobMarker
        -String value
        -String unit
        -AppLocalizations appLocalizations
        +Future~void~ requestFrequencyMeasurement()
    }

    class AppLocalizations {
        +String unitHz
    }

    MultimeterStateProvider --> ScienceLab : uses
    MultimeterStateProvider --> AppLocalizations : uses

    note for ScienceLab "Now returns -1 on errors in getFrequency instead of throwing due to null values or invalid channel."
    note for MultimeterStateProvider "Interprets frequency < 0 as error and sets value to Cannot measure! with empty unit."
Loading

File-Level Changes

Change Details Files
Harden logic analyzer frequency acquisition against null/invalid inputs and failures.
  • Wrap getFrequency logic so that any exception results in logging and a sentinel return value instead of propagating the error
  • Add explicit null check for the LA initial states data and early-return with an error code if it is null
  • Calculate the digital channel into a local variable, validate it for null, and return an error code when the channel is invalid rather than force-unwrapping
lib/communication/science_lab.dart
Update multimeter UI logic to handle frequency acquisition failures gracefully.
  • Interpret negative frequency values as an error condition after calling getFrequency
  • Show a user-facing "Cannot measure!" message with no unit when frequency measurement fails
  • Preserve the existing formatted value and Hz unit display for valid frequency measurements
lib/providers/multimeter_state_provider.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Using -1 as a magic error value for getFrequency makes the API harder to reason about; consider returning null, a Result/wrapper type, or throwing and handling a specific exception instead of overloading a numeric return value.
  • The user-facing string "Cannot measure!" should be sourced from your localization resources instead of being hard-coded so it can be translated consistently.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `-1` as a magic error value for `getFrequency` makes the API harder to reason about; consider returning `null`, a `Result`/wrapper type, or throwing and handling a specific exception instead of overloading a numeric return value.
- The user-facing string "Cannot measure!" should be sourced from your localization resources instead of being hard-coded so it can be translated consistently.

## Individual Comments

### Comment 1
<location> `lib/communication/science_lab.dart:612-621` </location>
<code_context>
       await Future.delayed(const Duration(milliseconds: 250));
     } catch (e) {
       logger.e("Error in getFrequency: $e");
+      return -1;
+    }
+
+    // Null check to prevent crash when getLAInitialStates fails
+    if (data == null) {
+      logger.e("Error in getFrequency: getLAInitialStates returned null");
+      return -1;
     }
-    return await fetchLAChannelFrequency(
-        calculateDigitalChannel(channel)!, data!);
+
+    int? channelNumber = calculateDigitalChannel(channel);
+    if (channelNumber == null) {
+      logger.e("Error in getFrequency: invalid channel $channel");
+      return -1;
+    }
+
+    return await fetchLAChannelFrequency(channelNumber, data);
   }

</code_context>

<issue_to_address>
**suggestion:** Consider avoiding a magic sentinel value (-1) to indicate frequency read errors.

Using `-1` as an error marker tightly couples the protocol to a magic value and relies on all callers remembering to treat `< 0` as an error. Prefer making the error path explicit in the type (e.g. `Future<double?>` or a small result type with `value` and `error`). If changing the signature is too disruptive, at least extract the sentinel into a named constant (e.g. `const double frequencyError = -1;`) to reduce the risk of misuse.

Suggested implementation:

```
    } catch (e) {
      logger.e("Error in getFrequency: $e");
      return frequencyError;
    }

```

```
    if (data == null) {
      logger.e("Error in getFrequency: getLAInitialStates returned null");
      return frequencyError;
    }

```

```
    int? channelNumber = calculateDigitalChannel(channel);
    if (channelNumber == null) {
      logger.e("Error in getFrequency: invalid channel $channel");
      return frequencyError;
    }

```

1. Define a shared sentinel constant in this file, for example near the top-level declarations (or close to the `getFrequency` function), to avoid repeating the magic value:
   ```dart
   const double frequencyError = -1;
   ```
2. If `getFrequency` returns `int` rather than `double`, adjust the type accordingly:
   ```dart
   const int frequencyError = -1;
   ```
3. Replace any other occurrences of `return -1;` from `getFrequency` (or related frequency-reading functions) with `return frequencyError;` to keep error handling consistent.
</issue_to_address>

### Comment 2
<location> `lib/providers/multimeter_state_provider.dart:249-250` </location>
<code_context>
-        value = frequency.toStringAsFixed(2);
-        unit = appLocalizations.unitHz;
+        if (frequency < 0) {
+          value = "Cannot measure!";
+          unit = "";
+        } else {
+          value = frequency.toStringAsFixed(2);
</code_context>

<issue_to_address>
**issue:** Use localized strings instead of a hardcoded error message for the UI.

This widget otherwise uses `appLocalizations`, but this error text is a hardcoded English string. Please add it to your localization resources (e.g. `appLocalizations.multimeterCannotMeasure`) and reference that here to keep the UI consistent and translatable.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@moksha-hub moksha-hub force-pushed the fix/2955-la-pin-crash branch from 1a0251a to b32d204 Compare February 8, 2026 05:02
@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

Build Status

Build successful. APKs to test: https://github.com/fossasia/pslab-app/actions/runs/22219339477/artifacts/5587422776.

Screenshots

Android Screenshots
iPhone Screenshots
iPad Screenshots

@moksha-hub
Copy link
Author

@h4ck3rm1k3 @fcartegnie @yarons @maxinbjohn Please have a review for the PR. Thank you.

@marcnause marcnause changed the title fix: add null safety checks to prevent LA pin crash in multimeter (#2… fix: prevent LA pin crash in multimeter (#2955) Feb 8, 2026
@marcnause marcnause requested a review from Copilot February 8, 2026 16:31

This comment was marked as outdated.

@moksha-hub
Copy link
Author

@h4ck3rm1k3 @fcartegnie @yarons @maxinbjohn please have a review on it.
Thank you.

This comment was marked as outdated.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

return frequencyError;
}

return await fetchLAChannelFrequency(channelNumber, data);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchLAChannelFrequency() can still throw (e.g., via null-assertions inside it when LA buffer fetch fails or expected keys are missing). Since getFrequency is now using a sentinel error value (frequencyError), it should also wrap the fetchLAChannelFrequency call in a try/catch (or otherwise guarantee it can’t throw) and return frequencyError on failure.

Suggested change
return await fetchLAChannelFrequency(channelNumber, data);
try {
return await fetchLAChannelFrequency(channelNumber, data);
} catch (e) {
logger.e("Error in getFrequency while fetching LA channel frequency: $e");
return frequencyError;
}

Copilot uses AI. Check for mistakes.
@marcnause
Copy link
Contributor

@moksha-hub, could you please have a look at the comments by Copilot (#3053 (review))? I think they make sense.

fetchLAChannelFrequency can throw exceptions (e.g., via null-assertions
when LA buffer fetch fails or expected keys are missing). Wrap it in a
try/catch so any failure returns frequencyError instead of propagating
the exception and crashing the multimeter.

Addresses Copilot review comment on PR fossasia#3053.
@moksha-hub
Copy link
Author

@marcnause please have a review.
Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash when selecting LA pin in multimeter

3 participants