メインコンテンツまでスキップ

IoT Application KitとCloudscapeを使ってみた

· 約10分
moritalous

IoT Application KitとCloudscapeを使ったWebアプリを作ってみました。

IoT Application Kitとは

IoT Application Kitとは

IoT Application Kit is a development library for creating web applications to visualize industrial data.

SiteWiseやTwinMakerをデータソースとして、独自ダッシュボードを作成するためのReactコンポーネントが提供されています。

Cloudscapeとは

Cloudscapeとは

An open source design system for the cloud Cloudscape offers user interface guidelines, front-end components, design resources, and development tools for building intuitive, engaging, and inclusive user experiences at scale.

AWSが提供するUIコンポーネント集です。

IoT Application KitのexampleではMaterial-UIが、aws-iot-app-kit-bottling-line-demo ではCloudscapeの前身のAWS-UIが使用されていますが、Cloudscapeを使った形式で挑戦してみました。

↓はIoT Application Kitのexampleの画面

image.png

前準備

以下の記事を参考に、React 17プロジェクトを作成し、Amplifyの認証機能を実装した状態まで持っていってください。

AWSクラウド側の設定

SiteWiseのデモデータを生成する

マネジメントコンソールのSiteWise管理画面にある「SiteWise デモ」を使用し、サンプルデータを生成します。デモを作成ボタンを押すと、自動でSiteWiseのアセットの作成やデータの登録が可能です。

image.png

自動で作成されたアセット

image.png

IAMロールにSiteWiseアクセスの権限を付与

Amplifyで認証とPubSubを実装する(React)のIAM権限の追加の章と同様に、SiteWiseへのアクセスにためにAWS管理ポリシーのAWSIoTSiteWiseReadOnlyAccessを付与します。

aws iam attach-role-policy \
--role-name ${IAMロール名} \
--policy-arn arn:aws:iam::aws:policy/AWSIoTSiteWiseReadOnlyAccess

Reactライブラリーのインストール

IoT Application Kitライブラリーのインストール

npm install @iot-app-kit/source-iotsitewise
npm install @iot-app-kit/components
npm install @iot-app-kit/react-components

Cloudscapeライブラリーのインストール

npm install @cloudscape-design/global-styles
npm install @cloudscape-design/components

Reactコードの修正

Import部

  • IoT Application Kitライブラリーのインポート
import { COMPARISON_OPERATOR } from '@iot-app-kit/components';
import "@iot-app-kit/components/styles.css";
import { LineChart, ResourceExplorer, StatusTimeline, WebglContext } from "@iot-app-kit/react-components";
import { initialize } from '@iot-app-kit/source-iotsitewise';
  • Cloudscapeライブラリーのインポート
import { AppLayout, BreadcrumbGroup, Container, Grid, Header, TopNavigation } from '@cloudscape-design/components';
import "@cloudscape-design/global-styles/index.css";

コンポーネント関数の作成

App

