IoT Application KitとCloudscapeを使ったWebアプリを作ってみました。
IoT Application Kitとは
IoT Application Kit is a development library for creating web applications to visualize industrial data.
SiteWiseやTwinMakerをデータソースとして、独自ダッシュボードを作成するためのReactコンポーネントが提供されています。
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の画面
前準備
以下の記事を参考に、React 17プロジェクトを作成し、Amplifyの認証機能を実装した状態まで持っていってください。
- 2022年12月になってもcreate-react-appでReact 17を新規作成したい
- Amplifyで認証とPubSubを実装する(React)のReactアプリに認証機能を追加の章まで
AWSクラウド側の設定
SiteWiseのデモデータを生成する
マネジメントコンソールのSiteWise管理画面にある「SiteWise デモ」を使用し、サンプルデータを生成します。デモを作成
ボタンを押すと、自動でSiteWiseのアセットの作成やデータの登録が可能です。
自動で作成されたアセット
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のアセットを選択する部分です。
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
折れ線グラフです。
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
ステータスのタイムラインです。
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にも対応しているので、色々活用できそうです。
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;