DIY Scene Editing Feature

Applicable: Wi-Fi + BLE Panel

🖼️ Preview:

The DIY scene editing feature module allows users to create and edit custom lighting scenes, supporting two editing modes: dynamic scene editing (based on colors and motion parameters) and static scene editing (based on LED bead painting). This module provides complete scene creation, editing, preview, saving, and management functionality, making it a core tool for users to personalize lighting effects.

Feature List

  1. Dynamic DIY Scene Editing: Create dynamic scenes through color lists and motion parameters
  2. Static DIY Scene Editing: Precisely control each LED bead color through painting
  3. Scene Preview: Real-time preview of scene effects
  4. Scene Saving: Save scenes to cloud storage
  5. Scene Loading: Load existing scenes from cloud storage for editing
  6. Scene Deletion: Delete unwanted scenes
  7. Scene List Management: View and manage all DIY scenes

Feature Implementation Details

Dynamic DIY Scene Editing

Dynamic DIY scene editing uses SceneData data structure, containing color lists and motion parameters. The editing page provides color management and parameter adjustment functionality, and scene data is sent through the rgbic_linerlight_scene DP point.

Core Components
  • src/pages/diyEdit/index.tsx: Dynamic DIY scene editing page
  • src/components/colors/index.tsx: Color list component
  • src/components/motion-config/index.tsx: Motion parameter configuration component
  • src/hooks/useSceneSet.ts: Scene setting Hook
Data Flow
flowchart TD
    A[Enter editing page] --> B[Load scene data]
    B --> C{Is edit mode}
    C -->|Yes| D[Load from cloud storage]
    C -->|No| E[Use default data]
    D --> F[Display color list]
    E --> F
    F --> G[User adds/removes colors]
    G --> H[Select motion type]
    H --> I[Adjust motion parameters]
    I --> J[Preview scene]
    J --> K[Save scene]
    K --> L[Encode scene data]
    L --> M[Save to cloud storage]
    M --> N[Send DP to device]
Key Code Snippets

Scene Save Implementation:

  // Reference: src/pages/diyEdit/index.tsx
  const handleSave = async (sceneName: string) => {
    const params = {
      ...DEFAULTOPTIONS,
      ...values,
      key: 100,
      name: sceneName || '',
      changeType: current,
      colors: getArray(diyEditColors).map(color => ({
        hue: color.h,
        saturation: Math.floor(color.s / 10),
        value: Math.floor(color.v / 10),
        brightness: 0,
        temperature: 0,
      })),
      dataType: 0,
    } as SceneData;

    if (isDataId(dataId)) {
      await storage.updateItem(dataId, encodeCloudSceneData(params));
      sceneApi.set(params);
      Dispatcher.instance.dispatch('diyCreated', dataId);
    } else {
      showLoading({
        title: '',
        mask: true,
      });
      const newDataId = await storage.addItem(encodeCloudSceneData(params), {
        dedup: (a, b) => {
          try {
            return JSON.parse(a).name === JSON.parse(b).name;
          } catch (error) {
            return false;
          }
        },
      });
      hideLoading();
      if (typeof newDataId === 'number') {
        sceneApi.set(params);
        Dispatcher.instance.dispatch('diyCreated', newDataId);
      } else {
        TipApi.show({
          show: true,
          content: (
            <View className={styles.tipWrap}>
              <Image src={res.icon_warn} className={styles.tipIcon} />
              <View className={styles.tipText}>{Strings.getLang('name_dedup')}</View>
            </View>
          ),
        });
        return -1;
      }
    }
  };
Data Structures

Scene Data Format:

type SceneData = {
  dataType: number; // 0: Dynamic scene
  name: string; // Scene name
  key: number; // Scene key value
  changeType: number; // Motion type
  colors: Array<{
    // Color list
    hue: number;
    saturation: number;
    value: number;
    brightness: number;
    temperature: number;
  }>;
  // Other motion parameters...
};

Static DIY Scene Editing

