From 0c283742261107668e9272ce604f2bfedef9d213 Mon Sep 17 00:00:00 2001 From: lucadruda Date: Wed, 31 Mar 2021 20:17:16 +0200 Subject: [PATCH] add tsi charts --- ios/Podfile.lock | 6 + .../project.pbxproj | 31 ++ package-lock.json | 22 +- package.json | 5 +- src/App.tsx | 233 +++++++---- src/Chart.tsx | 396 ++++++++++++++++++ src/Insight.tsx | 286 ------------- src/Settings.tsx | 114 ++--- src/hooks/common.ts | 16 +- src/types.ts | 46 +- 10 files changed, 705 insertions(+), 450 deletions(-) create mode 100644 src/Chart.tsx delete mode 100644 src/Insight.tsx diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 381ac85..675fdb3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -279,6 +279,8 @@ PODS: - React-Core - react-native-torch (1.2.0): - React + - react-native-webview (11.3.2): + - React-Core - React-perflogger (0.64.0) - React-RCTActionSheet (0.64.0): - React-Core/RCTActionSheetHeaders (= 0.64.0) @@ -445,6 +447,7 @@ DEPENDENCIES: - react-native-maps (from `../node_modules/react-native-maps`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-torch (from `../node_modules/react-native-torch`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -536,6 +539,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-safe-area-context" react-native-torch: :path: "../node_modules/react-native-torch" + react-native-webview: + :path: "../node_modules/react-native-webview" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -622,6 +627,7 @@ SPEC CHECKSUMS: react-native-maps: f4b89da81626ad7f151a8bfcb79733295d31ce5c react-native-safe-area-context: e471852c5ed67eea4b10c5d9d43c1cebae3b231d react-native-torch: 86f052de32cc110922292770cdb389f00b5d7320 + react-native-webview: aea3233f26253f5c360164ee87d01ef9f7b9a27f React-perflogger: 9c547d8f06b9bf00cb447f2b75e8d7f19b7e02af React-RCTActionSheet: 3080b6e12e0e1a5b313c8c0050699b5c794a1b11 React-RCTAnimation: 3f96f21a497ae7dabf4d2f150ee43f906aaf516f diff --git a/ios/my_first_pnp_device.xcodeproj/project.pbxproj b/ios/my_first_pnp_device.xcodeproj/project.pbxproj index a45df52..cd1b5cf 100644 --- a/ios/my_first_pnp_device.xcodeproj/project.pbxproj +++ b/ios/my_first_pnp_device.xcodeproj/project.pbxproj @@ -25,6 +25,13 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = my_first_pnp_device; }; + 4ABDA6BE260283330068B64E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4A45C58725FFDC06005A8543 /* RCTTorch.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DA5891D81BA9A9FC002B4DB2; + remoteInfo = RCTTorch; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -110,6 +117,14 @@ name = Frameworks; sourceTree = ""; }; + 4ABDA6BB260283330068B64E /* Products */ = { + isa = PBXGroup; + children = ( + 4ABDA6BF260283330068B64E /* libRCTTorch.a */, + ); + name = Products; + sourceTree = ""; + }; 6678D5C939B9D8E03D5C4EBA /* Pods */ = { isa = PBXGroup; children = ( @@ -229,6 +244,12 @@ mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 4ABDA6BB260283330068B64E /* Products */; + ProjectRef = 4A45C58725FFDC06005A8543 /* RCTTorch.xcodeproj */; + }, + ); projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* my_first_pnp_device */, @@ -237,6 +258,16 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + 4ABDA6BF260283330068B64E /* libRCTTorch.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTTorch.a; + remoteRef = 4ABDA6BE260283330068B64E /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ 00E356EC1AD99517003FC87E /* Resources */ = { isa = PBXResourcesBuildPhase; diff --git a/package-lock.json b/package-lock.json index 4cad5fc..ceb2f80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7653,9 +7653,9 @@ } }, "react-native-azure-iotcentral-client": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/react-native-azure-iotcentral-client/-/react-native-azure-iotcentral-client-1.1.7.tgz", - "integrity": "sha512-YOWAPS0j1/nFHFWskTKKZqcjeLq1De9TsEWohaQ6Ds9DrnG1U172ocqjaCKXpDYpGKWwY7mgvfQ77Dd+6NxgDA==", + "version": "1.1.8-b.0", + "resolved": "https://registry.npmjs.org/react-native-azure-iotcentral-client/-/react-native-azure-iotcentral-client-1.1.8-b.0.tgz", + "integrity": "sha512-4NAZwzxH59eFnwK/knVrydLzNvgXXuvP/YMuVChdth5v3zqJ30aK6G4qaVxGX+Ihp1e5+kcokyGDreGfoHH3Fg==", "requires": { "crypto-js": "^3.3.0", "react-native-get-random-values": "^1.4.0", @@ -7995,6 +7995,22 @@ } } }, + "react-native-webview": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-11.3.2.tgz", + "integrity": "sha512-j+0eUKYY3MCO7DRhZaIPY6+0q+Yo1Iyhz5f7cde+i5vR71CcJ/60DhZPw5SSqXZnZiVX0Myz1D46u8dtfRmQFg==", + "requires": { + "escape-string-regexp": "2.0.0", + "invariant": "2.2.4" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, "react-refresh": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", diff --git a/package.json b/package.json index 4729be0..7fa59f5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "react": "17.0.1", "react-native": "0.64.0", "react-native-animatable": "^1.3.3", - "react-native-azure-iotcentral-client": "^1.1.7", + "react-native-azure-iotcentral-client": "^1.1.8-b.0", "react-native-camera": "^3.43.0", "react-native-charts-wrapper": "^0.5.7", "react-native-circular-progress": "^1.3.7", @@ -44,7 +44,8 @@ "react-native-sensors": "^7.2.0", "react-native-svg": "^12.1.0", "react-native-torch": "^1.2.0", - "react-native-vector-icons": "^8.1.0" + "react-native-vector-icons": "^8.1.0", + "react-native-webview": "^11.3.2" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/src/App.tsx b/src/App.tsx index 71d58ed..070201d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useCallback, } from 'react'; -import {View, Platform, Alert} from 'react-native'; +import { View, Platform, Alert } from 'react-native'; import Settings from './Settings'; import { NavigationContainer, @@ -13,7 +13,7 @@ import { DefaultTheme, useTheme, } from '@react-navigation/native'; -import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Screens, NavigationScreens, @@ -26,8 +26,10 @@ import { ENABLE_DISABLE_COMMAND, SET_FREQUENCY_COMMAND, ItemProps, -} from './types'; -import {SafeAreaProvider} from 'react-native-safe-area-context'; + LIGHT_TOGGLE_COMMAND, + ChartType +} from 'types'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; import { LogsProvider, StorageProvider, @@ -37,11 +39,10 @@ import { ThemeMode, } from 'contexts'; import LogoIcon from './assets/IotcLogo.svg'; -import {Icon} from 'react-native-elements'; -import {createStackNavigator, HeaderTitle} from '@react-navigation/stack'; -import {camelToName, Text} from './components/typography'; -import Insight, {ChartType} from './Insight'; -import {Welcome} from './Welcome'; +import { Icon } from 'react-native-elements'; +import { createStackNavigator, HeaderTitle } from '@react-navigation/stack'; +import { camelToName, Text } from './components/typography'; +import { Welcome } from './Welcome'; import Logs from './Logs'; import { IIcon, @@ -52,9 +53,9 @@ import { useSimulation, } from 'hooks'; import FileUpload from './FileUpload'; -import {Registration} from './Registration'; +import { Registration } from './Registration'; import CardView from './CardView'; -import {Loader} from './components/loader'; +import { Loader } from './components/loader'; import { IIoTCCommand, IIoTCCommandResponse, @@ -62,10 +63,13 @@ import { IoTCClient, IOTC_EVENTS, } from 'react-native-azure-iotcentral-client'; -import {AVAILABLE_SENSORS} from 'sensors'; +import { AVAILABLE_SENSORS } from 'sensors'; +import Torch from 'react-native-torch'; +import Chart from 'Chart'; const Tab = createBottomTabNavigator(); const Stack = createStackNavigator(); +const TITLE = 'My First PnP Device'; export default function App() { const [initialized, setInitialized] = useState(false); @@ -88,7 +92,7 @@ export default function App() { } const Navigation = React.memo(() => { - const {mode} = useContext(ThemeContext); + const { mode } = useContext(ThemeContext); return ( @@ -96,14 +100,14 @@ const Navigation = React.memo(() => { {/* @ts-ignore */} ({ + options={({ navigation }: { navigation: NavigationProperty }) => ({ headerTitle: () => null, headerLeft: () => , headerRight: () => , })} component={Root} /> - { @@ -119,10 +123,27 @@ const Navigation = React.memo(() => { } return data; }} + /> */} + { + let data = {}; + if (route.params) { + const params = route.params as NavigationParams; + if (params.title) { + data = { ...data, headerTitle: params.title }; + } + if (params.backTitle) { + data = { ...data, headerBackTitle: params.backTitle }; + } + } + return data; + }} /> ({ + options={({ navigation }: { navigation: NavigationProperty }) => ({ stackAnimation: 'flip', headerTitle: Platform.select({ ios: undefined, @@ -156,7 +177,7 @@ const Root = React.memo(() => { await client.sendProperty({ [PROPERTY]: { __t: 'c', - ...properties.reduce((obj, p) => ({...obj, [p.id]: p.value}), {}), + ...properties.reduce((obj, p) => ({ ...obj, [p.id]: p.value }), {}), }, }); }, @@ -170,7 +191,7 @@ const Root = React.memo(() => { // connect client if credentials are retrieved - const iconsRef = useRef<{[x in ScreenNames]: IIcon}>({ + const iconsRef = useRef<{ [x in ScreenNames]: IIcon }>({ [Screens.TELEMETRY_SCREEN]: Platform.select({ ios: { name: 'stats-chart-outline', @@ -225,8 +246,8 @@ const Root = React.memo(() => { async (componentName: string, id: string, value: any) => { if (iotcentralClient && iotcentralClient.isConnected()) { await iotcentralClient.sendTelemetry( - {[id]: value}, - {'$.sub': componentName}, + { [id]: value }, + { '$.sub': componentName }, ); } }, @@ -236,18 +257,48 @@ const Root = React.memo(() => { const onCommandUpdate = useCallback(async (command: IIoTCCommand) => { let data: any; data = JSON.parse(command.requestPayload); + + if (command.name === LIGHT_TOGGLE_COMMAND) { + const fn = async () => { + return new Promise(resolve => { + console.log(`accendo per ${data.duration}`); + Torch.switchState(true); + setTimeout(() => { + console.log(`spengo per ${data.duration}`); + Torch.switchState(false); + resolve(); + }, data.duration * 1000); + }); + }; + if (data.pulses && data.pulses > 1) { + let count = 0; + const intv: number = setInterval(async () => { + if (count === data.pulses) { + clearInterval(intv); + await command.reply(IIoTCCommandResponse.SUCCESS, 'Executed'); + return; + } + await fn(); + count++; + }, data.duration * 1000 + data.delay); // repeat light on with 1s delay from each other + } else { + await fn(); + await command.reply(IIoTCCommandResponse.SUCCESS, 'Executed'); + } + return; + } if (data.sensor) { - if (command.name === ENABLE_DISABLE_COMMAND) { - const sensor = sensorRef.current.find(s => s.id === data.sensor); - if (sensor) { - sensor.enable(data.enable ? data.enable : false); - await command.reply(IIoTCCommandResponse.SUCCESS, 'Enable'); - } - } else if (command.name === SET_FREQUENCY_COMMAND) { - const sensor = sensorRef.current.find(s => s.id === data.sensor); - if (sensor) { - sensor.sendInterval(data.interval ? data.interval * 1000 : 5000); - await command.reply(IIoTCCommandResponse.SUCCESS, 'Frequency'); + const sensor = sensorRef.current.find(s => s.id === data.sensor); + if (sensor) { + switch (command.name) { + case ENABLE_DISABLE_COMMAND: + sensor.enable(data.enable ? data.enable : false); + await command.reply(IIoTCCommandResponse.SUCCESS, 'Enable'); + break; + case SET_FREQUENCY_COMMAND: + sensor.sendInterval(data.interval ? data.interval * 1000 : 5000); + await command.reply(IIoTCCommandResponse.SUCCESS, 'Frequency'); + break; } } } @@ -255,7 +306,7 @@ const Root = React.memo(() => { const onPropUpdate = useCallback( async (prop: IIoTCProperty) => { - let {name, value} = prop; + let { name, value } = prop; if (value.__t === 'c') { // inside a component: TODO: change sdk name = Object.keys(value).filter(v => v !== '__t')[0]; @@ -335,12 +386,12 @@ const Root = React.memo(() => { ( + tabBarIcon: ({ color, size }) => ( ), }}> @@ -358,48 +409,48 @@ const Root = React.memo(() => { ( + tabBarIcon: ({ color, size }) => ( ), }}> {propertiesLoading ? () => ( - - ) + + ) : () => ( - { - try { - await iotcentralClient?.sendProperty({ - [PROPERTY]: {__t: 'c', [item.id]: value}, - }); - Alert.alert( - 'Property', - `Property ${item.name} successfully sent to IoT Central`, - [{text: 'OK'}], - ); - } catch (e) { - Alert.alert( - 'Property', - `Property ${item.name} not sent to IoT Central`, - [{text: 'OK'}], - ); - } - }} - /> - )} + { + try { + await iotcentralClient?.sendProperty({ + [PROPERTY]: { __t: 'c', [item.id]: value }, + }); + Alert.alert( + 'Property', + `Property ${item.name} successfully sent to IoT Central`, + [{ text: 'OK' }], + ); + } catch (e) { + Alert.alert( + 'Property', + `Property ${item.name} not sent to IoT Central`, + [{ text: 'OK' }], + ); + } + }} + /> + )} ( + tabBarIcon: ({ color, size }) => ( { name={Screens.LOGS_SCREEN} component={Logs} options={{ - tabBarIcon: ({color, size}) => ( + tabBarIcon: ({ color, size }) => ( ), }} @@ -432,24 +483,24 @@ const getCardView = (items: ItemProps[], name: string, detail: boolean) => ({ onItemPress={ detail ? item => { - navigation.navigate('Insight', { - chartType: - item.id === AVAILABLE_SENSORS.GEOLOCATION - ? ChartType.MAP - : ChartType.DEFAULT, - currentValue: item.value, - telemetryId: item.id, - title: camelToName(item.id), - backTitle: 'Telemetry', - }); - } + navigation.navigate('Insight', { + chartType: + item.id === AVAILABLE_SENSORS.GEOLOCATION + ? ChartType.MAP + : ChartType.DEFAULT, + currentValue: item.value, + telemetryId: item.id, + title: camelToName(item.id), + backTitle: 'Telemetry', + }); + } : undefined } /> ); const Logo = React.memo(() => { - const {colors} = useTheme(); + const { colors } = useTheme(); return ( { fontSize: 16, letterSpacing: 0.1, }}> - Azure IoT Central + {TITLE} ); }); -const Profile = React.memo((props: {navigate: any}) => { - const {colors} = useTheme(); +const Profile = React.memo((props: { navigate: any }) => { + const { colors } = useTheme(); return ( - + { props.navigate('Settings'); @@ -495,21 +546,21 @@ const Profile = React.memo((props: {navigate: any}) => { ); }); -const BackButton = React.memo((props: {goBack: any; title: string}) => { - const {colors} = useTheme(); - const {goBack, title} = props; +const BackButton = React.memo((props: { goBack: any; title: string }) => { + const { colors } = useTheme(); + const { goBack, title } = props; return ( - + {Platform.OS === 'android' && ( - {title} + {title} )} ); }); -const TabBarIcon = React.memo<{icon: IIcon; color: string; size: number}>( - ({icon, color, size}) => { +const TabBarIcon = React.memo<{ icon: IIcon; color: string; size: number }>( + ({ icon, color, size }) => { return ( , + 'Insight' + >; +}>(({ route }) => { + const { colors, dark } = useTheme(); + const { screen } = useScreenDimensions(); + const { chartType, telemetryId, currentValue } = route.params; + const [data, setData] = React.useState({}); + const [metadata, setMetadata] = React.useState({}); + const chartRef = React.useRef(null); + const mapStyle = React.useMemo( + () => ({ + flex: 3, + margin: 20, + borderRadius: 20, + ...(!dark + ? { + shadowColor: "'rgba(0, 0, 0, 0.14)'", + shadowOffset: { + width: 0, + height: 3, + }, + shadowOpacity: 0.8, + shadowRadius: 3.84, + elevation: 5, + } + : {}), + }), + [dark], + ); + const chartOptions: LineChartOptions = React.useMemo( + () => ({ + noAnimate: true, + brushHandlesVisible: false, + snapBrush: false, + interpolationFunction: 'curveMonotoneX', + theme: dark ? 'dark' : 'light', + offset: 'Local', + legend: 'compact', + includeDots: true, + hideChartControlPanel: true, + }), + [dark], + ); + + const chartDataOptions = React.useMemo( + () => + Object.keys(metadata).map(telemetryId => ({ + alias: metadata[telemetryId].displayName, + color: metadata[telemetryId].color, + })), + [metadata], + ); + + const html = React.useMemo( + () => ` + + + + + + + +
+ + +`, + [colors, chartOptions, chartDataOptions], + ); + + const updateData = React.useCallback( + (id: string, value: any) => { + if (id !== telemetryId) { + return; + } + if (typeof value !== 'number') { + // data is composite + setData(current => { + Object.keys(value).forEach(fieldId => { + if (!metadata[fieldId]) { + setMetadata(currentMetadata => ({ + ...currentMetadata, + [fieldId]: { + displayName: `${id}/${fieldId}`, + color: getRandomColor(), + }, + })); + } + current = { + ...current, + [fieldId]: { + ...current[fieldId], + values: [ + ...(current[fieldId]?.values ?? []), + { timestamp: new Date().toISOString(), value: value[fieldId] }, + ], + }, + }; + }); + return current; + }); + } else { + if (!metadata[id]) { + setMetadata(currentMetadata => ({ + ...currentMetadata, + [id]: { + displayName: id, + color: getRandomColor(), + }, + })); + } + setData(current => ({ + ...current, + [id]: { + ...current[id], + values: [ + ...(current[id]?.values ?? []), + { timestamp: new Date().toISOString(), value }, + ], + }, + })); + } + }, + [telemetryId, metadata], + ); + + const getChartCompatibleData = React.useCallback( + (telemetryData: TelemetryData) => + Object.keys(telemetryData).reduce( + (data, telemetryId) => [ + ...data, + { + [telemetryId]: { + [metadata[telemetryId].displayName]: telemetryData[ + telemetryId + ].values.reduce( + (values, value) => ({ + ...values, + [value.timestamp]: { + [telemetryId]: value.value, + }, + }), + {}, + ), + }, + }, + ], + [], + ), + [metadata], + ); + + React.useEffect(() => { + const sensor = SensorMap[telemetryId]; // || HealthMap[telemetryId]; + sensor?.addListener(DATA_AVAILABLE_EVENT, updateData); + // init chart with current value + if (currentValue !== undefined) { + updateData(telemetryId, currentValue); + } + return () => { + sensor?.removeListener(DATA_AVAILABLE_EVENT, updateData); + }; + }, [telemetryId, currentValue, updateData]); + + // Init chart or update it with new data + React.useEffect(() => { + if (Object.keys(data).length === 0) { + return; + } + const run = ` + if(!tsiClient){ + tsiClient = new TsiClient(); + } + if(!lineChart){ + lineChart = new tsiClient.ux.LineChart(document.getElementById('chart1')); + } + data=${JSON.stringify(getChartCompatibleData(data))}; + lineChart.render(data, ${JSON.stringify( + chartOptions, + )}, ${JSON.stringify(chartDataOptions)}); + true; + `; + chartRef.current?.injectJavaScript(run); + }, [data, chartOptions, chartDataOptions, getChartCompatibleData]); + + if (chartType === ChartType.MAP) { + return ( + + + + + Latitude: {currentValue.lat} + + + Longitude: {currentValue.lon} + + + + ); + } + return ( + <> + {Object.keys(data).length === 0 ? ( + + ) : ( + + + } + /> + + + {Object.keys(data).map((telemetryId, i) => { + const telemetry = data[telemetryId]; + if (!telemetry) { + return null; + } + const avg = + telemetry.values.map(v => v.value).reduce((a, b) => a + b) / + telemetry.values.length; + const fill = avg > 1 || avg < -1 ? avg : Math.abs(avg * 1000); + return ( + + {() => { + const strVal = `${avg}`; + return ( + + {strVal.length > 6 + ? `${strVal.substring(0, 6)}...` + : strVal} + + ); + }} + + ); + })} + + + + + )} + + ); +}); + +const style = StyleSheet.create({ + container: { + flex: 1, + }, + chart: { + flex: 2, + marginTop: 30, + marginHorizontal: 10, + }, + summary: { + flex: 1, + padding: 20, + }, +}); + +export default Chart; diff --git a/src/Insight.tsx b/src/Insight.tsx deleted file mode 100644 index 52190b2..0000000 --- a/src/Insight.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View, StyleSheet, processColor} from 'react-native'; -import {LineChart} from 'react-native-charts-wrapper'; -import { - getRandomColor, - Text, - LightenDarkenColor, - Name, -} from './components/typography'; -import { - ExtendedLineData, - ItemData, - CustomLineDatasetConfig, - NavigationParams, - DATA_AVAILABLE_EVENT, -} from './types'; -import {useTheme, RouteProp} from '@react-navigation/native'; -import {AnimatedCircularProgress} from 'react-native-circular-progress'; -import {useScreenDimensions} from './hooks/layout'; -import {Loader} from './components/loader'; -import Map from './components/map'; -import {SensorMap} from './sensors'; - -export enum ChartType { - DEFAULT, - MAP, -} - -const Insight = React.memo<{ - route: RouteProp< - Record< - string, - NavigationParams & { - chartType: ChartType; - telemetryId: string; - currentValue: any; - } - >, - 'Insight' - >; -}>(({route}) => { - const {screen} = useScreenDimensions(); - const {colors, dark} = useTheme(); - const [data, setData] = useState({ - dataSets: [], - }); - const {current: start} = useRef(Date.now()); - const timestamp = Date.now() - start; - const {chartType, telemetryId, currentValue} = route.params; - - const mapStyle = useMemo( - () => ({ - flex: 3, - margin: 20, - borderRadius: 20, - ...(!dark - ? { - shadowColor: "'rgba(0, 0, 0, 0.14)'", - shadowOffset: { - width: 0, - height: 3, - }, - shadowOpacity: 0.8, - shadowRadius: 3.84, - elevation: 5, - } - : {}), - }), - [dark], - ); - - /** - * - * @param itemdata Current sample for the item - * @param startTime Start time of the sampling. Must be the same value used as "since" param in the chart - * @param setData Dispatch to update dataset with current sample - */ - const updateData = useCallback( - (id: string, value: any) => { - const item = {id, value}; - if (id !== telemetryId) { - return; - } - let itemToProcess: ItemData[] = [item]; - if (typeof item.value !== 'string' && typeof item.value !== 'number') { - // data is composite - itemToProcess = Object.keys(item.value).map(i => ({ - id: `${item.id}.${i}`, - value: item.value[i], - })); - } - itemToProcess.forEach(itemdata => { - setData(currentDataSet => { - const currentItemData = currentDataSet.dataSets.find( - d => d.itemId === itemdata.id, - ); - // Current sample time (x-axis) is the difference between current timestamp e the start time of sampling - const newSample = {x: Date.now() - start, y: itemdata.value}; - - if (!currentItemData) { - // current item is not in the dataset yet - const rgbcolor = getRandomColor(); - return { - ...currentDataSet, - dataSets: [ - ...currentDataSet.dataSets, - ...[ - { - itemId: itemdata.id, - values: [newSample], - label: itemdata.id, - config: { - color: processColor(rgbcolor), - valueTextColor: processColor(rgbcolor), - rgbcolor, - } as CustomLineDatasetConfig, - }, - ], - ], - }; - } - if ( - currentItemData.values?.[currentItemData.values?.length] === - itemdata.value - ) { - return {...currentDataSet}; - } - return { - ...currentDataSet, - dataSets: currentDataSet.dataSets.map(({...currentItem}) => { - if (currentItem.itemId === itemdata.id && currentItem.values) { - currentItem.values = [...currentItem.values, ...[newSample]]; - } - return currentItem; - }), - }; - }); - }); - }, - [start, telemetryId], - ); - - useEffect(() => { - const sensor = SensorMap[telemetryId]; // || HealthMap[telemetryId]; - sensor?.addListener(DATA_AVAILABLE_EVENT, updateData); - // init chart with current value - if (currentValue !== undefined) { - updateData(telemetryId, currentValue); - } - return () => { - sensor?.removeListener(DATA_AVAILABLE_EVENT, updateData); - }; - }, [telemetryId, currentValue, updateData]); - - if (chartType === ChartType.MAP) { - return ( - - - - - Latitude: {currentValue.lat} - - - Longitude: {currentValue.lon} - - - - ); - } - - return ( - <> - {data.dataSets.length === 0 ? ( - - ) : ( - - - - - - {data.dataSets.map((d, i) => { - if (!d.values) { - return null; - } - const val = (d.values[d.values.length - 1] as { - x: any; - y: any; - }).y; - const fill = val > 1 || val < -1 ? val : Math.abs(val * 1000); - return ( - - {() => { - const strVal = `${val}`; - return ( - - {strVal.length > 6 - ? `${strVal.substring(0, 6)}...` - : strVal} - - ); - }} - - ); - })} - - - - - )} - - ); -}); - -const style = StyleSheet.create({ - container: { - flex: 1, - }, - chart: { - flex: 1, - marginTop: 30, - marginHorizontal: 10, - }, - chartBox: { - flex: 2, - }, - summary: { - flex: 1, - padding: 20, - }, -}); - -export default Insight; diff --git a/src/Settings.tsx b/src/Settings.tsx index e9df9f2..02ff2d6 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -1,16 +1,16 @@ -import { useContext, useState } from 'react'; -import { ThemeContext } from './contexts/theme'; +import {useContext, useState} from 'react'; +import {ThemeContext} from './contexts/theme'; import React from 'react'; -import { View, Switch, ScrollView, Platform, Alert } from 'react-native'; -import { useTheme, useNavigation } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Icon, ListItem } from 'react-native-elements'; -import { Registration } from './Registration'; -import { createStackNavigator } from '@react-navigation/stack'; -import { useIoTCentralClient, useSimulation } from './hooks/iotc'; -import { defaults } from './contexts/defaults'; -import { StorageContext } from './contexts/storage'; -import { LogsContext } from './contexts/logs'; +import {View, Switch, ScrollView, Platform, Alert} from 'react-native'; +import {useTheme, useNavigation} from '@react-navigation/native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import {Icon, ListItem} from 'react-native-elements'; +import {Registration} from './Registration'; +import {createStackNavigator} from '@react-navigation/stack'; +import {useIoTCentralClient, useSimulation} from './hooks/iotc'; +import {defaults} from './contexts/defaults'; +import {StorageContext} from './contexts/storage'; +import {LogsContext} from './contexts/logs'; const Stack = createStackNavigator(); @@ -25,19 +25,19 @@ type ProfileItem = { }; export default function Settings() { - const { toggle } = useContext(ThemeContext); - const { clear } = useContext(StorageContext); - const { clear: clearLogs } = useContext(LogsContext); + const {toggle} = useContext(ThemeContext); + const {clear} = useContext(StorageContext); + const {clear: clearLogs} = useContext(LogsContext); const [client, clearClient] = useIoTCentralClient(); const [centralSimulated, simulate] = useSimulation(); - const { colors, dark } = useTheme(); + const {colors, dark} = useTheme(); const insets = useSafeAreaInsets(); const updateUIItems = (title: string, val: any) => { setItems(current => current.map(i => { if (i.title === title) { - i = { ...i, value: val }; + i = {...i, value: val}; } return i; }), @@ -51,7 +51,7 @@ export default function Settings() { action: { type: 'expand', fn: navigation => { - navigation.navigate('Registration', { previousScreen: 'root' }); + navigation.navigate('Registration', {previousScreen: 'root'}); }, }, }, @@ -83,26 +83,26 @@ export default function Settings() { }, ...(defaults.dev ? [ - { - title: 'Simulation Mode', - icon: dark ? 'sync-outline' : 'sync', - action: { - type: 'switch', - fn: async val => { - updateUIItems('Simulation Mode', val); - await simulate(val); + { + title: 'Simulation Mode', + icon: dark ? 'sync-outline' : 'sync', + action: { + type: 'switch', + fn: async val => { + updateUIItems('Simulation Mode', val); + await simulate(val); + }, }, - }, - value: centralSimulated, - } as ProfileItem - ] + value: centralSimulated, + } as ProfileItem, + ] : []), ]); return ( - + ({ + screenOptions={({route}) => ({ headerShown: false, // TODO: fix header })}> @@ -114,34 +114,40 @@ export default function Settings() { ); } -const RightElement = React.memo<{ item: ProfileItem; colors: any, dark: boolean }>( - ({ item, colors, dark }) => { - if (item.action && item.action.type === 'switch') { - return ( - - ); - } - return null; - }, -); +const RightElement = React.memo<{ + item: ProfileItem; + colors: any; + dark: boolean; +}>(({item, colors, dark}) => { + if (item.action && item.action.type === 'switch') { + return ( + + ); + } + return null; +}); -const Root = React.memo<{ items: ProfileItem[]; colors: any; dark: boolean }>( - ({ items, colors, dark }) => { +const Root = React.memo<{items: ProfileItem[]; colors: any; dark: boolean}>( + ({items, colors, dark}) => { const nav = useNavigation(); return ( - + {items.map((item, index) => ( ( }> - + {item.title} diff --git a/src/hooks/common.ts b/src/hooks/common.ts index f2b1052..c5392fc 100644 --- a/src/hooks/common.ts +++ b/src/hooks/common.ts @@ -1,5 +1,5 @@ -import { useNavigation } from '@react-navigation/native'; -import { defaults } from 'contexts/defaults'; +import {useNavigation} from '@react-navigation/native'; +import {defaults} from 'contexts/defaults'; import { Properties as PropertiesData, getDeviceInfo, @@ -13,8 +13,8 @@ import { useCallback, useMemo, } from 'react'; -import { Platform } from 'react-native'; -import { AVAILABLE_SENSORS, SensorMap } from 'sensors'; +import {Platform} from 'react-native'; +import {AVAILABLE_SENSORS, SensorMap} from 'sensors'; import { DATA_AVAILABLE_EVENT, ItemProps, @@ -22,7 +22,7 @@ import { SENSOR_UNAVAILABLE_EVENT, TimedLog, } from '../types'; -import { LogsContext } from '../contexts/logs'; +import {LogsContext} from '../contexts/logs'; export type IIcon = { name: string; @@ -33,12 +33,12 @@ export function useScreenIcon(icon: IIcon): void { const navigation = useNavigation(); useEffect(() => { - navigation.setParams({ icon }); + navigation.setParams({icon}); }, [navigation, icon]); } export function useLogger(): [TimedLog, (logItem: LogItem) => void] { - const { logs, append } = useContext(LogsContext); + const {logs, append} = useContext(LogsContext); return [logs, append]; } @@ -412,5 +412,5 @@ export function useProperties() { loadDeviceInfo(); }, [loadDeviceInfo]); - return { loading, properties, updateProperty }; + return {loading, properties, updateProperty}; } diff --git a/src/types.ts b/src/types.ts index fb94f7c..a8173a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,11 @@ -import {StackNavigationProp} from '@react-navigation/stack'; -import {GestureResponderEvent} from 'react-native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { GestureResponderEvent } from 'react-native'; import { LineData, LineValue, LineDatasetConfig, } from 'react-native-charts-wrapper'; -import {IconProps} from 'react-native-elements'; +import { IconProps } from 'react-native-elements'; export const Screens = { TELEMETRY_SCREEN: 'Telemetry', @@ -55,14 +55,14 @@ export type NavigationProperty = StackNavigationProp< */ export type StateUpdater = React.Dispatch>; -export type LogItem = {eventName: string; eventData: string}; -export type TimedLog = {timestamp: number | string; logItem: LogItem}[]; +export type LogItem = { eventName: string; eventData: string }; +export type TimedLog = { timestamp: number | string; logItem: LogItem }[]; /** * Chart typings */ -export type CustomLineDatasetConfig = LineDatasetConfig & {rgbcolor: string}; +export type CustomLineDatasetConfig = LineDatasetConfig & { rgbcolor: string }; export interface ExtendedLineData extends LineData { dataSets: { itemId: string; @@ -98,6 +98,39 @@ export type GeoCoordinates = { lonD?: number; }; +export type LineChartOptions = { + brushContextMenuActions?: any[]; + grid?: boolean; + includeDots?: boolean; + includeEnvelope?: boolean; + brushHandlesVisible?: boolean; + hideChartControlPanel?: boolean; + snapBrush?: boolean; + interpolationFunction?: '' | 'curveLinear' | 'curveMonotoneX'; + legend?: 'shown' | 'compact' | 'hidden'; + noAnimate?: boolean; + offset?: any; + spMeasures?: string[]; + isTemporal?: boolean; + spAxisLabels?: string[]; + stacked?: boolean; + theme?: 'dark' | 'light'; + timestamp?: string; + tooltip?: boolean; + yAxisState?: 'stacked' | 'shared' | 'overlap'; + yExtent?: [number, number]; +}; + +export type ChartDataOptions = { + color: string; + alias: string; + dataType?: 'numeric' | 'categorical' | 'events'; +}; +export enum ChartType { + DEFAULT, + MAP, +} + /** * Health typings */ @@ -135,6 +168,7 @@ export const LOG_DATA = 'LOG_DATA'; */ export const ENABLE_DISABLE_COMMAND = 'enableSensors'; export const SET_FREQUENCY_COMMAND = 'changeInterval'; +export const LIGHT_TOGGLE_COMMAND = 'lightOn'; /** * IOTC COMPONENT NAME