You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
580 lines
19 KiB
580 lines
19 KiB
/* |
|
* Copyright (C) 2012 Invensense, Inc. |
|
* |
|
* 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. |
|
*/ |
|
|
|
#ifndef ANDROID_MPL_SENSOR_H |
|
#define ANDROID_MPL_SENSOR_H |
|
|
|
#include <stdint.h> |
|
#include <errno.h> |
|
#include <sys/cdefs.h> |
|
#include <sys/types.h> |
|
#include <poll.h> |
|
#include <time.h> |
|
#include <utils/Vector.h> |
|
#include <utils/KeyedVector.h> |
|
#include <utils/String8.h> |
|
#include "sensors.h" |
|
#include "SensorBase.h" |
|
#include "InputEventReader.h" |
|
|
|
#ifndef INVENSENSE_COMPASS_CAL |
|
#pragma message("unified HAL for AKM") |
|
#include "CompassSensor.AKM.h" |
|
#endif |
|
|
|
#ifdef SENSOR_ON_PRIMARY_BUS |
|
#pragma message("Sensor on Primary Bus") |
|
#include "CompassSensor.IIO.primary.h" |
|
#else |
|
#pragma message("Sensor on Secondary Bus") |
|
#include "CompassSensor.IIO.9150.h" |
|
#endif |
|
|
|
class PressureSensor; |
|
|
|
/*****************************************************************************/ |
|
/* Sensors Enable/Disable Mask |
|
*****************************************************************************/ |
|
#define MAX_CHIP_ID_LEN (20) |
|
|
|
#define INV_THREE_AXIS_GYRO (0x000F) |
|
#define INV_THREE_AXIS_ACCEL (0x0070) |
|
#define INV_THREE_AXIS_COMPASS (0x0380) |
|
#define INV_ONE_AXIS_PRESSURE (0x0400) |
|
#define INV_ALL_SENSORS (0x7FFF) |
|
|
|
#ifdef INVENSENSE_COMPASS_CAL |
|
#define ALL_MPL_SENSORS_NP (INV_THREE_AXIS_ACCEL \ |
|
| INV_THREE_AXIS_COMPASS \ |
|
| INV_THREE_AXIS_GYRO) |
|
#else |
|
#define ALL_MPL_SENSORS_NP (INV_THREE_AXIS_ACCEL \ |
|
| INV_THREE_AXIS_COMPASS \ |
|
| INV_THREE_AXIS_GYRO) |
|
#endif |
|
|
|
// bit mask of current active features (mMplFeatureActiveMask) |
|
#define INV_COMPASS_CAL 0x01 |
|
#define INV_COMPASS_FIT 0x02 |
|
// bit mask of current active features (mFeatureActiveMask) |
|
#define INV_DMP_QUATERNION 0x001 //3 elements without real part, 32 bit each |
|
#define INV_DMP_DISPL_ORIENTATION 0x002 |
|
#define INV_DMP_SIGNIFICANT_MOTION 0x004 |
|
#define INV_DMP_PEDOMETER 0x008 |
|
#define INV_DMP_PEDOMETER_STEP 0x010 |
|
#define INV_DMP_PED_STANDALONE 0x020 //timestamps only |
|
#define INV_DMP_6AXIS_QUATERNION 0x040 //3 elements without real part, 32 bit each |
|
#define INV_DMP_PED_QUATERNION 0x080 //3 elements without real part, 16 bit each |
|
#define INV_DMP_PED_INDICATOR 0x100 //tag along header with step indciator |
|
#define INV_DMP_BATCH_MODE 0x200 |
|
#define INV_DMP_ACCEL_PED (0xffff) |
|
|
|
// bit mask of whether screen orientation is on |
|
/*#define SCREEN_ORIENTATION_MASK ( \ |
|
(isDmpDisplayOrientationOn() \ |
|
&& ((1 << INV_DMP_DISPL_ORIENTATION) \ |
|
|| !isDmpScreenAutoRotationEnabled())) \ |
|
)*/ |
|
// bit mask of whether DMP should be turned on |
|
#define DMP_FEATURE_MASK ( \ |
|
(INV_DMP_QUATERNION) \ |
|
| (INV_DMP_DISPL_ORIENTATION) \ |
|
| (INV_DMP_SIGNIFICANT_MOTION) \ |
|
| (INV_DMP_PEDOMETER) \ |
|
| (INV_DMP_PEDOMETER_STEP) \ |
|
| (INV_DMP_6AXIS_QUATERNION) \ |
|
| (INV_DMP_PED_QUATERNION) \ |
|
| (INV_DMP_BATCH_MODE) \ |
|
) |
|
// bit mask of DMP features as sensors |
|
#define DMP_SENSOR_MASK ( \ |
|
(INV_DMP_DISPL_ORIENTATION) \ |
|
| (INV_DMP_SIGNIFICANT_MOTION) \ |
|
| (INV_DMP_PEDOMETER) \ |
|
| (INV_DMP_PEDOMETER_STEP) \ |
|
| (INV_DMP_6AXIS_QUATERNION) \ |
|
) |
|
// data header format used by kernel driver. |
|
#define DATA_FORMAT_STEP 0x0001 |
|
#define DATA_FORMAT_MARKER 0x0010 |
|
#define DATA_FORMAT_PED_STANDALONE 0x0100 |
|
#define DATA_FORMAT_PED_QUAT 0x0200 |
|
#define DATA_FORMAT_6_AXIS 0x0400 |
|
#define DATA_FORMAT_QUAT 0x0800 |
|
#define DATA_FORMAT_COMPASS 0x1000 |
|
#define DATA_FORMAT_GYRO 0x2000 |
|
#define DATA_FORMAT_ACCEL 0x4000 |
|
#define DATA_FORMAT_PRESSURE 0x8000 |
|
#define DATA_FORMAT_MASK 0xffff |
|
|
|
#define BYTES_PER_SENSOR 8 |
|
#define BYTES_PER_SENSOR_PACKET 16 |
|
#define QUAT_ONLY_LAST_PACKET_OFFSET 16 |
|
#define BYTES_QUAT_DATA 24 |
|
#define MAX_SUSPEND_BATCH_PACKET_SIZE 1024 |
|
#define MAX_PACKET_SIZE 80 //8 * 4 + (2 * 24) |
|
|
|
/* Uncomment to enable Low Power Quaternion */ |
|
#define ENABLE_LP_QUAT_FEAT |
|
|
|
/* Uncomment to enable DMP display orientation |
|
(within the HAL, see below for Java framework) */ |
|
//#define ENABLE_DMP_DISPL_ORIENT_FEAT |
|
|
|
#ifdef ENABLE_DMP_DISPL_ORIENT_FEAT |
|
/* Uncomment following to expose the SENSOR_TYPE_SCREEN_ORIENTATION |
|
sensor type (DMP screen orientation) to the Java framework. |
|
NOTE: |
|
need Invensense customized |
|
'hardware/libhardware/include/hardware/sensors.h' to compile correctly. |
|
NOTE: |
|
need Invensense java framework changes to: |
|
'frameworks/base/core/java/android/view/WindowOrientationListener.java' |
|
'frameworks/base/core/java/android/hardware/Sensor.java' |
|
'frameworks/base/core/java/android/hardware/SensorEvent.java' |
|
for the 'Auto-rotate screen' to use this feature. |
|
*/ |
|
#define ENABLE_DMP_SCREEN_AUTO_ROTATION |
|
#pragma message("ENABLE_DMP_DISPL_ORIENT_FEAT is defined, framework changes are necessary for HAL to work properly") |
|
#endif |
|
|
|
/* Enable Pressure sensor support */ |
|
#define ENABLE_PRESSURE |
|
|
|
int isDmpScreenAutoRotationEnabled() |
|
{ |
|
#ifdef ENABLE_DMP_SCREEN_AUTO_ROTATION |
|
return 1; |
|
#else |
|
return 0; |
|
#endif |
|
} |
|
|
|
int (*m_pt2AccelCalLoadFunc)(long *bias) = NULL; |
|
/*****************************************************************************/ |
|
/** MPLSensor implementation which fits into the HAL example for crespo provided |
|
* by Google. |
|
* WARNING: there may only be one instance of MPLSensor, ever. |
|
*/ |
|
|
|
class MPLSensor: public SensorBase |
|
{ |
|
typedef int (MPLSensor::*hfunc_t)(sensors_event_t*); |
|
|
|
public: |
|
|
|
enum { |
|
Gyro = 0, |
|
RawGyro, |
|
Accelerometer, |
|
MagneticField, |
|
RawMagneticField, |
|
Pressure, |
|
Orientation, |
|
RotationVector, |
|
GameRotationVector, |
|
LinearAccel, |
|
Gravity, |
|
SignificantMotion, |
|
StepDetector, |
|
StepCounter, |
|
GeomagneticRotationVector, |
|
NumSensors |
|
}; |
|
|
|
MPLSensor(CompassSensor *, int (*m_pt2AccelCalLoadFunc)(long*) = 0); |
|
virtual ~MPLSensor(); |
|
|
|
virtual int setDelay(int32_t handle, int64_t ns); |
|
virtual int enable(int32_t handle, int enabled); |
|
virtual int batch(int handle, int flags, int64_t period_ns, int64_t timeout); |
|
virtual int flush(int handle); |
|
int checkBatchEnabled(); |
|
int setBatch(int en, int toggleEnable); |
|
int32_t getEnableMask() { return mEnabled; } |
|
void getHandle(int32_t handle, int &what, android::String8 &sname); |
|
|
|
virtual int readEvents(sensors_event_t *data, int count); |
|
virtual int getFd() const; |
|
virtual int getAccelFd() const; |
|
virtual int getCompassFd() const; |
|
virtual int getPollTime(); |
|
virtual int getStepCountPollTime(); |
|
virtual bool hasPendingEvents() const; |
|
virtual bool hasStepCountPendingEvents(); |
|
virtual void sleepEvent(); |
|
virtual void wakeEvent(); |
|
int populateSensorList(struct sensor_t *list, int len); |
|
void cbProcData(); |
|
|
|
//static pointer to the object that will handle callbacks |
|
static MPLSensor* gMPLSensor; |
|
|
|
int readAccelEvents(sensors_event_t* data, int count); |
|
void buildCompassEvent(); |
|
void buildMpuEvent(); |
|
|
|
int turnOffAccelFifo(); |
|
int turnOffGyroFifo(); |
|
int enableDmpOrientation(int); |
|
int dmpOrientHandler(int); |
|
int readDmpOrientEvents(sensors_event_t* data, int count); |
|
int getDmpOrientFd(); |
|
int openDmpOrientFd(); |
|
int closeDmpOrientFd(); |
|
|
|
int getDmpRate(int64_t *); |
|
int checkDMPOrientation(); |
|
|
|
int getDmpSignificantMotionFd(); |
|
int readDmpSignificantMotionEvents(sensors_event_t* data, int count); |
|
int enableDmpSignificantMotion(int); |
|
int significantMotionHandler(sensors_event_t* data); |
|
bool checkSmdSupport(){return (mDmpSignificantMotionEnabled);}; |
|
|
|
int enableDmpPedometer(int, int); |
|
int readDmpPedometerEvents(sensors_event_t* data, int count, int32_t id, int32_t type, int outputType); |
|
int getDmpPedometerFd(); |
|
bool checkPedometerSupport() {return (mDmpPedometerEnabled || mDmpStepCountEnabled);}; |
|
bool checkOrientationSupport() {return ((isDmpDisplayOrientationOn() |
|
&& (mDmpOrientationEnabled |
|
|| !isDmpScreenAutoRotationEnabled())));}; |
|
|
|
protected: |
|
CompassSensor *mCompassSensor; |
|
PressureSensor *mPressureSensor; |
|
|
|
int gyroHandler(sensors_event_t *data); |
|
int rawGyroHandler(sensors_event_t *data); |
|
int accelHandler(sensors_event_t *data); |
|
int compassHandler(sensors_event_t *data); |
|
int rawCompassHandler(sensors_event_t *data); |
|
int rvHandler(sensors_event_t *data); |
|
int grvHandler(sensors_event_t *data); |
|
int laHandler(sensors_event_t *data); |
|
int gravHandler(sensors_event_t *data); |
|
int orienHandler(sensors_event_t *data); |
|
int smHandler(sensors_event_t *data); |
|
int pHandler(sensors_event_t *data); |
|
int scHandler(sensors_event_t *data); |
|
int gmHandler(sensors_event_t *data); |
|
int psHandler(sensors_event_t *data); |
|
int metaHandler(sensors_event_t *data, int flags); |
|
void calcOrientationSensor(float *Rx, float *Val); |
|
virtual int update_delay(); |
|
|
|
void inv_set_device_properties(); |
|
int inv_constructor_init(); |
|
int inv_constructor_default_enable(); |
|
int setGyroInitialState(); |
|
int setAccelInitialState(); |
|
int masterEnable(int en); |
|
int enablePedStandalone(int en); |
|
int enablePedStandaloneData(int en); |
|
int enablePedQuaternion(int); |
|
int enablePedQuaternionData(int); |
|
int enable6AxisQuaternion(int); |
|
int enable6AxisQuaternionData(int); |
|
int enableLPQuaternion(int); |
|
int enableQuaternionData(int); |
|
int enableAccelPedometer(int); |
|
int enableAccelPedData(int); |
|
int onDmp(int); |
|
int enableGyro(int en); |
|
int enableLowPowerAccel(int en); |
|
int enableAccel(int en); |
|
int enableCompass(int en, int rawSensorOn); |
|
int enablePressure(int en); |
|
int enableBatch(int64_t timeout); |
|
void computeLocalSensorMask(int enabled_sensors); |
|
int computeBatchSensorMask(int enableSensor, int checkNewBatchSensor); |
|
int computeBatchDataOutput(); |
|
int enableSensors(unsigned long sensors, int en, uint32_t changed); |
|
int inv_read_gyro_buffer(int fd, short *data, long long *timestamp); |
|
int inv_float_to_q16(float *fdata, long *ldata); |
|
int inv_long_to_q16(long *fdata, long *ldata); |
|
int inv_float_to_round(float *fdata, long *ldata); |
|
int inv_float_to_round2(float *fdata, short *sdata); |
|
int inv_long_to_float(long *ldata, float *fdata); |
|
int inv_read_temperature(long long *data); |
|
int inv_read_dmp_state(int fd); |
|
int inv_read_sensor_bias(int fd, long *data); |
|
void inv_get_sensors_orientation(void); |
|
int inv_init_sysfs_attributes(void); |
|
int resetCompass(void); |
|
void setCompassDelay(int64_t ns); |
|
void enable_iio_sysfs(void); |
|
int enablePedometer(int); |
|
int enablePedIndicator(int en); |
|
int checkPedStandaloneEnabled(void); |
|
int checkPedQuatEnabled(); |
|
int check6AxisQuatEnabled(); |
|
int checkLPQRateSupported(); |
|
int checkLPQuaternion(); |
|
int checkAccelPed(); |
|
void setInitial6QuatValue(); |
|
int writeSignificantMotionParams(bool toggleEnable, |
|
uint32_t delayThreshold1, uint32_t delayThreshold2, |
|
uint32_t motionThreshold); |
|
|
|
int mNewData; // flag indicating that the MPL calculated new output values |
|
int mDmpStarted; |
|
long mMasterSensorMask; |
|
long mLocalSensorMask; |
|
int mPollTime; |
|
int mStepCountPollTime; |
|
bool mHaveGoodMpuCal; // flag indicating that the cal file can be written |
|
int mGyroAccuracy; // value indicating the quality of the gyro calibr. |
|
int mAccelAccuracy; // value indicating the quality of the accel calibr. |
|
int mCompassAccuracy; // value indicating the quality of the compass calibr. |
|
struct pollfd mPollFds[5]; |
|
int mSampleCount; |
|
pthread_mutex_t mMplMutex; |
|
pthread_mutex_t mHALMutex; |
|
|
|
char mIIOBuffer[(16 + 8 * 3 + 8) * IIO_BUFFER_LENGTH]; |
|
|
|
int iio_fd; |
|
int accel_fd; |
|
int mpufifo_fd; |
|
int gyro_temperature_fd; |
|
int accel_x_offset_fd; |
|
int accel_y_offset_fd; |
|
int accel_z_offset_fd; |
|
|
|
int accel_x_dmp_bias_fd; |
|
int accel_y_dmp_bias_fd; |
|
int accel_z_dmp_bias_fd; |
|
|
|
int gyro_x_offset_fd; |
|
int gyro_y_offset_fd; |
|
int gyro_z_offset_fd; |
|
|
|
int gyro_x_dmp_bias_fd; |
|
int gyro_y_dmp_bias_fd; |
|
int gyro_z_dmp_bias_fd; |
|
|
|
int dmp_orient_fd; |
|
int mDmpOrientationEnabled; |
|
|
|
int dmp_sign_motion_fd; |
|
int mDmpSignificantMotionEnabled; |
|
|
|
int dmp_pedometer_fd; |
|
int mDmpPedometerEnabled; |
|
int mDmpStepCountEnabled; |
|
|
|
uint32_t mEnabled; |
|
uint32_t mBatchEnabled; |
|
int32_t mFlushEnabled; |
|
uint32_t mOldBatchEnabledMask; |
|
int64_t mBatchTimeoutInMs; |
|
sensors_event_t mPendingEvents[NumSensors]; |
|
int64_t mDelays[NumSensors]; |
|
int64_t mBatchDelays[NumSensors]; |
|
int64_t mBatchTimeouts[NumSensors]; |
|
hfunc_t mHandlers[NumSensors]; |
|
short mCachedGyroData[3]; |
|
long mCachedAccelData[3]; |
|
long mCachedCompassData[3]; |
|
long mCachedQuaternionData[3]; |
|
long mCached6AxisQuaternionData[3]; |
|
long mCachedPedQuaternionData[3]; |
|
long mCachedPressureData; |
|
android::KeyedVector<int, int> mIrqFds; |
|
|
|
InputEventCircularReader mAccelInputReader; |
|
InputEventCircularReader mGyroInputReader; |
|
|
|
bool mFirstRead; |
|
short mTempScale; |
|
short mTempOffset; |
|
int64_t mTempCurrentTime; |
|
int mAccelScale; |
|
long mAccelSelfTestScale; |
|
long mGyroScale; |
|
long mGyroSelfTestScale; |
|
long mCompassScale; |
|
float mCompassBias[3]; |
|
bool mFactoryGyroBiasAvailable; |
|
long mFactoryGyroBias[3]; |
|
bool mGyroBiasAvailable; |
|
bool mGyroBiasApplied; |
|
float mGyroBias[3]; //in body frame |
|
long mGyroChipBias[3]; //in chip frame |
|
bool mFactoryAccelBiasAvailable; |
|
long mFactoryAccelBias[3]; |
|
bool mAccelBiasAvailable; |
|
bool mAccelBiasApplied; |
|
long mAccelBias[3]; //in chip frame |
|
|
|
uint32_t mPendingMask; |
|
unsigned long mSensorMask; |
|
|
|
char chip_ID[MAX_CHIP_ID_LEN]; |
|
char mSysfsPath[MAX_SYSFS_NAME_LEN]; |
|
|
|
signed char mGyroOrientation[9]; |
|
signed char mAccelOrientation[9]; |
|
|
|
int64_t mSensorTimestamp; |
|
int64_t mCompassTimestamp; |
|
int64_t mPressureTimestamp; |
|
|
|
struct sysfs_attrbs { |
|
char *chip_enable; |
|
char *power_state; |
|
char *master_enable; |
|
char *dmp_firmware; |
|
char *firmware_loaded; |
|
char *dmp_on; |
|
char *dmp_int_on; |
|
char *dmp_event_int_on; |
|
char *tap_on; |
|
char *key; |
|
char *self_test; |
|
char *temperature; |
|
|
|
char *gyro_enable; |
|
char *gyro_fifo_rate; |
|
char *gyro_fsr; |
|
char *gyro_orient; |
|
char *gyro_fifo_enable; |
|
char *gyro_rate; |
|
|
|
char *accel_enable; |
|
char *accel_fifo_rate; |
|
char *accel_fsr; |
|
char *accel_bias; |
|
char *accel_orient; |
|
char *accel_fifo_enable; |
|
char *accel_rate; |
|
|
|
char *three_axis_q_on; //formerly quaternion_on |
|
char *three_axis_q_rate; |
|
|
|
char *six_axis_q_on; |
|
char *six_axis_q_rate; |
|
|
|
char *six_axis_q_value; |
|
|
|
char *ped_q_on; |
|
char *ped_q_rate; |
|
|
|
char *step_detector_on; |
|
char *step_indicator_on; |
|
|
|
char *in_timestamp_en; |
|
char *in_timestamp_index; |
|
char *in_timestamp_type; |
|
|
|
char *buffer_length; |
|
|
|
char *display_orientation_on; |
|
char *event_display_orientation; |
|
|
|
char *in_accel_x_offset; |
|
char *in_accel_y_offset; |
|
char *in_accel_z_offset; |
|
char *in_accel_self_test_scale; |
|
|
|
char *in_accel_x_dmp_bias; |
|
char *in_accel_y_dmp_bias; |
|
char *in_accel_z_dmp_bias; |
|
|
|
char *in_gyro_x_offset; |
|
char *in_gyro_y_offset; |
|
char *in_gyro_z_offset; |
|
char *in_gyro_self_test_scale; |
|
|
|
char *in_gyro_x_dmp_bias; |
|
char *in_gyro_y_dmp_bias; |
|
char *in_gyro_z_dmp_bias; |
|
|
|
char *event_smd; |
|
char *smd_enable; |
|
char *smd_delay_threshold; |
|
char *smd_delay_threshold2; |
|
char *smd_threshold; |
|
char *batchmode_timeout; |
|
char *batchmode_wake_fifo_full_on; |
|
char *flush_batch; |
|
|
|
char *pedometer_on; |
|
char *pedometer_int_on; |
|
char *event_pedometer; |
|
char *pedometer_steps; |
|
|
|
char *motion_lpa_on; |
|
} mpu; |
|
|
|
char *sysfs_names_ptr; |
|
int mMplFeatureActiveMask; |
|
uint64_t mFeatureActiveMask; |
|
bool mDmpOn; |
|
int mPedUpdate; |
|
int mPressureUpdate; |
|
int64_t mQuatSensorTimestamp; |
|
int64_t mStepSensorTimestamp; |
|
uint64_t mLastStepCount; |
|
int mLeftOverBufferSize; |
|
char mLeftOverBuffer[24]; |
|
bool mInitial6QuatValueAvailable; |
|
long mInitial6QuatValue[4]; |
|
bool mFlushBatchSet; |
|
uint32_t mSkipReadEvents; |
|
|
|
private: |
|
/* added for dynamic get sensor list */ |
|
void fillAccel(const char* accel, struct sensor_t *list); |
|
void fillGyro(const char* gyro, struct sensor_t *list); |
|
void fillRV(struct sensor_t *list); |
|
void fillGMRV(struct sensor_t *list); |
|
void fillGRV(struct sensor_t *list); |
|
void fillOrientation(struct sensor_t *list); |
|
void fillGravity(struct sensor_t *list); |
|
void fillLinearAccel(struct sensor_t *list); |
|
void fillSignificantMotion(struct sensor_t *list); |
|
#ifdef ENABLE_DMP_SCREEN_AUTO_ROTATION |
|
void fillScreenOrientation(struct sensor_t *list); |
|
#endif |
|
void storeCalibration(); |
|
void loadDMP(); |
|
bool isMpuNonDmp(); |
|
int isLowPowerQuatEnabled(); |
|
int isDmpDisplayOrientationOn(); |
|
void getCompassBias(); |
|
void getFactoryGyroBias(); |
|
void setFactoryGyroBias(); |
|
void getGyroBias(); |
|
void setGyroBias(); |
|
void getFactoryAccelBias(); |
|
void setFactoryAccelBias(); |
|
void getAccelBias(); |
|
void setAccelBias(); |
|
int isCompassDisabled(); |
|
int setBatchDataRates(); |
|
int resetDataRates(); |
|
void initBias(); |
|
void sys_dump(bool fileMode); |
|
}; |
|
|
|
extern "C" { |
|
void setCallbackObject(MPLSensor*); |
|
MPLSensor *getCallbackObject(); |
|
} |
|
|
|
#endif // ANDROID_MPL_SENSOR_H
|
|
|