Auth.currentCredentials()でAmplifyにログインした認証情報を取得します。 認証情報を使用し、initialize関数でSiteWiseのデータとアセット情報(query)を取得します。 WebglContextはReact全体で一度だけ宣言する必要があるようです。 (参考:AWS IoT Application Kitを使ったIoT Webアプリケーションの構築

function App() {

const [mainContents, setMainContents] = useState<JSX.Element>(<Content />)

useEffect(() => {
Auth.currentCredentials()
.then((info) => {
// console.log(info)
const { query } = initialize({ awsCredentials: info, awsRegion: 'ap-northeast-1' });
setMainContents(<Content query={query} />)
})
}, [])


return (
<Authenticator>
{({ signOut, user }) => (
<>
<TopNavigation
identity={{
href: "#",
title: "AWS IoT App Kit Demo",
}}
i18nStrings={{ overflowMenuTriggerText: "More", overflowMenuTitleText: "More" }}
/>
<AppLayout
breadcrumbs={<Breadcrumbs />}
contentHeader={<PageHeader />}
content={mainContents}
navigationHide={true}
toolsHide={true}
/>
<WebglContext />
</>
)}
</Authenticator>
);
}

パンくずリスト

function Breadcrumbs() {
const breadcrumbItems = [
{
text: 'Demo',
href: '#'
},
{
text: 'Wind Farm',
href: '#'
}
];

return <BreadcrumbGroup items={breadcrumbItems} expandAriaLabel="Show path" ariaLabel="Breadcrumbs" />;
}

ヘッダー

function PageHeader() {
return <Header variant="h1">Demo Wind Farm Dashboard</Header>;
}

ResourceExplorer

画面左のSiteWiseのアセットを選択する部分です。

image.png

function SitwiseResourceExplorer(props: any) {
const columnDefinitions = [{
sortingField: 'name',
id: 'name',
header: 'Asset Name',
cell: ({ name }: any) => name,
}];

return (
<Container
disableContentPaddings={true}
header={<Header variant="h2" description="List of SiteWise assets"> SiteWise assets </Header>}
>
<ResourceExplorer
query={props.query.assetTree.fromRoot()}
onSelectionChange={(event) => {
console.log("changes asset", event);
props.setAssetId((event?.detail?.selectedItems?.[0] as any)?.id);
props.setAssetName((event?.detail?.selectedItems?.[0] as any)?.name);
}}
columnDefinitions={columnDefinitions}
/>
</Container>
);
}

LineChart

折れ線グラフです。

image.png

function LineChartContainer(props: any) {
return (
<Container disableContentPaddings={true} header={<Header variant="h2" description="LineChart"> {props.name} </Header>} >
<div style={{ height: "170px" }}>
<LineChart
viewport={{ duration: "30m" }}
queries={[
props.query.timeSeriesData({
assets: [
{
assetId: props.assetId,
properties: [
{
propertyId: props.propertyId
}
]
}
]
})
]}
/>
</div>
</Container>
);
}

StatusTimeline

ステータスのタイムラインです。

image.png

function StatusTimelineContainer(props: any) {
return (
<Container disableContentPaddings={true} header={<Header variant="h2" description="StatusTimeline"> {props.name} </Header>} >
<div style={{ height: "170px" }}>
<StatusTimeline
viewport={{ duration: '30m' }}
annotations={{
y: [
{ color: '#1D8102', comparisonOperator: COMPARISON_OPERATOR.EQUAL, value: 0 },
{ color: '#FF1111', comparisonOperator: COMPARISON_OPERATOR.EQUAL, value: 1 }
]
}}
queries={[
props.query.timeSeriesData({
assets: [{
assetId: props.assetId,
properties: [{
propertyId: props.propertyId
}]
}]
})
]}
/>
</div>
</Container>
);
}

Content

メイン部分のコンポーネント定義。 queryがセットされていないときは子コンポーネントを作成しないようにしました。

function Content(props: any) {

// Asset Id of the AWS IoT SiteWise asset that you want to display by default
const DEFAULT_MACHINE_ASSET_ID = '70332ef6-2fe5-42d3-8284-cc3da97ac875';
const [assetId, setAssetId] = useState(DEFAULT_MACHINE_ASSET_ID);
const [assetName, setAssetName] = useState('Demo Wind Farm Asset');

if (!props.query) {
return (<></>)
}

return (
<Grid gridDefinition={[
{ colspan: { l: 3, m: 3, default: 12 } },
{ colspan: { l: 9, m: 9, default: 12 } }
]}>
<SitwiseResourceExplorer setAssetId={setAssetId} setAssetName={setAssetName} query={props.query} />
<Grid gridDefinition={[
{ colspan: { default: 12 } },
{ colspan: { default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
]}>
<Container
header=<Header>{assetName}</Header>
/>
<StatusTimelineContainer
assetId={assetId}
propertyId={'9e3e455f-9b42-48df-9ef9-2b30a23f6ea4'}
query={props.query}
name={'Overdrive State'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'8dbfac9c-2ad0-48cd-b87f-96396ef1214c'}
query={props.query}
name={'RotationsPerMinute'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'0071ff1d-d01b-4cc6-b39d-42b899ab3a9e'}
query={props.query}
name={'Torque (KiloNewton Meter)'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'422f113d-94b3-4088-8daa-6e9b7326c54f'}
query={props.query}
name={'Wind Direction'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'a8b021bc-a4ed-4330-bb8f-fbbed1e01b66'}
query={props.query}
name={'Wind Speed'}
/>
</Grid>
</Grid>
);
}

完成

見た目はCloudscapeっぽさが結構出てますね。 これが独自アプリとして作成でき、SiteWiseの他にTwinMakerにも対応しているので、色々活用できそうです。

image.png

App.tsx全体
import { useEffect, useState } from 'react';
import './App.css';

import { Authenticator } from '@aws-amplify/ui-react';
import "@aws-amplify/ui-react/styles.css";
import { Auth } from 'aws-amplify';

import { AppLayout, BreadcrumbGroup, Container, Grid, Header, TopNavigation } from '@cloudscape-design/components';
import "@cloudscape-design/global-styles/index.css";

import { COMPARISON_OPERATOR } from '@iot-app-kit/components';
import "@iot-app-kit/components/styles.css";
import { LineChart, ResourceExplorer, StatusTimeline, WebglContext } from "@iot-app-kit/react-components";
import { initialize } from '@iot-app-kit/source-iotsitewise';


function Breadcrumbs() {
const breadcrumbItems = [
{
text: 'Demo',
href: '#'
},
{
text: 'Wind Farm',
href: '#'
}
];

return <BreadcrumbGroup items={breadcrumbItems} expandAriaLabel="Show path" ariaLabel="Breadcrumbs" />;
}


function PageHeader() {
return <Header variant="h1">Demo Wind Farm Dashboard</Header>;
}


function SitwiseResourceExplorer(props: any) {
const columnDefinitions = [{
sortingField: 'name',
id: 'name',
header: 'Asset Name',
cell: ({ name }: any) => name,
}];

return (
<Container
disableContentPaddings={true}
header={<Header variant="h2" description="List of SiteWise assets"> SiteWise assets </Header>}
>
<ResourceExplorer
query={props.query.assetTree.fromRoot()}
onSelectionChange={(event) => {
console.log("changes asset", event);
props.setAssetId((event?.detail?.selectedItems?.[0] as any)?.id);
props.setAssetName((event?.detail?.selectedItems?.[0] as any)?.name);
}}
columnDefinitions={columnDefinitions}
/>
</Container>
);
}


function LineChartContainer(props: any) {
return (
<Container disableContentPaddings={true} header={<Header variant="h2" description="LineChart"> {props.name} </Header>} >
<div style={{ height: "170px" }}>
<LineChart
viewport={{ duration: "30m" }}
queries={[
props.query.timeSeriesData({
assets: [
{
assetId: props.assetId,
properties: [
{
propertyId: props.propertyId
}
]
}
]
})
]}
/>
</div>
</Container>
);
}


function StatusTimelineContainer(props: any) {
return (
<Container disableContentPaddings={true} header={<Header variant="h2" description="StatusTimeline"> {props.name} </Header>} >
<div style={{ height: "170px" }}>
<StatusTimeline
viewport={{ duration: '30m' }}
annotations={{
y: [
{ color: '#1D8102', comparisonOperator: COMPARISON_OPERATOR.EQUAL, value: 0 },
{ color: '#FF1111', comparisonOperator: COMPARISON_OPERATOR.EQUAL, value: 1 }
]
}}
queries={[
props.query.timeSeriesData({
assets: [{
assetId: props.assetId,
properties: [{
propertyId: props.propertyId
}]
}]
})
]}
/>
</div>
</Container>
);
}


function Content(props: any) {

// Asset Id of the AWS IoT SiteWise asset that you want to display by default
const DEFAULT_MACHINE_ASSET_ID = '70332ef6-2fe5-42d3-8284-cc3da97ac875';
const [assetId, setAssetId] = useState(DEFAULT_MACHINE_ASSET_ID);
const [assetName, setAssetName] = useState('Demo Wind Farm Asset');

if (!props.query) {
return (<></>)
}

return (
<Grid gridDefinition={[
{ colspan: { l: 3, m: 3, default: 12 } },
{ colspan: { l: 9, m: 9, default: 12 } }
]}>
<SitwiseResourceExplorer setAssetId={setAssetId} setAssetName={setAssetName} query={props.query} />
<Grid gridDefinition={[
{ colspan: { default: 12 } },
{ colspan: { default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
{ colspan: { l: 6, m: 6, s: 6, default: 12 } },
]}>
<Container
header=<Header>{assetName}</Header>
/>
<StatusTimelineContainer
assetId={assetId}
propertyId={'9e3e455f-9b42-48df-9ef9-2b30a23f6ea4'}
query={props.query}
name={'Overdrive State'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'8dbfac9c-2ad0-48cd-b87f-96396ef1214c'}
query={props.query}
name={'RotationsPerMinute'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'0071ff1d-d01b-4cc6-b39d-42b899ab3a9e'}
query={props.query}
name={'Torque (KiloNewton Meter)'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'422f113d-94b3-4088-8daa-6e9b7326c54f'}
query={props.query}
name={'Wind Direction'}
/>
<LineChartContainer
assetId={assetId}
propertyId={'a8b021bc-a4ed-4330-bb8f-fbbed1e01b66'}
query={props.query}
name={'Wind Speed'}
/>
</Grid>
</Grid>
);
}


function App() {

const [mainContents, setMainContents] = useState<JSX.Element>(<Content />)

useEffect(() => {
Auth.currentCredentials()
.then((info) => {
// console.log(info)
const { query } = initialize({ awsCredentials: info, awsRegion: 'ap-northeast-1' });
setMainContents(<Content query={query} />)
})
}, [])


return (
<Authenticator>
{({ signOut, user }) => (
<>
<TopNavigation
identity={{
href: "#",
title: "AWS IoT App Kit Demo",
}}
i18nStrings={{ overflowMenuTriggerText: "More", overflowMenuTitleText: "More" }}
/>
<AppLayout
breadcrumbs={<Breadcrumbs />}
contentHeader={<PageHeader />}
content={mainContents}
navigationHide={true}
toolsHide={true}
/>
<WebglContext />
</>
)}
</Authenticator>
);
}

export default App;