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
29interface State<M extends object> {
30  data: M;
31  isAvailable?: boolean;
32}
33
34// See: https://github.com/expo/expo/pull/10229#discussion_r490961694
35// eslint-disable-next-line @typescript-eslint/ban-types
36abstract class SensorBlock<M extends object> extends React.Component<{}, State<M>> {
37  readonly state: State<M> = { data: {} as M };
38
39  _subscription?: Subscription;
40
41  componentDidMount() {
42    this.checkAvailability();
43  }
44
45  checkAvailability = async () => {
46    const isAvailable = await this.getSensor().isAvailableAsync();
47    this.setState({ isAvailable });
48  };
49
50  componentWillUnmount() {
51    this._unsubscribe();
52  }
53
54  abstract getName: () => string;
55  abstract getSensor: () => Sensors.DeviceSensor<M>;
56  abstract renderData: () => JSX.Element;
57
58  _toggle = () => {
59    if (this._subscription) {
60      this._unsubscribe();
61    } else {
62      this._subscribe();
63    }
64  };
65
66  _slow = () => {
67    this.getSensor().setUpdateInterval(SLOW_INTERVAL);
68  };
69
70  _fast = () => {
71    this.getSensor().setUpdateInterval(FAST_INTERVAL);
72  };
73
74  _subscribe = () => {
75    this._subscription = this.getSensor().addListener((data: any) => {
76      this.setState({ data });
77    });
78  };
79
80  _unsubscribe = () => {
81    this._subscription && this._subscription.remove();
82    this._subscription = undefined;
83  };
84
85  render() {
86    if (this.state.isAvailable !== true) {
87      return null;
88    }
89    return (
90      <View style={styles.sensor}>
91        <Text>{this.getName()}:</Text>
92        {this.renderData()}
93        <View style={styles.buttonContainer}>
94          <TouchableOpacity onPress={this._toggle} style={styles.button}>
95            <Text>Toggle</Text>
96          </TouchableOpacity>
97          <TouchableOpacity onPress={this._slow} style={[styles.button, styles.middleButton]}>
98            <Text>Slow</Text>
99          </TouchableOpacity>
100          <TouchableOpacity onPress={this._fast} style={styles.button}>
101            <Text>Fast</Text>
102          </TouchableOpacity>
103        </View>
104      </View>
105    );
106  }
107}
108
109abstract class ThreeAxisSensorBlock extends SensorBlock<Sensors.ThreeAxisMeasurement> {
110  renderData = () => (
111    <Text>
112      x: {round(this.state.data.x)} y: {round(this.state.data.y)} z: {round(this.state.data.z)}
113    </Text>
114  );
115}
116
117class GyroscopeSensor extends ThreeAxisSensorBlock {
118  getName = () => 'Gyroscope';
119  getSensor = () => Sensors.Gyroscope;
120}
121
122class AccelerometerSensor extends ThreeAxisSensorBlock {
123  getName = () => 'Accelerometer';
124  getSensor = () => Sensors.Accelerometer;
125}
126
127class MagnetometerSensor extends ThreeAxisSensorBlock {
128  getName = () => 'Magnetometer';
129  getSensor = () => Sensors.Magnetometer;
130}
131
132class MagnetometerUncalibratedSensor extends ThreeAxisSensorBlock {
133  getName = () => 'Magnetometer (Uncalibrated)';
134  getSensor = () => Sensors.MagnetometerUncalibrated;
135}
136
137class DeviceMotionSensor extends SensorBlock<Sensors.DeviceMotionMeasurement> {
138  getName = () => 'DeviceMotion';
139  getSensor = () => Sensors.DeviceMotion;
140  renderXYZBlock = (name: string, event: null | { x?: number; y?: number; z?: number } = {}) => {
141    if (!event) return null;
142    const { x, y, z } = event;
143    return (
144      <Text>
145        {name}: x: {round(x)} y: {round(y)} z: {round(z)}
146      </Text>
147    );
148  };
149  renderABGBlock = (
150    name: string,
151    event: null | { alpha?: number; beta?: number; gamma?: number } = {}
152  ) => {
153    if (!event) return null;
154
155    const { alpha, beta, gamma } = event;
156    return (
157      <Text>
158        {name}: α: {round(alpha)} β: {round(beta)} γ: {round(gamma)}
159      </Text>
160    );
161  };
162  renderData = () => (
163    <View>
164      {this.renderXYZBlock('Acceleration', this.state.data.acceleration)}
165      {this.renderXYZBlock('Acceleration w/gravity', this.state.data.accelerationIncludingGravity)}
166      {this.renderABGBlock('Rotation', this.state.data.rotation)}
167      {this.renderABGBlock('Rotation rate', this.state.data.rotationRate)}
168      <Text>Orientation: {Sensors.DeviceMotionOrientation[this.state.data.orientation]}</Text>
169    </View>
170  );
171}
172
173class BarometerSensor extends SensorBlock<Sensors.BarometerMeasurement> {
174  getName = () => 'Barometer';
175  getSensor = () => Sensors.Barometer;
176  renderData = () => (
177    <View>
178      <Text>Pressure: {this.state.data.pressure}</Text>
179      <Text>Relative Altitude: {this.state.data.relativeAltitude}</Text>
180    </View>
181  );
182}
183
184class LightSensor extends SensorBlock<Sensors.LightSensorMeasurement> {
185  getName = () => 'LightSensor';
186  getSensor = () => Sensors.LightSensor;
187  renderData = () => (
188    <View>
189      <Text>Illuminance: {this.state.data.illuminance}</Text>
190    </View>
191  );
192}
193
194function round(n?: number) {
195  if (!n) {
196    return 0;
197  }
198
199  return Math.floor(n * 100) / 100;
200}
201
202const styles = StyleSheet.create({
203  container: {
204    flex: 1,
205    marginBottom: 10,
206  },
207  buttonContainer: {
208    flexDirection: 'row',
209    alignItems: 'stretch',
210    marginTop: 15,
211  },
212  button: {
213    flex: 1,
214    justifyContent: 'center',
215    alignItems: 'center',
216    backgroundColor: '#eee',
217    padding: 10,
218  },
219  middleButton: {
220    borderLeftWidth: 1,
221    borderRightWidth: 1,
222    borderColor: '#ccc',
223  },
224  sensor: {
225    marginTop: 15,
226    paddingHorizontal: 10,
227  },
228});
229