/** Electron / Node.js に由来する機能に関する諸々 */ /** ブラウザ動作時にもちょびっと対応 */ 'use strict' // 定数 const ugj_const = { app_name: 'ocoge', mascot_dirname: 'img', mascot_defname: 'tamachee.png', library_dirname: 'lib', document_root: 'Documents', tmp_dir: '.ocogeclub/tmp', executable_path: '.ocogeclub/apps/', blocks_sensors_dir: 'blocks/sensors/', localStorage_fname: 'ocoge.json', error_ja_all: 'エラーが発生しました。\n『おこげ倶楽部』までお問い合わせください。', pig: 'pigpio', lg: 'lgpio', // 対応未定 i2c_defbus: '1', // 文字列リテラルで指定 lang: 'js', dev_hash: '4e9205f9b7e571bec1aa52ab7871f420684fcf96149672a4d550a95863d6b072' } /** クラス elUtil ****************************************************************** */ // Electron 動作用 class elUtil { constructor() { this.path = require('path') //window.ocogeapi.path this.fs = require('fs') //window.ocogeapi.fs this.ipcRenderer = require('electron').ipcRenderer //window.ocogeapi.electron_ipcRenderer this.shell = require('electron').shell //window.ocogeapi.electron_shell this.saveFilepath = null; this.wsChanged = false; this.children = []; this.gpio_backend = ugj_const.pig; this.i2c_bus = ugj_const.i2c_defbus; this.lang = ugj_const.lang; this.doc_root = this.path.join(process.env["HOME"], ugj_const.document_root); this.doc_current = this.path.join(process.env["HOME"], ugj_const.document_root); this.executable_path = this.path.join(process.env["HOME"], ugj_const.executable_path); // this.blocks_sensors_dir = this.path.join(process.env["HOME"], ugj_const.blocks_sensors_dir); this.tmp_dir = this.path.join(process.env["HOME"], ugj_const.tmp_dir); const EventEmitter = require('events'); this.ugjEmitter = new EventEmitter(); } // static init = async () => { // return new elUtil(await elUtil.get_app_path()); // } // test async init() { this.app_path = await this.ipcRenderer.invoke('get_app_path'); this.mascotFilePath = this.path.join(this.app_path, ugj_const.mascot_dirname, ugj_const.mascot_defname); this.library_path = this.path.join(this.app_path, ugj_const.library_dirname); this.blocks_sensors_dir = this.path.join(this.app_path, ugj_const.blocks_sensors_dir); this.loadSensorblocks(); } // センサーブロックのロード loadSensorblocks() { // ディレクトリの有無 if (!this.fs.existsSync(this.blocks_sensors_dir)) return; // ブロックデータ格納ディレクトリのリスト const allDirents = this.fs.readdirSync(this.blocks_sensors_dir, { withFileTypes: true }); const blocks_list = allDirents.filter(dirent => dirent.isDirectory()).map(({ name }) => name); // センサーカテゴリのインスタンス let category_sensors = workspace.getToolbox().getToolboxItemById('category_sensors'); let flyout_contents = []; // フライアウトのjsonのリスト for (let sensor_dir of blocks_list) { //ディレクトリ巡り if (sensor_dir.charAt(0) == '.') continue; //隠しディレクトリをスキップ // フライアウトのjsonを取得してパース、リストに追加 let fname = this.path.join(this.blocks_sensors_dir, sensor_dir, 'index.json'); let json_text = this.fs.readFileSync(fname); let obj = JSON.parse(json_text); flyout_contents = flyout_contents.concat(obj); // ブロック定義のスクリプト要素をbody要素の最後に追加 fname = this.path.join(this.blocks_sensors_dir, sensor_dir, 'index.js'); let script = document.createElement('script'); script.type = 'text/javascript'; script.src = fname; document.body.appendChild(script); } let lastline = [{ "kind": "label", "text": " ", "web-line": "4.0", "web-line-width": "200" }]; flyout_contents = flyout_contents.concat(lastline); // センサーカテゴリのフライアウトをアップデート category_sensors.updateFlyoutContents(flyout_contents); } // 0で数値の桁合わせ : NUM=値 LEN=桁数 zeroPadding(NUM, LEN) { return (Array(LEN).join('0') + NUM).slice(-LEN); } // 現在の日付時刻から workspace フォルダ内のユニークなファイルパスを作成 getUniqueFilepath() { let today = new Date(); let filename = today.getFullYear() + '-' + this.zeroPadding((today.getMonth() + 1), 2) + '-' + this.zeroPadding(today.getDate(), 2) + '-' + this.zeroPadding(today.getHours(), 2) + '-' + this.zeroPadding(today.getMinutes(), 2) + '-' + this.zeroPadding(today.getSeconds(), 2); let filepath = this.path.join(this.doc_current, filename); return filepath; } // リンクを外部ブラウザで開く openURL(url) { this.shell.openExternal(url); } // saveFilepath を更新 // ウィンドウタイトルバーテキストを変更 setSaveFilepath(filepath) { this.saveFilepath = filepath; this.setWsChanged(false); } // ワークスペースが変更された・保存された // ウィンドウタイトルバーテキストを変更 setWsChanged(changed) { let title; this.wsChanged = changed; if (this.saveFilepath) title = this.saveFilepath + ' - ' + ugj_const.app_name; else title = ugj_const.app_name; if (changed) title = '*' + title; this.ipcRenderer.send('set_title', title); } // 保存ファイルプロパティを更新 newFile() { this.setSaveFilepath(null); } // ワークスペースファイル読み込みの一連の動作のラッパ async loadWsFile() { let filepath = await this.openFile('xml', this.doc_current); if (filepath.length > 0) { if (this.saveFilepath === null) { this.setSaveFilepath(filepath); } //読み込みに失敗してもsaveFilepathが更新されてしまうのはちょっと具合が悪いかも this.doc_current = this.path.dirname(filepath); return this.readFromFile(filepath); } else { return ''; } } // その他ファイル読み込みの一連の動作のラッパ async loadFile(ext) { let filepath = await this.openFile(ext, this.doc_current); if (filepath.length > 0) { this.doc_current = this.path.dirname(filepath); return this.readFromFile(filepath); } else { return ''; } } async selectMascotFile() { return await this.openFile('png', ugj_const.mascot_path); } // オープンファイルダイアログ async openFile(ext, dpath) { let title = 'Select a file'; let filter; if (ext == 'xml') { filter = { name: 'XML - Extensible Markup Language', extensions: ['xml'] }; } else if (ext == 'js') { filter = { name: 'JS - JavaScript', extensions: ['js'] }; } else if (ext == 'png') { filter = { name: 'PNG - Portable Network Graphics', extensions: ['png'] }; } else { filter = { name: 'text file', extensions: ['txt'] }; } let filepaths = await this.ipcRenderer.invoke('open_dialog', title, dpath, filter); if (filepaths == undefined) { return ''; } else { return filepaths[0]; } } // ファイルからデータを読み込み readFromFile(filepath) { let data = ''; try { data = this.fs.readFileSync(filepath, 'utf-8'); } catch (err) { console.log(err); } return data; } // テキストファイル読み込み: 外部スクリプト動的読み込みに使用 readTextFile(filepath) { return this.readFromFile(filepath); } // ワークスペースファイル保存の一連の動作のラッパ async saveWsFile(data) { if (this.saveFilepath === null) { let filepath = await this.selectSaveFile('xml'); if (filepath === undefined) { //キャンセル return undefined; } else { this.setSaveFilepath(filepath); } //これも保存が成功したら変更するようにすべきかしら } else this.setWsChanged(false); return this.writeToFile(this.saveFilepath, data); } // その他ファイル保存の一連の動作のラッパ async saveFile(data, ext) { let filepath = await this.selectSaveFile(ext); if (filepath === undefined) { //キャンセル return undefined; } return this.writeToFile(filepath, data); } // ファイル保存ダイアログ async selectSaveFile(ext) { let title = '保存先を決定してください'; let filter, filter_name; let defName; if (ext == 'xml') { filter = { name: 'xml file', extensions: ['xml'] }; defName = this.getUniqueFilepath() + '.xml'; } else if (ext == 'js' || ext == 'py') { if (ext == 'js') filter_name = 'javascript file'; else filter_name = 'python file' filter = { name: filter_name, extensions: [ext] }; // ワークスペース保存名がある場合、それをベースにファイル名の候補を決める if (this.saveFilepath === null) { defName = this.getUniqueFilepath() + '.' + ext; } else { let dirname = this.path.dirname(this.saveFilepath); let basename = this.path.basename(this.saveFilepath, '.xml'); defName = this.path.join(dirname, basename) + '.' + ext; } } else { filter = { name: 'text file', extensions: ['txt'] }; } let filename = await this.ipcRenderer.invoke('save_dialog', title, defName, filter); if (filename) this.doc_current = this.path.dirname(filename); return filename; } // ファイル書き込み writeToFile(filepath, data) { try { this.fs.writeFileSync(filepath, data); return true; } catch (err) { return false; } } // GPIO 関連:リロードでGPIOをロックしたままハンドルを失うのを防ぐ cleanupGPIO() { // this.ugjEmitter.emit('device_stop');//デバイス停止イベント require('@ocoge.club/' + this.gpio_backend).close_all_handle(); } // 設定(保存ファイルパスと未保存フラグ)をローカルストレージに保存 savePrefsToLS() { let wc = '0'; if (this.wsChanged) wc = '1'; // const ser_port = document.getElementById('dlgPort').value; let o = { 'saveFilepath': this.saveFilepath, 'wsChanged': wc, 'mascotFilePath': this.mascotFilePath, 'doc_current': this.doc_current, 'i2c_bus': this.i2c_bus, 'lang': this.lang }; let s = JSON.stringify(o); localStorage.setItem(ugj_const.localStorage_fname, s); } // 設定(保存ファイルパスと未保存フラグ)をローカルストレージからロード loadPrefsFromLS() { let s = localStorage.getItem(ugj_const.localStorage_fname); if (s !== null) { let o = JSON.parse(s); this.setSaveFilepath(o.saveFilepath); if (o.wsChanged == '0') this.setWsChanged(false); else this.setWsChanged(true); if (o.mascotFilePath) this.setMascotFilePath(o.mascotFilePath); if (o.doc_current) this.doc_current = o.doc_current; if (o.i2c_bus) { this.i2c_bus = o.i2c_bus; this.ipcRenderer.send('i2c_check_menu', 'i2c-' + this.i2c_bus); } if (o.lang) { this.setLang(o.lang); this.ipcRenderer.send('lang_check_menu', this.lang); } } } // マスコット画像パスをプロパティにセット setMascotFilePath(fpath) { this.mascotFilePath = fpath; } getMascotFilePath() { return this.mascotFilePath; } // i2cバス番号変更 setI2cbusNo(n) { this.i2c_bus = n; } // 言語変更 setLang(l) { this.lang = l; const exp = document.getElementById('dlgExport'); if (l == 'js') { exp.innerText = 'ファイルへ保存'; exp.title = 'ソースコードをファイルに保存します。'; } else { exp.innerText = 'デプロイ'; exp.title = 'ソースコードをデバイスに転送します。'; } } // PyBfm を起動 launchPyBfm() { let script_path = this.path.join(elutil.library_path, 'pybfm.py'); require('child_process').spawn('python3', [script_path]); } // ファイル名にアプリケーションのドキュメントルートまでのパスをつけて返す getDocPath(filename) { return this.path.join(this.appDocRoot, filename); } } // ブラウザ動作用 class brUtil { constructor() { // GPIOブロックは使えません this.gpio_backend = ugj_const.pig; this.lang = 'js'; } // マスコット getMascotFilePath() { return `./img/${ugj_const.mascot_defname}`; } //ワークスペースのダウンロード saveWsFile(xml_text) { let blob = new Blob([xml_text], { "type": "text/xml" }); const downLoadLink = document.createElement("a"); document.body.appendChild(downLoadLink); downLoadLink.download = 'workspace.xml'; downLoadLink.href = URL.createObjectURL(blob); downLoadLink.click(); downLoadLink.parentElement.removeChild(downLoadLink); return true; } //ワークスペースのインポート loadWsFile() { const fileInputEl = document.createElement('input'); document.body.appendChild(fileInputEl); fileInputEl.type = 'file'; fileInputEl.addEventListener('change', ev => { let reader = new FileReader(); reader.readAsText(ev.target.files[0]); reader.addEventListener('load', () => { let xml = Blockly.Xml.textToDom(reader.result); Blockly.Xml.domToWorkspace(xml, workspace); }); }); fileInputEl.click(); fileInputEl.parentElement.removeChild(fileInputEl); return ''; } // index.jsから万一呼ばれても何もしない関数 openURL() { ; } selectMascotFile() { ; } setMascotFilePath(fname) { ; } saveFile() { ; } savePrefsToLS() { ; } loadPrefsFromLS() { ; } newFile() { ; } setWsChanged() { ; } killAllChildren() { ; } cleanupGPIO() { ; } path = { join: function (a = '', b = '', c = '', d = '', e = '') { return a + b + c + d + e; } } } // Electron 動作 / ブラウザ動作自動判別 // const is_el = (typeof window.ocogeapi !== 'undefined') const is_el = (typeof require === 'function'); // utilクラスのインスタンスを返す const elUtil_new = () => { if (is_el) { let el = new elUtil; el.init(); return el; } else return new brUtil; } // "require" for web browsers if contextIsolation is false && nodeIntegration is true: if (!is_el) { var require = module_name => { let block; switch (module_name) { case '@tensorflow/tfjs': block = 'TensorFlow'; break; case '@tensorflow-models/blazeface': block = '顔認識'; break; case 'axios': block = 'URLを取得'; break; case 'nodemailer': block = 'メール送信'; break; case '@ocoge.club/pigpio': block = 'GPIO'; break; case 'fs': block = 'ファイル'; break; case 'path': block = 'キャンバス保存'; break; case 'child_process': block = '外部プログラム実行'; break; default: throw new Error(ugj_const.error_ja_all); } throw `ブロック「${block}」は、Web体験版ではご利用になれません。\n詳しくは https://ocoge.club/ をご覧ください。`; } }