add tsi charts
This commit is contained in:
Родитель
c80a8ae472
Коммит
0c28374226
|
@ -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
|
||||
|
|
|
@ -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 = "<group>";
|
||||
};
|
||||
4ABDA6BB260283330068B64E /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4ABDA6BF260283330068B64E /* libRCTTorch.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
233
src/App.tsx
233
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<NavigationScreens>();
|
||||
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 (
|
||||
<NavigationContainer
|
||||
theme={mode === ThemeMode.DARK ? DarkTheme : DefaultTheme}>
|
||||
|
@ -96,14 +100,14 @@ const Navigation = React.memo(() => {
|
|||
{/* @ts-ignore */}
|
||||
<Stack.Screen
|
||||
name="root"
|
||||
options={({navigation}: {navigation: NavigationProperty}) => ({
|
||||
options={({ navigation }: { navigation: NavigationProperty }) => ({
|
||||
headerTitle: () => null,
|
||||
headerLeft: () => <Logo />,
|
||||
headerRight: () => <Profile navigate={navigation.navigate} />,
|
||||
})}
|
||||
component={Root}
|
||||
/>
|
||||
<Stack.Screen
|
||||
{/* <Stack.Screen
|
||||
name="Insight"
|
||||
component={Insight}
|
||||
options={({route}) => {
|
||||
|
@ -119,10 +123,27 @@ const Navigation = React.memo(() => {
|
|||
}
|
||||
return data;
|
||||
}}
|
||||
/> */}
|
||||
<Stack.Screen
|
||||
name="Insight"
|
||||
component={Chart}
|
||||
options={({ route }) => {
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Settings"
|
||||
options={({navigation}: {navigation: NavigationProperty}) => ({
|
||||
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<void>(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(() => {
|
|||
<Tab.Navigator
|
||||
key="tab"
|
||||
tabBarOptions={Platform.select({
|
||||
android: {safeAreaInsets: {bottom: 0}},
|
||||
android: { safeAreaInsets: { bottom: 0 } },
|
||||
})}>
|
||||
<Tab.Screen
|
||||
name={Screens.TELEMETRY_SCREEN}
|
||||
options={{
|
||||
tabBarIcon: ({color, size}) => (
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<TabBarIcon icon={icons.Telemetry} color={color} size={size} />
|
||||
),
|
||||
}}>
|
||||
|
@ -358,48 +409,48 @@ const Root = React.memo(() => {
|
|||
<Tab.Screen
|
||||
name={Screens.PROPERTIES_SCREEN}
|
||||
options={{
|
||||
tabBarIcon: ({color, size}) => (
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<TabBarIcon icon={icons.Properties} color={color} size={size} />
|
||||
),
|
||||
}}>
|
||||
{propertiesLoading
|
||||
? () => (
|
||||
<Loader
|
||||
message={'Waiting for properties...'}
|
||||
visible={true}
|
||||
style={{flex: 1, justifyContent: 'center'}}
|
||||
/>
|
||||
)
|
||||
<Loader
|
||||
message={'Waiting for properties...'}
|
||||
visible={true}
|
||||
style={{ flex: 1, justifyContent: 'center' }}
|
||||
/>
|
||||
)
|
||||
: () => (
|
||||
<CardView
|
||||
items={properties}
|
||||
componentName="Property"
|
||||
onEdit={async (item, value) => {
|
||||
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'}],
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CardView
|
||||
items={properties}
|
||||
componentName="Property"
|
||||
onEdit={async (item, value) => {
|
||||
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' }],
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tab.Screen>
|
||||
<Tab.Screen
|
||||
name={Screens.FILE_UPLOAD_SCREEN}
|
||||
component={FileUpload}
|
||||
options={{
|
||||
tabBarIcon: ({color, size}) => (
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<TabBarIcon
|
||||
icon={icons['Image Upload']}
|
||||
color={color}
|
||||
|
@ -412,7 +463,7 @@ const Root = React.memo(() => {
|
|||
name={Screens.LOGS_SCREEN}
|
||||
component={Logs}
|
||||
options={{
|
||||
tabBarIcon: ({color, size}) => (
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<TabBarIcon icon={icons.Logs} color={color} size={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 (
|
||||
<View
|
||||
style={{
|
||||
|
@ -467,25 +518,25 @@ const Logo = React.memo(() => {
|
|||
fontSize: 16,
|
||||
letterSpacing: 0.1,
|
||||
}}>
|
||||
Azure IoT Central
|
||||
{TITLE}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const Profile = React.memo((props: {navigate: any}) => {
|
||||
const {colors} = useTheme();
|
||||
const Profile = React.memo((props: { navigate: any }) => {
|
||||
const { colors } = useTheme();
|
||||
return (
|
||||
<View style={{marginHorizontal: 10}}>
|
||||
<View style={{ marginHorizontal: 10 }}>
|
||||
<Icon
|
||||
style={{marginEnd: 20}}
|
||||
style={{ marginEnd: 20 }}
|
||||
name={
|
||||
Platform.select({
|
||||
ios: 'settings-outline',
|
||||
android: 'settings',
|
||||
}) as string
|
||||
}
|
||||
type={Platform.select({ios: 'ionicon', android: 'material'})}
|
||||
type={Platform.select({ ios: 'ionicon', android: 'material' })}
|
||||
color={colors.text}
|
||||
onPress={() => {
|
||||
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 (
|
||||
<View style={{flexDirection: 'row', marginLeft: 10, alignItems: 'center'}}>
|
||||
<View style={{ flexDirection: 'row', marginLeft: 10, alignItems: 'center' }}>
|
||||
<Icon name="close" color={colors.text} onPress={goBack} />
|
||||
{Platform.OS === 'android' && (
|
||||
<HeaderTitle style={{marginLeft: 20}}>{title}</HeaderTitle>
|
||||
<HeaderTitle style={{ marginLeft: 20 }}>{title}</HeaderTitle>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Icon
|
||||
name={icon ? icon.name : 'home'}
|
||||
|
|
|
@ -0,0 +1,396 @@
|
|||
import { RouteProp, useTheme } from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { NavigationParams, DATA_AVAILABLE_EVENT, LineChartOptions, ChartType } from 'types';
|
||||
import { SensorMap } from 'sensors';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Name,
|
||||
Text,
|
||||
getRandomColor,
|
||||
LightenDarkenColor,
|
||||
} from 'components/typography';
|
||||
import { AnimatedCircularProgress } from 'react-native-circular-progress';
|
||||
import Map from 'components/map';
|
||||
import { Loader } from 'components/loader';
|
||||
import { useScreenDimensions } from 'hooks/layout';
|
||||
|
||||
type SeriesData = {
|
||||
[date: string]: {
|
||||
[field: string]: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ChartData = {
|
||||
[groupId: string]: {
|
||||
[telemetryId: string]: SeriesData;
|
||||
};
|
||||
};
|
||||
|
||||
type TelemetryData = {
|
||||
[telemetryId: string]: {
|
||||
values: {
|
||||
timestamp: string;
|
||||
value: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
type TelemetryMetaData = {
|
||||
[telemetryId: string]: {
|
||||
displayName: string;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
|
||||
const Chart = React.memo<{
|
||||
route: RouteProp<
|
||||
Record<
|
||||
string,
|
||||
NavigationParams & {
|
||||
chartType: ChartType;
|
||||
telemetryId: string;
|
||||
currentValue: any;
|
||||
}
|
||||
>,
|
||||
'Insight'
|
||||
>;
|
||||
}>(({ route }) => {
|
||||
const { colors, dark } = useTheme();
|
||||
const { screen } = useScreenDimensions();
|
||||
const { chartType, telemetryId, currentValue } = route.params;
|
||||
const [data, setData] = React.useState<TelemetryData>({});
|
||||
const [metadata, setMetadata] = React.useState<TelemetryMetaData>({});
|
||||
const chartRef = React.useRef<WebView>(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(
|
||||
() => `<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/tsiclient@latest/tsiclient.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/tsiclient@latest/tsiclient.css">
|
||||
</link>
|
||||
<style>
|
||||
body{
|
||||
background-color:${colors.background};
|
||||
}
|
||||
.tsi-seriesName {
|
||||
width: 100%;
|
||||
}
|
||||
.tsi-seriesName span {
|
||||
font-size: xx-large;
|
||||
}
|
||||
.tsi-lineChartSVG {
|
||||
height: 110% !important;
|
||||
}
|
||||
.tsi-legend.compact {
|
||||
margin-bottom:60px;
|
||||
}
|
||||
.tsi-legend.compact .tsi-seriesLabel .tsi-splitByContainer .tsi-splitByLabel {
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
padding: 0 4px 1px 4px;
|
||||
margin-top: 1px;
|
||||
height:100%;
|
||||
}
|
||||
.tsi-legend.compact .tsi-seriesLabel .tsi-splitByContainer .tsi-splitByLabel .tsi-seriesName {
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.tsi-legend.compact .tsi-seriesLabel .tsi-splitByContainer .tsi-splitByLabel .tsi-colorKey {
|
||||
top: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.tsi-lineChart .tsi-lineChartSVG text.standardYAxisText {
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.tsi-lineChart .tsi-lineChartSVG text {
|
||||
font-size: xx-large;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
window.onload = function () {
|
||||
tsiClient = new TsiClient();
|
||||
data = [];
|
||||
|
||||
lineChart = new tsiClient.ux.LineChart(document.getElementById('chart1'));
|
||||
lineChart.render(data, ${JSON.stringify(
|
||||
chartOptions,
|
||||
)}, ${JSON.stringify(chartDataOptions)});
|
||||
}
|
||||
</script>
|
||||
<body>
|
||||
<div id="chart1" style="width: 100%; height: 1000px; margin-top: 40px;"></div>
|
||||
</body>
|
||||
</head>
|
||||
</html>`,
|
||||
[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<ChartData[]>(
|
||||
(data, telemetryId) => [
|
||||
...data,
|
||||
{
|
||||
[telemetryId]: {
|
||||
[metadata[telemetryId].displayName]: telemetryData[
|
||||
telemetryId
|
||||
].values.reduce<SeriesData>(
|
||||
(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 (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Map style={mapStyle} location={currentValue} />
|
||||
<View style={style.summary}>
|
||||
<Text>
|
||||
<Name>Latitude:</Name> {currentValue.lat}
|
||||
</Text>
|
||||
<Text>
|
||||
<Name>Longitude:</Name> {currentValue.lon}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{Object.keys(data).length === 0 ? (
|
||||
<Loader message="" visible={true} />
|
||||
) : (
|
||||
<View style={style.container}>
|
||||
<View style={style.chart}>
|
||||
<WebView
|
||||
originWhitelist={['*']}
|
||||
containerStyle={{ flex: 2, justifyContent: 'flex-start' }}
|
||||
ref={chartRef}
|
||||
source={{
|
||||
html,
|
||||
}}
|
||||
startInLoadingState={true}
|
||||
// use theme background color when chart is loading
|
||||
// style allows to cover all webview space as per issue:
|
||||
// https://github.com/react-native-webview/react-native-webview/issues/1031
|
||||
renderLoading={() => <View style={{
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
backgroundColor: colors.background
|
||||
}} />}
|
||||
/>
|
||||
<View style={style.summary}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
marginTop: 40,
|
||||
}}>
|
||||
{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 (
|
||||
<AnimatedCircularProgress
|
||||
key={`circle - ${i}`}
|
||||
size={screen.width / 5}
|
||||
width={5}
|
||||
fill={fill}
|
||||
tintColor={metadata[telemetryId].color}
|
||||
backgroundColor={LightenDarkenColor(
|
||||
metadata[telemetryId].color,
|
||||
90,
|
||||
true,
|
||||
)}
|
||||
rotation={360}>
|
||||
{() => {
|
||||
const strVal = `${avg}`;
|
||||
return (
|
||||
<Text>
|
||||
{strVal.length > 6
|
||||
? `${strVal.substring(0, 6)}...`
|
||||
: strVal}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</AnimatedCircularProgress>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
chart: {
|
||||
flex: 2,
|
||||
marginTop: 30,
|
||||
marginHorizontal: 10,
|
||||
},
|
||||
summary: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default Chart;
|
286
src/Insight.tsx
286
src/Insight.tsx
|
@ -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<ExtendedLineData>({
|
||||
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 (
|
||||
<View style={style.container}>
|
||||
<Map style={mapStyle} location={currentValue} />
|
||||
<View style={style.summary}>
|
||||
<Text>
|
||||
<Name>Latitude:</Name> {currentValue.lat}
|
||||
</Text>
|
||||
<Text>
|
||||
<Name>Longitude:</Name> {currentValue.lon}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.dataSets.length === 0 ? (
|
||||
<Loader message="" visible={true} />
|
||||
) : (
|
||||
<View style={style.container}>
|
||||
<View style={style.chart}>
|
||||
<LineChart
|
||||
style={style.chartBox}
|
||||
chartDescription={{text: ''}}
|
||||
touchEnabled={true}
|
||||
dragEnabled={true}
|
||||
scaleEnabled={true}
|
||||
pinchZoom={true}
|
||||
extraOffsets={{bottom: 20}}
|
||||
legend={{
|
||||
wordWrapEnabled: true,
|
||||
textColor: processColor(colors.text),
|
||||
textSize: 16,
|
||||
}}
|
||||
xAxis={{
|
||||
position: 'BOTTOM',
|
||||
axisMaximum: timestamp + 500,
|
||||
axisMinimum: timestamp - 10000,
|
||||
valueFormatter: 'date',
|
||||
since: start,
|
||||
valueFormatterPattern: 'HH:mm:ss',
|
||||
timeUnit: 'MILLISECONDS',
|
||||
drawAxisLines: false,
|
||||
drawGridLines: false,
|
||||
textColor: processColor(colors.text),
|
||||
}}
|
||||
yAxis={{
|
||||
right: {
|
||||
drawAxisLines: false,
|
||||
textColor: processColor(colors.text),
|
||||
},
|
||||
left: {
|
||||
drawAxisLines: false,
|
||||
textColor: processColor(colors.text),
|
||||
},
|
||||
}}
|
||||
data={data}
|
||||
/>
|
||||
<View style={style.summary}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
marginTop: 40,
|
||||
}}>
|
||||
{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 (
|
||||
<AnimatedCircularProgress
|
||||
key={`circle-${i}`}
|
||||
size={screen.width / 5}
|
||||
width={5}
|
||||
fill={fill}
|
||||
tintColor={
|
||||
d.config ? d.config.rgbcolor : getRandomColor()
|
||||
}
|
||||
backgroundColor={
|
||||
d.config
|
||||
? LightenDarkenColor(d.config.rgbcolor, 90, true)
|
||||
: getRandomColor()
|
||||
}
|
||||
rotation={360}>
|
||||
{() => {
|
||||
const strVal = `${val}`;
|
||||
return (
|
||||
<Text>
|
||||
{strVal.length > 6
|
||||
? `${strVal.substring(0, 6)}...`
|
||||
: strVal}
|
||||
</Text>
|
||||
);
|
||||
}}
|
||||
</AnimatedCircularProgress>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
114
src/Settings.tsx
114
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 (
|
||||
<View style={{ flex: 1, marginTop: insets.top, marginBottom: insets.bottom }}>
|
||||
<View style={{flex: 1, marginTop: insets.top, marginBottom: insets.bottom}}>
|
||||
<Stack.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
screenOptions={({route}) => ({
|
||||
headerShown: false, // TODO: fix header
|
||||
})}>
|
||||
<Stack.Screen name="setting_root">
|
||||
|
@ -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 (
|
||||
<Switch
|
||||
value={item.value}
|
||||
onValueChange={item.action.fn}
|
||||
{...(Platform.OS === 'android' && {
|
||||
thumbColor: item.value ? colors.primary : (dark ? colors.text : colors.background),
|
||||
trackColor: { true: colors.border, false: colors.border }
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
const RightElement = React.memo<{
|
||||
item: ProfileItem;
|
||||
colors: any;
|
||||
dark: boolean;
|
||||
}>(({item, colors, dark}) => {
|
||||
if (item.action && item.action.type === 'switch') {
|
||||
return (
|
||||
<Switch
|
||||
value={item.value}
|
||||
onValueChange={item.action.fn}
|
||||
{...(Platform.OS === 'android' && {
|
||||
thumbColor: item.value
|
||||
? colors.primary
|
||||
: dark
|
||||
? colors.text
|
||||
: colors.background,
|
||||
trackColor: {true: colors.border, false: colors.border},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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<any>();
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<ScrollView style={{flex: 1}}>
|
||||
{items.map((item, index) => (
|
||||
<ListItem
|
||||
key={`setting-${index}`}
|
||||
bottomDivider
|
||||
containerStyle={{ backgroundColor: colors.card }}
|
||||
containerStyle={{backgroundColor: colors.card}}
|
||||
onPress={
|
||||
item.action && item.action.type !== 'switch'
|
||||
? item.action.fn.bind(null, nav)
|
||||
|
@ -149,7 +155,7 @@ const Root = React.memo<{ items: ProfileItem[]; colors: any; dark: boolean }>(
|
|||
}>
|
||||
<Icon name={item.icon} type="ionicon" color={colors.text} />
|
||||
<ListItem.Content>
|
||||
<ListItem.Title style={{ color: colors.text }}>
|
||||
<ListItem.Title style={{color: colors.text}}>
|
||||
{item.title}
|
||||
</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
46
src/types.ts
46
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<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||
|
||||
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
|
||||
|
|
Загрузка…
Ссылка в новой задаче