Skip to content

Conversation

@mountrcg
Copy link
Contributor

@mountrcg mountrcg commented Oct 17, 2025

The PR aims to

  1. make the Trio-Garmin watchface configurable from Trio on the phone.
  2. implement proper sensitivityRate for Trio users
  3. provide support for the popular AAPS watchface by Swissalpine
  • also provide structures to setup other watchfaces and datafields
  1. provide data queueing that supports the unique and seldomly used Garmin message queue
  2. remedy the issues of watchfaces becoming un-responsive after another watchface has been used, requiering the user to re-install the Trio watchface
  • this is due to Trio still providing messages after the watchface has been completely de-activated and some severe system disadvantages of the Garmin message queue

Above the hood only the Garmin configuration view has been expanded to support different watchfaces, data type selection and switching off the messaging queue for the watchface if it is not being used. Comprehensive Help sheets have been created to explain the mechanisms.

App config Watchface hint Data queueing hint
Simulator Screenshot - Dev 16 Pro - 2025-10-30 at 12 27 26 Simulator Screenshot - Dev 16 Pro - 2025-10-17 at 18 09 32 Simulator Screenshot - Dev 16 Pro - 2025-10-17 at 18 09 38

Also attached the workflow with Garmin device enabled and without to seperate Device connection setup from Garmin App settings.:

Simulator.Screen.Recording.-.Dev.16.Pro.-.2025-10-30.at.12.29.47.mov

Testing can only be done with real Garmin watches and appropriate prg-files sideloaded onto them.

The watchface & datafield has been re-worked to work with the config options from Trio, spacing for all models has been setup dynamically. The watchface can be found in a newly setup repository. Installables are in the release section. Happy to transfer that to nightscout if deemed appropriate.

Watchface Datafield
Screenshot 2025-10-15 at 18 00 59
Screenshot 2025-10-15 at 18 00 16

As one could also use the popular Swissalpine xDrip+/Spike watchface, I will make the derivative for Trio available in the release section as prg's only, once Swissalpine agrees. It works quite well!

Screenshot 2025-10-27 at 11 20 10

Of course one could always use the original Swissalpine watchface/datafield with nightscout, but it would be an online only solution needing LTE/Wifi, Nightscout site and Garmin Connect running on phone. Fine in most situations.
Here I strive for true offline capabilities, works even without Garmin Connect running.

@mountrcg
Copy link
Contributor Author

mountrcg commented Oct 18, 2025

mock Garmin watch commit shall be reverted when merging in dev, it is just for better testing

@avouspierre
Copy link
Contributor

I have the opportunity to have an access to vivoactive5 from my daughter. Use this feature + create the watch face for this specific watch 🔥 ➡️
IMG_3827

I had a issue for the update until I installed the data field (I started to install only the watch face) but not sure it

@mountrcg
Copy link
Contributor Author

mountrcg commented Oct 21, 2025

I had a issue for the update until I installed the data field (I started to install only the watch face) but not sure it

no the datafield / watchface are totally independant,is not needed. Just takes a minute or 2 sometimes, the g or sens arrows will disappear on the first update of watch. Up/Down also helps.

Copy link
Contributor

@marv-out marv-out left a comment

Choose a reason for hiding this comment

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

Haven't reviewed everything (its a lot), but I already made some comments from looking at the most critical parts. I also have no Garmin device so its all only code-based

"timeInRangeType": "timeInTightRange"
"timeInRangeType": "timeInTightRange",
"garminWatchface": "trio",
"garminDataType1": "cob",
Copy link
Contributor

Choose a reason for hiding this comment

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

there are probably more descriptive variables possible

@@ -1,17 +0,0 @@
import Foundation
Copy link
Contributor

Choose a reason for hiding this comment

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

why was there even autotune in this branch? was it based on latest dev?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes the files were still in dev, just the reference was removed


/// Primary data type selection for Garmin watchface and datafield.
/// Determines whether to display COB or Sensitivity Ratio alongside glucose data.
enum GarminDataType1: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
Copy link
Contributor

Choose a reason for hiding this comment

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

see comment above about naming

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GarminWatchSettings.swift

  • GarminDataType1 (enum) → GarminPrimaryDataField
  • GarminDataType2 (enum) → GarminSecondaryDataField
  • garminDisableWatchfaceDataisWatchfaceDataEnabled
    • ⚠️ Boolean logic reversed: Default changed from true (disabled) to false (disabled)

New struct definition:

struct GarminWatchSettings: Codable, Hashable {
    var watchface: GarminWatchface = .trio
    var primaryDataField: GarminPrimaryDataField = .cob
    var secondaryDataField: GarminSecondaryDataField = .tbr
    var isWatchfaceDataEnabled: Bool = false
}

WatchConfigStateModel.swift

  • garminDataType1primaryDataField
  • garminDataType2secondaryDataField
  • garminDisableWatchfaceDataisWatchfaceDataEnabled
    • ⚠️ Boolean logic reversed: Default changed from true to false
  • isDisableToggleLockedisWatchfaceDataCooldownActive
  • remainingCooldownSecondswatchfaceSwitchCooldownSeconds
  • cooldownTimerwatchfaceSwitchTimer
  • cooldownEndTimewatchfaceSwitchCooldownEndTime

Copy link
Contributor

Choose a reason for hiding this comment

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

GarminWatchSettings.swift

  • GarminDataType1 (enum) → GarminPrimaryDataField
  • GarminDataType2 (enum) → GarminSecondaryDataField
  • garminDisableWatchfaceDataisWatchfaceDataEnabled

@mountrcg These changes of the name of the attributes must be apply in the garminWatchFace and garminFieldFace code to allow to use it.