Static DIY scene editing uses DiySceneData data structure, containing color information for each LED bead. Control each LED bead through painting, and scene data is sent through the diy_scene DP point.

Core Components
  • src/pages/staticDiyEdit/index.tsx: Static DIY scene editing page
  • src/components/light-strip/index.tsx: Light strip visualization component
  • src/hooks/useDrawToolDataList.ts: Painting tool data management Hook
  • src/hooks/useCutDrawDiyScene.ts: Painting scene setting Hook
Data Flow
sequenceDiagram
    participant User as User
    participant Page as Editing Page
    participant Strip as Light Strip Component
    participant Hook as Data Hook
    participant Protocol as Protocol Parser
    participant Device as Device

    User->>Page: Select color and painting mode
    User->>Strip: Select LED beads
    Strip->>Page: Update selection state
    Page->>Hook: Call update function
    Hook->>Hook: Update light strip data
    Hook->>Protocol: Encode scene data
    Protocol->>Device: Send DP
    Device-->>User: Display effect
    User->>Page: Save scene
    Page->>Hook: Save to cloud storage
Key Code Snippets

Scene Save Implementation:

// Reference: src/pages/staticDiyEdit/index.tsx
  const handleSave = async (sceneName: string) => {
    const params: DiySceneData = {
      name: sceneName,
      ...(diy_scene || ({} as DiySceneData)),
      segments: hasChange ? diy_scene?.segments : getArray(getDefault().segments),
    };
    console.log('[handleSave]:', params);
    if (isDataId(dataId)) {
      await storage.updateItem(dataId, encodeCloudSceneData(params));
      cutDrawDiySceneSet(params);
      Dispatcher.instance.dispatch('diyCreated', dataId);
    } else {
      showLoading({ title: '', mask: true });

      const newDataId = await storage.addItem(encodeCloudSceneData(params));
      if (typeof newDataId === 'number') {
        cutDrawDiySceneSet(params);
        Dispatcher.instance.dispatch('diyCreated', newDataId);
        hideLoading();
      } else {
        hideLoading();
        TipApi.show({
          show: true,
          content: (
            <View className={styles.tipWrap}>
              <Image src={res.icon_warn} className={styles.tipIcon} />
              <View className={styles.tipText}>{Strings.getLang('name_dedup')}</View>
            </View>
          ),
        });
        return -1;
      }
    }
  };

Painting Scene Setting:

// Reference: src/hooks/useCutDrawDiyScene.ts
export const useCutDrawDiySceneSet = () => {
  const ledNumber = useProps(p => p[dpCodes.led_number_set]);
  const structuredActions = useStructuredActions();

  return (data: DiySceneData, options?: PublishDpsOptions) => {
    const newData = { ...(data || ({} as DiySceneData)) };
    const segments = getArray(newData?.segments);
    if (ledNumber) {
      if (segments.length >= ledNumber) {
        newData.segments = segments.slice(0, ledNumber);
      } else {
        const defaultColor: DiySceneData['segments'][0] = {
          isWhite: false,
          hue: 0,
          saturation: 1000,
          value: 1000,
          brightness: 0,
          temperature: 0,
        };
        newData.segments = segments.concat(
          ...new Array(ledNumber - segments.length).fill(0).map(() => defaultColor)
        );
      }
    }
    structuredActions.diy_scene.set(newData, options);
  };
};
Data Structures

DIY Scene Data Format:

type DiySceneData = {
  name?: string;
  version?: number;
  id?: number;
  daubType?: 'all' | 'single' | 'clear'; // Painting type
  effect?: number; // Effect type
  segments?: Array<{
    // LED bead data
    hue?: number;
    saturation?: number;
    value?: number;
    brightness?: number;
    temperature?: number;
    isWhite?: boolean;
  }>;
};

Scene Preview

Scene preview is implemented through the preview method of useSceneSet Hook, encoding scene data and sending to device without saving to cloud storage.

