- 投稿日:2020-08-24T22:51:45+09:00
Asciidoctor.jsでプレビューしながら編集する
AsciiDoc の処理系といえば、Ruby の Asciidoctor1が有名です。しかし、JavaScript な Asciidoctor.js2もあります。本記事は後者を使ってみた記録です。
関連
- https://qiita.com/Yamane@github/items/090973185791c174532f : ブラウザで生成結果を見つつ、AsciiDoc 文書を編集できます。反映までの時間が短かく、快適に編集できます。
- https://qiita.com/dbgso/items/927c4e3d0f739855f8d6 : こちらは Ruby 版の Asciidoctor を使っています。Docker で手軽に使い始められるところが良いですね。
バージョン
$ asciidoctor -v Asciidoctor 2.0.10 [https://asciidoctor.org] Runtime Environment (ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin19]) (lc:UTF-8 fs:UTF-8 in:UTF-8 ex:UTF-8) $ npm -v 6.14.8きっかけ
Ruby 版の Asciidoctor には監視機能がありません。つまり、AsciiDoc ファイルに変更があった際、自動的に変換処理が行われるようにするオプションを、
asciidoctor
コマンドに指定することはできません。$ asciidoctor --watch main.adoc # こうできたらいいなぁ……監視機能を進めると、プレビュー機能になります。ここでのプレビュー機能とは、生成結果が変更されたタイミングでその表示に反映されることです。HTML ファイルの場合、ブラウザで再読込する手間が省けます。
今回は HTML ファイルを生成し、プレビューすることを考えます。
Ruby 版 Asciidoctor で
Editing AsciiDoc with Live Preview3に紹介されていますが、Firefox 79.0 (64bit) では拡張機能が使えなくなっているようです。ただ、
guard
という gem4 は AsciiDoc から HTML への変換過程で使えそう。
asciidoctor
,guard
,guard-shell
をインストールします。$ bundle init $ bundle add asciidoctor guard guard-shell
main.adoc
という AsciiDoc ファイルを監視します。Guardfilerequire 'asciidoctor' guard :shell do watch('main.adoc') { |m| Asciidoctor.convert_file m[0] } endGuard を起動します。
$ guardAsciiDoc ファイルに変更があるたびに、HTML ファイルが更新されるようになりました。
live-server
登場npm に
live-server
5というパッケージがあり、これでプレビューを実現できます。使い方は簡単。$ live-server
guard
周りでもできそうですが、これが明快かと思います。Asciidoctor.js で
live-server
で視点が変わりました。監視はnpm-watch
6に任せましょう。$ npm i -D asciidoctor live-server npm-watch
package.json
に npm scripts7 を書きます。package.json{ "watch": { "convert": "main.adoc" }, "scripts": { "start": "live-server", "watch": "npm-watch", "convert": "asciidoctor main.adoc" }, "devDependencies": { "asciidoctor": "^2.2.0", "live-server": "^1.2.1", "npm-watch": "^0.7.0" } }注意:
$ npm run convert
で起動される Asciidoctor は Asciidoctor.js の方です。実際、$ npx asciidoctor --help
で Asciidoctor.js のヘルプが表示されます。AsciiDoc ファイルも準備しましょう。
main.adoc= Hello * ワン * ツー * スリー用意するファイルはこれだけ:
$ ls -1 main.adoc package.json監視、プレビュー……
$ npm run watch # terminal 1 $ npm run start # terminal 2 $ vim main.adoc # terminal 3そして編集……
main.adoc= Hello _Happy AsciiDocing!_
- 投稿日:2020-08-24T22:05:20+09:00
Expressとpassportで簡単にOpenID ConnectのRPを作成してみた!
目的と前提
認証/認可について少しづつですが備忘録としてまとめようと思います。
今回は、Nodejsを使ったRPの作成[1]です。
OpenID Connectのアクセストークン取得まで実装しています。
(UserInfoを取得するところは実装していません)IdPの作成にはオープンソースソフトのOpenAM[2]を使用しています。
認証/認可、基礎的なOpenID Connectの知識があることを前提としています。
環境
macOS Catalina v10.15.5
OpenAM 14.5.1 Build d8b8db3cac (2020-March-11 23:25)
node v13.13.0利用モジュール
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1",
"passport-openidconnect": "0.0.2"
}OpenAMの起動と初期設定
IdPはDockerで提供されているOpenAMを利用して作成します。
OpenAMのイメージはDockerHubよりゲットできます。$ docker pull openidentityplatform/openam $ docker run -h openam-01.domain.com -p 8080:8080 --name openam-01 openidentityplatform/openamこれでOpenAMが起動したはずです。
念のため、起動しているか確認してみます。
下記のような表示が出れば、問題なく起動できています。$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 91d60b3e3538 openidentityplatform/openam "/usr/local/tomcat/b…" 2 hours ago Up 2 hours 0.0.0.0:8080->8080/tcp openam-01それではOpenAMにアクセスしてみましょう。
http://localhost:8080/openam初回起動では設定事項がいろいろあるので、私は、OpenAMコンソーシアムの資料を参考に設定しました。
※設定オプションは、カスタム設定ではなく、デフォルト設定を選択しました。
https://www.openam.jp/wp-content/uploads/techtips_vol1.pdfIdPの作成
まずOpenAMにamAdminでログインします。
その後、Top Level Realm(トップレベルレルム)にアクセス後、Configure OAuth Providerを選択します。Configure OpenID Connectを選択します。
認可コードやアクセストークンの有効期限をチェックして、問題なければ作成を押します。
リフレッシュトークンを発行させたい場合は、リフレッシュトークンの発行にチェックを入れてください。
これでIdPが作成できました〜
下記URLにアクセスしてIdPができていることを確認します。
http://localhost:8080/openam/oauth2/.well-known/openid-configuration{"response_types_supported":["code token id_token","code","code id_token","id_token","code token","token","token id_token"],"claims_parameter_supported":false,"end_session_endpoint":"http://localhost:8080/openam/oauth2/connect/endSession","version":"3.0","check_session_iframe":"http://localhost:8080/openam/oauth2/connect/checkSession","scopes_supported":["address","phone","openid","profile","email"],"issuer":"http://localhost:8080/openam/oauth2","id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"acr_values_supported":[],"authorization_endpoint":"http://localhost:8080/openam/oauth2/authorize","userinfo_endpoint":"http://localhost:8080/openam/oauth2/userinfo","device_authorization_endpoint":"http://localhost:8080/openam/oauth2/device/code","claims_supported":["zoneinfo","address","profile","name","phone_number","given_name","locale","family_name","email"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","A128KW","RSA1_5","A256KW","dir","A192KW"],"jwks_uri":"http://localhost:8080/openam/oauth2/connect/jwk_uri","subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES384","HS256","HS512","ES256","RS256","HS384","ES512"],"registration_endpoint":"http://localhost:8080/openam/oauth2/connect/register","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"token_endpoint":"http://localhost:8080/openam/oauth2/access_token"}こんな風に表示されればOK!
RPの作成
初期設定
express公式サイトのGetting startedに従って、まずサンプルのWebアプリケーションを作成します。
$ mkdir myapp $ npx express-generatorこんな感じのディレクトリ構成になっているはず
$ ls app.js node_modules package.json routes bin package-lock.json public views実装
実装はForgeRockのOpenIDConnectのサンプルRP[3]を参考にしながら作成していきます。(非常に分かり易かったのでオススメ!)
SSO連携というリンクをクリックすると、
OpenIDConnectのフローが開始されるようにしていきます。
見た目はこんな感じ
viewsにリンクのボタンを加えます。
views/index.jadeextends layout block content h1= title p Welcome to #{title} hoge-button a(href="http://localhost:3000/auth/openidconnect", target="_blank") SSO連携routes/index.jsvar express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function (req, res, next) { res.render("index", { title: "Express" }); }); module.exports = router;コントローラー(app.js)にロジックを直接追加しちゃいます。
気になる方は分けていただいても問題ないです。app.js// 参考:https://github.com/ForgeRock/exampleOAuth2Clients/tree/master/node-passport-openidconnect // 各モジュールをインポート var createError = require("http-errors"); var express = require("express"); var path = require("path"); // sessionを使うのに求められる var cookieParser = require("cookie-parser"); var logger = require("morgan"); // pathを定義 // indexにログインボタンを設置 // ログイン失敗時 → loginfail // ログイン成功時 → login // に遷移するようにする var indexRouter = require("./routes/index"); var loginFRouter = require("./routes/loginfail"); var loginRouter = require("./routes/login"); var app = express(); //session有効 var session = require("express-session"); app.use( session({ //クッキー改ざん検証用ID secret: "YOUR_PASSWORD", //未初期化のセッションを保存するか saveUninitialized: false, //他にもsessionの寿命とか、httpsならsecureも設定できる }) ); // view engine setup app.set("views", path.join(__dirname, "views")); app.set("view engine", "jade"); app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); app.use("/", indexRouter); //追記ここから app.use("/loginfail", loginFRouter); app.use("/login", loginRouter); //認証セクション var passport = require("passport"); const { token } = require("morgan"); var OpenidConnectStrategy = require("passport-openidconnect").Strategy; app.use(passport.initialize()); app.use(passport.session()); passport.use( new OpenidConnectStrategy( { issuer: "http://localhost:8080/openam/oauth2", authorizationURL: "http://localhost:8080/openam/oauth2/authorize", tokenURL: "http://localhost:8080/openam/oauth2/access_token", userInfoURL: "http://localhost:8080/openam/oauth2/userinfo", clientID: "sampleRP", clientSecret: "RP_PASSWORD", callbackURL: "http://localhost:3000/oauth2callback", scope: ["openid", "email", "profile"], }, function ( issuer, sub, profile, jwtClaims, accessToken, refreshToken, tokenResponse, done ) { //認証成功したらこの関数が実行される //ここでID tokenの検証を行う console.log("issuer: ", issuer); console.log("sub: ", sub); console.log("profile: ", profile); console.log("jwtClaims: ", jwtClaims); console.log("accessToken: ", accessToken); console.log("refreshToken: ", refreshToken); console.log("tokenResponse: ", tokenResponse); return done(null, { profile: profile, accessToken: { token: accessToken, scope: tokenResponse.scope, token_type: tokenResponse.token_type, expires_in: tokenResponse.expires_in, }, idToken: { token: tokenResponse.id_token, claims: jwtClaims, }, }); } ) ); passport.serializeUser(function (user, done) { //userにはprofileが入る done(null, user); }); passport.deserializeUser(function (obj, done) { done(null, obj); }); app.get("/auth/openidconnect", passport.authenticate("openidconnect")); app.get( "/oauth2callback", passport.authenticate("openidconnect", { failureRedirect: "/loginfail", }), function (req, res) { // Successful authentication, redirect home. console.log("認可コード:" + req.query.code); req.session.user = req.session.passport.user.displayName; res.redirect("/login"); } ); //ここまで // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page res.status(err.status || 500); res.render("error"); }); module.exports = app;login成功後は、/loginというページに遷移させる予定なので、
views/login.jade
routes/login.js
をそれぞれ追加します。views/login.jadeextends layout block content h1= title p Welcome to #{title} p login成功!routes/login.jsvar express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function (req, res, next) { res.render("login", { title: "ログイン" }); }); module.exports = router;login失敗時のページも作っておきます。
views/loginfail.jade
routes/loginfail.js
views/loginfail.jadeextends layout block loginfail block content h1= title p Welcome to #{title} p Login失敗routes/loginfail.jsvar express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('loginfail', { title: 'ログインできなかったよ' }); }); module.exports = router;これでRPの作成は完了です。
最終的にはこんな感じのディレクトリ構成になりました。$ ls app.js node_modules package.json routes bin package-lock.json public views $ ls views error.jade index.jade layout.jade login.jade loginfail.jade $ ls routes index.js login.js loginfail.jsRPの登録
OpenAMにRPを登録します。
OpenAMにAdminでログイン後、Top Level Realm(トップレベルレルム)のApplications>OAuth2.0を選択します。
エージェントの名前とパスワードの入力を求められるので、
今回は下記のように入力し、作成を押します。名前:sampleRP
パスワード:passwordこのパスワードは、先ほど作成したapp.js内の
RP_PASSWORD
にあたります。
作成を押した後、メインページに戻るので、エージェントから、先ほど作成したエージェントを選択し、設定を追加していきます。
項目 設定内容 リダイレクトURI http://localhost:3000/oauth2callback スコープ openid, email, profole Token Endpoint Authentication Method client_secret_post そのほかの設定は、デフォルトのまま。
設定追加後、保存を押して登録完了です。
動作確認
早速RPを動かしてみます。
$ npm startRPにアクセス!
http://localhost:3000/SSO連携のリンクを押してみるとOpenAMのログイン画面に遷移します。
初期設定で作成したアカウントのID/PWを入れてログイン!
アカウントを作成した記憶がない方はデフォルトの下記アカウントでもログインできるはずです。
ID: demo
password: changeit
ログイン後、個人情報提供の同意画面に遷移するのでAllowを選択します。ターミナルにこんな感じに出力されていれば認証成功です。
これでアクセストークン、IDトークンが取得できているはずなので、
この後、ユーザーの情報を取得したい場合は、OpenAMのユーザー情報エンドポイントにアクセストークンを GETで渡せば大丈夫なはずです。
参考:
https://backstage.forgerock.com/docs/am/5/oauth2-guide/#oauth2-byo-client以上になります。
お疲れ様でした!
参考
[1] 株式会社オージス総研 テミストラクトソリューション部 氏縄 武尊."第三回 Relying Party の実装例 ~passport~".オブジェクトの広場.2016-03-10,(参照2020-08-24)
[2] Open Source Solution Technology Corporation.学認Shibboleth ShibbolethとOpenAMを連携させて学外と学内をシングルサインオン.2011
[3] ForgeRock."exampleOAuth2Clients/node-passport-openidconnect".Github.2020-3-25,(参照2020-08-24)
- 投稿日:2020-08-24T22:05:20+09:00
Expressとpassportで簡単にOpenID ConnectのRPを作成してみた
目的と前提
認証/認可について少しづつですが備忘録としてまとめようと思います。
今回は、Nodejsを使ったRPの作成[1]です。
OpenID Connectのアクセストークン取得まで実装しています。
(UserInfoを取得するところは実装していません)IdPの作成にはオープンソースソフトのOpenAM[2]を使用しています。
認証/認可、基礎的なOpenID Connectの知識があることを前提としています。
環境
macOS Catalina v10.15.5
OpenAM 14.5.1 Build d8b8db3cac (2020-March-11 23:25)
node v13.13.0利用モジュール
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1",
"passport-openidconnect": "0.0.2"
}OpenAMの起動と初期設定
IdPはDockerで提供されているOpenAMを利用して作成します。
OpenAMのイメージはDockerHubよりゲットできます。$ docker pull openidentityplatform/openam $ docker run -h openam-01.domain.com -p 8080:8080 --name openam-01 openidentityplatform/openamこれでOpenAMが起動したはずです。
念のため、起動しているか確認してみます。
下記のような表示が出れば、問題なく起動できています。$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 91d60b3e3538 openidentityplatform/openam "/usr/local/tomcat/b…" 2 hours ago Up 2 hours 0.0.0.0:8080->8080/tcp openam-01それではOpenAMにアクセスしてみましょう。
http://localhost:8080/openam初回起動では設定事項がいろいろあるので、私は、OpenAMコンソーシアムの資料を参考に設定しました。
※設定オプションは、カスタム設定ではなく、デフォルト設定を選択しました。
https://www.openam.jp/wp-content/uploads/techtips_vol1.pdfIdPの作成
まずOpenAMにamAdminでログインします。
その後、Top Level Realm(トップレベルレルム)にアクセス後、Configure OAuth Providerを選択します。Configure OpenID Connectを選択します。
認可コードやアクセストークンの有効期限をチェックして、問題なければ作成を押します。
リフレッシュトークンを発行させたい場合は、リフレッシュトークンの発行にチェックを入れてください。
これでIdPが作成できました〜
下記URLにアクセスしてIdPができていることを確認します。
http://localhost:8080/openam/oauth2/.well-known/openid-configuration{"response_types_supported":["code token id_token","code","code id_token","id_token","code token","token","token id_token"],"claims_parameter_supported":false,"end_session_endpoint":"http://localhost:8080/openam/oauth2/connect/endSession","version":"3.0","check_session_iframe":"http://localhost:8080/openam/oauth2/connect/checkSession","scopes_supported":["address","phone","openid","profile","email"],"issuer":"http://localhost:8080/openam/oauth2","id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"acr_values_supported":[],"authorization_endpoint":"http://localhost:8080/openam/oauth2/authorize","userinfo_endpoint":"http://localhost:8080/openam/oauth2/userinfo","device_authorization_endpoint":"http://localhost:8080/openam/oauth2/device/code","claims_supported":["zoneinfo","address","profile","name","phone_number","given_name","locale","family_name","email"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","A128KW","RSA1_5","A256KW","dir","A192KW"],"jwks_uri":"http://localhost:8080/openam/oauth2/connect/jwk_uri","subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES384","HS256","HS512","ES256","RS256","HS384","ES512"],"registration_endpoint":"http://localhost:8080/openam/oauth2/connect/register","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"token_endpoint":"http://localhost:8080/openam/oauth2/access_token"}こんな風に表示されればOK!
RPの作成
初期設定
express公式サイトのGetting startedに従って、まずサンプルのWebアプリケーションを作成します。
$ mkdir myapp $ npx express-generatorこんな感じのディレクトリ構成になっているはず
$ ls app.js node_modules package.json routes bin package-lock.json public views実装
実装はForgeRockのOpenIDConnectのサンプルRP[3]を参考にしながら作成していきます。(非常に分かり易かったのでオススメ!)
SSO連携というリンクをクリックすると、
OpenIDConnectのフローが開始されるようにしていきます。
見た目はこんな感じ
viewsにリンクのボタンを加えます。
views/index.jadeextends layout block content h1= title p Welcome to #{title} hoge-button a(href="http://localhost:3000/auth/openidconnect", target="_blank") SSO連携routes/index.jsvar express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function (req, res, next) { res.render("index", { title: "Express" }); }); module.exports = router;コントローラー(app.js)にロジックを直接追加しちゃいます。
気になる方は分けていただいても問題ないです。app.js// 参考:https://github.com/ForgeRock/exampleOAuth2Clients/tree/master/node-passport-openidconnect // 各モジュールをインポート var createError = require("http-errors"); var express = require("express"); var path = require("path"); // sessionを使うのに求められる var cookieParser = require("cookie-parser"); var logger = require("morgan"); // pathを定義 // indexにログインボタンを設置 // ログイン失敗時 → loginfail // ログイン成功時 → login // に遷移するようにする var indexRouter = require("./routes/index"); var loginFRouter = require("./routes/loginfail"); var loginRouter = require("./routes/login"); var app = express(); //session有効 var session = require("express-session"); app.use( session({ //クッキー改ざん検証用ID secret: "YOUR_PASSWORD", //未初期化のセッションを保存するか saveUninitialized: false, //他にもsessionの寿命とか、httpsならsecureも設定できる }) ); // view engine setup app.set("views", path.join(__dirname, "views")); app.set("view engine", "jade"); app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); app.use("/", indexRouter); //追記ここから app.use("/loginfail", loginFRouter); app.use("/login", loginRouter); //認証セクション var passport = require("passport"); const { token } = require("morgan"); var OpenidConnectStrategy = require("passport-openidconnect").Strategy; app.use(passport.initialize()); app.use(passport.session()); passport.use( new OpenidConnectStrategy( { issuer: "http://localhost:8080/openam/oauth2", authorizationURL: "http://localhost:8080/openam/oauth2/authorize", tokenURL: "http://localhost:8080/openam/oauth2/access_token", userInfoURL: "http://localhost:8080/openam/oauth2/userinfo", clientID: "sampleRP", clientSecret: "RP_PASSWORD", callbackURL: "http://localhost:3000/oauth2callback", scope: ["openid", "email", "profile"], }, function ( issuer, sub, profile, jwtClaims, accessToken, refreshToken, tokenResponse, done ) { //認証成功したらこの関数が実行される //ここでID tokenの検証を行う console.log("issuer: ", issuer); console.log("sub: ", sub); console.log("profile: ", profile); console.log("jwtClaims: ", jwtClaims); console.log("accessToken: ", accessToken); console.log("refreshToken: ", refreshToken); console.log("tokenResponse: ", tokenResponse); return done(null, { profile: profile, accessToken: { token: accessToken, scope: tokenResponse.scope, token_type: tokenResponse.token_type, expires_in: tokenResponse.expires_in, }, idToken: { token: tokenResponse.id_token, claims: jwtClaims, }, }); } ) ); passport.serializeUser(function (user, done) { //userにはprofileが入る done(null, user); }); passport.deserializeUser(function (obj, done) { done(null, obj); }); app.get("/auth/openidconnect", passport.authenticate("openidconnect")); app.get( "/oauth2callback", passport.authenticate("openidconnect", { failureRedirect: "/loginfail", }), function (req, res) { // Successful authentication, redirect home. console.log("認可コード:" + req.query.code); req.session.user = req.session.passport.user.displayName; res.redirect("/login"); } ); //ここまで // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page res.status(err.status || 500); res.render("error"); }); module.exports = app;login成功後は、/loginというページに遷移させる予定なので、
views/login.jade
routes/login.js
をそれぞれ追加します。views/login.jadeextends layout block content h1= title p Welcome to #{title} p login成功!routes/login.jsvar express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function (req, res, next) { res.render("login", { title: "ログイン" }); }); module.exports = router;login失敗時のページも作っておきます。
views/loginfail.jade
routes/loginfail.js
views/loginfail.jadeextends layout block loginfail block content h1= title p Welcome to #{title} p Login失敗routes/loginfail.jsvar express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('loginfail', { title: 'ログインできなかったよ' }); }); module.exports = router;これでRPの作成は完了です。
最終的にはこんな感じのディレクトリ構成になりました。$ ls app.js node_modules package.json routes bin package-lock.json public views $ ls views error.jade index.jade layout.jade login.jade loginfail.jade $ ls routes index.js login.js loginfail.jsRPの登録
OpenAMにRPを登録します。
OpenAMにAdminでログイン後、Top Level Realm(トップレベルレルム)のApplications>OAuth2.0を選択します。
エージェントの名前とパスワードの入力を求められるので、
今回は下記のように入力し、作成を押します。名前:sampleRP
パスワード:passwordこのパスワードは、先ほど作成したapp.js内の
RP_PASSWORD
にあたります。
作成を押した後、メインページに戻るので、エージェントから、先ほど作成したエージェントを選択し、設定を追加していきます。
項目 設定内容 リダイレクトURI http://localhost:3000/oauth2callback スコープ openid, email, profole Token Endpoint Authentication Method client_secret_post そのほかの設定は、デフォルトのまま。
設定追加後、保存を押して登録完了です。
動作確認
早速RPを動かしてみます。
$ npm startRPにアクセス!
http://localhost:3000/SSO連携のリンクを押してみるとOpenAMのログイン画面に遷移します。
初期設定で作成したアカウントのID/PWを入れてログイン!
アカウントを作成した記憶がない方はデフォルトの下記アカウントでもログインできるはずです。
ID: demo
password: changeit
ログイン後、個人情報提供の同意画面に遷移するのでAllowを選択します。ターミナルにこんな感じに出力されていれば認証成功です。
これでアクセストークン、IDトークンが取得できているはずなので、
この後、ユーザーの情報を取得したい場合は、OpenAMのユーザー情報エンドポイントにアクセストークンを GETで渡せば大丈夫なはずです。
参考:
https://backstage.forgerock.com/docs/am/5/oauth2-guide/#oauth2-byo-client以上になります。
お疲れ様でした!
参考
[1] 株式会社オージス総研 テミストラクトソリューション部 氏縄 武尊."第三回 Relying Party の実装例 ~passport~".オブジェクトの広場.2016-03-10,(参照2020-08-24)
[2] Open Source Solution Technology Corporation.学認Shibboleth ShibbolethとOpenAMを連携させて学外と学内をシングルサインオン.2011
[3] ForgeRock."exampleOAuth2Clients/node-passport-openidconnect".Github.2020-3-25,(参照2020-08-24)
- 投稿日:2020-08-24T22:05:20+09:00
Express+Passportで簡単にOpenID ConnectのRPを作成してみた
目的と前提
認証/認可について少しづつですが備忘録としてまとめようシリーズ2つめです。
前回はSAML2.0の仕様についてまとめてみました。
https://qiita.com/yuna-s/items/8aa318ca5426c3d9c7e6今回は、Nodejsを使ったRPの作成[1]です。
OpenID Connectのアクセストークン取得まで実装しています。
(UserInfoを取得するところは実装していません)IdPの作成にはオープンソースソフトのOpenAM[2]を使用しています。
認証/認可、基礎的なOpenID Connectの知識があることを前提としています。
環境
macOS Catalina v10.15.5
OpenAM 14.5.1 Build d8b8db3cac (2020-March-11 23:25)
node v13.13.0利用モジュール
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1",
"passport-openidconnect": "0.0.2"
}OpenAMの起動と初期設定
IdPはDockerで提供されているOpenAMを利用して作成します。
OpenAMのイメージはDockerHubよりゲットできます。$ docker pull openidentityplatform/openam $ docker run -h openam-01.domain.com -p 8080:8080 --name openam-01 openidentityplatform/openamこれでOpenAMが起動したはずです。
念のため、起動しているか確認してみます。
下記のような表示が出れば、問題なく起動できています。$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 91d60b3e3538 openidentityplatform/openam "/usr/local/tomcat/b…" 2 hours ago Up 2 hours 0.0.0.0:8080->8080/tcp openam-01それではOpenAMにアクセスしてみましょう。
http://localhost:8080/openam初回起動では設定事項がいろいろあるので、私は、OpenAMコンソーシアムの資料を参考に設定しました。
※設定オプションは、カスタム設定ではなく、デフォルト設定を選択しました。
https://www.openam.jp/wp-content/uploads/techtips_vol1.pdfIdPの作成
まずOpenAMにamAdminでログインします。
その後、Top Level Realm(トップレベルレルム)にアクセス後、Configure OAuth Providerを選択します。Configure OpenID Connectを選択します。
認可コードやアクセストークンの有効期限をチェックして、問題なければ作成を押します。
リフレッシュトークンを発行させたい場合は、リフレッシュトークンの発行にチェックを入れてください。
これでIdPが作成できました〜
下記URLにアクセスしてIdPができていることを確認します。
http://localhost:8080/openam/oauth2/.well-known/openid-configuration{"response_types_supported":["code token id_token","code","code id_token","id_token","code token","token","token id_token"],"claims_parameter_supported":false,"end_session_endpoint":"http://localhost:8080/openam/oauth2/connect/endSession","version":"3.0","check_session_iframe":"http://localhost:8080/openam/oauth2/connect/checkSession","scopes_supported":["address","phone","openid","profile","email"],"issuer":"http://localhost:8080/openam/oauth2","id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"acr_values_supported":[],"authorization_endpoint":"http://localhost:8080/openam/oauth2/authorize","userinfo_endpoint":"http://localhost:8080/openam/oauth2/userinfo","device_authorization_endpoint":"http://localhost:8080/openam/oauth2/device/code","claims_supported":["zoneinfo","address","profile","name","phone_number","given_name","locale","family_name","email"],"id_token_encryption_alg_values_supported":["RSA-OAEP","RSA-OAEP-256","A128KW","RSA1_5","A256KW","dir","A192KW"],"jwks_uri":"http://localhost:8080/openam/oauth2/connect/jwk_uri","subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES384","HS256","HS512","ES256","RS256","HS384","ES512"],"registration_endpoint":"http://localhost:8080/openam/oauth2/connect/register","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"token_endpoint":"http://localhost:8080/openam/oauth2/access_token"}こんな風に表示されればOK!
RPの作成
初期設定
express公式サイトのGetting startedに従って、まずサンプルのWebアプリケーションを作成します。
$ mkdir myapp $ npx express-generatorこんな感じのディレクトリ構成になっているはず
$ ls app.js node_modules package.json routes bin package-lock.json public views実装
実装はForgeRockのOpenIDConnectのサンプルRP[3]を参考にしながら作成していきます。(非常に分かり易かったのでオススメ!)
SSO連携というリンクをクリックすると、
OpenIDConnectのフローが開始されるようにしていきます。
見た目はこんな感じ
viewsにリンクのボタンを加えます。
views/index.jadeextends layout block content h1= title p Welcome to #{title} hoge-button a(href="http://localhost:3000/auth/openidconnect", target="_blank") SSO連携routes/index.jsvar express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function (req, res, next) { res.render("index", { title: "Express" }); }); module.exports = router;コントローラー(app.js)にロジックを直接追加しちゃいます。
気になる方は分けていただいても問題ないです。app.js// 参考:https://github.com/ForgeRock/exampleOAuth2Clients/tree/master/node-passport-openidconnect // 各モジュールをインポート var createError = require("http-errors"); var express = require("express"); var path = require("path"); // sessionを使うのに求められる var cookieParser = require("cookie-parser"); var logger = require("morgan"); // pathを定義 // indexにログインボタンを設置 // ログイン失敗時 → loginfail // ログイン成功時 → login // に遷移するようにする var indexRouter = require("./routes/index"); var loginFRouter = require("./routes/loginfail"); var loginRouter = require("./routes/login"); var app = express(); //session有効 var session = require("express-session"); app.use( session({ //クッキー改ざん検証用ID secret: "YOUR_PASSWORD", //未初期化のセッションを保存するか saveUninitialized: false, //他にもsessionの寿命とか、httpsならsecureも設定できる }) ); // view engine setup app.set("views", path.join(__dirname, "views")); app.set("view engine", "jade"); app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); app.use("/", indexRouter); //追記ここから app.use("/loginfail", loginFRouter); app.use("/login", loginRouter); //認証セクション var passport = require("passport"); const { token } = require("morgan"); var OpenidConnectStrategy = require("passport-openidconnect").Strategy; app.use(passport.initialize()); app.use(passport.session()); passport.use( new OpenidConnectStrategy( { issuer: "http://localhost:8080/openam/oauth2", authorizationURL: "http://localhost:8080/openam/oauth2/authorize", tokenURL: "http://localhost:8080/openam/oauth2/access_token", userInfoURL: "http://localhost:8080/openam/oauth2/userinfo", clientID: "sampleRP", clientSecret: "RP_PASSWORD", callbackURL: "http://localhost:3000/oauth2callback", scope: ["openid", "email", "profile"], }, function ( issuer, sub, profile, jwtClaims, accessToken, refreshToken, tokenResponse, done ) { //認証成功したらこの関数が実行される //ここでID tokenの検証を行う console.log("issuer: ", issuer); console.log("sub: ", sub); console.log("profile: ", profile); console.log("jwtClaims: ", jwtClaims); console.log("accessToken: ", accessToken); console.log("refreshToken: ", refreshToken); console.log("tokenResponse: ", tokenResponse); return done(null, { profile: profile, accessToken: { token: accessToken, scope: tokenResponse.scope, token_type: tokenResponse.token_type, expires_in: tokenResponse.expires_in, }, idToken: { token: tokenResponse.id_token, claims: jwtClaims, }, }); } ) ); passport.serializeUser(function (user, done) { //userにはprofileが入る done(null, user); }); passport.deserializeUser(function (obj, done) { done(null, obj); }); app.get("/auth/openidconnect", passport.authenticate("openidconnect")); app.get( "/oauth2callback", passport.authenticate("openidconnect", { failureRedirect: "/loginfail", }), function (req, res) { // Successful authentication, redirect home. console.log("認可コード:" + req.query.code); req.session.user = req.session.passport.user.displayName; res.redirect("/login"); } ); //ここまで // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); // error handler app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; // render the error page res.status(err.status || 500); res.render("error"); }); module.exports = app;login成功後は、/loginというページに遷移させる予定なので、
views/login.jade
routes/login.js
をそれぞれ追加します。views/login.jadeextends layout block content h1= title p Welcome to #{title} p login成功!routes/login.jsvar express = require("express"); var router = express.Router(); /* GET home page. */ router.get("/", function (req, res, next) { res.render("login", { title: "ログイン" }); }); module.exports = router;login失敗時のページも作っておきます。
views/loginfail.jade
routes/loginfail.js
views/loginfail.jadeextends layout block loginfail block content h1= title p Welcome to #{title} p Login失敗routes/loginfail.jsvar express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('loginfail', { title: 'ログインできなかったよ' }); }); module.exports = router;これでRPの作成は完了です。
最終的にはこんな感じのディレクトリ構成になりました。$ ls app.js node_modules package.json routes bin package-lock.json public views $ ls views error.jade index.jade layout.jade login.jade loginfail.jade $ ls routes index.js login.js loginfail.jsRPの登録
OpenAMにRPを登録します。
OpenAMにAdminでログイン後、Top Level Realm(トップレベルレルム)のApplications>OAuth2.0を選択します。
エージェントの名前とパスワードの入力を求められるので、
今回は下記のように入力し、作成を押します。名前:sampleRP
パスワード:passwordこのパスワードは、先ほど作成したapp.js内の
RP_PASSWORD
にあたります。
作成を押した後、メインページに戻るので、エージェントから、先ほど作成したエージェントを選択し、設定を追加していきます。
項目 設定内容 リダイレクトURI http://localhost:3000/oauth2callback スコープ openid, email, profole Token Endpoint Authentication Method client_secret_post そのほかの設定は、デフォルトのまま。
設定追加後、保存を押して登録完了です。
動作確認
app.js内の
RP_PASSWORD
をpasswordに、YOUR_PASSWORD
を好きな文字列に変更して、早速RPを動かしてみます。$ npm startRPにアクセス!
http://localhost:3000/SSO連携のリンクを押してみるとOpenAMのログイン画面に遷移します。
初期設定で作成したアカウントのID/PWを入れてログイン!
アカウントを作成した記憶がない方はデフォルトの下記アカウントでもログインできるはずです。
ID: demo
password: changeit
ログイン後、個人情報提供の同意画面に遷移するのでAllowを選択します。ターミナルにこんな感じに出力されていれば認証成功です。
これでアクセストークン、IDトークンが取得できているはずなので、
この後、ユーザーの情報を取得したい場合は、OpenAMのユーザー情報エンドポイントにアクセストークンを GETで渡せば大丈夫なはずです。
参考:
https://backstage.forgerock.com/docs/am/5/oauth2-guide/#oauth2-byo-client以上になります。
お疲れ様でした!
参考文献
[1] 株式会社オージス総研 テミストラクトソリューション部 氏縄 武尊."第三回 Relying Party の実装例 ~passport~".オブジェクトの広場.2016-03-10,(参照2020-08-24)
[2] Open Source Solution Technology Corporation.学認Shibboleth ShibbolethとOpenAMを連携させて学外と学内をシングルサインオン.2011
[3] ForgeRock."exampleOAuth2Clients/node-passport-openidconnect".Github.2020-3-25,(参照2020-08-24)
- 投稿日:2020-08-24T19:42:20+09:00
esbuildがwebpackより187倍早いらしいので環境構築しよう
はじめに
久しぶりの投稿になります。
今回は以下の記事で、esbuidがすごい!!という話を聞きつけこの記事を書くことにしました。
参考: [Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れないどのくらいすごいのでしょうか?
参考に挙げている記事によるとesbuild は Go 言語で書かれた JavaScript および TypeScript のビルドツールです。 esbuild 単体でトランスパイル + バンドル + ミニファイできます。 JSX / TSX もサポートされています。そしてめっちゃくちゃ速いという触れ込みです。最初から速度を意識して無駄がないように書かれており、構文解析・出力・ソースマップ生成は並列化され、ネイティブコードで動作します。公式の README では three.js のビルドが Rollup + terser より 100 倍速い と謳っています。
https://www.kabuku.co.jp/developers/ultrafast-tsx-build-tool-esbuildとのことです。
なるほどなるほどと。最近わたしもWebpack
のBuild遅いなあと思っていたのでこの情報を鵜呑みにして、esbuild
の環境構築をしたくなりました。また、ドキュメントによるとesbuildはwebpackの187倍もBuildが早いみたいです。
esbuildの環境構築
環境構築についてですが、だいたいのモジュールバンドラにはコンフィグファイルがつきものです。esbuildのgithubを参考にしましたが、ワンライナーのCL上での実行例のみでコンフィグファイルらしいものの書き方見当たりません。
少し詳細に調べたところ、
https://github.com/evanw/esbuild/issues/39
以下に書いてありました。
ふむふむ、一応、対応はしているみたいです。前準備
今回は、
React+TypeScript
で記述されたプロジェクトを対象にBuildを行う環境構築をします。Vueについては、vite
というモジュールバンドラがesbuild
を利用して、Buildを行っているため、そちらを利用してくださいとのことです。まずは以下の通りにnpmモジュールをインストールします。
npm install -D esbuild @types/node
Reactのプロジェクトは後ほど説明する注意点に気をつければなんでも大丈夫です。
わたしのgithubのプロジェクトを例に説明します。
https://github.com/olt556/esbuild-tmpBuildスクリプトの作成
まず最低限、以下の条件を実現したいです。
- developmentとproductionの環境でBuildを分ける
- エントリーポイントの指定
- Build後に出力されるESの規格の指定
- プラットフォームの指定(node, browser)
- production時にはminifyをかける
- 出力先ディレクトリの指定
- tsconfig.jsonの読み込み
以上の条件を元に作成した、esbuildのBuildファイルは以下の通りです。
build.tsconst { argv } = require('process') const { build } = require('esbuild') const path = require('path') // optionsの定義 const options = { // 以下のdefineプロパティを設定しない場合Reactのプロジェクトの実行時にエラーが出ます define: { 'process.env.NODE_ENV': process.env.NODE_ENV }, entryPoints: [path.resolve(__dirname, 'src/Index.tsx')], minify: argv[2] === 'production', bundle: true, target: 'es2016', platform: 'browser', outdir: path.resolve(__dirname, 'dist'), tsconfig: path.resolve(__dirname, 'tsconfig.json') } // Buildの実行 build(options).catch(err => { process.stderr.write(err.stderr) process.exit(1) })上記のファイルを以下のnodeコマンドで実行することでBuild可能です。
node build.ts production/development
npmスクリプトにして実行しやすくするのもいいかもしれませんね。
実装時の注意点(2020/08/24)
現時点でesbuildを利用する際には、以下のような注意点があります。
TypeScript
のトランスパイル時の型チェックに対応していませんcss-modules
に対応していませんplugins
に対応していないので、各種loaderの読み込みや詳細な設定をすることはできませんおわりに
webpack
をesbuild
に置き換えることによって、Reactを導入しているプロジェクトがCSSフレームワーク
や、CSS in JS
を利用している場合、かなりの効果を発揮するかもしれませんね!!もし何か質問やご指摘などありましたらお願いします!
- 投稿日:2020-08-24T14:42:15+09:00
npm install --save firebase で発生したエラーの回避法
記事執筆時点で安定版のfirebase 6.2.4をインストールを試みたところ以下のエラーが出ました。
$ npm install --save firebase@6.2.6 : npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! grpc@1.20.3 install: `node-pre-gyp install --fallback-to-build --library=static_library` npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the grpc@1.20.3 install script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/yokoyamaryouta/.npm/_logs/2020-05-10T05_49_16_325Z-debug.log結論
安定版で実行したらインストールできました。以下から確認&試してみて下さい。
$ node -v v14.8.0 //筆者の場合こちら から確認&インストールできます。
安定版インストール後に再度実行したらいけました。$ node -v v12.18.3 //執筆時 $ npm install --save firebase@6.2.6
- 投稿日:2020-08-24T13:01:29+09:00
JSON.stringifyで`TypeError: Converting circular structure to JSON`というエラーが出た時の対処法
オブジェクトをJSON.stringifyしたときに、循環参照が含まれていると以下のようなエラーが出ます。
コードconst a = { a1: "test" }; a.aa = a; JSON.stringify(a);実行結果JSON.stringify(a); ^ TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' --- property 'aa' closes the circle at JSON.stringify (<anonymous>)対処法
json-cyclicを使えば解決します。
json-cyclic - npm
https://www.npmjs.com/package/json-cyclicnpm install json-cyclic
コードconst { decycle, encycle } = require('json-cyclic'); const a = { a1: "test" }; a.aa = a; console.log(JSON.stringify(decycle(a)));実行結果{"a1":"test","aa":{"$ref":"$"}}json-cyclicは、内部で循環参照を検出したとき、復元ができるようにタグ付けを行い、参照を削除する仕組みです。
オブジェクトを復元したい場合は以下のように
encycle
を使います。コードconst { decycle, encycle } = require('json-cyclic'); const a = {a1: "test"}; a.aa = a; const json = JSON.stringify(decycle(a)); const obj = JSON.parse(json); const b = encycle(obj); console.log(b); console.log(b.aa.aa.aa.a1);実行結果{ a1: 'test', aa: [Circular] } testちゃんと復元できることが確認できました。
- 投稿日:2020-08-24T11:57:49+09:00
初めてLambdaとAPI Gatewayを触ってみる(まずは動かすお勉強)
こんにちは、端くれ駆け出しエンジニアの@Occhiii623です
これから実装予定のサービスで使うことになるであろう、AWSサービスであるLambda(ラムダ)とAPI Gatewayをまず触ってどんなものなのか知ろうとなりました。
そこで、この記事は私自身のアウトプットがいち目的ですが、自分のようなAWS超初心者で
「文字だけ読んでもわからんっ!とりあえず動かしてみたい!」
という方の参考になれば幸いです。
Qiita初投稿なので、お見苦しい点ご容赦くださいちなみに私はAWSのEC2ぐらいしか使ったことのない超初心者です。
そもそもLambdaって何?
Amazon公式ページによると・・・
AWS Lambda はサーバーをプロビジョニングしたり管理する必要なくコードを実行できるコンピューティングサービスです。 AWS Lambda は必要時にのみてコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで自動的にスケーリングします。使用したコンピューティング時間に対してのみお支払いいただきます。
これだけじゃピンとこなかったんですが、要はコードが実行される時だけコンテナを作って、サーバー稼働させるというように、常にサーバーが稼働しているわけではないんですよね。プログラムが実行される時だけ稼働します。
なので、Lambdaを利用するメリットとしては・・・
- AWSのような使う分だけのコストがかかる従量課金の料金体系だと、常にサーバーを稼働させているEC2のインスタンスよりも費用が安くなる。
- サーバーの構築や保守などの管理はAWSがやってくれ、利用者はサーバーの管理が一切不要になる(サーバー管理レス=サーバーレス)ため、メンテナンスを行う人的リソースも抑えられる(人件費もかなりお高いですからね)
とてもわかりやすくLambdaについて解説してくれている動画がありました。
【Schoo】AWS Lambdaを活用したサーバレス実践 -第1回- | 大澤 文孝 先生ここの動画でも解説されているように、デメリットもあって
どれだけアクセスがあるかによって料金が変わってくるので、コスト見積もりがしにくいです。それでもAWSの他サービスと連携できるメリットもあって、利用する恩恵の方が大きいと感じました。
Lambraを動かしてみよう
実際私がやってみた以下手順を追っていきたいと思います。
- 簡単なLambra関数を作ってみる
- Amazon API Gatewayと連携させてブラウザからLambda関数を呼び出してみる
簡単なLambra関数を作ってみる
コンソールから、サービス→Lambdaを選択して開きます。
そうすると、下記のような画面になります。
すでに作ってあるやつがありますが、右上の
関数の作成
を押します。
関数の作成画面になるので、左の一から作成
を選択して下の基本的な情報を入力します。関数名は、今回は「HelloWorldTest」とでもしておきます。
言語をいくつか選択できますが、今回はNode.jsを選択します。
アクセス権限については、既存のものを選択するか新しく作ることもできます。今回はテスト動作だけなので、「基本的な Lambda アクセス権限で新しいロールを作成」で進めます。関数コードというところに、すでに
Hello World
を出力するプログラムが書いてあります。
右上のテストというボタンをクリックして、適当な名前をつけて保存→再度テストのボタンをクリックするとプログラムが無事成功した表示とともに、コードを実行後の結果が出てきました。# 実行結果 { "statusCode": 200, "body": "\"Hello from Lambda!\"" }すでに書いてあるコードをコンソール上で編集することもできるのですが、あえてローカルでコードを作成し、ファイルをzip化して読み込んでみます。
簡単なコードを書きました。
Hello World!
をコンソール上で出すだけのメソッドと、戻り値としてaaaの中身を返すコードを作りました。index.jsexports.handler = async (event) => { // 冒頭のexports~は、JSでいうimportと同じように // Lambraで以下コードを読み込むために必要なLambda関数内のメソッド function hoge() { console.log("Hello World!"); }; const aaa = { test: "testです", name: "Mayumi", age: 31, } hoge(); return aaa; };できたら、zip化します。
$ zip HelloWorldTest(←zipファイル名は適当) index.js読み込めたら、index.jsの中身が変わってアップロードした内容に変わっていると思います。
右上のテストを実行すると、、、
無事動きました^^Amazon API Gatewayと連携させてブラウザからLambda関数を呼び出してみる
最初はHTTP APIで作ったAPIをトリガーにやったのですが、この記事ではREST APIでやってみます。
▼こちらを参考にしました
AWS Lambdaで簡単なREST APIを作ってみたAPIとは???ってところからわからずでしたが・・・^^;
わかりやすく教わったのでまたそれは別記事でまとめたいと思います!HTTP APIとREST APIの違いについては、こちらの記事がわかりやすかったです。
0からREST APIについて調べてみた▼では手順
Lambda画面の+トリガーを追加
からでも設定はできるのですがサービス検索からAPI Gatewayに飛んで新しいAPIを作成します。
右下のAPIの作成
ボタンを押します。
そうすると下のようなページになるので、プルダウンのアクション
から今回は特にデータを登録したり、更新したりするわけではないので、GET
を選択します。
Lambda関数は、先ほど作ったものを選択して保存します。
保存ボタンを押すと、Lambra関数にアクセスする権限を与えますか
といった表示されるので「はい」を押します。
※でないと、権限がなくAPI GatewayからLambra関数にアクセスできません。
このままだとエンドポイントがない状態みたいなので、統合リクエストから細かい設定をします。
(参考記事どおりに行いました)そしたら、最後にプルダウンの
アクション
から、APIのデプロイ
を選択して新しいステージを作成。
URLが発行されたら、アクセスしてみます!
Lambda関数の呼び出しができました!
Lambdaの画面に戻ると、API Gatewayがトリガーとして設定されていることが再確認できます。
これで、基本の「き」な操作ができました!複数のJSファイルをwebpackで1つにしてLambra関数で動かしてみる
次のステップとして「複数のJSファイルをwebpackで1つのファイルにしてLambra関数で動かす」ってこともやりました。
普通のアプリケーションであれば、一つのリポジトリで管理して、起動時に実行環境でコードをまとめて読み込むってことをしますが、lamnda関数を使う場合にはアプリケーションの機能ごとに実行環境が分かれるので、コードごとにリポジトリを分けるような管理になる難しさがあるようです。そこで、このwebpackの出番だ!というわけです。
webpackとは?
JSファイルをまとめる、高機能モジュールバンドラー
です。(知らなかった)
モジュールは、一つひとつのJSファイルのことを指していてそれをバンドル(=まとめる)するのがwebpackです。webpackを使ってファイルを一つにすることでどんないいことがあるのか?
- ブラウザからリクエストが飛んだ時に複数のファイルだと接続する分時間がかかるが、これを一つにすることで読み込み速度が早くなる
- 一つにまとめる時にes6やVue.js, TypeScript等で書かれたJSファイルをトランスパイルしてブラウザで実行可能なJavaScriptファイルへと変換してくれる(トランスパイルは、別言語を共通語にしてくれるようなものです)
なんだかとてもスグレものです。
導入にするには、Node.jsのバージョン13以上が必要です。
(それ12以下であれば、バージョンを更新してください)
私の場合、Ubuntu環境で行っています。▼導入手順
1.まずローカルにディレクトリを作り、そこにJSファイルを2つ作成します。(例) myProjectフォルダ ├─index.js └─service.js二つのファイルの中身を書いてしまいます。
webpackビルド時に読み込むファイルは、このindex.js
にするので別ファイルのservice.js
をimportで読み込むようにします。index.jsimport { aaa } from "./service.js" // service.jsの変数を、ここで変数hogeに代入 const hoge = aaa exports.handler = async (event) => { //Lambda関数内でイベント処理するのに必要なメソッドを書いておきます console.log(hoge); return hoge; };service.jsexport const aaa = { test: "testです", name: "ohnishi", age: 28, }このまま実行しても、
import
とexport
という書き方はNode.js
に対応していないのでうまく動作しません。そこを、webpackでトランスパイルすることで実行できるようになるため、あえてこのままにしておきます。2.できたらフォルダに移動
$ cd /Users/myName/Document/myProject3.
packege.json
ファイルを生成します。
-yをつけているのは、作成時に聞かれる質問をスキップするためです。$ npm init -yそうすると、ディレクトリ内に
packege.json
というファイルができていると思います。package.jsonとは?
package.jsonファイルは、Node.jsベースのJavaScriptアプリ開発において、npmでパッケージ(各種フレームワークやライブラリ)を管理するために使われる構成ファイル。
私はRuby on Railsを触っていたので、それになぞると・・・
- npm = RailsでいうGem(ジェム)
- package.json = RailsでいうGemfile
こんな感じに関連づけられたら、腹落ちしました。
世の中には、ライブラリという便利な道具箱がたくさんあるので、それをnpmで管理して、自分のアプリケーションに入れるライブラリを管理するのにpackage.jsonがあるという感じです。話が脱線しましたが、package.jsonが生成できたらwebpackをインストールします。
4.アプリケーションのディレクトリ上で以下コマンドを実行します。
npm i -D webpack webpack-cliこれで、webpackを使う準備ができました!
webpack.config.js
webpackを利用するためにはwebpack.config.jsというファイルに設定を記述する必要があります。
5.webpack.config.jsというファイルを作成して、中身のコードを書いていきます。
書く項目はある程度決まっているようなのですが、なかなか難しいため今回は最低限動かすための内容を書きます。webpack.config.jsconst path = require('path') const webpack = require('webpack') module.exports = { mode: "development", //今回は開発モードで記述 entry: { index: path.join(__dirname, 'index.js') //webpackビルド時に読み込むファイルを記述します。 //entry項目では、/myProject/index.jsを読み込んでねという設定をしています。 }, output: { path: path.join(__dirname, 'dist'), filename: 'index.js' //output項目では、webpackビルド時に出力するファイル名と作成フォルダ名を指定しています。 //今回は、distという名前のフォルダ内に、index.jsというファイルを作成します。 }, target: 'node', //'node'として、Node.jsのような環境で使用するためにコンパイルするようwebpackに指示します module: { rules: [ { test: /.js$/, //拡張子を.jsで設定しています loader: 'babel-loader' //.jsと設定したファイルをbabel-loaderで読み込む設定をしています } ] } }6.
babel-loader
がないと、webpackビルド時にエラーが出てしまうのでインストールします。$ npm i --save-dev @babel/core @babel/polyfill @babel/preset-env babel-loader7.まだこのままだと「buildなんてコマンドはないよ」と怒られてしまうので、今度は
package.json
にタスクを設定します。package.json{ "name": "myProject", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" →追記箇所 }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.11.4", "@babel/polyfill": "^7.10.4", "@babel/preset-env": "^7.11.0", "babel-loader": "^8.1.0", "webpack": "^4.44.1", "webpack-cli": "^3.3.12" } }8.インストールできたら、webpackを実行します。
$ npm run startそうすると、distフォルダが作成されて中に
index.js
というファイルができていると思います。それをLambdaで読み込めるように、zip化します。9.AWSのコンソールでLambda画面を開いて、生成したzipファイルを読み込みます。
読み込めたら、右上のテストから実行してみると無事実行されたことが確認できると思います。
すでに上に記述した手順を同じように踏んでREST APIも設定してLambra関数を呼び出してみます。
呼び出せてます!
複数のJSファイルを作成⇒webpack⇒Lambda関数として読み込み⇒API GatewayでブラウザからLambda関数を呼び出す
以上の一連の流れを確認することができました。
Lambdaの何もわからない状態から、なんとなくこんな風に動くんだなというイメージをつけることができたので、一つのアプリケーションを実際作る時の実践的な使い方までを知れたらまた自身の備忘録としてまとめたいと思います
参考URL
webpack.config.js の書き方をしっかり理解しよう - Qiita
最新版で学ぶwebpack 4入門 - JavaScriptのモジュールバンドラ
- 投稿日:2020-08-24T10:22:21+09:00
ejsでインクルードをするときに、パスをルート相対URLで指定したい
process.cwd()
Node.jsの
process.cwd()
は、コマンドが実行されたディレクトリを返します。たとえば、次のようなファイル構成だったとしましょう。
./project └ src/ └ *.ejs └ package.json └ gulpfile.jsこの場合、
project
ディレクトリでgulp
コマンドをたたくことになります。その時にprocess.cwd()
が返す値はpath/to/project
になるので、次のように1つ変数化しておけばルート相対URLのような感覚でパス名を記述することができます。<% const ROOT = `${process.cwd()}/src`; %> <%- include(`${ROOT}/path/to/_include.ejs`) -%> <%- include(`${ROOT}/path/to/_include.ejs`) -%> <%- include(`${ROOT}/path/to/_include.ejs`) -%> <%- include(`${ROOT}/path/to/_include.ejs`) -%>
- 投稿日:2020-08-24T08:00:13+09:00
[JavaScript]ESLintの設定をグローバルルールで保存する
初めに
学習用でESLintが使いたいけど、プロジェクトごとに設定ファイル(.eslintrc.*)を作成するのは面倒。
そこで、設定ファイルをグローバルルールとして保存してプロジェクトごとに作成しなくていいようにしたので、その時にやった方法を備忘録としてまとめてみました。方法
1. ESLintのインストール
まずはESLintをグローバルインストール。
> npm -g i eslintここで、-gは--global、iはinstallを省略したもの。
2. ESLintの設定ファイルをグローバルとして保存
.eslintrc.json
を作成し、ユーザーディレクトリ直下に保存する。
- .eslintrc.jsonを保存する場所
MacOS:/ Users / {USERNAME} Windows:C:\ Users \ {USERNAME}
- .eslintrc.jsonの中身
.eslint.json{ "extends": "eslint:recommended", "env": { "node": true, "es6": true }, "rules": { "semi": [ "error", "never" ] } }各設定の簡単な説明
extends
プロパティ
共有設定を指定する。
eslint:recommended
はESLintの推奨ルールがセットになったもの。
env
プロパティ
環境を指定する。
"node": true
Node.js 固有の変数や構文 (requireや特殊なトップレベル スコープ等) が定義され、利用できるようになる。"es6": true
ECMAScript 2015(ES6)で追加された構文や組込みオブジェクトが利用できるようになる。
rules
プロパティ
ルールを設定する。
大体のルールはextends
プロパティで設定されるため、extends
で設定されていないルールを追加したいときや設定しているルールを省きたいときに利用する。
また、extendsで共有設定を指定しない場合はここで一からルールを設定する。
semi
プロパティ
セミコロンを付けるか付けないかの設定をする。
"error"
でルールが守れてないときはエラー表示。
"never"
で文末のセミコロン;
を禁止する。
3. ESLintの実行
JavaScriptのプログラムを作成してグローバルルールが通っているか確認する。
sample.jsを作成し保存。sample.jsconsole.log("hello world");sample.jsが保存されているディレクトリに移動して以下のコマンドを実行。
> eslint sample.jsグローバルルールが通っていれば以下のようなエラーが表示されるはず。
1:27 error Extra semicolon semi ✖ 1 problem (1 error, 0 warnings) 1 error and 0 warnings potentially fixable with the `--fix` option.
- 投稿日:2020-08-24T04:32:21+09:00
[Node.js] Handlebars Tips
概要
テンプレートエンジンであるHandlebarsをいろいろ触ってみた。
Handlebars自体は珍しいライブラリではなく、他に記事があるので、日本語情報が見つからなかった機能を主に紹介する。https://handlebarsjs.com/
Handlebarsを使う際は、ここを一度は目を通すことをお勧めする。ここでは、通常のテンプレートとしての機能は紹介しない。
環境は、AWS Lambda Node.js上で動かす事を想定している。pertialとhelper
Handlebarsで機能を拡張する方法には、この2種類がある。
- partial : いわゆるサブテンプレート、テンプレートに動的に他のテンプレートを埋め込む事ができる。
- helper : 関数。テンプレートの中でヘルパ関数を実行し、その結果をtemplateに埋め込む事ができる。
{{!sample.hbs}} {{#if hoge }} {{ sample.sub.hbs hoge }} {{/if}}例えばこんな感じで、
sample.hbs
というテンプレートに、ある条件に合致したときのみsample.sub.hbs
という別のテンプレートを埋め込む。{{!sample.hbs}} {{>hoge a "aaa"}} {{/if}}// helperes.js const hoge = (key1, key2) => { return key + ':' + value; }; Handlebars.registerHelper('hoge', hoge);たとえばこんなことをすると、
a
の値と":aaa"
文字列を結合した結果を出力してくれる。
こんな単純なHelper関数だけではなく、{{#if}}
のようなブロックヘルパー(内部に構造を持つヘルパー)を作成することもできる。Tips紹介
precompile
コードの中でreadFileしてコンパイルすることも可能だが、簡単にprecompileできる。
templateの場合
{{!test.hbs}} {{a}}たとえばあらかじめこんなテンプレートを作成しておき、
$ handlebars test.hbs -f test.hbs.jsコマンドを実行することで、jsファイルにコンパイルされる。
global.Handlebars = require('handlebars') require('test.hbs') const template = Handlebars.templates['test.hbs'] console.log(template({a: 1})) // 1プリコンパイルしたコードを読み込めば、templateを実行することができる。
前もってglobalにHandlebarsを設定する必要がある。
この情報がどこにも書かれていない。サンプルを見ると、precompileしたコードをfileとして読んで、
Handlebars.template
でロードしろと言っているようにも見えるが・・・そんなことをするよりこっちのほうが簡単で便利なきがするのだが・・・(もちろん名前空間の問題を抜きにすればだが)テンプレートは、Handlebars.templatesにファイル名をキーに登録される。
partialの場合
partialもtemplateと同様にプリコンパイルできる。
{{!test.hbs}} {{test.sub.hbs a}} {{! partialにはコンテキストを渡すことができる。}}{{!test.sub.hbs}} {{b}}たとえばあらかじめこんなテンプレート、サブテンプレートを作成しておき、
$ handlebars test.hbs -f test.hbs.js $ handlebars test.sub.hbs -p -f test.sub.hbs.jsコマンドを実行することで、jsファイルにコンパイルされる。
global.Handlebars = require('handlebars') require('test.hbs') require('test.sub.hbs') const template = Handlebars.templates['test.hbs'] console.log(template({a:{b:1}})) // 1
- partialの扱いもtemplateとほぼ同じ。プリコンパイルオプションに
-p
を設定するだけ。- partialタグがインデントされていた場合、中身も自動的にインデントされる。
global.Handlebarsについて
globalにHandlebarsをセットする方法は推奨ではない可能性もある。
とすると以下のようにする必要がある。const Handlebars = require('handlebars'); Handlebars.partials['sample.hbs'] = Handlebars.template(fs.readFileSync('sample.hbs.js'));ただ、せっかくprecompileしたコードをわざわざreadFileで読むのか?それとももっといい方法があるのか?
globalに設定する場合、通常はpreloadすることになるだろう。
// setup.js global.Handlebars = require('handlebars')// package.json { "jest": { "verbose": true, "setupFilesAfterEnv": [ "<rootDir>/setup.js" ] } }jestでは、これでpreloadされた。
$ node -r ./setup.js言わずもがなだと思うが、nodeであればこれでpreloadできる。
Helperについて
helperから
@root
を取得したいhelperからコンテキストの情報や
@root
が欲しくなった場合、どうすればいいだろうか?// helpers.js const getRoot = (key, _) => { return _.data.root[key]; }; Handlebars.registerHelper('getRoot', getRoot);{{!test.hbs}} {{#with a}} {{b}} {{getRoot "c"}} {{/with}}ヘルパー関数の通常の引数の後ろに、handlebarsの環境情報が全て格納されているからここから取得できる。
この情報も見つける事ができなかった。APIドキュメントにも記載はなかった。
重要な情報だと思うのだが・・・ちなみに、現在のコンテキストだけならもっと簡単に取れる。
// helpers.js const getValue = function(key){ return this[key]; }; Handlebars.registerHelper('getValue', getValue);ラムダ式ではなくfunctionで関数定義すれば、thisでコンテキストを参照できる。
helperの一括登録
// helpers.js const helper1 = () => {...} const helper2 = () => {...} Handlebars.registerHelper({ helper1, helper2 });複数のHelper関数を一括で登録できる。
Helperのテスト
helperに対して単体テストを書く場合、
// helpers.test.js // must preload global.Handlebars require('helpers.js') const _ = Handlebars.compile; describe('test helpers', () => { test('getRoot', () => { const template = _('{{#with a}}{{b}}{{getRoot "c"}}{{/with}}'); expect(template({a:{b:1},c:2})).toEqual('12'); }); });こんな感じで、小さなテンプレートを使ってテストを書いていけばいい。
巨大なproductionテンプレートを使ってhelperのテストを書こうとすると、膨大な量のテストになるだろう。
単体テストをしよう。結論
とりあえず上記により、Handlebarsを使ってやりたかった事はできました。
ただし、正直これが正しい作法かどうかはわかりません。ご意見があれば下さい。もっと言えば、もうtemplateはReactあたりで書いて、jsx ==> jsにプリコンパイルすればいいんじゃないかという気もしなくもありません。
- 投稿日:2020-08-24T02:18:20+09:00
Node.jsでOSの環境変数を読み込む
- 投稿日:2020-08-24T00:01:34+09:00
コロナで危険度の高い人を見つけたらイ〇ジンをぶっ掛けて治療するアプリ作ってみた
はじめに
この記事は chillSAP 夏の自由研究2020 の記事として執筆しています。
猛暑が続いておりますが、皆さまいかがお過ごしでしょうか。
今年はコロナの影響で、日々言い知れぬ不安に苛まれることも多いのではないでしょうか。
この状況下で発熱してしまった方のご心境は察するに余りあります。
このような方の症状を一刻でも早く緩和させるため、今回は特に治療の緊急度の高いご高齢&高熱の方を判定してイ〇ジンをかけて治療を行うアプリを開発しました。
(注:本記事はコロナウイルスに対する治療法を提案するものでもなければ、イ〇ジンの効果を証明するものでも無いことをご承知おきください。また、ご気分を害された方がいらっしゃったら誠に申し訳ございません。)概要
Raspberry Piに接続されたカメラモジュール及びサーマルカメラモジュールにより、測定者の年齢を及び体温を推測します(①、②)。
推定年齢及び体温が基準値を超えた場合は危険性ありと判断し、サーボモータを介して水鉄砲からイソジンを発射します(③)。
またイソジンが足りなくなると困るので、同時にSAP Cloud Platform上のNode.jsアプリケーションを経由して(④)、SAP S/4HANA上の購買依頼伝票作成ODataサービスから購買依頼伝票を作成して購入を進めます(⑤)。年齢推定や体温測定の結果はRaspberry Pi上でこんな感じで表示されます。
必要なもの
製品 補足 Raspberry Pi 4 model:B サーマルカメラモジュール (MLX90640) Raspberry Pi Camera V2 サーボモータ(MG996R) それなりの力が出ないとと水鉄砲のトリガーを引けません 水鉄砲 イ〇ジン(ポピドンヨード入り) キッコーマン醤油SAP S/4 HANA 1709 オンプレミスver 実装手順
1. Raspberry Piへの各種モジュールの接続
今回はカメラモジュール、サーマルカメラモジュール、サーボモータをRaspberry Piに接続します。
またイソジン発射用の水鉄砲を適当な台に取り付け、サーボモータと接続します。
Raspberry Piへの各種モジュールの接続は以下の端子及びGPIOピンを使用しています。
カメラモジュール:ラズパイのカメラ用端子
サーマルカメラモジュール:SDAをGPIO 2ピン、SCLをGPIO 3ピン
サーボモータ:GPIO 18ピンサーボモータと水鉄砲との接続は強めの糸で行い、サーボモータが半回転したときにトリガーが引かれるようにしておきます。
水鉄砲のトリガーは基本的に結構固く、生半可な接続だと糸がずれてしまったりトリガーでは無く水鉄砲やサーボモータ自体が動いてしまうので、しっかり固定するようにしましょう。
また、水鉄砲の中には今回の主役である
キッコーマン醤油イ〇ジン(ポピドンヨード入り)を充填しておきましょう。
2. Pythonアプリの実装
Raspberry Piに接続した各種モジュールはPythonで記述したスクリプトにより制御します。
Pythonアプリの構成は以下の通りです。. ├── face │ └── face.py ├── httpclient │ └── purchase_create.py ├── templates │ └── index.html ├── thermal_camera │ └── thermal_camera.py ├── watergun │ └── watergun.py └── camera_app.py複数のカメラからの動画を別々のウィンドウに表示するのであればopencvのimshowメソッドを使用するだけでOKなのですが、今回は1画面内で(かつブラウザからサーバにアクセスして)複数の動画を表示したかったため、Python用の軽量WebアプリケーションフレームワークであるFlaskを利用しています。
まずエンドポイントに対してリクエストを送った際に表示される画面をindex.html内に記載します。
index.html<html> <head> <title>Video Streaming Demonstration</title> </head> <body> <img src="{{ url_for('video_feed1') }}"> <img src="{{ url_for('video_feed2') }}"> </body> </html>index.html 内のimgタグで動画(厳密にはカメラモジュールで撮影した動画の1フレーム)を表示するようになっています。
ソースとなる動画は、url_forで指定したルート名と同一のルートをcamera_app.pyから探して取得しています。camera_app.py#!/usr/bin/env python import os from flask import Flask, render_template, Response import adafruit_mlx90640 import time from thermal_camera.thermal_camera import ThermalCamera from face.face import Face from httpclient.purchase_create import HttpClient from watergun.watergun import Watergun app = Flask(__name__) age = 0 temperature = 0 last_posted_time = time.time() @app.route('/') def index(): """Video streaming home page.""" return render_template('index.html') def generate(camera, value): """Video streaming generator function.""" while True: #画像のバイナリデータを取得 frame = camera.get_frame() #推定年齢と体温を取得する global age global temperature global last_posted_time if value == 'face': age = frame[1] elif value == 'temperature': temperature = frame[1] #推定年齢が40歳以上、体温が36℃であり、前回連携時間から5秒以上が経過している場合 if age >= 40 and temperature >= 36 and (time.time() - last_posted_time) >=5: last_posted_time = time.time() #水鉄砲を発射 Watergun.shoot() #購買依頼伝票を作成 HttpClient.createPurchaseReq() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame[0] + b'\r\n') @app.route('/video_feed1') def video_feed1(): """Video streaming route. Put this in the src attribute of an img tag.""" return Response(generate(Face(),'face'), mimetype='multipart/x-mixed-replace; boundary=frame') @app.route('/video_feed2') def video_feed2(): """Video streaming route. Put this in the src attribute of an img tag.""" return Response(generate(ThermalCamera(), 'temperature'), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', threaded=True, port=5000)camera_app.pyでは、①Flaskアプリを起動、②顔認識プログラムからの画像及び推定年齢取得、③体温測定プログラムからの画像及び体温取得、④水鉄砲からのイ〇ジン噴射、⑤購買依頼伝票作成用APIへのリクエスト送信を行っています。
②③についてですが、バイト型の動画フレームをyieldで逐次出力しており、その際mimetypeをmultipart/x-mixed-replaceと指定することでサーバが紙芝居的にレンダリングするようにしています。thermal_camera.pyではサーマルカメラモジュールからバイナリイメージを取得し、温度の情報と共に呼び出し元に返す実装がなされています。
thermal_camera.py#!/usr/bin/env python3 import cv2 import time from PIL import Image, ImageDraw, ImageFont import os import board import busio import adafruit_mlx90640 import math import numpy class ThermalCamera: # some utility functions def constrain(self, val, min_val, max_val): return min(max_val, max(min_val, val)) def map_value(self, x, in_min, in_max, out_min, out_max): return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min def gaussian(self, x, a, b, c, d=0): return a * math.exp(-((x - b) ** 2) / (2 * c ** 2)) + d def gradient(self, x, width, cmap, spread=1): width = float(width) r = sum( [self.gaussian(x, p[1][0], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) g = sum( [self.gaussian(x, p[1][1], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) b = sum( [self.gaussian(x, p[1][2], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) r = int(self.constrain(r * 255, 0, 255)) g = int(self.constrain(g * 255, 0, 255)) b = int(self.constrain(b * 255, 0, 255)) return r, g, b def get_frame(self): while True: INTERPOLATE = 10 # MUST set I2C freq to 1MHz in /boot/config.txt i2c = busio.I2C(board.SCL, board.SDA) # low range of the sensor (this will be black on the screen) MINTEMP = 20.0 # high range of the sensor (this will be white on the screen) MAXTEMP = 45.0 # the list of colors we can choose from heatmap = ( (0.0, (0, 0, 0)), (0.20, (0, 0, 0.5)), (0.40, (0, 0.5, 0)), (0.60, (0.5, 0, 0)), (0.80, (0.75, 0.75, 0)), (0.90, (1.0, 0.75, 0)), (1.00, (1.0, 1.0, 1.0)), ) # how many color values we can have COLORDEPTH = 1000 colormap = [0] * COLORDEPTH for i in range(COLORDEPTH): colormap[i] = self.gradient(i, COLORDEPTH, heatmap) # initialize the sensor mlx = adafruit_mlx90640.MLX90640(i2c) mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_2_HZ frame = [0] * 768 try: mlx.getFrame(frame) except ValueError: continue # these happen, no biggie - retry pixels = [0] * 768 for i, pixel in enumerate(frame): coloridx = self.map_value(pixel, MINTEMP, MAXTEMP, 0, COLORDEPTH - 1) coloridx = int(self.constrain(coloridx, 0, COLORDEPTH - 1)) pixels[i] = colormap[coloridx] for h in range(24): for w in range(32): pixel = pixels[h * 32 + w] image = Image.new("RGB", (32, 24)) image.putdata(pixels) #numpy配列(RGBA) <- PILイメージ img_numpy = numpy.asarray(image) #numpy配列(BGR) <- numpy配列(RGBA) img_numpy_bgr = cv2.cvtColor(img_numpy, cv2.COLOR_RGBA2BGR) img = cv2.resize(img_numpy_bgr, (320, 240)) #画像の中から温度の最大値を取得 max_temperature = numpy.max(frame) #温度をテキストから画像に変換 canvasSize = (320, 60) backgroundRGB = (255, 255, 255) textRGB = (0, 0, 0) if max_temperature >= 36.0: textRGB = (225, 0, 0) backgroundRGB = (255, 255, 0) else: textRGB = (0, 0, 0) backgroundRGB = (255, 255, 255) text = "体温は" + str(max_temperature)[:4] + "℃です。" image_temperature = Image.new('RGB', canvasSize, backgroundRGB) draw = ImageDraw.Draw(image_temperature) font = ImageFont.load_default() font = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 30) draw.text((1, 1), text, fill=textRGB, font=font) #numpy配列(RGBA) <- PILイメージ img_numpy_temperature = numpy.asarray(image_temperature) #numpy配列(BGR) <- numpy配列(RGBA) img_temperature = cv2.cvtColor(img_numpy_temperature, cv2.COLOR_RGBA2BGR) #サーマル画像と温度テキスト画像を結合する img_concatenated = cv2.vconcat([img, img_temperature]) # encode as a jpeg image and return it return cv2.imencode('.jpg', img_concatenated)[1].tobytes(), max_temperatureface.pyについては諸事情によりソースコードを非公開とさせていただきます。中身としてはカメラモジュールから取得した画像から顔認識を行い、年齢を推定した上で顔パーツをプロットしたバイナリイメージ、及び推定年齢を返す実装がなされています。
watergun.pyではサーボモータを適切な角度に動かす実装がなされており、これにより接続された水鉄砲のトリガーが引かれます。
ご使用の水鉄砲によって角度等の設定が変わってきます。watergun.pyimport pigpio import time class Watergun: def shoot(): gpio_pin0 = 18 pi = pigpio.pi() pi.set_mode(gpio_pin0, pigpio.OUTPUT) # GPIO18: 50Hz、duty比2.5% pi.hardware_PWM(gpio_pin0,50,25000) time.sleep(2) # GPIO18: 50Hz、duty比7.25% pi.hardware_PWM(gpio_pin0,50,72500) pi.set_mode(gpio_pin0,pigpio.INPUT) pi.stop()purchase_create.pyでSAP Cloud Platform上のNode.js APIを叩き、S/4HANA上で購買依頼伝票を作成します。
HttpClientでアクセスしているAPIのエンドポイントは「3. SAP Cloud SDK for JavaScriptを用いたAPI実装」でAPIをSAP Cloud Foundry上にデプロイした後にセットしてください。purchase_create.pyimport urllib.request import urllib.error import lxml import base64 import json import re import ssl from bs4 import BeautifulSoup from collections import defaultdict class HttpClient : #POSTメソッドによる購買依頼伝票の作成 def createPurchaseReq(): url = <<APIのエンドポイント>> #ヘッダ情報を設定 headers = {"Content-Type" : "application/json"} #ボディ情報を設定 json_obj = { "matnr": "300002", "menge": 1, "meins": "PC" } # POSTリクエスト送信 json_body = json.dumps(json_obj).encode("utf-8") req = urllib.request.Request(url, data=json_body, headers=headers, method='POST') try: with urllib.request.urlopen(req) as response: #レスポンスを取得する response_body = response.read().decode("utf-8") print() # JSONとして読み込む json_obj = json.loads(response_body) # 値の取り出し pr_number = json_obj['matnr'] print('購買依頼伝票:' + pr_number + 'を作成しました。') #レスポンスから購買依頼伝票番号を取得する #例外処理 except urllib.error.HTTPError as err: soup = BeautifulSoup(err, "lxml") print(soup)3. SAP Cloud SDK for JavaScriptを用いたAPI実装
次にS/4 HANA上の購買依頼伝票作成用ODataサービスを呼び出すためのAPIを実装していきます。
普段はSAP Cloud SDKの中でもJavaの方を用いてS/4 HANA上にアクセスしていますが、最近NodeJsを触る機会があったので今回はSAP Cloud SDK for JavaScriptを用いてAPIを作成していきたいと思います。
(ODataサービスは相も変わらず「だっふんだ 」と言うとSAP S/4HANAで購買依頼伝票を打てるiOSアプリを実装する(2/2)で紹介されているものを使用しています。)まず最初にコマンドライン上で以下を実行してNodeプロジェクトを立ち上げます。
$ npm init次に必要なモジュールを順にインストールしていきます。
まずはS/4接続に必須のcloud-sdk-generatorをインストールします。$ npm install @sap/cloud-sdk-generatorこのライブラリを使用することでODataサービスのメタデータからVDMを作成したりS/4 HANAに簡単にアクセスすることが出来ます。
試しにルートにedmxフォルダを作成し、その中に拡張子がedmxのファイル(中身はODataサービスのメタデータをコピペ)を置いて以下のコマンドを実行しましょう。$ npx generate-odata-client --inputDir edmx --outputDir vdmするとルートにvdmというフォルダが作成され、edmxフォルダ下のメタデータに対応するvdmが作成されていることが分かります。
次にTypeScriptのコンパイルや実行に必要なモジュールをインストールしましょう。
$ npm install --save-dev typescript ts-nodeその後tscを用いてコンパイルに関する設定ファイルであるtsconfig.jsonファイルを作成します。
$ npx tsc --inittsconfig.jsonを開き、"experimentalDecorators"と"emitDecoratorMetadata"を有効にしておきましょう。
tsconfig.json... /* Experimental Options */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. ...次に、ビルド時に最初にcleanを行うためにrimrafをインストールします。
$ npm install rimrafまた今のうちにpackage.jsonの"scripts"を修正してコマンドライン上での実行が楽になるようにしておきます。
package.json... "scripts": { "vdm": "npx generate-odata-client --inputDir edmx --outputDir vdm --forceOverwrite", "clean": "rimraf lib reports", "build": "npm run clean && npx tsc", "start": "node ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, ...最後に、APIの実装に必要なモジュールをインストールしておきます。
$ npm install routing-controllers reflect-metadata typeormNode.jsアプリケーション実行時に起動されるロジックをindex.tsに記載します。
index.tsimport "reflect-metadata" import { createExpressServer } from "routing-controllers"; import { PurchaseRequestController } from './PurchaseRequestController'; const app = createExpressServer({ controllers: [PurchaseRequestController] }); app.listen(8080);Entity、Controller、Serviceを必要に応じて実装します。
PurchaseRequestEntity.tsimport { Entity } from 'typeorm'; @Entity() export class PurchaseRequestEntity{ matnr: string; menge: number; meins: string; constructor(matnr: string, menge: number, meins: string){ this.matnr =matnr; this.menge = menge; this.meins = meins; } }PurchaseRequestController.tsimport { JsonController, Get, Post, Body } from 'routing-controllers'; import { PurchaseRequestEntity } from './PurchaseRequestEntity'; import { PurchaseRequestService } from './PurchaseRequestService'; @JsonController("/tr") export class PurchaseRequestController { @Get("/all") getAllReq() { return new PurchaseRequestService().getAllPurchaseRequest(); } @Post("/single") createReq(@Body() pr:PurchaseRequestEntity) { return new PurchaseRequestService().createPurchaseRequest(pr); } }PurchaseRequestService.tsimport { PurchaseReqSet } from '../vdm/ypurchase-request-create-service/PurchaseReqSet'; import * as PRSrv from '../vdm/ypurchase-request-create-service'; import { BigNumber } from 'bignumber.js'; import { PurchaseRequestEntity } from './PurchaseRequestEntity'; export class PurchaseRequestService { getAllPurchaseRequest() { return PRSrv.PurchaseReqSet.requestBuilder() .getAll() //照会処理用のメソッド .top(10) .select( PRSrv.PurchaseReqSet.ALL_FIELDS ) .execute({destinationName: '<<Destination名>>'}); } createPurchaseRequest(pr: PurchaseRequestEntity) { const purchaseReqSet = PRSrv.PurchaseReqSet.builder() .matnr(pr.matnr) .menge(new BigNumber(pr.menge)) .meins(pr.meins) .build(); return PurchaseReqSet.requestBuilder() .create(purchaseReqSet) .execute({destinationName: '<<Destination名>>'}); } }さて、CF環境へのデプロイに必要不可欠なmanifest.ymlをルートディレクトリに追加しましょう。
ここにConnectivityやDestinationをバインドするよう記述しても良いですが、今回は面倒なのでデプロイ後にマニュアルでSAP Cloud Platform上からバインドしています。manifest.yml--- applications: - name: ynodejs_purchase_request memory: 128M buildpacks: - https://github.com/cloudfoundry/nodejs-buildpack最終的なディレクトリ構成は以下の通りです。
. ├── edmx │ └── YPURCHASE_REQUEST_CREATE_SRV.edmx ├── node_modules ├── src │ ├── index.ts │ ├── PurchaseRequestController.ts │ ├── PurchaseRequestEntity.ts │ └── PurchaseRequestService.ts ├── vdm ├── manifest.yml ├── package.json ├── package-lock.json └── tsconfig.jsonビルドまで完了したらSCP Cloud Foundryにデプロイします。ルート階層でcf pushを行いましょう。manifest.yml内に記載したアプリ名を指定すればOKです。
$ cf push ynodejs_purchase_requestこれでS/4HANA上で購買依頼伝票を作成するためのAPIが完成しました。
※「2. Pythonアプリの実装」のpurchase_create.py内でStubで置いていたAPIのエンドポイントを書き換えてください。完成
#夏の自由研究2020 #chillSAP 8月24日分の投稿です。
— サイコパスのりお (@norio_psycho) August 23, 2020
Qiita記事「コロナで危険度の高い人を見つけたらイ〇ジンをぶっ掛けて治療するアプリ作ってみた」のデモ動画をご共有いたします、、!#コロナ #イソジン #ご気分を害した方がいらっしゃったら #誠に申し訳ございません #一切の責任は取りかねます pic.twitter.com/ZxO3xmQYH2同期の間で老け顔キャラだったのですが、機械にまで実年齢+7歳くらいで評価されたのは地味にショックでした。
- 投稿日:2020-08-24T00:01:34+09:00
コロナで危険度の高い人を見つけたらすぐ治療するアプリ作ってみた
はじめに
この記事は chillSAP 夏の自由研究2020 の記事として執筆しています。
猛暑が続いておりますが、皆さまいかがお過ごしでしょうか。
今年はコロナの影響で、日々言い知れぬ不安に苛まれることも多いのではないでしょうか。
この状況下で発熱してしまった方のご心境は察するに余りあります。
このような方の症状を一刻でも早く緩和させるため、今回は特に治療の緊急度の高いご高齢&高熱の方を判定してイ〇ジンをかけて治療を行うアプリを開発しました。
(注:本記事はコロナウイルスに対する治療法を提案するものでもなければ、イ〇ジンの効果を証明するものでも無いことをご承知おきください。また、ご気分を害された方がいらっしゃったら誠に申し訳ございません。)概要
Raspberry Piに接続されたカメラモジュール及びサーマルカメラモジュールにより、測定者の年齢を及び体温を推測します(①、②)。
推定年齢及び体温が基準値を超えた場合は危険性ありと判断し、サーボモータを介して水鉄砲からイソジンを発射します(③)。
またイソジンが足りなくなると困るので、同時にSAP Cloud Platform上のNode.jsアプリケーションを経由して(④)、SAP S/4HANA上の購買依頼伝票作成ODataサービスから購買依頼伝票を作成して購入を進めます(⑤)。年齢推定や体温測定の結果はRaspberry Pi上でこんな感じで表示されます。
必要なもの
製品 補足 Raspberry Pi 4 model:B サーマルカメラモジュール (MLX90640) Raspberry Pi Camera V2 サーボモータ(MG996R) それなりの力が出ないとと水鉄砲のトリガーを引けません 水鉄砲 イ〇ジン(ポピドンヨード入り) キッコーマン醤油SAP S/4 HANA 1709 オンプレミスver 実装手順
1. Raspberry Piへの各種モジュールの接続
今回はカメラモジュール、サーマルカメラモジュール、サーボモータをRaspberry Piに接続します。
またイソジン発射用の水鉄砲を適当な台に取り付け、サーボモータと接続します。
Raspberry Piへの各種モジュールの接続は以下の端子及びGPIOピンを使用しています。
カメラモジュール:ラズパイのカメラ用端子
サーマルカメラモジュール:SDAをGPIO 2ピン、SCLをGPIO 3ピン
サーボモータ:GPIO 18ピンサーボモータと水鉄砲との接続は強めの糸で行い、サーボモータが半回転したときにトリガーが引かれるようにしておきます。
水鉄砲のトリガーは基本的に結構固く、生半可な接続だと糸がずれてしまったりトリガーでは無く水鉄砲やサーボモータ自体が動いてしまうので、しっかり固定するようにしましょう。
また、水鉄砲の中には今回の主役である
キッコーマン醤油イ〇ジン(ポピドンヨード入り)を充填しておきましょう。
2. Pythonアプリの実装
Raspberry Piに接続した各種モジュールはPythonで記述したスクリプトにより制御します。
Pythonアプリの構成は以下の通りです。. ├── face │ └── face.py ├── httpclient │ └── purchase_create.py ├── templates │ └── index.html ├── thermal_camera │ └── thermal_camera.py ├── watergun │ └── watergun.py └── camera_app.py複数のカメラからの動画を別々のウィンドウに表示するのであればopencvのimshowメソッドを使用するだけでOKなのですが、今回は1画面内で(かつブラウザからサーバにアクセスして)複数の動画を表示したかったため、Python用の軽量WebアプリケーションフレームワークであるFlaskを利用しています。
まずエンドポイントに対してリクエストを送った際に表示される画面をindex.html内に記載します。
index.html<html> <head> <title>Video Streaming Demonstration</title> </head> <body> <img src="{{ url_for('video_feed1') }}"> <img src="{{ url_for('video_feed2') }}"> </body> </html>index.html 内のimgタグで動画(厳密にはカメラモジュールで撮影した動画の1フレーム)を表示するようになっています。
ソースとなる動画は、url_forで指定したルート名と同一のルートをcamera_app.pyから探して取得しています。camera_app.py#!/usr/bin/env python import os from flask import Flask, render_template, Response import adafruit_mlx90640 import time from thermal_camera.thermal_camera import ThermalCamera from face.face import Face from httpclient.purchase_create import HttpClient from watergun.watergun import Watergun app = Flask(__name__) age = 0 temperature = 0 last_posted_time = time.time() @app.route('/') def index(): """Video streaming home page.""" return render_template('index.html') def generate(camera, value): """Video streaming generator function.""" while True: #画像のバイナリデータを取得 frame = camera.get_frame() #推定年齢と体温を取得する global age global temperature global last_posted_time if value == 'face': age = frame[1] elif value == 'temperature': temperature = frame[1] #推定年齢が40歳以上、体温が36℃であり、前回連携時間から5秒以上が経過している場合 if age >= 40 and temperature >= 36 and (time.time() - last_posted_time) >=5: last_posted_time = time.time() #水鉄砲を発射 Watergun.shoot() #購買依頼伝票を作成 HttpClient.createPurchaseReq() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame[0] + b'\r\n') @app.route('/video_feed1') def video_feed1(): """Video streaming route. Put this in the src attribute of an img tag.""" return Response(generate(Face(),'face'), mimetype='multipart/x-mixed-replace; boundary=frame') @app.route('/video_feed2') def video_feed2(): """Video streaming route. Put this in the src attribute of an img tag.""" return Response(generate(ThermalCamera(), 'temperature'), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', threaded=True, port=5000)camera_app.pyでは、①Flaskアプリを起動、②顔認識プログラムからの画像及び推定年齢取得、③体温測定プログラムからの画像及び体温取得、④水鉄砲からのイ〇ジン噴射、⑤購買依頼伝票作成用APIへのリクエスト送信を行っています。
②③についてですが、バイト型の動画フレームをyieldで逐次出力しており、その際mimetypeをmultipart/x-mixed-replaceと指定することでサーバが紙芝居的にレンダリングするようにしています。thermal_camera.pyではサーマルカメラモジュールからバイナリイメージを取得し、温度の情報と共に呼び出し元に返す実装がなされています。
thermal_camera.py#!/usr/bin/env python3 import cv2 import time from PIL import Image, ImageDraw, ImageFont import os import board import busio import adafruit_mlx90640 import math import numpy class ThermalCamera: # some utility functions def constrain(self, val, min_val, max_val): return min(max_val, max(min_val, val)) def map_value(self, x, in_min, in_max, out_min, out_max): return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min def gaussian(self, x, a, b, c, d=0): return a * math.exp(-((x - b) ** 2) / (2 * c ** 2)) + d def gradient(self, x, width, cmap, spread=1): width = float(width) r = sum( [self.gaussian(x, p[1][0], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) g = sum( [self.gaussian(x, p[1][1], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) b = sum( [self.gaussian(x, p[1][2], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) r = int(self.constrain(r * 255, 0, 255)) g = int(self.constrain(g * 255, 0, 255)) b = int(self.constrain(b * 255, 0, 255)) return r, g, b def get_frame(self): while True: INTERPOLATE = 10 # MUST set I2C freq to 1MHz in /boot/config.txt i2c = busio.I2C(board.SCL, board.SDA) # low range of the sensor (this will be black on the screen) MINTEMP = 20.0 # high range of the sensor (this will be white on the screen) MAXTEMP = 45.0 # the list of colors we can choose from heatmap = ( (0.0, (0, 0, 0)), (0.20, (0, 0, 0.5)), (0.40, (0, 0.5, 0)), (0.60, (0.5, 0, 0)), (0.80, (0.75, 0.75, 0)), (0.90, (1.0, 0.75, 0)), (1.00, (1.0, 1.0, 1.0)), ) # how many color values we can have COLORDEPTH = 1000 colormap = [0] * COLORDEPTH for i in range(COLORDEPTH): colormap[i] = self.gradient(i, COLORDEPTH, heatmap) # initialize the sensor mlx = adafruit_mlx90640.MLX90640(i2c) mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_2_HZ frame = [0] * 768 try: mlx.getFrame(frame) except ValueError: continue # these happen, no biggie - retry pixels = [0] * 768 for i, pixel in enumerate(frame): coloridx = self.map_value(pixel, MINTEMP, MAXTEMP, 0, COLORDEPTH - 1) coloridx = int(self.constrain(coloridx, 0, COLORDEPTH - 1)) pixels[i] = colormap[coloridx] for h in range(24): for w in range(32): pixel = pixels[h * 32 + w] image = Image.new("RGB", (32, 24)) image.putdata(pixels) #numpy配列(RGBA) <- PILイメージ img_numpy = numpy.asarray(image) #numpy配列(BGR) <- numpy配列(RGBA) img_numpy_bgr = cv2.cvtColor(img_numpy, cv2.COLOR_RGBA2BGR) img = cv2.resize(img_numpy_bgr, (320, 240)) #画像の中から温度の最大値を取得 max_temperature = numpy.max(frame) #温度をテキストから画像に変換 canvasSize = (320, 60) backgroundRGB = (255, 255, 255) textRGB = (0, 0, 0) if max_temperature >= 36.0: textRGB = (225, 0, 0) backgroundRGB = (255, 255, 0) else: textRGB = (0, 0, 0) backgroundRGB = (255, 255, 255) text = "体温は" + str(max_temperature)[:4] + "℃です。" image_temperature = Image.new('RGB', canvasSize, backgroundRGB) draw = ImageDraw.Draw(image_temperature) font = ImageFont.load_default() font = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 30) draw.text((1, 1), text, fill=textRGB, font=font) #numpy配列(RGBA) <- PILイメージ img_numpy_temperature = numpy.asarray(image_temperature) #numpy配列(BGR) <- numpy配列(RGBA) img_temperature = cv2.cvtColor(img_numpy_temperature, cv2.COLOR_RGBA2BGR) #サーマル画像と温度テキスト画像を結合する img_concatenated = cv2.vconcat([img, img_temperature]) # encode as a jpeg image and return it return cv2.imencode('.jpg', img_concatenated)[1].tobytes(), max_temperatureface.pyについては諸事情によりソースコードを非公開とさせていただきます。中身としてはカメラモジュールから取得した画像から顔認識を行い、年齢を推定した上で顔パーツをプロットしたバイナリイメージ、及び推定年齢を返す実装がなされています。
watergun.pyではサーボモータを適切な角度に動かす実装がなされており、これにより接続された水鉄砲のトリガーが引かれます。
ご使用の水鉄砲によって角度等の設定が変わってきます。watergun.pyimport pigpio import time class Watergun: def shoot(): gpio_pin0 = 18 pi = pigpio.pi() pi.set_mode(gpio_pin0, pigpio.OUTPUT) # GPIO18: 50Hz、duty比2.5% pi.hardware_PWM(gpio_pin0,50,25000) time.sleep(2) # GPIO18: 50Hz、duty比7.25% pi.hardware_PWM(gpio_pin0,50,72500) pi.set_mode(gpio_pin0,pigpio.INPUT) pi.stop()purchase_create.pyでSAP Cloud Platform上のNode.js APIを叩き、S/4HANA上で購買依頼伝票を作成します。
HttpClientでアクセスしているAPIのエンドポイントは「3. SAP Cloud SDK for JavaScriptを用いたAPI実装」でAPIをSAP Cloud Foundry上にデプロイした後にセットしてください。purchase_create.pyimport urllib.request import urllib.error import lxml import base64 import json import re import ssl from bs4 import BeautifulSoup from collections import defaultdict class HttpClient : #POSTメソッドによる購買依頼伝票の作成 def createPurchaseReq(): url = <<APIのエンドポイント>> #ヘッダ情報を設定 headers = {"Content-Type" : "application/json"} #ボディ情報を設定 json_obj = { "matnr": "300002", "menge": 1, "meins": "PC" } # POSTリクエスト送信 json_body = json.dumps(json_obj).encode("utf-8") req = urllib.request.Request(url, data=json_body, headers=headers, method='POST') try: with urllib.request.urlopen(req) as response: #レスポンスを取得する response_body = response.read().decode("utf-8") print() # JSONとして読み込む json_obj = json.loads(response_body) # 値の取り出し pr_number = json_obj['matnr'] print('購買依頼伝票:' + pr_number + 'を作成しました。') #レスポンスから購買依頼伝票番号を取得する #例外処理 except urllib.error.HTTPError as err: soup = BeautifulSoup(err, "lxml") print(soup)3. SAP Cloud SDK for JavaScriptを用いたAPI実装
次にS/4 HANA上の購買依頼伝票作成用ODataサービスを呼び出すためのAPIを実装していきます。
普段はSAP Cloud SDKの中でもJavaの方を用いてS/4 HANA上にアクセスしていますが、最近NodeJsを触る機会があったので今回はSAP Cloud SDK for JavaScriptを用いてAPIを作成していきたいと思います。
(ODataサービスは相も変わらず「だっふんだ 」と言うとSAP S/4HANAで購買依頼伝票を打てるiOSアプリを実装する(2/2)で紹介されているものを使用しています。)まず最初にコマンドライン上で以下を実行してNodeプロジェクトを立ち上げます。
$ npm init次に必要なモジュールを順にインストールしていきます。
まずはS/4接続に必須のcloud-sdk-generatorをインストールします。$ npm install @sap/cloud-sdk-generatorこのライブラリを使用することでODataサービスのメタデータからVDMを作成したりS/4 HANAに簡単にアクセスすることが出来ます。
試しにルートにedmxフォルダを作成し、その中に拡張子がedmxのファイル(中身はODataサービスのメタデータをコピペ)を置いて以下のコマンドを実行しましょう。$ npx generate-odata-client --inputDir edmx --outputDir vdmするとルートにvdmというフォルダが作成され、edmxフォルダ下のメタデータに対応するvdmが作成されていることが分かります。
次にTypeScriptのコンパイルや実行に必要なモジュールをインストールしましょう。
$ npm install --save-dev typescript ts-nodeその後tscを用いてコンパイルに関する設定ファイルであるtsconfig.jsonファイルを作成します。
$ npx tsc --inittsconfig.jsonを開き、"experimentalDecorators"と"emitDecoratorMetadata"を有効にしておきましょう。
tsconfig.json... /* Experimental Options */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. ...次に、ビルド時に最初にcleanを行うためにrimrafをインストールします。
$ npm install rimrafまた今のうちにpackage.jsonの"scripts"を修正してコマンドライン上での実行が楽になるようにしておきます。
package.json... "scripts": { "vdm": "npx generate-odata-client --inputDir edmx --outputDir vdm --forceOverwrite", "clean": "rimraf lib reports", "build": "npm run clean && npx tsc", "start": "node ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, ...最後に、APIの実装に必要なモジュールをインストールしておきます。
$ npm install routing-controllers reflect-metadata typeormNode.jsアプリケーション実行時に起動されるロジックをindex.tsに記載します。
index.tsimport "reflect-metadata" import { createExpressServer } from "routing-controllers"; import { PurchaseRequestController } from './PurchaseRequestController'; const app = createExpressServer({ controllers: [PurchaseRequestController] }); app.listen(8080);Entity、Controller、Serviceを必要に応じて実装します。
PurchaseRequestEntity.tsimport { Entity } from 'typeorm'; @Entity() export class PurchaseRequestEntity{ matnr: string; menge: number; meins: string; constructor(matnr: string, menge: number, meins: string){ this.matnr =matnr; this.menge = menge; this.meins = meins; } }PurchaseRequestController.tsimport { JsonController, Get, Post, Body } from 'routing-controllers'; import { PurchaseRequestEntity } from './PurchaseRequestEntity'; import { PurchaseRequestService } from './PurchaseRequestService'; @JsonController("/tr") export class PurchaseRequestController { @Get("/all") getAllReq() { return new PurchaseRequestService().getAllPurchaseRequest(); } @Post("/single") createReq(@Body() pr:PurchaseRequestEntity) { return new PurchaseRequestService().createPurchaseRequest(pr); } }PurchaseRequestService.tsimport { PurchaseReqSet } from '../vdm/ypurchase-request-create-service/PurchaseReqSet'; import * as PRSrv from '../vdm/ypurchase-request-create-service'; import { BigNumber } from 'bignumber.js'; import { PurchaseRequestEntity } from './PurchaseRequestEntity'; export class PurchaseRequestService { getAllPurchaseRequest() { return PRSrv.PurchaseReqSet.requestBuilder() .getAll() //照会処理用のメソッド .top(10) .select( PRSrv.PurchaseReqSet.ALL_FIELDS ) .execute({destinationName: '<<Destination名>>'}); } createPurchaseRequest(pr: PurchaseRequestEntity) { const purchaseReqSet = PRSrv.PurchaseReqSet.builder() .matnr(pr.matnr) .menge(new BigNumber(pr.menge)) .meins(pr.meins) .build(); return PurchaseReqSet.requestBuilder() .create(purchaseReqSet) .execute({destinationName: '<<Destination名>>'}); } }さて、CF環境へのデプロイに必要不可欠なmanifest.ymlをルートディレクトリに追加しましょう。
ここにConnectivityやDestinationをバインドするよう記述しても良いですが、今回は面倒なのでデプロイ後にマニュアルでSAP Cloud Platform上からバインドしています。manifest.yml--- applications: - name: ynodejs_purchase_request memory: 128M buildpacks: - https://github.com/cloudfoundry/nodejs-buildpack最終的なディレクトリ構成は以下の通りです。
. ├── edmx │ └── YPURCHASE_REQUEST_CREATE_SRV.edmx ├── node_modules ├── src │ ├── index.ts │ ├── PurchaseRequestController.ts │ ├── PurchaseRequestEntity.ts │ └── PurchaseRequestService.ts ├── vdm ├── manifest.yml ├── package.json ├── package-lock.json └── tsconfig.jsonビルドまで完了したらSCP Cloud Foundryにデプロイします。ルート階層でcf pushを行いましょう。manifest.yml内に記載したアプリ名を指定すればOKです。
$ cf push ynodejs_purchase_requestこれでS/4HANA上で購買依頼伝票を作成するためのAPIが完成しました。
※「2. Pythonアプリの実装」のpurchase_create.py内でStubで置いていたAPIのエンドポイントを書き換えてください。完成
#夏の自由研究2020 #chillSAP 8月24日分の投稿です。
— サイコパスのりお (@norio_psycho) August 23, 2020
Qiita記事「コロナで危険度の高い人を見つけたらイ〇ジンをぶっ掛けて治療するアプリ作ってみた」のデモ動画をご共有いたします、、!#コロナ #イソジン #ご気分を害した方がいらっしゃったら #誠に申し訳ございません #一切の責任は取りかねます pic.twitter.com/ZxO3xmQYH2同期の間で老け顔キャラだったのですが、機械にまで実年齢+7歳くらいで評価されたのは地味にショックでした。
- 投稿日:2020-08-24T00:01:34+09:00
コロナで危険度の高い人を見つけたらすぐ治療しようとするアプリ作ってみた
はじめに
この記事は chillSAP 夏の自由研究2020 の記事として執筆しています。
猛暑が続いておりますが、皆さまいかがお過ごしでしょうか。
今年はコロナの影響で、日々言い知れぬ不安に苛まれることも多いのではないでしょうか。
この状況下で発熱してしまった方のご心境は察するに余りあります。
このような方の症状を一刻でも早く緩和させるため、今回は特に治療の緊急度の高いご高齢&高熱の方を判定してイ〇ジンをかけて治療を行うアプリを開発しました。
(注:本記事はコロナウイルスに対する治療法を提案するものでもなければ、イ〇ジンの効果を証明するものでも無いことをご承知おきください。また、ご気分を害された方がいらっしゃったら誠に申し訳ございません。)概要
Raspberry Piに接続されたカメラモジュール及びサーマルカメラモジュールにより、測定者の年齢を及び体温を推測します(①、②)。
推定年齢及び体温が基準値を超えた場合は危険性ありと判断し、サーボモータを介して水鉄砲からイ〇ジンを発射します(③)。
またイ〇ジンが足りなくなると困るので、同時にSAP Cloud Platform上のNode.jsアプリケーションを経由して(④)、SAP S/4HANA上の購買依頼伝票作成ODataサービスから購買依頼伝票を作成して購入を進めます(⑤)。年齢推定や体温測定の結果はRaspberry Pi上でこんな感じで表示されます。
必要なもの
製品 補足 Raspberry Pi 4 model:B サーマルカメラモジュール (MLX90640) Raspberry Pi Camera V2 サーボモータ(MG996R) それなりの力が出ないとと水鉄砲のトリガーを引けません 水鉄砲 イ〇ジン(ポピドンヨード入り) キッコーマン醤油SAP S/4 HANA 1709 オンプレミスver 実装手順
1. Raspberry Piへの各種モジュールの接続
今回はカメラモジュール、サーマルカメラモジュール、サーボモータをRaspberry Piに接続します。
またイ〇ジン発射用の水鉄砲を適当な台に取り付け、サーボモータと接続します。
Raspberry Piへの各種モジュールの接続は以下の端子及びGPIOピンを使用しています。
カメラモジュール:ラズパイのカメラ用端子
サーマルカメラモジュール:SDAをGPIO 2ピン、SCLをGPIO 3ピン
サーボモータ:GPIO 18ピンサーボモータと水鉄砲との接続は強めの糸で行い、サーボモータが半回転したときにトリガーが引かれるようにしておきます。
水鉄砲のトリガーは基本的に結構固く、生半可な接続だと糸がずれてしまったりトリガーでは無く水鉄砲やサーボモータ自体が動いてしまうので、しっかり固定するようにしましょう。
また、水鉄砲の中には今回の主役である
キッコーマン醤油イ〇ジン(ポピドンヨード入り)を充填しておきましょう。
2. Pythonアプリの実装
Raspberry Piに接続した各種モジュールはPythonで記述したスクリプトにより制御します。
Pythonアプリの構成は以下の通りです。. ├── face │ └── face.py ├── httpclient │ └── purchase_create.py ├── templates │ └── index.html ├── thermal_camera │ └── thermal_camera.py ├── watergun │ └── watergun.py └── camera_app.py複数のカメラからの動画を別々のウィンドウに表示するのであればopencvのimshowメソッドを使用するだけでOKなのですが、今回は1画面内で(かつブラウザからサーバにアクセスして)複数の動画を表示したかったため、Python用の軽量WebアプリケーションフレームワークであるFlaskを利用しています。
まずエンドポイントに対してリクエストを送った際に表示される画面をindex.html内に記載します。
index.html<html> <head> <title>Video Streaming Demonstration</title> </head> <body> <img src="{{ url_for('video_feed1') }}"> <img src="{{ url_for('video_feed2') }}"> </body> </html>index.html 内のimgタグで動画(厳密にはカメラモジュールで撮影した動画の1フレーム)を表示するようになっています。
ソースとなる動画は、url_forで指定したルート名と同一のルートをcamera_app.pyから探して取得しています。camera_app.py#!/usr/bin/env python import os from flask import Flask, render_template, Response import adafruit_mlx90640 import time from thermal_camera.thermal_camera import ThermalCamera from face.face import Face from httpclient.purchase_create import HttpClient from watergun.watergun import Watergun app = Flask(__name__) age = 0 temperature = 0 last_posted_time = time.time() @app.route('/') def index(): """Video streaming home page.""" return render_template('index.html') def generate(camera, value): """Video streaming generator function.""" while True: #画像のバイナリデータを取得 frame = camera.get_frame() #推定年齢と体温を取得する global age global temperature global last_posted_time if value == 'face': age = frame[1] elif value == 'temperature': temperature = frame[1] #推定年齢が40歳以上、体温が36℃であり、前回連携時間から5秒以上が経過している場合 if age >= 40 and temperature >= 36 and (time.time() - last_posted_time) >=5: last_posted_time = time.time() #水鉄砲を発射 Watergun.shoot() #購買依頼伝票を作成 HttpClient.createPurchaseReq() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame[0] + b'\r\n') @app.route('/video_feed1') def video_feed1(): """Video streaming route. Put this in the src attribute of an img tag.""" return Response(generate(Face(),'face'), mimetype='multipart/x-mixed-replace; boundary=frame') @app.route('/video_feed2') def video_feed2(): """Video streaming route. Put this in the src attribute of an img tag.""" return Response(generate(ThermalCamera(), 'temperature'), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == '__main__': app.run(host='0.0.0.0', threaded=True, port=5000)camera_app.pyでは、①Flaskアプリを起動、②顔認識プログラムからの画像及び推定年齢取得、③体温測定プログラムからの画像及び体温取得、④水鉄砲からのイ〇ジン噴射、⑤購買依頼伝票作成用APIへのリクエスト送信を行っています。
②③についてですが、バイト型の動画フレームをyieldで逐次出力しており、その際mimetypeをmultipart/x-mixed-replaceと指定することでサーバが紙芝居的にレンダリングするようにしています。thermal_camera.pyではサーマルカメラモジュールからバイナリイメージを取得し、温度の情報と共に呼び出し元に返す実装がなされています。
thermal_camera.py#!/usr/bin/env python3 import cv2 import time from PIL import Image, ImageDraw, ImageFont import os import board import busio import adafruit_mlx90640 import math import numpy class ThermalCamera: # some utility functions def constrain(self, val, min_val, max_val): return min(max_val, max(min_val, val)) def map_value(self, x, in_min, in_max, out_min, out_max): return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min def gaussian(self, x, a, b, c, d=0): return a * math.exp(-((x - b) ** 2) / (2 * c ** 2)) + d def gradient(self, x, width, cmap, spread=1): width = float(width) r = sum( [self.gaussian(x, p[1][0], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) g = sum( [self.gaussian(x, p[1][1], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) b = sum( [self.gaussian(x, p[1][2], p[0] * width, width / (spread * len(cmap))) for p in cmap] ) r = int(self.constrain(r * 255, 0, 255)) g = int(self.constrain(g * 255, 0, 255)) b = int(self.constrain(b * 255, 0, 255)) return r, g, b def get_frame(self): while True: INTERPOLATE = 10 # MUST set I2C freq to 1MHz in /boot/config.txt i2c = busio.I2C(board.SCL, board.SDA) # low range of the sensor (this will be black on the screen) MINTEMP = 20.0 # high range of the sensor (this will be white on the screen) MAXTEMP = 45.0 # the list of colors we can choose from heatmap = ( (0.0, (0, 0, 0)), (0.20, (0, 0, 0.5)), (0.40, (0, 0.5, 0)), (0.60, (0.5, 0, 0)), (0.80, (0.75, 0.75, 0)), (0.90, (1.0, 0.75, 0)), (1.00, (1.0, 1.0, 1.0)), ) # how many color values we can have COLORDEPTH = 1000 colormap = [0] * COLORDEPTH for i in range(COLORDEPTH): colormap[i] = self.gradient(i, COLORDEPTH, heatmap) # initialize the sensor mlx = adafruit_mlx90640.MLX90640(i2c) mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_2_HZ frame = [0] * 768 try: mlx.getFrame(frame) except ValueError: continue # these happen, no biggie - retry pixels = [0] * 768 for i, pixel in enumerate(frame): coloridx = self.map_value(pixel, MINTEMP, MAXTEMP, 0, COLORDEPTH - 1) coloridx = int(self.constrain(coloridx, 0, COLORDEPTH - 1)) pixels[i] = colormap[coloridx] for h in range(24): for w in range(32): pixel = pixels[h * 32 + w] image = Image.new("RGB", (32, 24)) image.putdata(pixels) #numpy配列(RGBA) <- PILイメージ img_numpy = numpy.asarray(image) #numpy配列(BGR) <- numpy配列(RGBA) img_numpy_bgr = cv2.cvtColor(img_numpy, cv2.COLOR_RGBA2BGR) img = cv2.resize(img_numpy_bgr, (320, 240)) #画像の中から温度の最大値を取得 max_temperature = numpy.max(frame) #温度をテキストから画像に変換 canvasSize = (320, 60) backgroundRGB = (255, 255, 255) textRGB = (0, 0, 0) if max_temperature >= 36.0: textRGB = (225, 0, 0) backgroundRGB = (255, 255, 0) else: textRGB = (0, 0, 0) backgroundRGB = (255, 255, 255) text = "体温は" + str(max_temperature)[:4] + "℃です。" image_temperature = Image.new('RGB', canvasSize, backgroundRGB) draw = ImageDraw.Draw(image_temperature) font = ImageFont.load_default() font = ImageFont.truetype("/usr/share/fonts/truetype/fonts-japanese-gothic.ttf", 30) draw.text((1, 1), text, fill=textRGB, font=font) #numpy配列(RGBA) <- PILイメージ img_numpy_temperature = numpy.asarray(image_temperature) #numpy配列(BGR) <- numpy配列(RGBA) img_temperature = cv2.cvtColor(img_numpy_temperature, cv2.COLOR_RGBA2BGR) #サーマル画像と温度テキスト画像を結合する img_concatenated = cv2.vconcat([img, img_temperature]) # encode as a jpeg image and return it return cv2.imencode('.jpg', img_concatenated)[1].tobytes(), max_temperatureface.pyについては諸事情によりソースコードを非公開とさせていただきます。中身としてはカメラモジュールから取得した画像から顔認識を行い、年齢を推定した上で顔パーツをプロットしたバイナリイメージ、及び推定年齢を返す実装がなされています。
watergun.pyではサーボモータを適切な角度に動かす実装がなされており、これにより接続された水鉄砲のトリガーが引かれます。
ご使用の水鉄砲によって角度等の設定が変わってきます。watergun.pyimport pigpio import time class Watergun: def shoot(): gpio_pin0 = 18 pi = pigpio.pi() pi.set_mode(gpio_pin0, pigpio.OUTPUT) # GPIO18: 50Hz、duty比2.5% pi.hardware_PWM(gpio_pin0,50,25000) time.sleep(2) # GPIO18: 50Hz、duty比7.25% pi.hardware_PWM(gpio_pin0,50,72500) pi.set_mode(gpio_pin0,pigpio.INPUT) pi.stop()purchase_create.pyでSAP Cloud Platform上のNode.js APIを叩き、S/4HANA上で購買依頼伝票を作成します。
HttpClientでアクセスしているAPIのエンドポイントは「3. SAP Cloud SDK for JavaScriptを用いたAPI実装」でAPIをSAP Cloud Foundry上にデプロイした後にセットしてください。purchase_create.pyimport urllib.request import urllib.error import lxml import base64 import json import re import ssl from bs4 import BeautifulSoup from collections import defaultdict class HttpClient : #POSTメソッドによる購買依頼伝票の作成 def createPurchaseReq(): url = <<APIのエンドポイント>> #ヘッダ情報を設定 headers = {"Content-Type" : "application/json"} #ボディ情報を設定 json_obj = { "matnr": "300002", "menge": 1, "meins": "PC" } # POSTリクエスト送信 json_body = json.dumps(json_obj).encode("utf-8") req = urllib.request.Request(url, data=json_body, headers=headers, method='POST') try: with urllib.request.urlopen(req) as response: #レスポンスを取得する response_body = response.read().decode("utf-8") print() # JSONとして読み込む json_obj = json.loads(response_body) # 値の取り出し pr_number = json_obj['matnr'] print('購買依頼伝票:' + pr_number + 'を作成しました。') #レスポンスから購買依頼伝票番号を取得する #例外処理 except urllib.error.HTTPError as err: soup = BeautifulSoup(err, "lxml") print(soup)3. SAP Cloud SDK for JavaScriptを用いたAPI実装
次にS/4 HANA上の購買依頼伝票作成用ODataサービスを呼び出すためのAPIを実装していきます。
普段はSAP Cloud SDKの中でもJavaの方を用いてS/4 HANA上にアクセスしていますが、最近NodeJsを触る機会があったので今回はSAP Cloud SDK for JavaScriptを用いてAPIを作成していきたいと思います。
(ODataサービスは相も変わらず「だっふんだ 」と言うとSAP S/4HANAで購買依頼伝票を打てるiOSアプリを実装する(2/2)で紹介されているものを使用しています。)まず最初にコマンドライン上で以下を実行してNodeプロジェクトを立ち上げます。
$ npm init次に必要なモジュールを順にインストールしていきます。
まずはS/4接続に必須のcloud-sdk-generatorをインストールします。$ npm install @sap/cloud-sdk-generatorこのライブラリを使用することでODataサービスのメタデータからVDMを作成したりS/4 HANAに簡単にアクセスすることが出来ます。
試しにルートにedmxフォルダを作成し、その中に拡張子がedmxのファイル(中身はODataサービスのメタデータをコピペ)を置いて以下のコマンドを実行しましょう。$ npx generate-odata-client --inputDir edmx --outputDir vdmするとルートにvdmというフォルダが作成され、edmxフォルダ下のメタデータに対応するvdmが作成されていることが分かります。
次にTypeScriptのコンパイルや実行に必要なモジュールをインストールしましょう。
$ npm install --save-dev typescript ts-nodeその後tscを用いてコンパイルに関する設定ファイルであるtsconfig.jsonファイルを作成します。
$ npx tsc --inittsconfig.jsonを開き、"experimentalDecorators"と"emitDecoratorMetadata"を有効にしておきましょう。
tsconfig.json... /* Experimental Options */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. ...次に、ビルド時に最初にcleanを行うためにrimrafをインストールします。
$ npm install rimrafまた今のうちにpackage.jsonの"scripts"を修正してコマンドライン上での実行が楽になるようにしておきます。
package.json... "scripts": { "vdm": "npx generate-odata-client --inputDir edmx --outputDir vdm --forceOverwrite", "clean": "rimraf lib reports", "build": "npm run clean && npx tsc", "start": "node ./src/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, ...最後に、APIの実装に必要なモジュールをインストールしておきます。
$ npm install routing-controllers reflect-metadata typeormNode.jsアプリケーション実行時に起動されるロジックをindex.tsに記載します。
index.tsimport "reflect-metadata" import { createExpressServer } from "routing-controllers"; import { PurchaseRequestController } from './PurchaseRequestController'; const app = createExpressServer({ controllers: [PurchaseRequestController] }); app.listen(8080);Entity、Controller、Serviceを必要に応じて実装します。
PurchaseRequestEntity.tsimport { Entity } from 'typeorm'; @Entity() export class PurchaseRequestEntity{ matnr: string; menge: number; meins: string; constructor(matnr: string, menge: number, meins: string){ this.matnr =matnr; this.menge = menge; this.meins = meins; } }PurchaseRequestController.tsimport { JsonController, Get, Post, Body } from 'routing-controllers'; import { PurchaseRequestEntity } from './PurchaseRequestEntity'; import { PurchaseRequestService } from './PurchaseRequestService'; @JsonController("/tr") export class PurchaseRequestController { @Get("/all") getAllReq() { return new PurchaseRequestService().getAllPurchaseRequest(); } @Post("/single") createReq(@Body() pr:PurchaseRequestEntity) { return new PurchaseRequestService().createPurchaseRequest(pr); } }PurchaseRequestService.tsimport { PurchaseReqSet } from '../vdm/ypurchase-request-create-service/PurchaseReqSet'; import * as PRSrv from '../vdm/ypurchase-request-create-service'; import { BigNumber } from 'bignumber.js'; import { PurchaseRequestEntity } from './PurchaseRequestEntity'; export class PurchaseRequestService { getAllPurchaseRequest() { return PRSrv.PurchaseReqSet.requestBuilder() .getAll() //照会処理用のメソッド .top(10) .select( PRSrv.PurchaseReqSet.ALL_FIELDS ) .execute({destinationName: '<<Destination名>>'}); } createPurchaseRequest(pr: PurchaseRequestEntity) { const purchaseReqSet = PRSrv.PurchaseReqSet.builder() .matnr(pr.matnr) .menge(new BigNumber(pr.menge)) .meins(pr.meins) .build(); return PurchaseReqSet.requestBuilder() .create(purchaseReqSet) .execute({destinationName: '<<Destination名>>'}); } }さて、CF環境へのデプロイに必要不可欠なmanifest.ymlをルートディレクトリに追加しましょう。
ここにConnectivityやDestinationをバインドするよう記述しても良いですが、今回は面倒なのでデプロイ後にマニュアルでSAP Cloud Platform上からバインドしています。manifest.yml--- applications: - name: ynodejs_purchase_request memory: 128M buildpacks: - https://github.com/cloudfoundry/nodejs-buildpack最終的なディレクトリ構成は以下の通りです。
. ├── edmx │ └── YPURCHASE_REQUEST_CREATE_SRV.edmx ├── node_modules ├── src │ ├── index.ts │ ├── PurchaseRequestController.ts │ ├── PurchaseRequestEntity.ts │ └── PurchaseRequestService.ts ├── vdm ├── manifest.yml ├── package.json ├── package-lock.json └── tsconfig.jsonビルドまで完了したらSCP Cloud Foundryにデプロイします。ルート階層でcf pushを行いましょう。manifest.yml内に記載したアプリ名を指定すればOKです。
$ cf push ynodejs_purchase_requestこれでS/4HANA上で購買依頼伝票を作成するためのAPIが完成しました。
※「2. Pythonアプリの実装」のpurchase_create.py内でStubで置いていたAPIのエンドポイントを書き換えてください。完成
#夏の自由研究2020 #chillSAP 8月24日分の投稿です。
— サイコパスのりお (@norio_psycho) August 23, 2020
Qiita記事「コロナで危険度の高い人を見つけたらイ〇ジンをぶっ掛けて治療するアプリ作ってみた」のデモ動画をご共有いたします、、!#コロナ #イソジン #ご気分を害した方がいらっしゃったら #誠に申し訳ございません #一切の責任は取りかねます pic.twitter.com/ZxO3xmQYH2同期の間で老け顔キャラだったのですが、機械にまで実年齢+7歳くらいで評価されたのは地味にショックでした。