Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
967 changes: 967 additions & 0 deletions Docs/AI-Integration.md

Large diffs are not rendered by default.

82 changes: 60 additions & 22 deletions Docs/Codebase-Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ NeewerLite is a **native macOS app** (Swift, AppKit) that controls Neewer Blueto
**Minimum deployment target:** macOS 13 (Ventura)

**Dependencies** (via SPM):
- [Swifter](https://github.com/httpswift/swifter) — Lightweight HTTP server
- [Vapor](https://github.com/vapor/vapor) — HTTP server framework
- [MCP Swift SDK](https://github.com/modelcontextprotocol/swift-sdk) — Model Context Protocol server (0.12.0)
- [Sparkle](https://github.com/sparkle-project/Sparkle) — Auto-update framework
- [swift-atomics](https://github.com/apple/swift-atomics) — Lock-free atomic operations

Expand Down Expand Up @@ -76,13 +77,13 @@ NeewerLite/ ← Root
│ │ ├── AppDelegate.swift ← App lifecycle, BLE scanning, UI orchestration
│ │ ├── ContentManager.swift ← Light database loading & caching
│ │ ├── NeewerLiteApplication.swift ← Custom NSApplication (suppress activation)
│ │ ├── Server.swift ← HTTP API (localhost:18486)
│ │ ├── Server.swift ← HTTP + MCP server (localhost:18486)
│ │ │
│ │ ├── Model/ ← Data model
│ │ │ ├── NeewerLight.swift ← Core light model + BLE comms
│ │ │ ├── NeewerLightConstant.swift ← BLE constants, type mapping
│ │ │ ├── NeewerLightFX.swift ← Scene effect definitions
│ │ │ ├── NeewerLightSource.swift ← Light source presets
│ │ │ ├── NeewerLightSource.swift ← Light source presets (with default CCT/GM)
│ │ │ ├── Command.swift ← URL scheme command routing
│ │ │ ├── CommandPatternParser.swift ← BLE command template engine
│ │ │ ├── NeewerGel.swift ← Gel presets + stacking math
Expand All @@ -99,6 +100,10 @@ NeewerLite/ ← Root
│ │ │ ├── NSBezierPathExtensions.swift
│ │ │ └── Utils.swift
│ │ │
│ │ ├── Views/
│ │ │ ├── SettingsView.swift ← Settings: launch at login, server toggle
│ │ │ └── ... ← (see View Layer section)
│ │ │
│ │ ├── Spectrogram/ ← Sound-to-Light engine
│ │ │ ├── AudioSpectrogram.swift ← Audio capture + mel-spectrogram
│ │ │ ├── AudioAnalysisEngine.swift ← Feature extraction + beat detection
Expand Down Expand Up @@ -135,7 +140,9 @@ NeewerLite/ ← Root
│ ├── CommandParserTests.swift ← BLE command generation
│ ├── AudioAnalysisEngineTests.swift ← Audio feature extraction
│ ├── SoundToLightModeTests.swift ← Mapping modes + reactivity
│ └── GelsTests.swift ← Gel stacking math
│ ├── GelsTests.swift ← Gel stacking math
│ ├── MCPServerTests.swift ← MCP tool discovery & Value coercion
│ └── StringLocalizedTests.swift ← Localization string tests
├── NeewerLiteStreamDeck/ ← Elgato Stream Deck plugin
│ ├── build.sh ← Build & package plugin
Expand Down Expand Up @@ -218,7 +225,7 @@ cd Tools
│ │ │ │ │ │
│ ┌───────┴───────┐ │ ┌───────┴──────┐ │ ┌─────────────────┐│
│ │ CBCentralMgr │ │ │ Server │ │ │ ContentManager ││
│ │ (BLE scan & │ │ │ (HTTP API │ │ │ (Light DB, ││
│ │ (BLE scan & │ │ │ (HTTP+MCP │ │ │ (Light DB, ││
│ │ connection) │ │ │ port 18486) │ │ │ remote fetch) ││
│ └───────┬───────┘ │ └──────────────┘ │ └─────────────────┘│
│ │ │ │ │
Expand Down Expand Up @@ -322,7 +329,7 @@ A timer fires every **10 seconds** per connected light, sending a read request o
The core model representing a single physical LED light. Holds:

- **BLE state**: `peripheral: CBPeripheral`, `deviceCtlCharacteristic`, `gattCharacteristic`
- **Light state** (all `Observable<T>`): `isOn`, `brrValue`, `cctValue`, `hueValue`, `satValue`, `gmmValue`, `channel`
- **Light state** (all `Observable<T>`): `isOn`, `brrValue`, `cctValue`, `hueValue`, `satValue`, `gmmValue`, `channel`, `sourceChannel`
- **Identity**: `userLightName`, `projectName`, `nickName`, `lightType: UInt8`
- **Capabilities**: `supportRGB`, `supportCCTGM`, `supportMusic`, `support9FX`, `support17FX`, `cctRange`
- **Sound-to-Light**: `followMusic: Bool` — whether this light follows the audio engine
Expand Down Expand Up @@ -355,6 +362,17 @@ Static utilities for:
- **CCT range**: Per-type min/max Kelvin (default 32–56, extended to 85 for SL80/SL140)
- **FX/Source lookup**: `getLightFX(lightType:)`, `getLightSources(lightType:)` → arrays from database

### NeewerLightSource (`Model/NeewerLightSource.swift`)

Light source presets (Sunlight, Halogen, Tungsten, etc.) loaded from the database. Each source has:
- `id`, `name` (localized), `iconName`
- `cmdPattern` / `defaultCmdPattern` — BLE command templates
- `needBRR`, `needCCT`, `needGM` — which sliders to show
- `featureValues` — per-source parameter dictionary
- `defaultCCTValue`, `defaultGMValue` — factory-set defaults (not persisted via Codable), reset on each source selection so slider changes don't permanently mutate the preset

10 factory presets with calibrated CCT/GM defaults: Sunlight (56K/+4), White Halogen (32K/+2), Xenon short-arc (60K/−8), Horizon daylight (25K/+8), Daylight (55K/0), Tungsten (32K/−4), Studio Bulb (34K/−2), Modeling Lights (45K/0), Dysprosic (58K/−6), HMI6000 (60K/+2).

### Command (`Model/Command.swift`)

URL scheme command routing. Defines:
Expand Down Expand Up @@ -542,16 +560,34 @@ If `light` is omitted, the command targets all connected lights.

### HTTP Server (port 18486)

For Stream Deck plugin and programmatic control:
The server (built on Vapor) hosts both the Stream Deck HTTP API and a Model Context Protocol (MCP) endpoint.

**Stream Deck HTTP routes** (require `User-Agent: neewerlite.sdPlugin/*`):

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/listLights` | GET | JSON array of connected lights with state |
| `/ping` | GET | Health check (`{"status": "pong"}`) |
| `/switch` | POST | Toggle lights by ID/name |
| `/setLight` | POST | Set light parameters (CCT/HSI/Scene) |
| `/sd/listLights` | GET | JSON array of connected lights with state |
| `/sd/ping` | GET | Health check (`{"status": "pong"}`) |
| `/sd/switch` | POST | Toggle lights by ID/name |
| `/sd/setLight` | POST | Set light parameters (CCT/HSI/Scene) |

**MCP endpoint** (`POST /mcp`):

Exposes light control to AI assistants and automation tools via the [Model Context Protocol](https://modelcontextprotocol.io). Uses `StatefulHTTPServerTransport` from the MCP Swift SDK.

| Tool | Description |
|------|-------------|
| `list_lights` | List all lights with state, mode, and capabilities |
| `turn_on` | Turn on lights by name/index |
| `turn_off` | Turn off lights by name/index |
| `set_light_cct` | Set CCT mode (brightness, color temperature, GM) |
| `set_light_hsi` | Set HSI mode (hue, saturation, brightness) |
| `set_light_scene` | Set scene effect by name |
| `get_light_image` | Get product image for a light |
| `scan` | Trigger BLE scan for new lights |
| `get_logs` | Retrieve recent app logs |

**Authentication:** All requests must include `User-Agent: neewerlite.sdPlugin/*` header.
The server can be enabled/disabled from Settings (persisted as `HTTPServerEnabled` in UserDefaults, defaults to on).

### Custom NSApplication

Expand Down Expand Up @@ -640,20 +676,22 @@ Two top-level arrays: `lights` (60+ entries) and `gels` (39 entries).

## Testing

**103 tests**, all under `NeewerLiteTests/`:
**222 tests**, all under `NeewerLiteTests/`:

| File | Tests | Coverage |
|------|-------|----------|
| `NeewerLiteTests.swift` | ~18 | Light name parsing, type mapping from BLE names |
| `CommandParserTests.swift` | ~25 | BLE command generation: power, CCT, HSI, range validation, checksum |
| `AudioAnalysisEngineTests.swift` | ~24 | Silence, per-band isolation, AGC, beat detection, spectral flux |
| `SoundToLightModeTests.swift` | ~33 | PulseMode, ColorFlowMode, BassCannon, reactivity scaling, palette, presets |
| `GelsTests.swift` | ~3 | Subtractive mixing, mired addition, transmission compounding |
|------|-------|---------|
| `NeewerLiteTests.swift` | 3 | Light name parsing, type mapping from BLE names |
| `CommandParserTests.swift` | 57 | BLE command generation: power, CCT, HSI, range validation, checksum |
| `AudioAnalysisEngineTests.swift` | 45 | Silence, per-band isolation, AGC, beat detection, spectral flux |
| `SoundToLightModeTests.swift` | 47 | PulseMode, ColorFlowMode, BassCannon, reactivity scaling, palette, presets |
| `GelsTests.swift` | 24 | Subtractive mixing, mired addition, transmission compounding |
| `MCPServerTests.swift` | 34 | MCP tool discovery, Value numeric coercion, tool metadata |
| `StringLocalizedTests.swift` | 12 | Localization string lookups and fallbacks |

**Run:**
```bash
cd NeewerLite/NeewerLite
xcodebuild test -project NeewerLite.xcodeproj -scheme NeewerLiteTests -destination 'platform=macOS'
xcodebuild test -project NeewerLite.xcodeproj -scheme NeewerLite -destination 'platform=macOS'
```

---
Expand Down Expand Up @@ -771,8 +809,8 @@ Zero code changes needed:
### "I want to add a new HTTP endpoint"

1. Open `Server.swift`
2. Add route handler (follow existing `/listLights` pattern)
3. Remember to respect the User-Agent authentication middleware
2. For Stream Deck routes: add handler in the `/sd` group (auth middleware applies)
3. For MCP tools: add a `Tool` entry in `registerMCPTools()` and a handler in the tool dispatch switch
4. Update Stream Deck plugin `ipc.ts` if the SD plugin should use it

### "I want to add a new URL scheme command"
Expand Down
16 changes: 3 additions & 13 deletions NeewerLite/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ line_length:
type_name:
allowed_symbols: "_"

excluded:
- Package.swift
- .build
- .swiftpm
- DerivedData
- Carthage
- Pods
- .build/**
- "**/Package.swift"
- "**/.build/**"
- "**/checkouts/**"
- "**/SourcePackages/**"
- "**/DerivedData/**"
included:
- NeewerLite
- NeewerLiteTests
59 changes: 44 additions & 15 deletions NeewerLite/NeewerLite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
A708F58A2E33006E00564DCF /* CommandParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A708F5892E33006E00564DCF /* CommandParserTests.swift */; };
A7MCP0012F13A00000000001 /* MCPServerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7MCP0012F13A00000000002 /* MCPServerTests.swift */; };
A708F58C2E3300A300564DCF /* CommandPatternParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = A708F58B2E3300A300564DCF /* CommandPatternParser.swift */; };
A708F5902E333E8E00564DCF /* PatternEditorPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A708F58F2E333E8E00564DCF /* PatternEditorPanel.swift */; };
A76C38702E00000000000001 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76C38712E00000000000001 /* SettingsView.swift */; };
Expand Down Expand Up @@ -36,11 +37,13 @@
A753109D2AFC996F009F533C /* StorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A753109C2AFC996F009F533C /* StorageManager.swift */; };
A753109F2AFCAA25009F533C /* NeewerLightSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A753109E2AFCAA25009F533C /* NeewerLightSource.swift */; };
A75310A12AFF2A19009F533C /* ContentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75310A02AFF2A19009F533C /* ContentManager.swift */; };
A76C38632DFD4B64006B7C38 /* Swifter in Frameworks */ = {isa = PBXBuildFile; productRef = A76C38622DFD4B64006B7C38 /* Swifter */; };
A7F1A2B32F10000000000004 /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = A7F1A2B32F10000000000002 /* Vapor */; };
A7F1A2B32F20000000000004 /* MCP in Frameworks */ = {isa = PBXBuildFile; productRef = A7F1A2B32F20000000000002 /* MCP */; };
A76C38692DFFFCFA006B7C38 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76C38682DFFFCFA006B7C38 /* Utils.swift */; };
A76C386F2E002B95006B7C38 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A76C386E2E002B95006B7C38 /* Sparkle */; };
A76C38712E002B98006B7C38 /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = A76C38702E002B98006B7C38 /* Atomics */; };
A76C38732E002B9A006B7C38 /* Swifter in Frameworks */ = {isa = PBXBuildFile; productRef = A76C38722E002B9A006B7C38 /* Swifter */; };
A7F1A2B32F10000000000005 /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = A7F1A2B32F10000000000003 /* Vapor */; };
A7F1A2B32F20000000000005 /* MCP in Frameworks */ = {isa = PBXBuildFile; productRef = A7F1A2B32F20000000000003 /* MCP */; };
A76C4B6D275474FA00D2F145 /* AudioSpectrogram.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76C4B6C275474FA00D2F145 /* AudioSpectrogram.swift */; };
A76E1F21266496FC00E5788B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A76E1F20266496FC00E5788B /* Sparkle */; };
A76E1F2326649F2A00E5788B /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = A76E1F2226649F2A00E5788B /* Credits.rtf */; };
Expand Down Expand Up @@ -84,6 +87,7 @@

