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