-
-
Notifications
You must be signed in to change notification settings - Fork 22
Codebase Guide
A comprehensive reference for anyone working on this codebase. Last updated: April 2026
- What Is NeewerLite?
- Repository Layout
- Build & Run
- Architecture Overview
- BLE Protocol & Light Communication
- Model Layer
- View Layer
- Sound-to-Light System
- External Integration
- Stream Deck Plugin
- Light Database
- Testing
- CI / Release Pipeline
- Key Patterns & Conventions
- Common Tasks
NeewerLite is a native macOS app (Swift, AppKit) that controls Neewer Bluetooth LED lights — the same lights Neewer only provides iOS/Android apps for. It runs as a menu-bar app and provides:
- Full light control: power, brightness, CCT (3200K–8500K), RGB (HSI mode), scene effects, light source presets
- 39 professional gel presets (Lee/Rosco standards) with submultiplicative color stacking
- Sound-to-Light: real-time audio-reactive lighting with beat detection
- Automation: URL scheme (
neewerlite://), HTTP server for Stream Deck, scriptable from Terminal/Shortcuts - Multi-light management with per-light independent control
Minimum deployment target: macOS 13 (Ventura)
Dependencies (via SPM):
- Vapor — HTTP server framework
- MCP Swift SDK — Model Context Protocol server (0.12.0)
- Sparkle — Auto-update framework
- swift-atomics — Lock-free atomic operations
NeewerLite/ ← Root
├── README.md ← User-facing readme
├── LICENSE
├── appcast.xml ← Sparkle update feed
│
├── Database/ ← Light database (deployed to GitHub CDN)
│ ├── lights.json ← Master light definitions (types, commands, FX, gels)
│ ├── light_images/ ← Product photos (60+)
│ └── scene_images/ ← Scene effect thumbnails
│
├── Design/ ← Sketch design files
│
├── Docs/ ← Documentation & shell script examples
│ ├── Codebase-Guide.md ← This file
│ ├── Sound-to-Light-Engine.md ← STL engine design, modes, noise gate, architecture
│ ├── Gels.md
│ ├── Integrate-with-shortcut.md
│ ├── Integrate-with-streamdeck.md
│ └── *.sh ← Example automation scripts
│
├── NeewerLite/ ← Xcode project root
│ ├── Package.swift ← SPM dependencies
│ ├── NeewerLite.xcodeproj/ ← Xcode project
│ ├── ci_scripts/ ← Xcode Cloud CI hooks
│ │
│ ├── NeewerLite/ ← App source
│ │ ├── AppDelegate.swift ← App lifecycle, BLE scanning, UI orchestration
│ │ ├── ContentManager.swift ← Light database loading & caching
│ │ ├── NeewerLiteApplication.swift ← Custom NSApplication (suppress activation)
│ │ ├── 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 (with default CCT/GM)
│ │ │ ├── Command.swift ← URL scheme command routing
│ │ │ ├── CommandPatternParser.swift ← BLE command template engine
│ │ │ ├── NeewerGel.swift ← Gel presets + stacking math
│ │ │ └── ImageFile.swift ← Product image thumbnails
│ │ │
│ │ ├── Common/ ← Shared utilities
│ │ │ ├── Observable.swift ← Simple reactive <T> wrapper
│ │ │ ├── Logger.swift ← Structured logging + batch upload
│ │ │ ├── StorageManager.swift ← App Support directory I/O
│ │ │ ├── ColorUtils.swift ← HSV ↔ RGB conversion
│ │ │ ├── DataExtensions.swift ← Data.hexString, Comparable.clamped
│ │ │ ├── CBCharacteristicExtensions.swift ← BLE helpers
│ │ │ ├── CodableValue.swift ← Type-erased Codable wrapper
│ │ │ ├── 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
│ │ │ └── SoundToLightMode.swift ← Mapping modes + throttle + presets
│ │ │
│ │ ├── ViewModels/ ← MVVM binding layer
│ │ │ ├── DeviceViewObject.swift ← Light model ↔ UI bindings
│ │ │ ├── SpectrogramViewObject.swift ← Spectrogram display state
│ │ │ └── MySplitViewDelegate.swift ← Split view sizing
│ │ │
│ │ ├── Views/ ← UI components
│ │ │ ├── CollectionViewItem.swift ← Per-light control card (main UI)
│ │ │ ├── CollectionViewItem+Gels.swift ← Gel tab extension
│ │ │ ├── ColorWheel.swift ← HSV color picker
│ │ │ ├── FXView.swift ← Scene effect parameter editor
│ │ │ ├── GelSwatchCell.swift ← Gel color swatch cell
│ │ │ ├── NLSlider.swift ← Custom slider control
│ │ │ ├── PatternEditorPanel.swift ← BLE command pattern editor
│ │ │ ├── RenameViewController.swift ← Light rename dialog
│ │ │ ├── MyLightTableCellView.swift ← Scan view table cell
│ │ │ ├── BlockingOverlayView.swift ← Disconnected light overlay
│ │ │ ├── RoundedScrollView.swift
│ │ │ └── LogMonitorViewController.swift ← Debug log viewer
│ │ │
│ │ └── Resources/
│ │ ├── Info.plist
│ │ ├── lights_db.json ← Bundled fallback database
│ │ ├── Assets.xcassets/
│ │ ├── Base.lproj/MainMenu.xib ← Main menu + window layouts
│ │ └── CollectionViewItem.xib ← Per-light card layout
│ │
│ └── NeewerLiteTests/ ← Unit tests
│ ├── NeewerLiteTests.swift ← Light naming & type mapping
│ ├── CommandParserTests.swift ← BLE command generation
│ ├── AudioAnalysisEngineTests.swift ← Audio feature extraction
│ ├── SoundToLightModeTests.swift ← Mapping modes + reactivity
│ ├── 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
│ ├── neewerlite/
│ │ ├── package.json
│ │ ├── rollup.config.mjs
│ │ ├── src/ ← TypeScript source
│ │ │ ├── actions/ ← Stream Deck actions
│ │ │ └── ipc.ts ← HTTP client to NeewerLite server
│ │ └── com.beyondcow.neewerlite.sdPlugin/ ← Plugin bundle
│ │ └── manifest.json
│ └── *.sh ← Dev scripts (watch, reload, setup)
│
└── Tools/ ← Build & release tooling
├── build.sh ← Archive + sign + DMG
├── publish.sh ← GitHub release + appcast update
├── validate.sh ← Code signing & notarization checks
├── clean.sh
└── generate_appcast ← Sparkle feed generator
- Xcode 16+ (Swift 5.10+)
- macOS 13+ (Ventura)
- A Neewer Bluetooth LED light (optional — the app runs without one)
cd NeewerLite/NeewerLite
# Debug build
xcodebuild build \
-project NeewerLite.xcodeproj \
-scheme NeewerLite \
-configuration Debug \
-destination 'platform=macOS'xcodebuild test \
-project NeewerLite.xcodeproj \
-scheme NeewerLiteTests \
-destination 'platform=macOS'cd NeewerLiteStreamDeck
./build.shRequires: Node.js, npm, Elgato Stream Deck CLI (streamdeck).
cd Tools
./build.sh # Creates .xcarchive + .dmg
./publish.sh # Uploads to GitHub Releases + updates appcast.xml┌─────────────────────────────────────────────────────────────────┐
│ AppDelegate │
│ (App lifecycle, BLE scanning, UI orchestration, command hub) │
├──────────┬──────────┬──────────┬──────────┬─────────────────────┤
│ │ │ │ │ │
│ ┌───────┴───────┐ │ ┌───────┴──────┐ │ ┌─────────────────┐│
│ │ CBCentralMgr │ │ │ Server │ │ │ ContentManager ││
│ │ (BLE scan & │ │ │ (HTTP+MCP │ │ │ (Light DB, ││
│ │ connection) │ │ │ port 18486) │ │ │ remote fetch) ││
│ └───────┬───────┘ │ └──────────────┘ │ └─────────────────┘│
│ │ │ │ │
│ ┌───────┴───────┐ │ │ │
│ │ NeewerLight[] │ │ Sound-to-Light │ │
│ │ (BLE model) │ │ ┌────────────┐ │ │
│ └───────┬───────┘ │ │AudioSpectro│ │ │
│ │ │ │ (capture) │ │ │
│ ┌───────┴───────┐ │ └─────┬──────┘ │ │
│ │DeviceViewObj[]│ │ ┌─────┴──────┐ │ │
│ │ (MVVM bind) │ │ │AnalysisEng │ │ │
│ └───────┬───────┘ │ │ (features) │ │ │
│ │ │ └─────┬──────┘ │ │
│ ┌───────┴───────┐ │ ┌─────┴──────┐ │ │
│ │CollectionView │ │ │ S2L Mode │ │ │
│ │ Item (UI) │ │ │ (mapping) │ │ │
│ └───────────────┘ │ └─────┬──────┘ │ │
│ │ ┌─────┴──────┐ │ │
│ │ │BLE Throttle│ │ │
│ │ └────────────┘ │ │
└─────────────────────┴────────────────────┴─────────────────────┘
│
┌───────┴───────┐
│ Neewer LED │
│ (Bluetooth) │
└───────────────┘
| Decision | Why |
|---|---|
| AppKit, not SwiftUI | Started before SwiftUI was mature enough for BLE + custom drawing. Menu bar integration is cleaner with NSApplication. |
| Observable<T>, not Combine | Lightweight, synchronous, no framework dependency. One-liner bindings: value.bind { ... }. |
| Code-driven UI (mostly) |
CollectionViewItem builds tabs and controls programmatically. Only the outer frame comes from XIB. This makes it easy to adapt UI per light capabilities. |
| Command pattern templates | BLE commands vary by light model. Instead of hardcoding, each light type defines command patterns in JSON ({cmdtag} {ccttag} {size} {brr:uint8:range(0,100)} ...). The CommandPatternParser fills in values at runtime. |
| Database-driven light support | Adding a new light model requires zero code changes — just add an entry to Database/lights.json. The app fetches the latest DB from GitHub automatically. |
User adjusts slider → light changes color:
CollectionViewItem (slider action)
→ DeviceViewObject.updateHSI(hue:sat:brr:)
→ NeewerLight.sendHSICommand(hue:sat:brr:)
→ CommandPatternParser.buildCommand(pattern, values)
→ Data([0x78, 0x89, ...]) // raw BLE packet
→ peripheral.writeValue(data, for: ctlCharacteristic)
→ [Light hardware executes command]
Light reports state change → UI updates:
CBPeripheral notification (GATT characteristic)
→ NeewerLight.parseNotification(data)
→ Observable<Int>.value = newBrightness (triggers didSet)
→ DeviceViewObject binding closure
→ DispatchQueue.main.async { view.updateSlider() }
See also: Neewer-Light-Protocol.md (raw packet captures & reverse-engineering notes) · Neewer-Home-Protocol.md (Neewer Home app protocol)
All Neewer lights use a single BLE service with two characteristics:
| UUID | Name | Direction |
|---|---|---|
69400001-B5A3-F393-E0A9-E50E24DCCA99 |
Service | — |
69400002-B5A3-F393-E0A9-E50E24DCCA99 |
Control | App → Light (write) |
69400003-B5A3-F393-E0A9-E50E24DCCA99 |
GATT | Light → App (notify) |
Every command is a [0x78] [tag] [payload...] [checksum] packet. Tags include power (0x81), CCT (0x87), HSI (0x89), scene (0x88), and others. Checksum is XOR of all preceding bytes. See Neewer-Light-Protocol.md for packet format details, worked examples, and the full tag reference.
Instead of hardcoding commands per light model, the database defines templates like "{cmdtag} {powertag} {size} {state:uint8:enum(1=on,2=off)} {checksum}". Tokens include variables with types and constraints ({var:type:constraint}), auto-calculated {size}, and literal hex values. CommandPatternParser.buildCommand(from:values:) takes a template + a dictionary of values and returns the raw Data packet.
See Command-Patterns.md for the full template grammar, token types, and worked examples.
The app discovers lights by scanning for BLE peripherals whose advertised name contains any of: "nwr", "neewer", "nee", "sl", "nw-" (case-insensitive). Once found, it connects, discovers the service/characteristics above, and wraps the peripheral in a NeewerLight model.
A timer fires every 10 seconds per connected light, sending a read request on the GATT characteristic. If the read fails (no response), connectionBreakCounter increments. After 2+ failures, the light is shown as disconnected (grayed out with BlockingOverlayView).
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,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
Light Modes:
| Mode | Value | Description |
|---|---|---|
CCTMode |
1 | Bi-color: brightness + color temperature |
HSIMode |
2 | RGB: hue + saturation + brightness |
SCEMode |
3 | Scene effects (per-channel animation) |
SRCMode |
4 | Light source presets (sunlight, halogen, etc.) |
Key methods:
-
sendPowerOnCommand()/sendPowerOffCommand() -
sendCCTCommand(brr:cct:gm:)— builds command from pattern, writes to BLE -
sendHSICommand(hue:sat:brr:)— same for RGB mode -
sendSceneCommand(scene:brr:)— scene effects -
getConfig()→[String: CodableValue]— serializable state for persistence -
startLightOnNotify()— subscribe to GATT notifications
Persistence: All managed lights are saved to ~/Library/Application Support/NeewerLite/MyLights.dat as JSON. Loaded at app launch so lights reconnect automatically.
Static utilities for:
-
Type mapping: BLE advertised name → light type ID (
getLightType(nickName:projectName:)) -
Name parsing: Raw BLE name → (nickName, projectName) (
getLightNames(rawName:identifier:)) - 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
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).
URL scheme command routing. Defines:
-
CommandTypeenum:turnOnLight,turnOffLight,toggleLight,scanLight,setLightHSI,setLightCCT,setLightScene -
CommandParameter: extracts typed values from URL query strings (?light=KeyLight&CCT=5600&Brightness=100) -
CommandHandler: registry of (name → action closure), dispatches URL events -
ControlTagenum: UI control identifiers (brr=10, cct=11, hue=13, sat=14, etc.) -
TabIdenum: tab identifiers ("cctTab", "hsiTab", "gelTab", "sourceTab", "sceTab")
39 professional gel presets loaded from the database. Each gel has:
-
hue,saturation,transmissionPercent,mireds -
category: ColorCorrection, Creative, or Diffusion -
manufacturer,code(e.g., Lee 201)
Multi-gel stacking uses physically-based math:
- RGB multiplication (subtractive mixing)
- Mired addition (color temp shifts add linearly)
- Transmission compounding
The template engine that turns pattern strings + value dictionaries into raw BLE Data packets. Handles type encoding (uint8, uint16_le/be), range clamping, enum mapping, checksum calculation. This is the bridge between the database-defined patterns and actual BLE writes.
NeewerLight (Model)
↕ Observable<T> bindings
DeviceViewObject (ViewModel)
↕ IBOutlet / direct reference
CollectionViewItem (View)
The binding layer between NeewerLight and the UI. One created per managed light. Responsibilities:
- Binds all Observable properties to UI update closures (dispatched to main queue)
- Exposes action methods:
turnOnLight(),changeToMode(),updateHSI(),updateCCT() - Tracks per-device UI state: selected tab, current mode, follow-music flag
The main per-light control card (~1800 lines). This is the most complex view in the app. Each instance represents one connected light, displayed in a grid layout.
Layout (520×300px):
┌──────────────────────────────────────────────┐
│ ┌──────────┐ ┌────────────────────────────┐ │
│ │ │ │ [CCT] [HSI] [Gel] [Src] │ │
│ │ Product │ │ [FX] │ │
│ │ Image │ │ │ │
│ │ │ │ (Tab content area) │ │
│ │ │ │ Sliders, color wheel, │ │
│ │ │ │ gel swatches, FX params │ │
│ │ │ │ │ │
│ ├──────────┤ └────────────────────────────┘ │
│ │ LightName│ │
│ │ [🔌][🎵]│ │
│ │ [⚙️] │ │
│ └──────────┘ │
└──────────────────────────────────────────────┘
Left panel (0–140px): image, name, power switch, follow-music 🎵, gear menu
Right panel (140–520px): NSTabView with mode-specific controls
Tab building is dynamic — buildView() inspects light capabilities and only adds supported tabs. A CCT-only light won't get HSI or Gel tabs.
Key tabs:
- CCT: brightness slider + color temperature slider (+ optional GM slider)
- HSI: color wheel or hue slider + saturation + brightness
- Gel: category picker + gel swatch grid (39 presets) + multi-stack UI
- Light Source: preset buttons (Sunlight, Halogen, Tungsten, etc.)
- FX/Scene: channel dial + per-effect parameter sliders
| File | Purpose |
|---|---|
CollectionViewItem+Gels.swift |
Gel tab: filtering, stacking UI, swatch selection |
ColorWheel.swift |
Interactive HSV color wheel with handle dragging |
FXView.swift |
Scene effect parameter editor (speed, sparks, color picks) |
GelSwatchCell.swift |
Individual gel color swatch in the grid |
NLSlider.swift |
Custom slider control used for brightness, CCT, etc. |
PatternEditorPanel.swift |
Advanced panel for editing BLE command patterns (for unsupported lights) |
RenameViewController.swift |
Light rename dialog |
MyLightTableCellView.swift |
Table cell in the Scan View list |
BlockingOverlayView.swift |
Gray overlay shown when a light disconnects |
LogMonitorViewController.swift |
Debug log viewer panel |
RoundedScrollView.swift |
Styled scroll view |
Defines the app's window structure with multiple views:
| View | Name | Purpose |
|---|---|---|
view0 |
Scan View | BLE discovery, list of found lights |
view1 |
Control View | Per-light grid (NSCollectionView of CollectionViewItem) |
view2 |
Music View | Sound-to-Light controls + spectrogram visualization |
view3 |
Screen View | Reserved |
The Music View (view2) layout at 639×424:
- Top row (~y=379): "Listen" label, audio on/off switch, visualization popup
- Controls row (~y=345): Mode | Reactivity | Palette | Preset dropdowns
- Visualization area (~y=37, 549×295): Spectrogram/waveform/spectrum display
See also: Sound-to-Light-Engine.md (design & architecture) · Sound-to-Light-Technical-Report.md (algorithm details & benchmarks)
The Sound-to-Light system turns live audio into real-time lighting commands. The pipeline:
Microphone → AudioSpectrogram → AudioAnalysisEngine → SoundToLightMode → BLESmartThrottle → BLE
(44.1kHz) (mel spectrum) (features + beats) (light commands) (rate limit) (write)
Captures system audio via AVFoundation and produces a 60-bin mel-spectrogram at ~46 Hz (44.1 kHz → 1024-point FFT → 60 mel filters → log-scale power).
Extracts musical features from the raw mel spectrum. Output: AudioFeatures struct with per-band energy (bass/mid/high), spectral flux, beat detection (isBeat, beatIntensity), BPM estimation, and overallEnergy. Uses AGC with slow decay, half-wave rectified spectral flux, and adaptive beat thresholding.
Protocol for mapping AudioFeatures → LightCommand. Three built-in modes:
- PulseMode — Beat-driven brightness pulsing (HSI or CCT)
-
ColorFlowMode — Frequency-to-hue mapping via
ColorPalettewarm/cool endpoints (HSI only) - BassCannonMode — Bass-driven intensity spikes with beat overlays (HSI only)
Each mode is configured with a Reactivity level (Subtle → Extreme) that scales sensitivity, decay, and smoothing. Six presets combine mode + reactivity + palette for common scenarios (DJ Booth, Film Score, Rock Concert, etc.).
Rate-limits BLE writes to prevent saturating the radio. Skips sends under 67ms apart; forces send after 200ms heartbeat; otherwise only sends if perceptual change (brightness, hue, saturation, CCT) exceeds thresholds.
For full details on the audio pipeline, feature extraction, per-mode algorithms, noise gate design, and reactivity/palette/preset tables, see Sound-to-Light-Engine.md. For industry comparison and competitive positioning, see Sound-to-Light-Technical-Report.md.
See also: Integrate-with-shortcut.md (macOS Shortcuts walkthrough)
Registered in Info.plist. Commands:
# Power
neewerlite://turnOnLight[?light=<name>]
neewerlite://turnOffLight[?light=<name>]
neewerlite://toggleLight[?light=<name>]
# CCT mode
neewerlite://setLightCCT?CCT=<3200-8500>&Brightness=<0-100>[&GM=<-50 to 50>][&light=<name>]
# HSI mode (by RGB hex or by hue)
neewerlite://setLightHSI?RGB=<hex>&Saturation=<0-100>&Brightness=<0-100>[&light=<name>]
neewerlite://setLightHSI?HUE=<0-360>&Saturation=<0-100>&Brightness=<0-100>[&light=<name>]
# Scene effects
neewerlite://setLightScene?Scene=<name>&Brightness=<0-100>[&light=<name>]
# Scene names: SquadCar, Ambulance, FireEngine, Firework, Party, CandleLight, Lightning, Paparazzi, TVScreen
# Rescan
neewerlite://scanLightIf light is omitted, the command targets all connected lights.
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 |
|---|---|---|
/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. 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 |
The server can be enabled/disabled from Settings (persisted as HTTPServerEnabled in UserDefaults, defaults to on).
NeewerLiteApplication subclass suppresses app activation when triggered via URL scheme — the app stays in the menu bar without stealing focus.
See also: Integrate-with-streamdeck.md (setup & usage guide)
Located in NeewerLiteStreamDeck/. TypeScript-based Elgato Stream Deck plugin.
Stack: TypeScript 5.8 + Rollup + Terser → com.beyondcow.neewerlite.sdPlugin bundle
Communication: HTTP to localhost:18486 via ipc.ts (matches Server.swift endpoints).
Actions:
| Action | Type | Control |
|---|---|---|
| Switch | Key press | Toggle lights on/off |
| Brightness | Encoder dial | Adjust brightness |
| CCT Key | Key press | Set color temperature preset |
| Temperature | Encoder dial | Adjust CCT continuously |
| Hue | Encoder dial | Adjust hue |
| Saturation | Encoder dial | Adjust saturation |
| FX Key | Key press | Select scene effect |
Build:
-
./build.shextractsSDPluginVersionfrom app's Info.plist - Updates plugin
manifest.jsonversion to match -
npm run buildcompiles TypeScript -
streamdeck validate+streamdeck packproduces.streamDeckPlugin
See also: Gels.md (gel filter system design & subtractive color mixing)
Two top-level arrays: lights (60+ entries) and gels (39 entries).
Light entry:
{
"type": 3, // Unique light type ID (uint8)
"image": "<GitHub raw URL>", // Product photo
"link": "<product URL>",
"supportRGB": true,
"supportCCTGM": false,
"supportMusic": false,
"support9FX": true,
"support17FX": false,
"cctRange": { "min": 32, "max": 56 }, // Neewer units (×100 = Kelvin)
"newPowerLightCommand": false,
"newRGBLightCommand": false,
"commandPatterns": {
"power": "{cmdtag} {powertag} {size} {state:uint8:enum(1=on,2=off)} {checksum}",
"cct": "...",
"hsi": "..."
},
"sourcePatterns": [ ... ], // Light source presets
"fxPatterns": [ ... ] // Scene effects
}Database loading:
- Bundled fallback in
Resources/lights_db.json - On launch, fetches from
beyondcow.com/neewerlite/lights_db.json - Cached to
~/Library/Application Support/NeewerLite/ - Re-fetched every 12 hours
- Use a BLE scanner to find the light's advertised Bluetooth name
- Add name-to-type mapping in
Database/lights.json - Define
commandPatterns(use an existing similar light as template) - Set capability flags (
supportRGB,cctRange, etc.) - Add product image to
Database/light_images/ - Push to GitHub — every user gets the update automatically (no app update needed)
222 tests, all under NeewerLiteTests/:
| File | Tests | Coverage |
|---|---|---|
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:
cd NeewerLite/NeewerLite
xcodebuild test -project NeewerLite.xcodeproj -scheme NeewerLite -destination 'platform=macOS'Scripts in ci_scripts/:
-
ci_post_clone.sh— dependency setup -
ci_pre_xcodebuild.sh— pre-build configuration -
ci_post_xcodebuild.sh— post-build packaging
cd Tools
# 1. Build archive + DMG
./build.sh
# 2. Validate code signing & notarization
./validate.sh
# 3. Publish to GitHub Releases + update Sparkle appcast
./publish.shAuto-update uses Sparkle. The feed is appcast.xml at repo root. Tools/generate_appcast regenerates it from the latest release.
// Model
class NeewerLight {
let brrValue = Observable<Int>(0)
}
// ViewModel binds model to view
device.brrValue.bind { [weak self] val in
DispatchQueue.main.async {
self?.view?.updateBrightnessSlider(val)
}
}
// Setting a value triggers the binding
device.brrValue.value = 75 // → slider animates to 75| Key | Type | Purpose |
|---|---|---|
"stlMode" |
String | Sound-to-Light mode type (pulse/colorFlow/bassCannon) |
"stlReactivity" |
Int | Reactivity level (0–3) |
"stlPalette" |
Int | Color palette index (-1 = default) |
enum SoundToLightModeType {
case pulse, colorFlow, bassCannon
func createMode(reactivity: Reactivity, palette: ColorPalette?) -> SoundToLightMode
}- Model types:
NeewerLight.swift,NeewerGel.swift - Extensions:
CollectionViewItem+Gels.swift - ViewModels:
DeviceViewObject.swift - Constants go in
*Constant.swiftfiles
Logger.log(.bluetooth, "Connected to \(lightName)")
// Tags: .app, .click, .bluetooth, .wifi, .heart, .serverLogs are written to ~/Library/Application Support/NeewerLite/Logs/ and batch-uploaded.
- Open
CollectionViewItem.swift - Find the relevant
build*View()method (e.g.,buildHSIView()for HSI tab) - Add your NSControl programmatically (most controls are code-driven, not XIB)
- Wire the action to the view, which delegates to
DeviceViewObject, which callsNeewerLight
- Create a struct conforming to
SoundToLightModeinSoundToLightMode.swift - Implement
process(_ features: AudioFeatures) -> LightCommand - Add a case to
SoundToLightModeTypeenum - Update
createMode()factory - The mode auto-appears in the Music View dropdown (AppDelegate reads the enum)
- Add tests in
SoundToLightModeTests.swift
Zero code changes needed:
- Add entry to
Database/lights.json - Define
commandPatterns, capability flags,cctRange - Push to GitHub — the app fetches the updated DB automatically
- Open
Server.swift - For Stream Deck routes: add handler in the
/sdgroup (auth middleware applies) - For MCP tools: add a
Toolentry inregisterMCPTools()and a handler in the tool dispatch switch - Update Stream Deck plugin
ipc.tsif the SD plugin should use it
- Add a case to
CommandTypeinCommand.swift - Register the handler in
AppDelegate(search forcommandHandler.register) - Implement the action closure
- Document in README.md
- Check
Loggeroutput (orLogMonitorViewControllerin-app) - Verify the command pattern in
Database/lights.jsonfor that light type - Use
PatternEditorPanel(in-app gear menu) to inspect/edit patterns - Check
NeewerLightConstant.getLightType()— is the BLE name recognized? - Check
connectionBreakCounter— has the keep-alive detected a disconnect?