statusData = {
                "sgv" => status["sgv"],
                "..."
                "displayDataType1" => status["displayPrimaryAttributeChoice"],
                "displayDataType2" => status["displaySecondaryAttributeChoice"],
                "date" => status["date"]
            };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@avouspierre I moved that around a lot. It is fixed now, but moved it to different repo, sending you invites.

let watchState = try await self.setupGarminWatchState()
let watchStateData = try JSONEncoder().encode(watchState)
self.sendWatchStateData(watchStateData)
// Check loop age
Copy link
Contributor

Choose a reason for hiding this comment

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

theres a lot happening now in the sink closure of the publisher. I would probably separate this into smaller parts for readability

Copy link
Contributor Author

Choose a reason for hiding this comment

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

could you elaborate on this please, may be small example

let watchState = try await self.setupGarminWatchState()
let watchStateData = try JSONEncoder().encode(watchState)
self.sendWatchStateData(watchStateData)
let watchface = self.currentWatchface
Copy link
Contributor

Choose a reason for hiding this comment

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

see comment above reg. readability


/// Formats a Date to HH:mm:ss string for logging
private func formatTimeForLog(_ date: Date = Date()) -> String {
let formatter = DateFormatter()
Copy link
Contributor

Choose a reason for hiding this comment

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

same problem with the formatter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

} else {
let parsedIsf = Double(truncating: insulinSensitivity).asMmolL
let parsedEventualBG = Double(truncating: eventualBG).asMmolL
// EventualBG with validation (stored as Int16 mg/dL in CoreData)
Copy link
Contributor

Choose a reason for hiding this comment

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

you are validating those values several times in the same way. Please use helper methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tried, not sure it's better


/// Enable simulated Garmin device for Xcode Simulator testing
/// When true, creates a fake Garmin device so you can test the workflow in Simulator
#if targetEnvironment(simulator)
Copy link
Contributor

Choose a reason for hiding this comment

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

does this work?

Copy link
Contributor Author

@mountrcg mountrcg Oct 25, 2025

Choose a reason for hiding this comment

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

yep, very nicely, it just creates a fake garmin device which allows the testing of UI elements in sim, which would not be possible without real phone and watch

return
}

// Start new 30s timer ONLY if none exists
Copy link
Contributor

Choose a reason for hiding this comment

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

creating a timer on the main thread is probably a bad idea and will block the UI. I would suggest using a dedicated queue

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}

// Process each glucose reading (up to 24)
for (index, glucose) in glucoseObjects.enumerated() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this could be optimized in terms of efficiency and readability. Calculate timestamp outside of the loop, use an early return to avoid unnecessary watchstate initialization...

maybe sth like this:

  let mostRecentTimestamp: UInt64? = {
                    if let latestDetermination = determinationObjects.first,
                       let loopTimestamp = latestDetermination.timestamp {
                        return UInt64(loopTimestamp.timeIntervalSince1970 * 1000)
                    }
                    return nil
                }()

                // Process each glucose reading (up to 24)
                for (index, glucose) in glucoseObjects.enumerated() {
                    // Validate glucose value first
                    let glucoseValue = glucose.glucose
                    guard glucoseValue >= 0, glucoseValue <= 500 else {
                        if self.debugWatchState {
                            debug(.watchManager, "⌚️ SwissAlpine: Invalid glucose value (\(glucoseValue)), excluding from data")
                        }
                        continue
                    }

                    var watchState = GarminSwissAlpineWatchState()

                    // Set timestamp: use determination timestamp for most recent, glucose timestamp for everything previously
                    if index == 0 {
                        watchState.date = mostRecentTimestamp ?? glucose.date.map { UInt64($0.timeIntervalSince1970 * 1000) }
                    } else {
                        watchState.date = glucose.date.map { UInt64($0.timeIntervalSince1970 * 1000) }
                    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@mountrcg mountrcg changed the title Initialize Garmin feature branch incl. Mock Garmin watch for Sim Draft: Initialize Garmin feature branch incl. Mock Garmin watch for Sim Oct 25, 2025
*make the Trio-Garmin watchface configurable from Trio on the phone.
* implement proper sensitivityRate for Trio users
* provide support for the popular AAPS watchface by Swissalpine
  * also provide structures to setup other watchfaces and datafields
* provide data queueing that supports the unique and seldomly used Garmin message queue
* remedy the issues of watchfaces becoming un-responsive after another watchface has been used, requiering the user to re-install the Trio watchface
  * This is due to Trio still providing messages after the watchface has been completely de-activated and some severe system disadvantages of the Garmin message queue

Above the hood only the Garmin configuration view has been expanded to support different watchfaces, data type selection and switching off the messaging queue for the watchface if it is not being used. Comprehensive Help sheets have been created to explain the mechanisms.
seperate watch settings from device list

customize navigation to skip device list

add a mock garmin device in simulator

doc

common data structure


fix unnecessary number conversions

harmonize guards and default values across watch states

swissalpine datafield uuid

common data structure from AAPS
clean logging

app installed check before prep

further optimizations

debounce WatchState data prep from multiple determination CD updates
add app installed check to more instances
marv-out suggestion
marv-out suggestion
marv-out suggestion
The hash was being marked as "sent" BEFORE the actual send completed, as getAppStatus callback never fires or is severely delayed
All future broadcasts with same/similar data were blocked :
The hash from 21:40:07 is still stored in lastSentDataHash
Even though it was never actually sent via sendMessage!
So every subsequent call logs "Skipping duplicate broadcast"
@mountrcg
Copy link
Contributor Author

@Sjoerd-Bo3 & @dnzxy
I have added 2 watchface to connectIQ. Can we move the PR into the feature branch to commence testing?

I moved the code for watchfaces and also 2 datafields into private repos, too much interdependance with Trio, the message queueing and energy optimization within GarminManager are too tricky.

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.

3 participants