Initial commit: 提交控制器版本2核心代码
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
// ============================================================
|
||||
// LUXSIN X9 API — Client Library
|
||||
// Custom Base64 decode + HTTP API wrappers
|
||||
// ============================================================
|
||||
|
||||
const ALPHABET_CUSTOM = "KLMPQRSTUVWXYZABCGHdefIJjkNOlmnopqrstuvwxyzabcghiDEF34501289+67/";
|
||||
const ALPHABET_STANDARD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// ============================================================
|
||||
// Volume Conversion: dB (-100 to 0) <-> Device Value (0 to 200)
|
||||
// ============================================================
|
||||
export function dbToVolume(db: number): number {
|
||||
// Formula: volume = 2 * db + 200
|
||||
// e.g., -56dB -> 2 * (-56) + 200 = 88
|
||||
return Math.round(2 * db + 200);
|
||||
}
|
||||
|
||||
export function volumeToDb(vol: number): number {
|
||||
// Formula: db = (vol - 200) / 2
|
||||
// e.g., 88 -> (88 - 200) / 2 = -56dB
|
||||
return (vol - 200) / 2;
|
||||
}
|
||||
|
||||
export function decodeCustomBase64(encoded: string): string {
|
||||
let translated = "";
|
||||
for (let i = 0; i < encoded.length; i++) {
|
||||
const char = encoded.charAt(i);
|
||||
const index = ALPHABET_CUSTOM.indexOf(char);
|
||||
translated += index !== -1 ? ALPHABET_STANDARD.charAt(index) : char;
|
||||
}
|
||||
const decoded = atob(translated);
|
||||
const bytes = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder("utf-8").decode(bytes);
|
||||
}
|
||||
|
||||
export function encodeCustomBase64(data: string): string {
|
||||
const standard = btoa(data);
|
||||
let translated = "";
|
||||
for (let i = 0; i < standard.length; i++) {
|
||||
const char = standard.charAt(i);
|
||||
const index = ALPHABET_STANDARD.indexOf(char);
|
||||
translated += index !== -1 ? ALPHABET_CUSTOM.charAt(index) : char;
|
||||
}
|
||||
return translated;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Device State Type
|
||||
// ============================================================
|
||||
export interface DeviceState {
|
||||
device: string;
|
||||
version: string;
|
||||
mac: string;
|
||||
language: number;
|
||||
volume: number;
|
||||
soundStep: number;
|
||||
input: number;
|
||||
output: number;
|
||||
audioFormat: string;
|
||||
pcm: number;
|
||||
vu: number;
|
||||
vu_count: number;
|
||||
screenLight: number;
|
||||
knob_breathlight: number;
|
||||
buttonLight: number;
|
||||
buttonShort: number;
|
||||
screenOff: number;
|
||||
sleep: number;
|
||||
autoHome: number;
|
||||
bootSound: number;
|
||||
dsp_enable: number;
|
||||
audio_enable: number;
|
||||
peqEnable: number;
|
||||
peqSelect: number;
|
||||
balance: number;
|
||||
xlr: number;
|
||||
dacGain: number;
|
||||
dacArc: number;
|
||||
dacImpedance: number;
|
||||
dacVolumeDirect: number;
|
||||
analogGain: number;
|
||||
effect_enable: number;
|
||||
effect_value: number;
|
||||
width_enable: number;
|
||||
width_value: number;
|
||||
scene_enable: number;
|
||||
scene_value: number;
|
||||
crossfeed_enable: number;
|
||||
crossfeed_value: number;
|
||||
subwoofer_enable: number;
|
||||
subwoofer_value: number;
|
||||
subwoofer_rate: number;
|
||||
subwoofer_gain: number;
|
||||
loudness_enable: number;
|
||||
loudness_bass_gain: number;
|
||||
loudness_treble_gain: number;
|
||||
loudness_threshold_gain: number;
|
||||
bt_status: number;
|
||||
bt_srcname: string;
|
||||
bt_title: string;
|
||||
bt_artist: string;
|
||||
msgCount: number;
|
||||
}
|
||||
|
||||
export interface PeqFilter {
|
||||
fc: number;
|
||||
gain: number;
|
||||
q: number;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface PeqState {
|
||||
filters: PeqFilter[];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Input/Output Labels
|
||||
// ============================================================
|
||||
export const INPUT_LABELS = ["USB", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA"];
|
||||
export const OUTPUT_LABELS = ["XLR", "RCA", "Headset", "XLR/RCA"];
|
||||
export const LANGUAGE_LABELS = ["English", "繁體中文", "简体中文"];
|
||||
export const SCREEN_LIGHT_LABELS = ["Bright", "Medium", "Dark"];
|
||||
export const KNOB_LIGHT_LABELS = ["Off", "Bright", "Medium", "Dark"];
|
||||
export const SCREEN_OFF_LABELS = ["Off", "30s", "1 min", "3 min", "5 min"];
|
||||
export const SLEEP_LABELS = ["Off", "1 min", "5 min", "10 min"];
|
||||
export const AUTO_HOME_LABELS = ["Off", "20s", "40s", "60s"];
|
||||
export const FILTER_TYPE_LABELS = ["Low Pass", "High Pass", "Band Pass", "Notch", "Peak", "Low Shelf", "High Shelf", "All Pass"];
|
||||
export const DAC_ARC_LABELS = ["ARC", "eARC"];
|
||||
export const BT_STATUS_LABELS = ["Disconnected", "Playing", "Paused"];
|
||||
|
||||
// ============================================================
|
||||
// API Class
|
||||
// ============================================================
|
||||
export class LuxsinAPI {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(ip: string) {
|
||||
this.baseUrl = `http://${ip}`;
|
||||
}
|
||||
|
||||
async getDeviceState(): Promise<DeviceState> {
|
||||
const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncData`);
|
||||
const text = await response.text();
|
||||
const decoded = decodeCustomBase64(text.trim());
|
||||
return JSON.parse(decoded) as DeviceState;
|
||||
}
|
||||
|
||||
async getMsgCount(): Promise<number> {
|
||||
const response = await fetch(`${this.baseUrl}/msgCount`);
|
||||
const text = await response.text();
|
||||
return parseInt(text.trim(), 10);
|
||||
}
|
||||
|
||||
async setSetting(params: Record<string, string | number>): Promise<void> {
|
||||
const query = Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("&");
|
||||
await fetch(`${this.baseUrl}/dev/info.cgi?action=setting&${query}`);
|
||||
}
|
||||
|
||||
async getPeqState(): Promise<PeqState> {
|
||||
const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncPeq`);
|
||||
const text = await response.text();
|
||||
const decoded = decodeCustomBase64(text.trim());
|
||||
return JSON.parse(decoded) as PeqState;
|
||||
}
|
||||
|
||||
async setPeqFilters(filters: PeqFilter[]): Promise<void> {
|
||||
const payload = JSON.stringify({ peqChange: { filters } });
|
||||
const encoded = encodeCustomBase64(payload);
|
||||
await fetch(`${this.baseUrl}/dev/info.cgi`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: `json=${encodeURIComponent(encoded)}`,
|
||||
});
|
||||
}
|
||||
|
||||
setVolume(volume: number) { return this.setSetting({ volume }); }
|
||||
setInput(input: number) { return this.setSetting({ input }); }
|
||||
setOutput(output: number) { return this.setSetting({ output }); }
|
||||
setBalance(balance: number) { return this.setSetting({ balance }); }
|
||||
setLanguage(language: number) { return this.setSetting({ language }); }
|
||||
setDspEnable(enable: boolean) { return this.setSetting({ dsp_enable: enable ? 0 : 1 }); }
|
||||
setAudioEnable(enable: boolean) { return this.setSetting({ audio_enable: enable ? 1 : 0 }); }
|
||||
setPeqEnable(enable: boolean) { return this.setSetting({ peqEnable: enable ? 1 : 0 }); }
|
||||
setPeqSelect(index: number) { return this.setSetting({ peqSelect: index }); }
|
||||
setVu(vu: number) { return this.setSetting({ vu }); }
|
||||
setScreenLight(level: number) { return this.setSetting({ screenLight: level }); }
|
||||
setKnobLight(level: number) { return this.setSetting({ knob_breathlight: level }); }
|
||||
setButtonLight(on: boolean) { return this.setSetting({ buttonLight: on ? 0 : 1 }); }
|
||||
setScreenOff(timer: number) { return this.setSetting({ screenOff: timer }); }
|
||||
setSleep(timer: number) { return this.setSetting({ sleep: timer }); }
|
||||
setAutoHome(timer: number) { return this.setSetting({ autoHome: timer }); }
|
||||
setLoudnessEnable(enable: boolean) { return this.setSetting({ loudness_enable: enable ? 1 : 0 }); }
|
||||
setSubwooferEnable(enable: boolean) { return this.setSetting({ subwoofer_enable: enable ? 1 : 0 }); }
|
||||
btPlayPause() { return this.setSetting({ bt_play: 1 }); }
|
||||
btNext() { return this.setSetting({ bt_next: 1 }); }
|
||||
btPrev() { return this.setSetting({ bt_next: 0 }); }
|
||||
setEffectEnable(enable: boolean) { return this.setSetting({ effect_enable: enable ? 1 : 0 }); }
|
||||
setWidthEnable(enable: boolean) { return this.setSetting({ width_enable: enable ? 1 : 0 }); }
|
||||
setSceneEnable(enable: boolean) { return this.setSetting({ scene_enable: enable ? 1 : 0 }); }
|
||||
setCrossfeedEnable(enable: boolean) { return this.setSetting({ crossfeed_enable: enable ? 1 : 0 }); }
|
||||
setXlrPolarity(reverse: boolean) { return this.setSetting({ xlr: reverse ? 1 : 0 }); }
|
||||
setDacArc(earc: boolean) { return this.setSetting({ dacArc: earc ? 1 : 0 }); }
|
||||
setBootSound(on: boolean) { return this.setSetting({ bootSound: on ? 1 : 0 }); }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mock data for demo/offline mode
|
||||
// ============================================================
|
||||
export const MOCK_DEVICE_STATE: DeviceState = {
|
||||
device: "Luxsin-X9",
|
||||
version: "1.2.3",
|
||||
mac: "AA:BB:CC:DD:EE:FF",
|
||||
language: 0,
|
||||
volume: 75,
|
||||
soundStep: 2,
|
||||
input: 0,
|
||||
output: 0,
|
||||
audioFormat: "PCM 44.1 KHz",
|
||||
pcm: 1,
|
||||
vu: 0,
|
||||
vu_count: 5,
|
||||
screenLight: 0,
|
||||
knob_breathlight: 1,
|
||||
buttonLight: 0,
|
||||
buttonShort: 0,
|
||||
screenOff: 2,
|
||||
sleep: 0,
|
||||
autoHome: 1,
|
||||
bootSound: 1,
|
||||
dsp_enable: 0,
|
||||
audio_enable: 1,
|
||||
peqEnable: 1,
|
||||
peqSelect: 0,
|
||||
balance: 0,
|
||||
xlr: 0,
|
||||
dacGain: 0,
|
||||
dacArc: 0,
|
||||
dacImpedance: 0,
|
||||
dacVolumeDirect: 0,
|
||||
analogGain: 0,
|
||||
effect_enable: 0,
|
||||
effect_value: 50,
|
||||
width_enable: 0,
|
||||
width_value: 50,
|
||||
scene_enable: 0,
|
||||
scene_value: 0,
|
||||
crossfeed_enable: 0,
|
||||
crossfeed_value: 0,
|
||||
subwoofer_enable: 0,
|
||||
subwoofer_value: 80,
|
||||
subwoofer_rate: 0,
|
||||
subwoofer_gain: 0,
|
||||
loudness_enable: 0,
|
||||
loudness_bass_gain: 3,
|
||||
loudness_treble_gain: 2,
|
||||
loudness_threshold_gain: 60,
|
||||
bt_status: 1,
|
||||
bt_srcname: "iPhone 15 Pro",
|
||||
bt_title: "Bohemian Rhapsody",
|
||||
bt_artist: "Queen",
|
||||
msgCount: 42,
|
||||
};
|
||||
|
||||
export const MOCK_PEQ_STATE: PeqState = {
|
||||
filters: [
|
||||
{ fc: 80, gain: 3.0, q: 0.7, type: 5 },
|
||||
{ fc: 250, gain: -1.5, q: 1.4, type: 4 },
|
||||
{ fc: 1000, gain: 0.0, q: 1.0, type: 4 },
|
||||
{ fc: 4000, gain: 2.0, q: 1.4, type: 4 },
|
||||
{ fc: 12000, gain: 1.5, q: 0.7, type: 6 },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user