Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save diogotito/da45ddbdaef8fe799ac535d912cbe2d5 to your computer and use it in GitHub Desktop.
Save diogotito/da45ddbdaef8fe799ac535d912cbe2d5 to your computer and use it in GitHub Desktop.
Scriptable: MF910 Dashboard
{
"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