Created
March 13, 2023 16:41
-
-
Save diogotito/da45ddbdaef8fe799ac535d912cbe2d5 to your computer and use it in GitHub Desktop.
Scriptable: MF910 Dashboard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"always_run_in_app" : false, | |
"icon" : { | |
"color" : "pink", | |
"glyph" : "wifi" | |
}, | |
"name" : "MF910 Dashboard: iPhone layout", | |
"script" : "\/\/ -- Networking utils ------------------------------------------------------\n\nconst query = opts => Object.entries(opts)\n .map(([k, v]) => `${k}=${v}`)\n .join('&')\n\nconst routerDo = (goformId, options={}) => Object.assign(new Request(''), {\n url: 'http:\/\/192.168.0.1\/goform\/goform_set_cmd_process',\n headers: {Referer: 'http:\/\/192.168.0.1\/index.html'},\n method: 'POST',\n allowInsecureRequest: true,\n body: query({isTest: false, ...options, goformId})\n}).loadJSON()\n\nconst routerGet = (...cmd) => Object.assign(new Request(''), {\n url: 'http:\/\/192.168.0.1\/goform\/goform_get_cmd_process?' +\n query({...(cmd.length > 1 ? {multi_data: 1} : {}), cmd}),\n headers: {Referer: 'http:\/\/192.168.0.1\/index.html'},\n}).loadJSON()\n .then(res => cmd.length === 1\n ? res[cmd[0]]\n : res)\n\n\n\/\/ -- Less boring UITable API -----------------------------------------------\n\nUITableRow.prototype.assign =\nUITableCell.prototype.assign = function(props) {\n return Object.assign(this, props)\n}\n\nUITable.prototype.row = function(props={}) {\n const newRow = new UITableRow().assign(props)\n this.addRow(newRow)\n return newRow\n}\n\nconst wait = ms => new Promise(cb => Timer.schedule(ms, false, cb))\n\n\n\/\/ -- Transition engine -----------------------------------------------------\n\/\/ TODO só há uma tabela. Embrulhar isto numa closure como no rowLogger abaixo\n\/\/ ou tornar a tabela global\n\nconst transitions = []\nlet processTransitions\n\n(async function runTransitions() {\n const tablesToReload = new Set()\n const transitionsToDelete = [] \/\/ Índices da array transitions\n \n for (;;) {\n if (transitions.length === 0) {\n \/\/ Espera que alguém chame \"processTransitions()\"\n await new Promise( resolve => processTransitions = resolve )\n }\n \n for (const [i, {table, coroutine}] of transitions.entries()) {\n \/\/ Avança a transição 1 passo\n const { done } = coroutine.next()\n \n if (done) {\n transitionsToDelete.push(i)\n }\n \n tablesToReload.add(table)\n }\n\n \/\/ Atualizar todas as tabelas com linhas animadas\n tablesToReload.forEach( table => table.reload() )\n tablesToReload.clear()\n \n \/\/ Apagar as transições que já terminaram\n for (const index of transitionsToDelete) {\n transitions.splice(index, 1)\n }\n transitionsToDelete.length = 0\n \n await wait(30)\n }\n})()\n\nfunction addTransition(table, coroutine) {\n transitions.push({table, coroutine})\n processTransitions()\n}\n\nconst TRANSITIONS = {\n *slideIn(row, speed) {\n const targetHeight = row.height\n for (row.height = 0; row.height < targetHeight; row.height += speed) {\n yield\n }\n row.height = targetHeight\n },\n \n *slideOut(table, row, speed) {\n for ( ; row.height > 0; row.height -= speed) {\n yield\n }\n table.removeRow(row)\n }\n}\n\nObject.assign(UITable.prototype, {\n slideIn(row, speed=16) {\n addTransition(this, TRANSITIONS.slideIn(row, speed))\n },\n \n slideOut(row, speed=9) {\n addTransition(this, TRANSITIONS.slideOut(this, row, speed))\n }\n})\n\n\n\/\/ -- Templates for rows and cells ------------------------------------------\n\nconst STYLES = {\n space: {\n height: 44\n },\n border: {\n height: 1,\n backgroundColor: new Color('888888', 0.2)\n },\n logRow: {\n height: 33,\n backgroundColor: new Color('cceeaa', 0.05)\n },\n logCell: {\n titleColor: Color.gray()\n }\n}\n\n\n\/\/ -- Row logger ------------------------------------------------------------\n\nconst rowLogger = (table, timeout) =>\n msg => {\n let msgRow = table.row(STYLES.logRow)\n let msgCell = msgRow.addText(msg).assign(STYLES.logCell)\n table.reload()\n \n let alreadyDismissed = false\n let dismissedCb = () => {}\n \n function dismiss() {\n alreadyDismissed = true\n dismissedCb()\n }\n \n table.slideIn(msgRow)\n \n wait(timeout).then(() => {\n if (!alreadyDismissed) {\n dismissedCb()\n table.slideOut(msgRow)\n }\n })\n \n \/\/ Return references to the row and the cell alongside convenient\n \/\/ methods to style them in-place, dismiss the row manually and\n \/\/ schedule a function to run when the row gets dismissed\n return {\n row: msgRow,\n cell: msgCell,\n rowStyle(style) { this.row.assign(style); return this },\n cellStyle(style) { this.cell.assign(style); return this },\n sink() { table.removeRow(this.row); table.addRow(this.row);\n return this },\n slideOut() { dismiss(); return table.slideOut(this.row) },\n remove() { dismiss(); table.removeRow(this.row) },\n onDismiss(cb) { dismissedCb = cb.call.bind(cb, this, this);\n return this }\n }\n }\n\n\/\/ -----------------------------------------------------------------------------\n\ntry {\n let UI = new UITable()\n var LOG = rowLogger(UI, 2000),\n l\n \n l = LOG(\"Logging in...\")\n res = await routerDo('LOGIN', {password: 'YWRtaW4%3D'})\n l.row.addText(JSON.stringify(res))\n UI.present(true)\n \n const station_list = await routerGet('station_list')\n populateStationList(UI, station_list)\n l = void l.sink().slideOut()\n UI.row(STYLES.space)\n UI.reload()\n \n l = LOG('Fetching stats')\n const router_stats = await routerGet(\n 'network_provider',\n 'signalbar',\n 'battery_charging',\n 'battery_vol_percent',\n\/\/ 'realtime_tx_bytes',\n\/\/ 'realtime_rx_bytes',\n\/\/ 'realtime_tx_thrpt',\n\/\/ 'realtime_rx_thrpt',\n 'monthly_rx_bytes',\n 'monthly_tx_bytes',\n\/\/ 'ppp_status',\n )\n l = void l.slideOut()\n populateStats(UI, router_stats)\n UI.row(STYLES.space)\n UI.reload()\n \n populateCommands(UI)\n UI.reload()\n} catch (e) {\n console.error(\"!! ERRO !!\")\n throw e\n}\n\n\n\/\/ -- Tabela de dispositivos ligados ----------------------------------------\n\nfunction populateStationList(table, station_list) {\n table.row().assign(STYLES.border)\n const header = table.row({\n isHeader: true,\n backgroundColor: new Color('888888', 0.2),\n })\n table.row().assign(STYLES.border)\n \n header.addText('#').widthWeight = 1\n header.addText('IP Address', 'MAC Address').widthWeight = 7\n header.addText('Hostname').widthWeight = 8\n \n for (const [i, {ip_addr, hostname, mac_addr}] of station_list.entries()) {\n const row = table.row({\n backgroundColor: new Color('888888', 0.05 * (i % 2)),\n onSelect() { Safari.openInApp('http:\/\/' + ip_addr, false) },\n dismissOnSelect: false\n })\n row.addText(i + 1 + \"\").widthWeight = 1\n row.addText(ip_addr, mac_addr).assign({\n widthWeight: 7,\n titleFont: Font.regularMonospacedSystemFont(14),\n subtitleFont: Font.regularMonospacedSystemFont(11),\n })\n row.addText(hostname).widthWeight = 8\n }\n \/\/ table.row().assign(STYLES.border)\n \n}\n\n\nfunction populateStats(table, stats) {\n const\n battery = parseInt(stats.battery_vol_percent) \/ 100,\n charging = stats.battery_charging === 1,\n signal = parseInt(stats.signalbar),\n monthlyUsage = (parseInt(stats.monthly_rx_bytes) +\n parseInt(stats.monthly_tx_bytes)) \/ 10 ** 9,\n monthlyLimit = 30\n \n const row1 = table.row({height: 100, cellSpacing: 10})\n row1.addImage(drawSignalBars()).rightAligned()\n row1.addText(stats.network_provider)\n row1.addText(stats.battery_vol_percent + '%').rightAligned()\n row1.addImage(drawBattery())\n \n const row2 = table.row({height: 60, cellSpacing: 20})\n row2.addImage(drawMonthlyUsage()).rightAligned()\n row2.addText('Dados este mês',\n `${monthlyUsage.toFixed(2)} GB \/ ${monthlyLimit} GB`)\n \n \/\/ -- Desenhos ----------------------------------------------------------\n \n function drawBattery() {\n const ctx = new DrawContext()\n ctx.opaque = false\n ctx.respectScreenScale = true\n ctx.size = new Size(1150, 800)\n \n \/\/ TODO Isto não corresponde à grelha de píxeis que queria.\n \/\/ É melhor ver isto em papel quadriculado e definir umas\n \/\/ constantes.\n \n \/\/ Contorno da bateria\n \n const outline = new Path()\n outline.addRects([\n new Rect(0, 0, 950, 100), \/\/ topo\n new Rect(950, 0, 100, 200), \/\/ canto\n new Rect(950, 100, 100, 100), \/\/ reentrância\n new Rect(1000, 100, 100, 600), \/\/ polo\n new Rect(950, 600, 100, 100), \/\/ reentrância\n new Rect(950, 600, 100, 200), \/\/ canto\n new Rect(0, 700, 1000, 100), \/\/ fundo\n new Rect(0, 0, 100, 800) \/\/ lado\n ])\n \n \/\/ As 4 barras da bateria\n \n const bars = new Path()\n const barsRects = [150, 350, 550, 750]\n .map(x => new Rect(x, 150, 150, 500))\n \n for (const [i, bar] of barsRects.entries()) {\n iPct = i \/ barsRects.length\n if (iPct >= battery) {\n break\n }\n let barFract = (battery - iPct) * barsRects.length\n barFract = Math.max(0, Math.min(1, barFract))\n bar.width *= barFract\n bars.addRect(bar)\n }\n \n ctx.setFillColor(Color.green())\n ctx.setStrokeColor(Color.green())\n ctx.setLineWidth(20)\n \n ctx.addPath(outline)\n ctx.fillPath()\n \n ctx.addPath(bars)\n if (battery <= 0.20) {\n ctx.setFillColor(Color.red())\n }\n ctx.fillPath()\n \n \/\/ Stamp a bolt when charging\n if (charging) {\n ctx.setTextColor(Color.blue())\n const bolt = SFSymbol.named('bolt')\n bolt.applyBoldWeight()\n ctx.drawImageInRect(bolt.image, new Rect(200, 200, 800, 400))\n }\n \n return ctx.getImage()\n }\n \n function drawSignalBars() {\n const FG = Color.blue()\n \n const ctx = new DrawContext()\n ctx.size = new Size(400, 400)\n ctx.opaque = false\n ctx.respectScreenScale = true\n ctx.setStrokeColor(FG)\n ctx.setFillColor(FG)\n ctx.setLineWidth(10)\n \n \/\/ Signal bars\n const rect = new Rect(0, 320, 60, 40)\n for (let bar = 0; bar < 5; bar++) {\n ctx.stroke(rect)\n if (bar < signal) {\n ctx.fill(rect)\n }\n rect.x += 80\n rect.y -= 60\n rect.height += 60\n }\n \n return ctx.getImage()\n }\n \n function drawMonthlyUsage() {\n const FG = Color.brown()\n \n const ctx = new DrawContext()\n ctx.size = new Size(400, 100)\n ctx.opaque = false\n ctx.respectScreenScale = true\n ctx.setStrokeColor(FG)\n ctx.setFillColor(FG)\n ctx.setLineWidth(10)\n \n function gauge(rect, percent) {\n rect = new Rect(rect.x, rect.y, rect.width, rect.height)\n ctx.stroke(rect)\n rect.width *= percent\n ctx.fill(rect)\n }\n \n \/\/ data usage\n gauge(new Rect(0, 0, 400, 100),\n monthlyUsage \/ monthlyLimit)\n \n return ctx.getImage()\n }\n \n}\n\n\n\/\/ -- Rodapé ----------------------------------------------------------------\n\nfunction populateCommands(table) {\n const styles = {\n commandRow: {\n backgroundColor: new Color('66aacc', 0.05),\n height: 33\n },\n commandLog: {\n backgroundColor: Color.clear(),\n height: 22,\n },\n commandBorder: {\n height: 1,\n backgroundColor: new Color('66aacc', 0.2)\n },\n padding: {\n height: 11,\n },\n }\n \n table.row(styles.commandBorder)\n table.row({...styles.commandRow, ...styles.padding})\n const rows = [...Array(7)].map(() => table.row(styles.commandRow))\n rows[6].assign(styles.commandBorder)\n \n rows[0].isHeader = true\n rows[0].addText('Páginas:'\/*, 'do router'*\/)\n rows[0].addText('Ações:')\n \n \/\/ Coluna 'Páginas'\n rows[1].addButton('📄 Inicial').onTap = () =>\n Safari.openInApp('http:\/\/192.168.0.1\/index.html')\n \n rows[2].addButton('📄 Consumos').onTap = () =>\n Safari.openInApp('http:\/\/192.168.0.1\/index.html#traffic_statistics')\n \n \/\/ Coluna 'Ações'\n let _LOGS = []\n let _LOG = (msg) => ( _LOGS[_LOGS.length] = LOG(msg && '• ' + msg) )\n .rowStyle(styles.commandLog)\n .onDismiss( row => _LOGS.splice(_LOGS.indexOf(row), 1) )\n \n const switchConnection = async (goformId, logMsg=goformId) => {\n _LOG().rowStyle(styles.padding)\n _LOG(logMsg).cellStyle({ titleColor: Color.blue() })\n await routerDo(goformId, {notCallback: true})\n showConnectionStatus()\n _LOG('Feito!').cellStyle({ titleColor: Color.green() })\n _LOG().rowStyle(styles.padding)\n }\n \n let connect = switchConnection.bind(null, 'CONNECT_NETWORK', 'A ligar o router à internet...'),\n disconnect = switchConnection.bind(null, 'DISCONNECT_NETWORK', 'A DESLIGAR o router da internet...')\n \n rows[1].addButton('✔️ Ligar').onTap = connect\n rows[2].addButton('❌ Desligar').onTap = disconnect \n \n async function showConnectionStatus() {\n let ppp_status = ''\n do {\n ppp_status = await routerGet('ppp_status')\n \n rows.slice(4).forEach(row => table.removeRow(row))\n \n rows[4] = new UITableRow().assign({\n ...styles.commandRow,\n height: 66,\n onSelect() {\n if (ppp_status === \"ppp_connected\") {\n disconnect()\n } else if (ppp_status === \"ppp_disconnected\") {\n connect()\n }\n },\n dismissOnSelect: false\n })\n \n const subtitle = ppp_status.substr(4).toUpperCase()\n const cell = rows[4].addText('O router neste momento encontra-se', subtitle)\n cell.titleFont = Font.regularSystemFont(14)\n cell.subtitleFont = Font.blackRoundedSystemFont(18)\n if (ppp_status.endsWith('ing')) cell.subtitleColor = Color.blue()\n if (ppp_status.endsWith('connected')) cell.subtitleColor = Color.green()\n if (ppp_status.endsWith('disconnected')) cell.subtitleColor = Color.red()\n cell.centerAligned()\n \n rows.slice(4).forEach(row => table.addRow(row))\n _LOGS.forEach(log => log.sink())\n \n table.reload()\n \n await wait(500)\n } while (ppp_status.endsWith('ing'))\n }\n \n showConnectionStatus()\n \n rows[3].assign({ height: 1 })\n rows[5].assign({...styles.padding})\n}\n\n\n\n\n\n\n\n\n\n\n\n", | |
"share_sheet_inputs" : [ | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment