This commit is contained in:
lucadruda 2021-03-31 20:17:16 +02:00
Родитель c80a8ae472
Коммит 0c28374226
10 изменённых файлов: 705 добавлений и 450 удалений

Просмотреть файл

@ -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;

22
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",

Просмотреть файл

@ -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",

Просмотреть файл

@ -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'}

396
src/Chart.tsx Normal file
Просмотреть файл

@ -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;

Просмотреть файл

@ -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;

Просмотреть файл

@ -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};
}

Просмотреть файл

@ -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