Core Components
  • src/hooks/useSceneSet.ts: Scene setting Hook
Data Flow
flowchart TD
    A[User clicks preview] --> B[Build scene data]
    B --> C[useSceneSet.preview]
    C --> D[Encode scene data]
    D --> E[Send DP to device]
    E --> F[Device executes scene]
    F --> G[User observes effect]
    G --> H{Continue editing?}
    H -->|Yes| I[Return to editing]
    H -->|No| J[Save scene]
Key Code Snippets

Preview Implementation:

// Reference: src/hooks/useSceneSet.ts
  const preview = async (scene: SceneData) => {
    if (getArray(scene?.colors).length > 0) {
      await set(scene);
    }
  };

Scene Saving

Scene saving includes three steps: data encoding, cloud storage saving, and DP sending. It supports both add and update modes, and triggers scene creation event after successful save.

Core Components
  • src/pages/diyEdit/index.tsx: Dynamic scene save
  • src/pages/staticDiyEdit/index.tsx: Static scene save
  • src/hooks/useCloudStorageCombinedList.ts: Cloud storage list management
  • src/hooks/useCloudDrawToolList.ts: Scene data encoding/decoding
Data Flow

Already detailed in Feature 1 and Feature 2.

Scene Loading

Scene loading reads scene data from cloud storage, decodes and fills into editing page. Load to corresponding editing page based on scene type (dynamic or static).

Core Components
  • src/pages/diyEdit/index.tsx: Dynamic scene loading
  • src/pages/staticDiyEdit/index.tsx: Static scene loading
  • src/hooks/useCloudDrawToolList.ts: Scene data decoding
Data Flow
flowchart TD
    A[User selects edit] --> B[Get scene ID]
    B --> C[Read from cloud storage]
    C --> D[Decode scene data]
    D --> E{Scene type}
    E -->|Dynamic| F[Load to dynamic edit page]
    E -->|Static| G[Load to static edit page]
    F --> H[Fill color list]
    G --> I[Fill LED bead data]
    H --> J[Fill motion parameters]
    I --> K[Display light strip state]

Scene Deletion

Scene deletion removes scene data from cloud storage and updates scene list. User confirmation is required before deletion.

Core Components
  • src/pages/diyEdit/index.tsx: Delete functionality
  • src/pages/staticDiyEdit/index.tsx: Delete functionality
  • src/hooks/useCloudStorageCombinedList.ts: Cloud storage deletion
Key Code Snippets

Delete Implementation (using dynamic scene as example):

// Reference: src/pages/diyEdit/index.tsx
        onRightClick={() => {
          showConfirmDeleteModal({
            content: Strings.getLang('deleteSceneTip'),
            success(res) {
              if (res.confirm) {
                storage.delItem(dataId);
                router.back();
              }
            },
          });
        }}

Scene List Management

Scene list management is implemented through useDiySceneInit Hook, reading all scenes from cloud storage, categorizing by type, and supporting scene selection and switching.

Core Components
  • src/components/tab-diy/index.tsx: DIY scene list component
  • src/hooks/useDiySceneInit.ts: DIY scene initialization Hook
Data Flow
flowchart TD
    A[Enter DIY tab] --> B[useDiySceneInit initialization]
    B --> C[Read scene list from cloud storage]
    C --> D[Decode scene data]
    D --> E[Categorize by type]
    E --> F[Display scene cards]
    F --> G[User selects scene]
    G --> H{Scene type}
    H -->|Dynamic| I[Apply dynamic scene]
    H -->|Static| J[Apply static scene]
    I --> K[Send scene DP]
    J --> K
    K --> L[Update current scene ID]
Key Code Snippets

Scene Initialization:

