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