A .NET MAUI iOS app ("BrewBuddy") to control a Jura E8 coffee machine via Bluetooth, track coffee beans, brew history, and preferences, with AI-powered recommendations using Microsoft.Extensions.AI and AppleIntelligenceChatClient. Fun coffee-themed color scheme.
- .NET 10 MAUI targeting
net10.0-iosandnet10.0-maccatalyst - CommunityToolkit.Mvvm for [ObservableProperty] and [RelayCommand]
- SQLite via sqlite-net-pcl for local persistence
- Plugin.BLE v3 for Bluetooth LE
- Microsoft.Extensions.AI v10.3.0 for IChatClient abstraction
- Microsoft.Maui.Essentials.AI (nightly) for AppleIntelligenceChatClient (iOS 26+)
- NuGet source for nightly:
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json
BrewBuddy/
├── Models/
│ ├── CoffeeBean.cs
│ ├── BrewHistory.cs
│ ├── CoffeePreference.cs
│ ├── MachineStatus.cs
│ ├── DrinkOption.cs # Has ProductCode + DefaultWaterMl for BlueFrog protocol
│ └── ChatEntry.cs
├── Services/
│ ├── DatabaseService.cs # SQLite CRUD + GetProfileSummaryAsync for AI
│ ├── JuraBtService.cs # BlueFrog BLE protocol (rewritten from Jutta-Proto)
│ └── CoffeeAdvisor.cs # MEAI chat with debug logging
├── ViewModels/
│ ├── DashboardViewModel.cs # Connect, brew, ConnectionLog property
│ ├── BeansViewModel.cs
│ ├── HistoryViewModel.cs
│ └── AdvisorViewModel.cs # Debug info banner
├── Views/
│ ├── DashboardPage.xaml(.cs) # Hero header, connection card, BT log, brew grid
│ ├── BeansPage.xaml(.cs)
│ ├── HistoryPage.xaml(.cs)
│ └── AdvisorPage.xaml(.cs) # Chat UI with debug banner
├── Converters/Converters.cs # BoolToColor, InverseBool, PercentToWidth, BoolToString, StringNotEmpty
├── Resources/
│ ├── Styles/CoffeeColors.xaml # Espresso #3C1518, DarkRoast #69140E, Caramel #A44200, Crema #F2E8CF, Latte #F5F0E8, SteamedMilk #E8DCC8
│ ├── Styles/CoffeeStyles.xaml # CardFrame, PageTitle, PrimaryButton, DrinkButton, etc.
│ ├── Images/tab_dashboard.png # PNG tab icons (Pillow-generated, black on transparent)
│ ├── Images/tab_beans.png
│ ├── Images/tab_history.png
│ ├── Images/tab_ai.png
│ ├── AppIcon/appicon.svg + appiconfg.svg
│ └── Splash/splash.svg
├── Platforms/iOS/
│ ├── AppDelegate.cs # ContinueUserActivity for Siri
│ ├── SiriIntentDonor.cs # NSUserActivity donations
│ ├── SiriIntentHandler.cs # Activity continuation handler
│ ├── EdgeToEdgeHandler.cs # PageHandler mapper (responder chain approach)
│ ├── Entitlements.plist # Siri entitlement REMOVED (wildcard profile)
│ └── Info.plist # BLE + Siri permissions
├── App.xaml # Coffee resource dictionaries
├── AppShell.xaml # 4-tab layout, NavBarIsVisible=False, PNG icons
├── MauiProgram.cs # Full DI, AI client selection, EdgeToEdge, NoOpChatClient
├── nuget.config # dotnet10-nightly feed
└── BrewBuddy.csproj # iOS targets, packages, signing, MAUIAI0001 suppression
Jura BlueFrog BLE Protocol (from https://github.com/Jutta-Proto/protocol-bt-cpp)
- Device advertises as "TT214H BlueFrog" or "TT214H BlueProg" — match with
StartsWith("TT")orContains("BlueProg/BlueFrog") - Encryption key extracted from manufacturer advertisement data byte 0
- Nibble-shuffle cipher (symmetric) — same function encrypts/decrypts:
- Lookup tables: Numbers1 = [14,4,3,2,1,13,8,11,6,15,12,7,10,5,0,9], Numbers2 = [10,6,13,12,14,11,1,9,15,7,0,5,3,2,4,8]
- Each byte split into nibbles, shuffled with key nibbles
- Heartbeat every 8s to P Mode characteristic (0x5a401529) — encode [key, 0x7F, 0x80]
- Service UUID: 5a401523-ab2e-2548-c435-08c300000710
- Characteristics:
- Machine Status: 5a401524 (read/notify, encoded)
- Start Product: 5a401525 (write, encoded) — brew commands
- Product Progress: 5a401527 (notify, encoded)
- P Mode: 5a401529 (write, encoded) — heartbeat
- Barista Mode: 5a401530 (write, encoded) — lock/unlock
- About Machine: 5a401531 (read, NOT encoded)
- Stats Command: 5a401533 (write, encoded)
- Stats Data: 5a401534 (read, encoded)
- Brew command format: [key, productCode, 0x00, strength(1-8), waterSec, 0x00, 0x00, temp(01=normal/02=high), padding..., key]
- Status bits: byte1 bit0=tray missing, bit1=water empty
- Characteristic write: must check Properties for Write vs WriteWithoutResponse and set WriteType accordingly
- Package:
Microsoft.Maui.Essentials.AIversion10.0.50-ci.*from dotnet10-nightly - Provides
AppleIntelligenceChatClient— implements IChatClient on-device via FoundationModels - Guarded by
OperatingSystem.IsIOSVersionAtLeast(26)runtime check - Falls back to OpenAI (if API key in Preferences) then NoOpChatClient
- Suppress MAUIAI0001 via
<NoWarn>in csproj - Works on iOS 26+ simulator (confirmed on 26.3)
- Does NOT work on older iOS (no FoundationModels)
- Signing identity: "Apple Development: stephane@delcroix.org (8XS436J752)"
- Team ID: 8DGK8L344W
- Build:
dotnet build -f net10.0-ios -r ios-arm64 -p:EnableAutomaticSigning=true -p:CodesignKey="Apple Development" -p:DevelopmentTeam=8DGK8L344W - Deploy:
xcrun devicectl device install app --device D1DB8E7B-0E4A-5976-9DA3-711A89FF7783 <path> - Launch:
xcrun devicectl device process launch --device D1DB8E7B-0E4A-5976-9DA3-711A89FF7783 com.companyname.brewbuddy - Siri entitlement removed from Entitlements.plist (wildcard profile doesn't support it)
- iPhone 15 Pro "sdx" UDID: D1DB8E7B-0E4A-5976-9DA3-711A89FF7783
- iPhone 17 Pro simulator (iOS 26.3) UDID: AA261DAB-BFF9-4FE2-A35D-94D92790556B
- All 4 pages have
ios:Page.UseSafeArea="False"(xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls") - Headers use
Padding="20,60,20,24"to push content below status bar while extending espresso background to screen top - EdgeToEdgeHandler.cs uses PageHandler mapper walking responder chain to find UIViewController
- SVG icons failed on physical device (iOS template rendering issues)
- Switched to Pillow-generated PNG icons (60x60, black on transparent)
- tab_dashboard.png (coffee cup), tab_beans.png (beans), tab_history.png (clock), tab_ai.png (sparkle)
- Shell.TabBarForegroundColor=Caramel, Shell.TabBarUnselectedColor=SteamedMilk
DisplayAlert→DisplayAlertAsync(old is obsolete)DisplayActionSheet→DisplayActionSheetAsyncAddDebug()needsusing Microsoft.Extensions.Logging;- Multi-byte emoji (☕) can't be used with
new string(char, count)— usestring.Concat(Enumerable.Repeat())
- Script: /Users/sde/.copilot/installed-plugins/maui-copilot-plugins/appium-automation/skills/appium-automation/scripts/automate.py
- Must --server-stop between sessions if app was reinstalled
- Usage:
python3 "$SCRIPT" --platform ios --app-id com.companyname.brewbuddy --screenshot /tmp/out.png
- Full MAUI project scaffold with all packages
- All data models (with ProductCode/DefaultWaterMl for BlueFrog)
- SQLite database service with CRUD + analytics
- Jura BLE service rewritten with proper BlueFrog protocol (nibble-shuffle cipher, heartbeat, per-characteristic handling)
- MEAI coffee advisor with debug logging
- Coffee theme (colors + styles)
- All 4 pages with XAML UI (ios:Page.UseSafeArea="False" on all)
- All 4 ViewModels with commands
- AppShell tab navigation with PNG icons
- Siri NSUserActivity donations + handler
- AppleIntelligenceChatClient integration (works on iOS 26.3 sim!)
- App icon and splash screen
- Debug diagnostics for AI (backend type, latency, response length)
- ConnectionLog property on DashboardViewModel for BT debug
- StringNotEmpty converter for connection log visibility
- Deployed to both iPhone 15 Pro and iPhone 17 Pro sim (26.3)
- EMOJI RENDERING: All emojis show as
?on iOS 26.3 simulator. Need to investigate — may be a simulator font issue or an iOS 26 regression. Need to check physical device too. Options: replace emojis with text/SF Symbols, or check if it's sim-only. - BLE not yet tested end-to-end: The BlueFrog protocol rewrite (cipher, heartbeat, per-characteristic) compiles but hasn't been tested against the real Jura E8 machine yet.
- Edge-to-edge verification: UseSafeArea=False added to all pages but not visually confirmed on physical device after rebuild.
- Siri: Entitlement removed due to wildcard profile. Needs proper provisioning profile with Siri capability.
- Settings page: No UI for configuring OpenAI API key (currently only via Preferences API).
- Stats parsing: ReadTotalProductCountAsync written but not wired into UI.