-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Draft: Initialize Garmin feature branch incl. Mock Garmin watch for Sim #814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/garmin
Are you sure you want to change the base?
Conversation
|
mock Garmin watch commit shall be reverted when merging in dev, it is just for better testing |
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. |
marv-out
left a comment
There was a problem hiding this 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", |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GarminWatchSettings.swift
GarminDataType1(enum) →GarminPrimaryDataFieldGarminDataType2(enum) →GarminSecondaryDataFieldgarminDisableWatchfaceData→isWatchfaceDataEnabled⚠️ Boolean logic reversed: Default changed fromtrue(disabled) tofalse(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
garminDataType1→primaryDataFieldgarminDataType2→secondaryDataFieldgarminDisableWatchfaceData→isWatchfaceDataEnabled⚠️ Boolean logic reversed: Default changed fromtruetofalse
isDisableToggleLocked→isWatchfaceDataCooldownActiveremainingCooldownSeconds→watchfaceSwitchCooldownSecondscooldownTimer→watchfaceSwitchTimercooldownEndTime→watchfaceSwitchCooldownEndTime
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GarminWatchSettings.swift
GarminDataType1(enum) →GarminPrimaryDataFieldGarminDataType2(enum) →GarminSecondaryDataFieldgarminDisableWatchfaceData→isWatchfaceDataEnabled
@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"]
};
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this work?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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) }
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
*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
hasher fix
marv-out suggestion
marv-out suggestion
marv-out suggestion
marv-out suggestion
marv-out suggestion
marv-out suggestion
more validations
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"
|
@Sjoerd-Bo3 & @dnzxy 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. |
c737e22 to
39a6b41
Compare

The PR aims to
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.
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.
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!
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.