1import { Subscription } from 'expo-modules-core';
2import * as Sensors from 'expo-sensors';
3import React from 'react';
4import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
5
6const FAST_INTERVAL = 16;
7const SLOW_INTERVAL = 1000;
8
9export default class SensorScreen extends React.Component {
10  static navigationOptions = {
11    title: 'Sensors',
12  };
13
14  render() {
15    return (
16      <ScrollView style={styles.container}>
17        <GyroscopeSensor />
18        <AccelerometerSensor />
19        <MagnetometerSensor />
20        <MagnetometerUncalibratedSensor />
21        <BarometerSensor />
22        <LightSensor />
23        <DeviceMotionSensor />
24      </ScrollView>
25    );
26  }
27}
28
29type State<Measurement> = {
30  data: Measurement;
31  isListening: boolean;
32  isAvailable?: boolean;
33};
34
35abstract class SensorBlock<Measurement> extends React.Component<object, State<Measurement>> {
36  readonly state: State<Measurement> = {
37    data: {} as Measurement,
38    isListening: false,
39  };
40
41  _subscription?: Subscription;
42
43  componentDidMount() {
44    this.checkAvailability();
45  }
46
47  checkAvailability = async () => {
48    const isAvailable = await this.getSensor().isAvailableAsync();
49    this.setState({ isAvailable });
50  };
51
52  componentWillUnmount() {
53    this._unsubscribe();
54  }
55
56  abstract getName: () => string;
57  abstract getSensor: () => Sensors.DeviceSensor<Measurement>;
58
59  _toggle = () => {
60    if (this._subscription) {
61      this._unsubscribe();
62      this.setState({ isListening: false });
63    } else {
64      this._subscribe();
65      this.setState({ isListening: true });
66    }
67  };
68
69  _slow = () => {
70    this.getSensor().setUpdateInterval(SLOW_INTERVAL);
71  };
72
73  _fast = () => {
74    this.getSensor().setUpdateInterval(FAST_INTERVAL);
75  };
76
77  _subscribe = () => {
78    this._subscription = this.getSensor().addListener((data: Measurement) => {
79      this.setState({ data });
80    });
81  };
82
83  _unsubscribe = () => {
84    this._subscription && this._subscription.remove();
85    this._subscription = undefined;
86  };
87
88  renderData() {
89    return (
90      this.state.data && (
91        <Text>
92          {Object.entries(this.state.data)
93            .sort(([keyA], [keyB]) => {
94              return keyA.localeCompare(keyB);
95            })
96            .map(([key, value]) => `${key}: ${typeof value === 'number' ? round(value) : 0}`)
97            .join('\n')}
98        </Text>
99      )
100    );
101  }
102
103  render() {
104    if (this.state.isAvailable !== true) {
105      return null;
106    }
107    return (
108      <View style={styles.sensor}>
109        <Text>{this.getName()}:</Text>
110        {this.renderData()}
111        <View style={styles.buttonContainer}>
112          <TouchableOpacity onPress={this._toggle} style={styles.button}>
113            <Text>{this.state.isListening ? 'Stop' : 'Start'}</Text>
114          </TouchableOpacity>
115          <TouchableOpacity onPress={this._slow} style={[styles.button, styles.middleButton]}>
116            <Text>Slow</Text>
117          </TouchableOpacity>
118          <TouchableOpacity onPress={this._fast} style={styles.button}>
119            <Text>Fast</Text>
120          </TouchableOpacity>
121        </View>
122      </View>
123    );
124  }
125}
126
127class GyroscopeSensor extends SensorBlock<Sensors.GyroscopeMeasurement> {
128  getName = () => 'Gyroscope';
129  getSensor = () => Sensors.Gyroscope;
130}
131
132class AccelerometerSensor extends SensorBlock<Sensors.AccelerometerMeasurement> {
133  getName = () => 'Accelerometer';
134  getSensor = () => Sensors.Accelerometer;
135}
136
137class MagnetometerSensor extends SensorBlock<Sensors.MagnetometerMeasurement> {
138  getName = () => 'Magnetometer';
139  getSensor = () => Sensors.Magnetometer;
140}
141
142class MagnetometerUncalibratedSensor extends SensorBlock<Sensors.MagnetometerUncalibratedMeasurement> {
143  getName = () => 'Magnetometer (Uncalibrated)';
144  getSensor = () => Sensors.MagnetometerUncalibrated;
145}
146
147class DeviceMotionSensor extends SensorBlock<Sensors.DeviceMotionMeasurement> {
148  getName = () => 'DeviceMotion';
149  getSensor = () => Sensors.DeviceMotion;
150  renderXYZBlock = (name: string, event: null | { x?: number; y?: number; z?: number } = {}) => {
151    if (!event) return null;
152    const { x, y, z } = event;
153    return (
154      <Text>
155        {name}: x: {round(x)} y: {round(y)} z: {round(z)}
156      </Text>
157    );
158  };
159  renderABGBlock = (
160    name: string,
161    event: null | { alpha?: number; beta?: number; gamma?: number } = {}
162  ) => {
163    if (!event) return null;
164
165    const { alpha, beta, gamma } = event;
166    return (
167      <Text>
168        {name}: α: {round(alpha)} β: {round(beta)} γ: {round(gamma)}
169      </Text>
170    );
171  };
172  renderData = () => (
173    <View>
174      {this.renderXYZBlock('Acceleration', this.state.data.acceleration)}
175      {this.renderXYZBlock('Acceleration w/gravity', this.state.data.accelerationIncludingGravity)}
176      {this.renderABGBlock('Rotation', this.state.data.rotation)}
177      {this.renderABGBlock('Rotation rate', this.state.data.rotationRate)}
178      <Text>Orientation: {Sensors.DeviceMotionOrientation[this.state.data.orientation]}</Text>
179    </View>
180  );
181}
182
183class BarometerSensor extends SensorBlock<Sensors.BarometerMeasurement> {
184  getName = () => 'Barometer';
185  getSensor = () => Sensors.Barometer;
186  renderData = () => (
187    <View>
188      <Text>Pressure: {this.state.data.pressure}</Text>
189      <Text>Relative Altitude: {this.state.data.relativeAltitude}</Text>
190    </View>
191  );
192}
193
194class LightSensor extends SensorBlock<Sensors.LightSensorMeasurement> {
195  getName = () => 'LightSensor';
196  getSensor = () => Sensors.LightSensor;
197  renderData = () => (
198    <View>
199      <Text>Illuminance: {this.state.data.illuminance}</Text>
200    </View>
201  );
202}
203
204function round(n?: number) {
205  return n ? Math.floor(n * 100) / 100 : 0;
206}
207
208const styles = StyleSheet.create({
209  container: {
210    flex: 1,
211    marginBottom: 10,
212  },
213  buttonContainer: {
214    flexDirection: 'row',
215    alignItems: 'stretch',
216    marginTop: 15,
217  },
218  button: {
219    flex: 1,
220    justifyContent: 'center',
221    alignItems: 'center',
222    backgroundColor: '#eee',
223    padding: 10,
224  },
225  middleButton: {
226    borderLeftWidth: 1,
227    borderRightWidth: 1,
228    borderColor: '#ccc',
229  },
230  sensor: {
231    marginTop: 15,
232    paddingHorizontal: 10,
233  },
234});
235