// Reference: src/hooks/useDiySceneInit.ts
  const initDiyScene = useCallback(() => {
    if (!inited) return;
    if (!switch_led) return;
    console.log('init diy', current);
    if (current) {
      if (+current !== -1) {
        const item = getArray(storage.list).find(item => +item.id === +current);
        if (item?.value) {
          const props = decodeCloudSceneData(item?.value);
          if (props.type === 'scene') {
            sceneApi.set(props.ret);
            actions.work_mode.set('scene');
            setCurrent(String(item?.id));
          }
          if (props.type === 'draw') {
            cutDrawDiySceneSet(props?.ret);
            setCurrent(String(item?.id));
          }
        }
      } else {
        const item = getArray(storage.list)[0];
        if (item?.value) {
          const props = decodeCloudSceneData(item?.value);
          if (props.type === 'scene') {
            sceneApi.set(props.ret);
            actions.work_mode.set('scene');
            setCurrent(String(item?.id));
          }
          if (props.type === 'draw') {
            cutDrawDiySceneSet(props?.ret);
            setCurrent(String(item?.id));
          }
        }
      }
    }
  }, [current, storage.list, switch_led, inited]);

Scene List Generation:

// Reference: src/hooks/useDiySceneInit.ts
  const diySceneList = useMemo(
    () =>
      splitArray(
        getArray(storage.list)
          .sort((a, b) => b.id - a.id)
          .map(item => {
            const props = decodeCloudSceneData(item.value);
            return {
              dataId: item.id,
              data: props?.ret || {},
              cloudType: props.type,
            };
          }),
        2
      ),
    [storage.list]
  );

Notes

Implementation Challenges

  1. Scene Data Encoding/Decoding:

    • Challenge: Dynamic scenes and static scenes use different data formats and DP points
    • Solution: Use encodeCloudSceneData and decodeCloudSceneData for unified encoding/decoding, distinguish scene type through type field
  2. LED Bead Data Alignment:

    • Challenge: Static scene LED bead data needs to align with device LED bead count
    • Solution: Check data length in useCutDrawDiySceneSet, pad with default colors when insufficient, truncate when exceeded
  3. Scene Type Differentiation:

    • Challenge: Need to distinguish dynamic scenes (scene) and static scenes (draw)
    • Solution: Use type field in cloud storage data for identification, select corresponding processing logic based on type when loading
  4. Scene Name Deduplication:

    • Challenge: Prevent duplicate scene names
    • Solution: Use dedup function to check names, prompt user to modify when saving

Common Issues

  1. Issue: Data incorrect after scene loading

    • Cause: Data decoding failed or format mismatch
    • Solution: Check data format, add error handling, use default values as fallback
  2. Issue: Preview has no effect

    • Cause: Device not connected or DP sending failed
    • Solution: Check device connection status, view DP sending logs
  3. Issue: Static scene LED bead count mismatch

    • Cause: Device LED bead count changed or data corrupted
    • Solution: Automatically align LED bead count, pad or truncate data

Performance Optimization

  1. Scene List Caching:

    • Use useMemo to cache scene list, avoid repeated calculations
    • Scene list sorted by ID in descending order, newest scenes first
  2. Data Encoding Optimization:

    • Scene data encoding uses efficient string concatenation
    • Avoid unnecessary deep copies
  3. Lazy Loading:

    • Scene list paginated display, avoid loading all scenes at once
    • Scene data decoded on demand
  4. Debounce Processing:

    • Scene saving uses debounce, avoid repeated saves
    • Scene preview uses throttle, avoid frequent DP sending

Edge Cases

  1. Empty Scene List:

    • Handling: Display empty state prompt, guide user to create scene
    • Provide default scene options
  2. Scene Data Corrupted:

    • Handling: Use default data when decoding fails
    • Prompt user about scene data anomaly
  3. Device LED Bead Count Changes:

    • Handling: Automatically align LED bead count
    • Truncate exceeded parts, pad insufficient parts with default colors
  4. Scene Name Too Long:

    • Handling: Limit scene name length
    • Truncate exceeded parts or prompt user
  5. Editing Multiple Scenes Simultaneously:

    • Handling: Only one scene can be edited at a time
    • Save current scene when switching edits