diff --git a/OAT.xml b/OAT.xml index 66da5bc990b244aea3789bfc27b0ea657c5ad9b0..7cc66e0dbebe672d20e40dbc04b527cdc53c2dc0 100755 --- a/OAT.xml +++ b/OAT.xml @@ -33,6 +33,7 @@ + diff --git a/utils/tools/haptic_format_converter/.gitignore b/utils/tools/haptic_format_converter/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..472ecfa9b8f13c4e45e0977b54207ad5979f00ed --- /dev/null +++ b/utils/tools/haptic_format_converter/.gitignore @@ -0,0 +1,4 @@ +output_dir/* +*/output_dir/* +__pycache__/* +*/__pycache__/* diff --git a/utils/tools/haptic_format_converter/README.md b/utils/tools/haptic_format_converter/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bf420273013dffd1cb02857592fe11486986f459 --- /dev/null +++ b/utils/tools/haptic_format_converter/README.md @@ -0,0 +1,48 @@ +## Introduction + +This is a python script to convert between OH Haptic JSON and HE Haptic JSON formats. + +## Requirements + +- Python 3.8+ +- `pip3 install jsonschema` if needed + +## Usage +``` +usage: converter.py [-h] [-o OUTPUT] -f {oh,he_v1,he_v2} [-s SCHEMA_DIR] [-v] input + +Convert between OH Haptic JSON and HE Haptic JSON formats. + +positional arguments: + input Path to the input JSON file or directory. + +options: + -h, --help show this help message and exit + -o OUTPUT, --output OUTPUT + Path to the output directory (default: input directory with '_out' suffix). + -f {oh,he_v1,he_v2}, --format {oh,he_v1,he_v2} + Target format: 'oh', 'he_v1', or 'he_v2'. + -s SCHEMA_DIR, --schema_dir SCHEMA_DIR + Directory containing JSON schema files (default: 'schemas'). + -v, --version_suffix Include version suffix ('_v1' or '_v2') in output HE file names. + +``` + + +## Run command example + +convert oh to he_v2 + +``` +./converter.py ../tests/test_data/oh_sample.json -o output_dir -f he_v2 + +``` + + +## Test command example + +Need add more test cases to cover corner case + +``` +python3 -m unittest discover -s tests +``` diff --git a/utils/tools/haptic_format_converter/converter/__init__.py b/utils/tools/haptic_format_converter/converter/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0985da10bf2f5835269174ce3199a486b432bd6a --- /dev/null +++ b/utils/tools/haptic_format_converter/converter/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# converter/__init__.py +from .converter import ( + convert_oh_to_he_v1, + convert_oh_to_he_v2, + convert_he_v1_to_oh, + convert_he_v2_to_oh, + convert_he_v1_to_v2, + convert_he_v2_to_v1 +) diff --git a/utils/tools/haptic_format_converter/converter/converter.py b/utils/tools/haptic_format_converter/converter/converter.py new file mode 100644 index 0000000000000000000000000000000000000000..d67341d8eec27739e66d9ae403d5cc8ed08c9fbe --- /dev/null +++ b/utils/tools/haptic_format_converter/converter/converter.py @@ -0,0 +1,635 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Haptic JSON Converter + +This tool converts between OH Haptic JSON and HE Haptic JSON formats (v1 and v2). + +Version: v0.0.1 + +Usage: + converter.py [-o ] -f [-s ] [-v] + +Arguments: + input Path to the input JSON file or directory. + -o, --output Path to the output directory (default: input directory with '_out' suffix). + -f, --format Target format: 'oh', 'he_v1', or 'he_v2'. + -s, --schema_dir Directory containing JSON schema files (default: 'schemas'). + -v, --version_suffix Include version suffix ('_v1' or '_v2') in output HE file names. +""" + +import json +import argparse +import os +from pathlib import Path +import logging +from typing import Union, Dict, Any, Tuple + +import jsonschema +from jsonschema import validate + +# Configure logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + +# Constants for target formats +FORMAT_OH = 'oh' +FORMAT_HE_V1 = 'he_v1' +FORMAT_HE_V2 = 'he_v2' + +# Type alias for JSON data +JsonData = Dict[str, Any] + + +def load_schema(file_path: Union[str, Path]) -> JsonData: + """ + Load a JSON schema from the specified file. + + Args: + file_path (Union[str, Path]): Path to the JSON schema file. + + Returns: + JsonData: Parsed JSON schema. + """ + file_path = Path(file_path) + try: + with file_path.open('r', encoding='utf-8') as file: + return json.load(file) + except (FileNotFoundError, json.JSONDecodeError) as err: + logging.error("Error loading schema from %s: %s", file_path, err) + raise + + +def read_json(file_path: Union[str, Path]) -> JsonData: + """ + Read JSON data from a file. + + Args: + file_path (Union[str, Path]): Path to the JSON file. + + Returns: + JsonData: Parsed JSON data. + """ + file_path = Path(file_path) + try: + with file_path.open('r', encoding='utf-8') as file: + return json.load(file) + except (FileNotFoundError, json.JSONDecodeError) as err: + logging.error("Error reading JSON file %s: %s", file_path, err) + raise + + +def write_json(data: JsonData, file_path: Path) -> None: + """ + Write JSON data to a file. + + Args: + data (JsonData): JSON data to write. + file_path (Path): Path to the output file. + """ + try: + file_path.parent.mkdir(parents=True, exist_ok=True) + with file_path.open('w', encoding='utf-8') as file: + json.dump(data, file, indent=2) + except (OSError, json.JSONDecodeError) as err: + logging.error("Error writing JSON file %s: %s", file_path, err) + raise + + +def validate_json(data: JsonData, schema: JsonData) -> Tuple[bool, Union[None, jsonschema.exceptions.ValidationError]]: + """ + Validate JSON data against a schema. + + Args: + data (JsonData): JSON data to validate. + schema (JsonData): JSON schema to validate against. + + Returns: + Tuple[bool, Union[None, jsonschema.exceptions.ValidationError]]: Validation result and error if invalid. + """ + try: + validate(data, schema) + return True, None + except jsonschema.exceptions.ValidationError as err: + return False, err + + +def convert_oh_to_he_v1(oh_data: JsonData) -> JsonData: + """ + Convert OH JSON data to HE v1 format. + + Args: + oh_data (JsonData): OH JSON data. + + Returns: + JsonData: Converted HE v1 JSON data. + """ + he_data = { + "Metadata": { + "Version": 1 + }, + "Pattern": [] + } + + for channel in oh_data['Channels']: + for pattern in channel['Pattern']: + event = pattern['Event'] + he_event = { + "Type": event['Type'], + "RelativeTime": event['StartTime'], + "Parameters": { + "Intensity": event['Parameters']['Intensity'], + "Frequency": event['Parameters']['Frequency'] + } + } + if event['Type'] == 'continuous': + he_event['Duration'] = event['Duration'] + if 'Curve' in event['Parameters']: + he_event['Parameters']['Curve'] = [ + { + "Time": curve_point['Time'], + "Intensity": curve_point.get('Intensity', 0), + "Frequency": curve_point.get('Frequency', 0) + } + for curve_point in event['Parameters']['Curve'] + ] + else: + # Add default curve information if 'Curve' is not in parameters + he_event['Parameters']['Curve'] = [ + { + "Time": time, + "Intensity": 100, + "Frequency": 0 + } + for time in range(4) # Add four default points to satisfy CURVE_POINT_NUM_MIN + ] + he_data['Pattern'].append(he_event) + + return he_data + + +def convert_oh_to_he_v2(oh_data: JsonData) -> JsonData: + """ + Convert OH JSON data to HE v2 format. + + Args: + oh_data (JsonData): OH JSON data. + + Returns: + JsonData: Converted HE v2 JSON data. + """ + he_v2_data = { + "Metadata": { + "Version": 2 + }, + "PatternList": [] + } + + current_pattern = None + last_event_end_time = -1 # Tracks the end time of the last event in the current pattern + + for channel in oh_data["Channels"]: + for pattern in channel["Pattern"]: + event = pattern["Event"] + event_start_time = event["StartTime"] + event_end_time = event_start_time + \ + (event["Duration"] if event["Type"] == + "continuous" else 48) # Default transient duration + + if current_pattern is None or not isinstance(current_pattern, dict) or len(current_pattern.get("Pattern", [])) >= 16 \ + or (last_event_end_time != -1 and event_start_time - last_event_end_time > 1000): + if current_pattern and isinstance(current_pattern, dict): + he_v2_data["PatternList"].append(current_pattern) + current_pattern = { + "AbsoluteTime": event_start_time, + "Pattern": [] + } + + if event["Type"] == "continuous": + he_event = { + "Type": event["Type"], + "RelativeTime": event_start_time - current_pattern["AbsoluteTime"], + "Duration": event["Duration"], + "Parameters": { + "Intensity": event["Parameters"]["Intensity"], + "Frequency": event["Parameters"].get("Frequency", 50) + } + } + if "Curve" in event["Parameters"]: + he_event["Parameters"]["Curve"] = [ + { + "Time": point["Time"], + "Intensity": point.get("Intensity", 100), + # Assigning default value of 0 if Frequency is None + "Frequency": point.get("Frequency", 0) + } + for point in event["Parameters"]["Curve"] + if "Intensity" in point or "Frequency" in point + ] + else: + # Add default curve information if 'Curve' is not in parameters + he_event["Parameters"]["Curve"] = [ + { + "Time": time, + "Intensity": 100, + "Frequency": 0 + } + for time in range(0, 4) # Add four default points to satisfy least need curve in hev2 + ] + current_pattern["Pattern"].append(he_event) + else: + he_event = { + "Type": event["Type"], + "RelativeTime": event_start_time - current_pattern["AbsoluteTime"], + "Parameters": { + "Intensity": event["Parameters"]["Intensity"], + "Frequency": event["Parameters"].get("Frequency", 50) + } + } + current_pattern["Pattern"].append(he_event) + last_event_end_time = event_end_time + + if current_pattern: + he_v2_data["PatternList"].append(current_pattern) + + return he_v2_data + + +def convert_he_v1_to_v2(he_v1_data: JsonData) -> JsonData: + """ + Convert HE V1 JSON data to HE V2 format. + + Args: + data (JsonData): HE V1 JSON data. + + Returns: + JsonData: Converted HE V2 JSON data. + """ + converted_data = { + "Metadata": { + "Version": 2 + }, + "PatternList": [] + } + + current_pattern = None + last_event_end_time = -1 # Tracks the end time of the last event in the current pattern + + for event in he_v1_data["Pattern"]: + event_start_time = event["RelativeTime"] + event_end_time = event_start_time + \ + (event["Duration"] if event["Type"] == "continuous" else 48) + + if current_pattern is None or not isinstance(current_pattern, dict) or len(current_pattern.get("Pattern", [])) >= 16 \ + or (last_event_end_time != -1 and event_start_time - last_event_end_time > 1000): + if current_pattern and isinstance(current_pattern, dict): + converted_data["PatternList"].append(current_pattern) + current_pattern = { + "AbsoluteTime": event_start_time, + "Pattern": [] + } + + # Order of dictionary insertion matters + event_dict = { + "Type": event["Type"], + "RelativeTime": event_start_time - current_pattern["AbsoluteTime"] + } + + if event["Type"] == "continuous": + event_dict["Duration"] = event["Duration"] + + event_dict["Parameters"] = event["Parameters"] + + current_pattern["Pattern"].append(event_dict) + last_event_end_time = event_end_time + + if current_pattern: + converted_data["PatternList"].append(current_pattern) + + return converted_data + + +def convert_he_v2_to_v1(he_v2_data: JsonData) -> JsonData: + """ + Convert HE v2 JSON data to HE v1 format. + + Args: + he_v2_data (JsonData): HE v2 JSON data. + + Returns: + JsonData: Converted HE v1 JSON data. + """ + he_v1_data = { + "Metadata": { + "Version": 1 + }, + "Pattern": [] + } + + for pattern_list_entry in he_v2_data['PatternList']: + for event in pattern_list_entry['Pattern']: + he_v1_event = event.copy() + he_v1_event['RelativeTime'] = event['RelativeTime'] + \ + pattern_list_entry['AbsoluteTime'] + he_v1_data['Pattern'].append(he_v1_event) + + return he_v1_data + + +def clamp(value: float, min_value: float, max_value: float) -> float: + """ + Clamp a value between a minimum and maximum value. + + Args: + value (float): Value to clamp. + min_value (float): Minimum value. + max_value (float): Maximum value. + + Returns: + float: Clamped value. + """ + return max(min_value, min(value, max_value)) + + +def convert_he_v1_to_oh(he_v1_data: JsonData) -> JsonData: + """ + Convert HE v1 JSON data to OH format. + + Args: + he_v1_data (JsonData): HE v1 JSON data. + + Returns: + JsonData: Converted OH JSON data. + """ + output_data: JsonData = { + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [{ + "Parameters": { + "Index": 0 + }, + "Pattern": [] + }] + } + + for pattern in he_v1_data.get('Pattern', []): + event = { + "Event": { + "Type": pattern['Type'], + "StartTime": pattern['RelativeTime'], + "Parameters": { + "Intensity": clamp(pattern['Parameters']['Intensity'], 0, 100), + "Frequency": clamp(pattern['Parameters'].get('Frequency', 50), -100, 100) + } + } + } + if pattern['Type'] == 'continuous': + event['Event']['Duration'] = pattern['Duration'] + event['Event']['Parameters']['Curve'] = [ + { + "Time": point['Time'], + "Intensity": clamp(point.get('Intensity', 100), 0, 100), + "Frequency": clamp(point.get('Frequency', 0), -100, 100) + } + for point in pattern['Parameters'].get('Curve', []) + ] + output_data['Channels'][0]['Pattern'].append(event) + + return output_data + + +def convert_he_v2_to_oh(he_v2_data: JsonData) -> JsonData: + """ + Convert HE v2 JSON data to OH format. + + Args: + he_v2_data (JsonData): HE v2 JSON data. + + Returns: + JsonData: Converted OH JSON data. + """ + event_num_max = 128 + + output_data = { + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [ + { + "Parameters": { + "Index": 0 + }, + "Pattern": [] + } + ] + } + + all_events = [] + + for pattern_list_entry in he_v2_data.get('PatternList', []): + absolute_time = pattern_list_entry.get('AbsoluteTime', 0) + for pattern in pattern_list_entry.get('Pattern', []): + if pattern['Type'] == 'continuous': + event = { + "Event": { + "Type": pattern['Type'], + "StartTime": absolute_time + pattern.get('RelativeTime', 0), + "Duration": clamp(pattern.get('Duration', 1000), 1, 5000), + "Parameters": { + "Intensity": clamp(pattern['Parameters'].get('Intensity', 100), 0, 100), + "Frequency": clamp(pattern['Parameters'].get('Frequency', 50), -100, 100), + "Curve": [ + { + "Time": clamp(point.get('Time', 0), 0, 10000), + "Intensity": clamp(point.get('Intensity', 100), 0, 100), + "Frequency": clamp(point.get('Frequency', 0), -100, 100) + } + for point in pattern['Parameters'].get('Curve', []) + ] + } + } + } + else: + event = { + "Event": { + "Type": pattern['Type'], + "StartTime": absolute_time + pattern.get('RelativeTime', 0), + "Parameters": { + "Intensity": clamp(pattern['Parameters'].get('Intensity', 100), 0, 100), + "Frequency": clamp(pattern['Parameters'].get('Frequency', 50), -100, 100) + } + } + } + all_events.append(event) + + all_events.sort(key=lambda e: e['Event']['StartTime']) + + for event in all_events: + index = event['Event'].get('Index', 0) + channel_found = False + + for channel in output_data['Channels']: + if channel['Parameters']['Index'] == index: + if len(channel['Pattern']) < event_num_max: + channel['Pattern'].append(event) + channel_found = True + break + + if not channel_found: + if len(output_data['Channels']) < 3: + new_channel = { + "Parameters": { + "Index": index + }, + "Pattern": [event] + } + output_data['Channels'].append(new_channel) + + return output_data + + +def process_file(input_file: Union[str, Path], output_dir: Union[str, Path], target_format: str, schema_dir: Union[str, Path], version_suffix: bool) -> None: + """ + Process a single JSON file and convert it to the target format. + + Args: + input_file (Union[str, Path]): Path to the input JSON file. + output_dir (Union[str, Path]): Path to the output directory. + target_format (str): Target format: 'oh', 'he_v1', or 'he_v2'. + schema_dir (Union[str, Path]): Directory containing JSON schema files. + version_suffix (bool): Include version suffix ('_v1' or '_v2') in output HE file names. + """ + input_file = Path(input_file) + output_dir = Path(output_dir) + schema_dir = Path(schema_dir) + + try: + input_data = read_json(input_file) + + # Load schemas + schemas = { + FORMAT_OH: load_schema(os.path.join(schema_dir, 'oh_schema.json')), + FORMAT_HE_V1: load_schema(os.path.join(schema_dir, 'he_v1_schema.json')), + FORMAT_HE_V2: load_schema(os.path.join(schema_dir, 'he_v2_schema.json')), + } + + # Determine the schema to validate against + input_format = None + for in_format, schema in schemas.items(): + is_valid, _ = validate_json(input_data, schema) + if is_valid: + input_format = in_format + break + + if not input_format: + logging.error("No valid schema found for file %s", input_file) + return + + # Define a mapping of conversion functions and output extensions + conversion_map = { + (FORMAT_OH, FORMAT_HE_V1): (convert_oh_to_he_v1, '_v1.he', '.he'), + (FORMAT_OH, FORMAT_HE_V2): (convert_oh_to_he_v2, '_v2.he', '.he'), + (FORMAT_HE_V1, FORMAT_OH): (convert_he_v1_to_oh, '.json', '.json'), + (FORMAT_HE_V1, FORMAT_HE_V2): (convert_he_v1_to_v2, '_v2.he', '.he'), + (FORMAT_HE_V2, FORMAT_OH): (convert_he_v2_to_oh, '.json', '.json'), + (FORMAT_HE_V2, FORMAT_HE_V1): (convert_he_v2_to_v1, '_v1.he', '.he'), + } + + # Perform conversion based on input format and target format + if (input_format, target_format) in conversion_map: + convert_func, versioned_ext, default_ext = conversion_map[(input_format, target_format)] + output_data = convert_func(input_data) + output_ext = versioned_ext if version_suffix else default_ext + else: + output_data = input_data if input_format in [FORMAT_OH, FORMAT_HE_V1, FORMAT_HE_V2] else None + output_ext = '.json' if input_format == FORMAT_OH else (f'_{input_format.lower()}.he' if version_suffix else '.he') + if output_data is None: + logging.error("Unsupported input format for file %s", input_file) + return + + # Validate the output data with the target format schema + target_schema = schemas[target_format] + is_valid, error = validate_json(output_data, target_schema) + if not is_valid: + logging.error("Validation error for the converted data against %s schema: %s", target_format, error) + return + + output_file = output_dir / (input_file.stem + output_ext) + write_json(output_data, output_file) + except (FileNotFoundError, json.JSONDecodeError, jsonschema.exceptions.ValidationError) as err: + logging.error("Error processing file %s: %s", input_file, err) + + +def process_directory(input_dir: Union[str, Path], output_dir: Union[str, Path], target_format: str, schema_dir: Union[str, Path], version_suffix: bool) -> None: + """ + Process all JSON files in a directory and convert them to the target format. + + Args: + input_dir (Union[str, Path]): Path to the input directory. + output_dir (Union[str, Path]): Path to the output directory. + target_format (str): Target format: 'oh', 'he_v1', or 'he_v2'. + schema_dir (Union[str, Path]): Directory containing JSON schema files. + version_suffix (bool): Include version suffix ('_v1' or '_v2') in output HE file names. + """ + input_dir = Path(input_dir) + output_dir = Path(output_dir) + + for root, _, files in os.walk(input_dir): + for file in files: + if file.endswith('.json') or file.endswith('.he'): + input_file = os.path.join(root, file) + process_file(input_file, output_dir, target_format, + schema_dir, version_suffix) + + +def main() -> None: + """ + Main entry point of the script. Parses arguments and processes the input accordingly. + """ + parser = argparse.ArgumentParser( + description="Convert between OH Haptic JSON and HE Haptic JSON formats.") + parser.add_argument("input", type=Path, + help="Path to the input JSON file or directory.") + parser.add_argument("-o", "--output", type=Path, + help="Path to the output dir (default: input dir with '_out' suffix).") + parser.add_argument("-f", "--format", choices=[FORMAT_OH, FORMAT_HE_V1, FORMAT_HE_V2], + required=True, help="Target format: 'oh', 'he_v1', or 'he_v2'.") + parser.add_argument("-s", "--schema_dir", type=Path, default=Path("schemas"), + help="Directory containing JSON schema files (default: 'schemas').") + parser.add_argument("-v", "--version_suffix", action="store_true", + help="Include version suffix ('_v1' or '_v2') in output HE file names.") + + args = parser.parse_args() + + if args.input.is_file(): + input_file = args.input + output_dir = args.output or args.input.parent + process_file(input_file, output_dir, args.format, + args.schema_dir, args.version_suffix) + elif args.input.is_dir(): + input_dir = args.input + output_dir = args.output or input_dir.with_name( + input_dir.name + '_out') + process_directory(input_dir, output_dir, args.format, + args.schema_dir, args.version_suffix) + else: + raise ValueError(f"Invalid input path: {args.input}") + +if __name__ == "__main__": + main() diff --git a/utils/tools/haptic_format_converter/converter/schemas/he_v1_schema.json b/utils/tools/haptic_format_converter/converter/schemas/he_v1_schema.json new file mode 100644 index 0000000000000000000000000000000000000000..a3f561dd1b706ec25b27dbfeef5b854db9059066 --- /dev/null +++ b/utils/tools/haptic_format_converter/converter/schemas/he_v1_schema.json @@ -0,0 +1,148 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HE HapticJsonSchema V1", + "type": "object", + "properties": { + "Metadata": { + "type": "object", + "properties": { + "Version": { + "type": "integer", + "enum": [1], + "description": "Supported version is 1" + } + }, + "required": ["Version"] + }, + "Pattern": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + }, + "minItems": 1, + "maxItems": 16, + "description": "List of events in the pattern. Required and only applicable for version 1." + } + }, + "definitions": { + "Event": { + "type": "object", + "properties": { + "Type": { + "type": "string", + "enum": ["transient", "continuous"], + "description": "Type of the event, can be transient or continuous" + }, + "Duration": { + "type": "integer", + "minimum": 1, + "maximum": 5000, + "description": "Duration of the event in milliseconds. Only applicable for continuous events." + }, + "Index": { + "type": "integer", + "minimum": 0, + "maximum": 2, + "default": 0, + "description": "Index of the event" + }, + "RelativeTime": { + "type": "integer", + "minimum": 0, + "description": "Relative start time of the event within the pattern" + }, + "Parameters": { + "type": "object", + "properties": { + "Intensity": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Intensity of the event" + }, + "Frequency": { + "type": "integer", + "minimum": -100, + "maximum": 150, + "description": "Frequency of the event" + }, + "Curve": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Time": { + "type": "integer", + "minimum": 0, + "description": "Relative time of the curve point within the event duration" + }, + "Intensity": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Intensity of the curve point, scaled from 0 to 100" + }, + "Frequency": { + "type": "integer", + "minimum": -100, + "maximum": 100, + "description": "Frequency of the curve point" + } + }, + "required": ["Time", "Intensity", "Frequency"] + }, + "minItems": 4, + "maxItems": 16, + "description": "Curve points defining the haptic effect over time. Only applicable for continuous events." + } + }, + "required": ["Intensity", "Frequency"] + } + }, + "required": ["Type", "RelativeTime", "Parameters"], + "if": { + "properties": { "Type": { "const": "transient" } } + }, + "then": { + "properties": { + "Duration": { + "const": 48, + "description": "Duration for transient events is fixed to 48 milliseconds" + }, + "Parameters": { + "properties": { + "Frequency": { + "minimum": -50, + "maximum": 150 + } + } + } + } + }, + "else": { + "properties": { + "Parameters": { + "properties": { + "Frequency": { + "minimum": 0, + "maximum": 100 + }, + "Curve": { + "minItems": 4, + "maxItems": 16 + } + }, + "required": ["Curve"] + }, + "Duration": { + "minimum": 1, + "maximum": 5000 + } + }, + "required": ["Duration"] + } + } + }, + "required": ["Metadata", "Pattern"] +} + diff --git a/utils/tools/haptic_format_converter/converter/schemas/he_v2_schema.json b/utils/tools/haptic_format_converter/converter/schemas/he_v2_schema.json new file mode 100644 index 0000000000000000000000000000000000000000..e5e03d49eccee445750a3cbc0cab6cb7ec98a0e3 --- /dev/null +++ b/utils/tools/haptic_format_converter/converter/schemas/he_v2_schema.json @@ -0,0 +1,164 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HE HapticJsonSchema V2", + "type": "object", + "properties": { + "Metadata": { + "type": "object", + "properties": { + "Version": { + "type": "integer", + "enum": [2], + "description": "Supported version is 2" + } + }, + "required": ["Version"] + }, + "PatternList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "AbsoluteTime": { + "type": "integer", + "minimum": 0, + "description": "Absolute start time of the pattern. Should be greater than the previous pattern's start time." + }, + "Pattern": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + }, + "minItems": 1, + "maxItems": 16, + "description": "List of events in the pattern" + } + }, + "required": ["AbsoluteTime", "Pattern"] + }, + "minItems": 1, + "description": "List of patterns with absolute start times. Required and only applicable for version 2." + } + }, + "definitions": { + "Event": { + "type": "object", + "properties": { + "Type": { + "type": "string", + "enum": ["transient", "continuous"], + "description": "Type of the event, can be transient or continuous" + }, + "Duration": { + "type": "integer", + "minimum": 1, + "maximum": 5000, + "description": "Duration of the event in milliseconds. Only applicable for continuous events." + }, + "Index": { + "type": "integer", + "minimum": 0, + "maximum": 2, + "default": 0, + "description": "Index of the event" + }, + "RelativeTime": { + "type": "integer", + "minimum": 0, + "description": "Relative start time of the event within the pattern" + }, + "Parameters": { + "type": "object", + "properties": { + "Intensity": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Intensity of the event" + }, + "Frequency": { + "type": "integer", + "minimum": -100, + "maximum": 150, + "description": "Frequency of the event" + }, + "Curve": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Time": { + "type": "integer", + "minimum": 0, + "description": "Relative time of the curve point within the event duration" + }, + "Intensity": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Intensity of the curve point, scaled from 0 to 100" + }, + "Frequency": { + "type": "integer", + "minimum": -100, + "maximum": 100, + "description": "Frequency of the curve point" + } + }, + "required": ["Time", "Intensity", "Frequency"] + }, + "minItems": 4, + "maxItems": 16, + "description": "Curve points defining the haptic effect over time. Only applicable for continuous events." + } + }, + "required": ["Intensity", "Frequency"] + } + }, + "required": ["Type", "RelativeTime", "Parameters"], + "if": { + "properties": { "Type": { "const": "transient" } } + }, + "then": { + "properties": { + "Duration": { + "const": 48, + "description": "Duration for transient events is fixed to 48 milliseconds" + }, + "Parameters": { + "properties": { + "Frequency": { + "minimum": -50, + "maximum": 150 + } + } + } + } + }, + "else": { + "properties": { + "Parameters": { + "properties": { + "Frequency": { + "minimum": 0, + "maximum": 100 + }, + "Curve": { + "minItems": 4, + "maxItems": 16 + } + }, + "required": ["Curve"] + }, + "Duration": { + "minimum": 1, + "maximum": 5000 + } + }, + "required": ["Duration"] + } + } + }, + "required": ["Metadata", "PatternList"] +} + diff --git a/utils/tools/haptic_format_converter/converter/schemas/oh_schema.json b/utils/tools/haptic_format_converter/converter/schemas/oh_schema.json new file mode 100644 index 0000000000000000000000000000000000000000..e2b1ae64f95a7f37d8c1704cff99375dc8067705 --- /dev/null +++ b/utils/tools/haptic_format_converter/converter/schemas/oh_schema.json @@ -0,0 +1,175 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OH Haptic JSON Schema", + "type": "object", + "properties": { + "MetaData": { + "type": "object", + "properties": { + "Version": { + "type": "number", + "const": 1.0 + }, + "ChannelNumber": { + "type": "integer", + "minimum": 1, + "maximum": 3 + } + }, + "required": ["Version", "ChannelNumber"], + "additionalProperties": false + }, + "Channels": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "items": { + "type": "object", + "properties": { + "Parameters": { + "type": "object", + "properties": { + "Index": { + "type": "integer", + "minimum": 0, + "maximum": 2 + } + }, + "required": ["Index"], + "additionalProperties": false + }, + "Pattern": { + "type": "array", + "maxItems": 128, + "items": { + "type": "object", + "properties": { + "Event": { + "type": "object", + "properties": { + "Type": { + "type": "string", + "enum": ["continuous", "transient"] + }, + "StartTime": { + "type": "integer", + "minimum": 0, + "maximum": 1800000 + }, + "Duration": { + "type": "integer", + "minimum": 0, + "maximum": 5000 + }, + "Parameters": { + "type": "object", + "properties": { + "Intensity": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "Frequency": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "Curve": { + "type": "array", + "minItems": 4, + "maxItems": 16, + "items": { + "type": "object", + "properties": { + "Time": { + "type": "integer", + "minimum": 0 + }, + "Intensity": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "Frequency": { + "type": "integer", + "minimum": -100, + "maximum": 100 + } + }, + "required": ["Time"], + "anyOf": [ + { + "required": ["Intensity"] + }, + { + "required": ["Frequency"] + } + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "required": ["Type", "StartTime"], + "allOf": [ + { + "if": { + "properties": { + "Type": { + "const": "continuous" + } + } + }, + "then": { + "properties": { + "Parameters": { + "required": ["Intensity", "Frequency"] + } + }, + "required": ["Duration"] + } + }, + { + "if": { + "properties": { + "Type": { + "const": "transient" + } + } + }, + "then": { + "properties": { + "Duration": { + "const": 48 + }, + "Parameters": { + "required": ["Intensity"], + "properties": { + "Frequency": { + "default": 50 + } + } + } + } + } + } + ], + "additionalProperties": false + } + }, + "required": ["Event"], + "additionalProperties": false + } + } + }, + "required": ["Parameters", "Pattern"], + "additionalProperties": false + } + } + }, + "required": ["MetaData", "Channels"], + "additionalProperties": false +} + diff --git a/utils/tools/haptic_format_converter/requirements.txt b/utils/tools/haptic_format_converter/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d89304b1a89d0f441beaefdc5ae5de22c95f06a2 --- /dev/null +++ b/utils/tools/haptic_format_converter/requirements.txt @@ -0,0 +1 @@ +jsonschema diff --git a/utils/tools/haptic_format_converter/tests/__init__.py b/utils/tools/haptic_format_converter/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0985da10bf2f5835269174ce3199a486b432bd6a --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/__init__.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# converter/__init__.py +from .converter import ( + convert_oh_to_he_v1, + convert_oh_to_he_v2, + convert_he_v1_to_oh, + convert_he_v2_to_oh, + convert_he_v1_to_v2, + convert_he_v2_to_v1 +) diff --git a/utils/tools/haptic_format_converter/tests/test_converter.py b/utils/tools/haptic_format_converter/tests/test_converter.py new file mode 100644 index 0000000000000000000000000000000000000000..7f431fe3830406b77a23c25ecc65c534bea7d611 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_converter.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Huawei Device Co., Ltd. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Haptic JSON Converter testcases + +run `python3 -m unittest discover -s tests` in repo base folder: + +""" + +import unittest +from pathlib import Path +import json + +from converter.converter import ( + convert_oh_to_he_v1, + convert_oh_to_he_v2, + convert_he_v1_to_oh, + convert_he_v2_to_oh, + convert_he_v1_to_v2, + convert_he_v2_to_v1 +) + + +class TestConverter(unittest.TestCase): + + def setUp(self): + self.schema_dir = Path('converter/schemas') + self.test_data_dir = Path('tests/test_data') + self.output_dir = Path('converter/output_dir') + self.output_dir.mkdir(parents=True, exist_ok=True) + + def read_json(self, file_path): + with open(file_path, 'r', encoding='utf-8') as file: + return json.load(file) + + def test_oh_to_he_v1(self): + input_data = self.read_json(self.test_data_dir / 'oh_sample.json') + expected_output = self.read_json( + self.test_data_dir / 'oh_sample_to_he_v1.he') + converted_data = convert_oh_to_he_v1(input_data) + self.assertEqual(converted_data, expected_output) + + def test_oh_to_he_v2(self): + input_data = self.read_json(self.test_data_dir / 'oh_sample.json') + expected_output = self.read_json( + self.test_data_dir / 'oh_sample_to_he_v2.he') + converted_data = convert_oh_to_he_v2(input_data) + self.assertEqual(converted_data, expected_output) + + def test_he_v1_to_oh(self): + input_data = self.read_json(self.test_data_dir / 'he_v1_sample.he') + expected_output = self.read_json( + self.test_data_dir / 'he_v1_sample_to_oh.json') + converted_data = convert_he_v1_to_oh(input_data) + self.assertEqual(converted_data, expected_output) + + def test_he_v2_to_oh(self): + input_data = self.read_json(self.test_data_dir / 'he_v2_sample.he') + expected_output = self.read_json( + self.test_data_dir / 'he_v2_sample_to_oh.json') + converted_data = convert_he_v2_to_oh(input_data) + self.assertEqual(converted_data, expected_output) + + def test_he_v1_to_v2(self): + input_data = self.read_json(self.test_data_dir / 'he_v1_sample.he') + expected_output = self.read_json( + self.test_data_dir / 'he_v1_sample_to_he_v2.he') + converted_data = convert_he_v1_to_v2(input_data) + self.assertEqual(converted_data, expected_output) + + def test_he_v2_to_v1(self): + input_data = self.read_json(self.test_data_dir / 'he_v2_sample.he') + expected_output = self.read_json( + self.test_data_dir / 'he_v2_sample_to_he_v1.he') + converted_data = convert_he_v2_to_v1(input_data) + self.assertEqual(converted_data, expected_output) + + def test_he_v1_to_v2_to_v1(self): + input_data = self.read_json(self.test_data_dir / 'he_v1_sample.he') + expected_output1 = self.read_json( + self.test_data_dir / 'he_v1_sample_to_he_v2.he') + converted_data1 = convert_he_v1_to_v2(input_data) + self.assertEqual(converted_data1, expected_output1) + converted_data2 = convert_he_v2_to_v1(converted_data1) + self.assertEqual(converted_data2, input_data) + + def test_he_v2_to_he_v1_to_he_v2(self): + input_data = self.read_json(self.test_data_dir / 'he_v2_sample.he') + expected_output1 = self.read_json( + self.test_data_dir / 'he_v2_sample_to_he_v1.he') + converted_data1 = convert_he_v2_to_v1(input_data) + self.assertEqual(converted_data1, expected_output1) + converted_data2 = convert_he_v1_to_v2(converted_data1) + self.assertEqual(converted_data2, input_data) + + def test_oh_to_he_v2_to_oh(self): + input_data = self.read_json(self.test_data_dir / 'oh_sample_1.json') + expected_output1 = self.read_json( + self.test_data_dir / 'oh_sample_to_he_v2.he') + converted_data1 = convert_oh_to_he_v2(input_data) + self.assertEqual(converted_data1, expected_output1) + converted_data2 = convert_he_v2_to_oh(converted_data1) + self.assertEqual(converted_data2, input_data) + + def test_he_v2_to_oh_to_he_v2(self): + input_data = self.read_json(self.test_data_dir / 'he_v2_sample.he') + expected_output1 = self.read_json( + self.test_data_dir / 'he_v2_sample_to_oh.json') + converted_data1 = convert_he_v2_to_oh(input_data) + self.assertEqual(converted_data1, expected_output1) + converted_data2 = convert_oh_to_he_v2(converted_data1) + self.assertEqual(converted_data2, input_data) + + def test_oh_to_he_v2_with_default_curve(self): + input_data = self.read_json(self.test_data_dir / 'oh_no_curve_sample.json') + expected_output = self.read_json( + self.test_data_dir / 'oh_no_curve_sample_to_he_v2.json') + converted_data = convert_oh_to_he_v2(input_data) + self.assertEqual(converted_data, expected_output) + + def test_oh_to_he_v1_with_default_curve(self): + input_data = self.read_json(self.test_data_dir / 'oh_no_curve_sample.json') + expected_output = self.read_json( + self.test_data_dir / 'oh_no_curve_sample_to_he_v1.json') + converted_data = convert_oh_to_he_v1(input_data) + self.assertEqual(converted_data, expected_output) + +if __name__ == '__main__': + unittest.main() diff --git a/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample.he b/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample.he new file mode 100644 index 0000000000000000000000000000000000000000..5a833850977cbf2d459063bf935fff9e7fcf8c06 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample.he @@ -0,0 +1,46 @@ +{ + "Metadata": { + "Version": 1 + }, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 20 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 30 + } + ] + } + } + ] +} diff --git a/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample_to_he_v2.he b/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample_to_he_v2.he new file mode 100644 index 0000000000000000000000000000000000000000..c9fa3f9d68f3f6cfde1a22098bedcfb5abc02d25 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample_to_he_v2.he @@ -0,0 +1,51 @@ +{ + "Metadata": { + "Version": 2 + }, + "PatternList": [ + { + "AbsoluteTime": 0, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 20 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 30 + } + ] + } + } + ] + } + ] +} diff --git a/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample_to_oh.json b/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample_to_oh.json new file mode 100644 index 0000000000000000000000000000000000000000..4af4fa9f6d8f8a7da5a19fc6abdaf9747d6c64a4 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/he_v1_sample_to_oh.json @@ -0,0 +1,58 @@ +{ + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [ + { + "Parameters": { + "Index": 0 + }, + "Pattern": [ + { + "Event": { + "Type": "transient", + "StartTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + } + }, + { + "Event": { + "Type": "continuous", + "StartTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 20 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 30 + } + ] + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample.he b/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample.he new file mode 100644 index 0000000000000000000000000000000000000000..a6abf48901fcb3543abe6a16fbc8b32fa214c70c --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample.he @@ -0,0 +1,95 @@ +{ + "Metadata": { + "Version": 2 + }, + "PatternList": [ + { + "AbsoluteTime": 0, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 20 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 30 + } + ] + } + } + ] + }, + { + "AbsoluteTime": 5000, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 80, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime":200, + "Duration": 1500, + "Parameters": { + "Intensity": 60, + "Frequency": 90, + "Curve": [ + { + "Time": 0, + "Intensity": 30, + "Frequency": -10 + }, + { + "Time": 200, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 600, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 1500, + "Intensity": 90, + "Frequency": 20 + } + ] + } + } + ] + } + ] +} diff --git a/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample_to_he_v1.he b/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample_to_he_v1.he new file mode 100644 index 0000000000000000000000000000000000000000..018bb0316e8abc1e9386761d4940e4f42cf73b7c --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample_to_he_v1.he @@ -0,0 +1,85 @@ +{ + "Metadata": { + "Version": 1 + }, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 20 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 30 + } + ] + } + }, + { + "Type": "transient", + "RelativeTime": 5000, + "Parameters": { + "Intensity": 80, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 5200, + "Duration": 1500, + "Parameters": { + "Intensity": 60, + "Frequency": 90, + "Curve": [ + { + "Time": 0, + "Intensity": 30, + "Frequency": -10 + }, + { + "Time": 200, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 600, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 1500, + "Intensity": 90, + "Frequency": 20 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample_to_oh.json b/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample_to_oh.json new file mode 100644 index 0000000000000000000000000000000000000000..73f0359dda2e9b9c2846cc9dd45faaca6f1b6a11 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/he_v2_sample_to_oh.json @@ -0,0 +1,101 @@ +{ + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [ + { + "Parameters": { + "Index": 0 + }, + "Pattern": [ + { + "Event": { + "Type": "transient", + "StartTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + } + }, + { + "Event": { + "Type": "continuous", + "StartTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 20 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 30 + } + ] + } + } + }, + { + "Event": { + "Type": "transient", + "StartTime": 5000, + "Parameters": { + "Intensity": 80, + "Frequency": 100 + } + } + }, + { + "Event": { + "Type": "continuous", + "StartTime": 5200, + "Duration": 1500, + "Parameters": { + "Intensity": 60, + "Frequency": 90, + "Curve": [ + { + "Time": 0, + "Intensity": 30, + "Frequency": -10 + }, + { + "Time": 200, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 600, + "Intensity": 70, + "Frequency": 10 + }, + { + "Time": 1500, + "Intensity": 90, + "Frequency": 20 + } + ] + } + } + } + ] + } + ] +} diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample.json b/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample.json new file mode 100644 index 0000000000000000000000000000000000000000..909ae11b081673197cdbfac10658ee2a97d7d91a --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample.json @@ -0,0 +1,27 @@ +{ + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [ + { + "Parameters": { + "Index": 0 + }, + "Pattern": [ + { + "Event": { + "Type": "continuous", + "StartTime": 100, + "Duration": 1200, + "Parameters": { + "Intensity": 90, + "Frequency": 60 + } + } + } + ] + } + ] +} + diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample_to_he_v1.json b/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample_to_he_v1.json new file mode 100644 index 0000000000000000000000000000000000000000..b7ceb33ab727d595470633695929e7ad4cd618b8 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample_to_he_v1.json @@ -0,0 +1,38 @@ +{ + "Metadata": { + "Version": 1 + }, + "Pattern": [ + { + "Type": "continuous", + "RelativeTime": 100, + "Duration": 1200, + "Parameters": { + "Intensity": 90, + "Frequency": 60, + "Curve": [ + { + "Time": 0, + "Intensity": 100, + "Frequency": 0 + }, + { + "Time": 1, + "Intensity": 100, + "Frequency": 0 + }, + { + "Time": 2, + "Intensity": 100, + "Frequency": 0 + }, + { + "Time": 3, + "Intensity": 100, + "Frequency": 0 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample_to_he_v2.json b/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample_to_he_v2.json new file mode 100644 index 0000000000000000000000000000000000000000..f6af7199a1502abb122eac861e4595f38c2da399 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_no_curve_sample_to_he_v2.json @@ -0,0 +1,28 @@ +{ + "Metadata": { + "Version": 2 + }, + "PatternList": [ + { + "AbsoluteTime": 100, + "Pattern": [ + { + "Type": "continuous", + "RelativeTime": 0, + "Duration": 1200, + "Parameters": { + "Intensity": 90, + "Frequency": 60, + "Curve": [ + {"Time": 0, "Intensity": 100, "Frequency": 0}, + {"Time": 1, "Intensity": 100, "Frequency": 0}, + {"Time": 2, "Intensity": 100, "Frequency": 0}, + {"Time": 3, "Intensity": 100, "Frequency": 0} + ] + } + } + ] + } + ] +} + diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_sample.json b/utils/tools/haptic_format_converter/tests/test_data/oh_sample.json new file mode 100644 index 0000000000000000000000000000000000000000..3284233ee931dbaaf3af2f50f859cee6d3878121 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_sample.json @@ -0,0 +1,55 @@ +{ + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [ + { + "Parameters": { + "Index": 0 + }, + "Pattern": [ + { + "Event": { + "Type": "transient", + "StartTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + } + }, + { + "Event": { + "Type": "continuous", + "StartTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50 + }, + { + "Time": 100, + "Intensity": 70 + }, + { + "Time": 300, + "Intensity": 80 + }, + { + "Time": 500, + "Intensity": 100 + } + ] + } + } + } + ] + } + ] +} + diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_sample_1.json b/utils/tools/haptic_format_converter/tests/test_data/oh_sample_1.json new file mode 100644 index 0000000000000000000000000000000000000000..0145eb1fce5cca3993500cc82b7669885166900e --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_sample_1.json @@ -0,0 +1,58 @@ +{ + "MetaData": { + "Version": 1.0, + "ChannelNumber": 1 + }, + "Channels": [ + { + "Parameters": { + "Index": 0 + }, + "Pattern": [ + { + "Event": { + "Type": "transient", + "StartTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + } + }, + { + "Event": { + "Type": "continuous", + "StartTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 0 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 0 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 0 + } + ] + } + } + } + ] + } + ] +} diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_sample_to_he_v1.he b/utils/tools/haptic_format_converter/tests/test_data/oh_sample_to_he_v1.he new file mode 100644 index 0000000000000000000000000000000000000000..db6c7cd6bfa388af3e335ca384c8d9eb05128592 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_sample_to_he_v1.he @@ -0,0 +1,46 @@ +{ + "Metadata": { + "Version": 1 + }, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 0 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 0 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 0 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/utils/tools/haptic_format_converter/tests/test_data/oh_sample_to_he_v2.he b/utils/tools/haptic_format_converter/tests/test_data/oh_sample_to_he_v2.he new file mode 100644 index 0000000000000000000000000000000000000000..dace96546e744203d8833cf39f50918577ba5a76 --- /dev/null +++ b/utils/tools/haptic_format_converter/tests/test_data/oh_sample_to_he_v2.he @@ -0,0 +1,51 @@ +{ + "Metadata": { + "Version": 2 + }, + "PatternList": [ + { + "AbsoluteTime": 0, + "Pattern": [ + { + "Type": "transient", + "RelativeTime": 0, + "Parameters": { + "Intensity": 100, + "Frequency": 100 + } + }, + { + "Type": "continuous", + "RelativeTime": 500, + "Duration": 1000, + "Parameters": { + "Intensity": 75, + "Frequency": 100, + "Curve": [ + { + "Time": 0, + "Intensity": 50, + "Frequency": 0 + }, + { + "Time": 100, + "Intensity": 70, + "Frequency": 0 + }, + { + "Time": 300, + "Intensity": 80, + "Frequency": 0 + }, + { + "Time": 500, + "Intensity": 100, + "Frequency": 0 + } + ] + } + } + ] + } + ] +} \ No newline at end of file