/* Begin PBXFileReference section */
A708F5892E33006E00564DCF /* CommandParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandParserTests.swift; sourceTree = "<group>"; };
A7MCP0012F13A00000000002 /* MCPServerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPServerTests.swift; sourceTree = "<group>"; };
A708F58B2E3300A300564DCF /* CommandPatternParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPatternParser.swift; sourceTree = "<group>"; };
A708F58F2E333E8E00564DCF /* PatternEditorPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatternEditorPanel.swift; sourceTree = "<group>"; };
A76C38712E00000000000001 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -153,7 +157,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A76C38632DFD4B64006B7C38 /* Swifter in Frameworks */,
A7F1A2B32F10000000000004 /* Vapor in Frameworks */,
A7F1A2B32F20000000000004 /* MCP in Frameworks */,
A76E1F21266496FC00E5788B /* Sparkle in Frameworks */,
A71B39BA275726800005E271 /* Atomics in Frameworks */,
);
Expand All @@ -163,7 +168,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A76C38732E002B9A006B7C38 /* Swifter in Frameworks */,
A76C386F2E002B95006B7C38 /* Sparkle in Frameworks */,
A76C38712E002B98006B7C38 /* Atomics in Frameworks */,
);
Expand Down Expand Up @@ -226,6 +230,7 @@
isa = PBXGroup;
children = (
A708F5892E33006E00564DCF /* CommandParserTests.swift */,
A7MCP0012F13A00000000002 /* MCPServerTests.swift */,
B5D83B6A59A5B72CDFC3ED5E /* GelsTests.swift */,
B2C3D4E5F6A7B8C9D0E1F3A1 /* AudioAnalysisEngineTests.swift */,
D3E4F5A6B7C8D9E0F1A2B3C4 /* SoundToLightModeTests.swift */,
Expand Down Expand Up @@ -349,7 +354,8 @@
packageProductDependencies = (
A76E1F20266496FC00E5788B /* Sparkle */,
A71B39B9275726800005E271 /* Atomics */,
A76C38622DFD4B64006B7C38 /* Swifter */,
A7F1A2B32F10000000000002 /* Vapor */,
A7F1A2B32F20000000000002 /* MCP */,
);
productName = NeewerLite;
productReference = A731DD3025A57D0B00302E25 /* NeewerLite.app */;
Expand All @@ -369,6 +375,9 @@
A731DD4225A57D0B00302E25 /* PBXTargetDependency */,
);
name = NeewerLiteTests;
packageProductDependencies = (
A7F1A2B32F20000000000003 /* MCP */,
);
productName = NeewerLiteTests;
productReference = A731DD4025A57D0B00302E25 /* NeewerLiteTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
Expand Down Expand Up @@ -409,7 +418,8 @@
packageReferences = (
A76E1F1F266496FC00E5788B /* XCRemoteSwiftPackageReference "Sparkle" */,
A71B39B8275726800005E271 /* XCRemoteSwiftPackageReference "swift-atomics" */,
A7B4822A2DFD49C200EB4450 /* XCRemoteSwiftPackageReference "swifter" */,
A7F1A2B32F10000000000001 /* XCRemoteSwiftPackageReference "vapor" */,
A7F1A2B32F20000000000001 /* XCRemoteSwiftPackageReference "swift-sdk" */,
);
productRefGroup = A731DD3125A57D0B00302E25 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -522,6 +532,7 @@
buildActionMask = 2147483647;
files = (
A708F58A2E33006E00564DCF /* CommandParserTests.swift in Sources */,
A7MCP0012F13A00000000001 /* MCPServerTests.swift in Sources */,
CCF4C298B8A837A3ED2D4DDB /* GelsTests.swift in Sources */,
A1B2C3D4E5F6A7B8C9D0E1F3 /* AudioAnalysisEngineTests.swift in Sources */,
E2F3A4B5C6D7E8F9A0B1C2D3 /* SoundToLightModeTests.swift in Sources */,
Expand Down Expand Up @@ -816,12 +827,20 @@
minimumVersion = 1.26.0;
};
};
A7B4822A2DFD49C200EB4450 /* XCRemoteSwiftPackageReference "swifter" */ = {
A7F1A2B32F10000000000001 /* XCRemoteSwiftPackageReference "vapor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/httpswift/swifter";
repositoryURL = "https://github.com/vapor/vapor.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
minimumVersion = 4.89.0;
};
};
A7F1A2B32F20000000000001 /* XCRemoteSwiftPackageReference "swift-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/modelcontextprotocol/swift-sdk.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.12.0;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand All @@ -832,10 +851,15 @@
package = A71B39B8275726800005E271 /* XCRemoteSwiftPackageReference "swift-atomics" */;
productName = Atomics;
};
A76C38622DFD4B64006B7C38 /* Swifter */ = {
A7F1A2B32F10000000000002 /* Vapor */ = {
isa = XCSwiftPackageProductDependency;
package = A7F1A2B32F10000000000001 /* XCRemoteSwiftPackageReference "vapor" */;
productName = Vapor;
};
A7F1A2B32F20000000000002 /* MCP */ = {
isa = XCSwiftPackageProductDependency;
package = A7B4822A2DFD49C200EB4450 /* XCRemoteSwiftPackageReference "swifter" */;
productName = Swifter;
package = A7F1A2B32F20000000000001 /* XCRemoteSwiftPackageReference "swift-sdk" */;
productName = MCP;
};
A76C386E2E002B95006B7C38 /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
Expand All @@ -847,10 +871,15 @@
package = A71B39B8275726800005E271 /* XCRemoteSwiftPackageReference "swift-atomics" */;
productName = Atomics;
};
A76C38722E002B9A006B7C38 /* Swifter */ = {
A7F1A2B32F10000000000003 /* Vapor */ = {
isa = XCSwiftPackageProductDependency;
package = A7F1A2B32F10000000000001 /* XCRemoteSwiftPackageReference "vapor" */;
productName = Vapor;
};
A7F1A2B32F20000000000003 /* MCP */ = {
isa = XCSwiftPackageProductDependency;
package = A7B4822A2DFD49C200EB4450 /* XCRemoteSwiftPackageReference "swifter" */;
productName = Swifter;
package = A7F1A2B32F20000000000001 /* XCRemoteSwiftPackageReference "swift-sdk" */;
productName = MCP;
};
A76E1F20266496FC00E5788B /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
Expand Down
Loading