mirror of
https://github.com/viarotel-org/escrcpy.git
synced 2025-01-19 01:24:12 +01:00
perf: 📸 Support viewing real-time images and power information of the device
This commit is contained in:
parent
d5caaa915c
commit
d262adf54d
91
electron/exposes/adb/helpers/battery/index.js
Normal file
91
electron/exposes/adb/helpers/battery/index.js
Normal file
@ -0,0 +1,91 @@
|
||||
import { camelCase } from 'lodash-es'
|
||||
/**
|
||||
* Parse ADB battery dump data into a structured object
|
||||
*
|
||||
* @param {string} dumpData - Raw battery dump data from ADB
|
||||
* @returns {Object} Parsed and normalized battery data
|
||||
*
|
||||
* // Example usage:
|
||||
* const dumpData = fs.readFileSync('battery-dump.txt', 'utf8');
|
||||
* const batteryInfo = parseBatteryDump(dumpData);
|
||||
* console.log(batteryInfo);
|
||||
*
|
||||
*/
|
||||
export function parseBatteryDump(dumpData) {
|
||||
// Helper to convert string values to appropriate types
|
||||
const parseValue = (value) => {
|
||||
value = value.trim()
|
||||
|
||||
// Handle booleans
|
||||
if (value.toLowerCase() === 'true')
|
||||
return true
|
||||
if (value.toLowerCase() === 'false')
|
||||
return false
|
||||
|
||||
// Handle numbers
|
||||
if (!Number.isNaN(Number(value)) && value !== '') {
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const result = {
|
||||
raw: {},
|
||||
computed: {},
|
||||
}
|
||||
|
||||
// Split into lines and process each line
|
||||
const lines = dumpData.split('\n').filter(line => line.trim())
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.includes('Battery Service state:')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
const separatorIndex = line.indexOf(':')
|
||||
if (separatorIndex === -1)
|
||||
return
|
||||
|
||||
const key = line.substring(0, separatorIndex).trim()
|
||||
const value = line.substring(separatorIndex + 1).trim()
|
||||
|
||||
// Skip empty key/values
|
||||
if (!key || !value)
|
||||
return
|
||||
|
||||
// Convert key to camelCase
|
||||
const camelKey = camelCase(key)
|
||||
|
||||
// Add to appropriate section
|
||||
result.raw[camelKey] = parseValue(value)
|
||||
})
|
||||
|
||||
// Add computed values
|
||||
result.computed = {
|
||||
// Convert temperatures to actual degrees
|
||||
temperatureCelsius: result.raw.temperature ? result.raw.temperature / 10 : null,
|
||||
|
||||
// Battery percentage normalized to 0-100
|
||||
batteryPercentage: result.raw.level || 0,
|
||||
|
||||
// Charging state as string
|
||||
isCharging: !!(result.raw.usbPowered || result.raw.acPowered
|
||||
|| result.raw.wirelessPowered || result.raw.dockPowered),
|
||||
|
||||
// Voltage in V instead of mV
|
||||
voltageV: result.raw.voltage ? result.raw.voltage / 1000 : null,
|
||||
|
||||
// Power source type
|
||||
powerSource: result.raw.acPowered
|
||||
? 'AC'
|
||||
: result.raw.usbPowered
|
||||
? 'USB'
|
||||
: result.raw.wirelessPowered
|
||||
? 'Wireless'
|
||||
: result.raw.dockPowered ? 'Dock' : 'Battery',
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -9,6 +9,8 @@ import { Adb } from '@devicefarmer/adbkit'
|
||||
import dayjs from 'dayjs'
|
||||
import { uniq } from 'lodash-es'
|
||||
import adbConnectionMonitor from './helpers/adbConnectionMonitor/index.js'
|
||||
import { streamToBase64 } from '$electron/helpers/index.js'
|
||||
import { parseBatteryDump } from './helpers/battery/index.js'
|
||||
|
||||
const exec = util.promisify(_exec)
|
||||
|
||||
@ -124,6 +126,8 @@ const getDeviceIP = async (id) => {
|
||||
const tcpip = async (id, port = 5555) => client.getDevice(id).tcpip(port)
|
||||
|
||||
const screencap = async (deviceId, options = {}) => {
|
||||
const { returnBase64 = false } = options
|
||||
|
||||
let fileStream = null
|
||||
try {
|
||||
const device = client.getDevice(deviceId)
|
||||
@ -138,6 +142,11 @@ const screencap = async (deviceId, options = {}) => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (returnBase64) {
|
||||
const base64 = await streamToBase64(fileStream)
|
||||
return base64
|
||||
}
|
||||
|
||||
const fileName = `Screencap-${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.png`
|
||||
const savePath = options.savePath || path.resolve('../', fileName)
|
||||
|
||||
@ -278,10 +287,24 @@ async function connectCode(password, options = {}) {
|
||||
pair,
|
||||
connect,
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
async function battery(id) {
|
||||
try {
|
||||
const res = await deviceShell(id, 'dumpsys battery')
|
||||
|
||||
const value = parseBatteryDump(res)
|
||||
|
||||
return value
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(error?.message || error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
const bin = appStore.get('common.adbPath') || adbPath
|
||||
|
||||
@ -312,4 +335,5 @@ export default {
|
||||
watch,
|
||||
readdir,
|
||||
connectCode,
|
||||
battery,
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { join, resolve } from 'node:path'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { contextBridge } from 'electron'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
@ -68,3 +69,19 @@ export function loadPage(win, prefix = '') {
|
||||
win.loadFile(join(process.env.DIST, prefix, 'index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
export function streamToBase64(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = []
|
||||
stream.on('data', (chunk) => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks)
|
||||
resolve(buffer.toString('base64'))
|
||||
})
|
||||
stream.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
18
package.json
18
package.json
@ -33,7 +33,7 @@
|
||||
"@viarotel-org/unocss-preset-shades": "0.8.2",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"bonjour-service": "^1.3.0",
|
||||
"bonjour-service": "1.3.0",
|
||||
"dayjs": "1.11.11",
|
||||
"electron": "33.0.2",
|
||||
"electron-builder": "25.1.8",
|
||||
@ -43,7 +43,7 @@
|
||||
"electron-log": "5.2.0",
|
||||
"electron-store": "9.0.0",
|
||||
"electron-updater": "6.3.9",
|
||||
"element-plus": "2.9.0",
|
||||
"element-plus": "2.9.1",
|
||||
"eslint": "9.13.0",
|
||||
"fix-path": "4.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
@ -52,17 +52,17 @@
|
||||
"nanoid": "5.0.7",
|
||||
"pinia": "2.1.7",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"pinyin-pro": "3.26.0",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-nested": "6.0.1",
|
||||
"postcss-scss": "4.0.9",
|
||||
"qrcode": "^1.5.4",
|
||||
"rimraf": "^6.0.1",
|
||||
"simple-git": "^3.27.0",
|
||||
"qrcode": "1.5.4",
|
||||
"rimraf": "6.0.1",
|
||||
"simple-git": "3.27.0",
|
||||
"unocss": "0.62.3",
|
||||
"unplugin-auto-import": "0.18.3",
|
||||
"unplugin-vue-components": "0.27.4",
|
||||
"unplugin-vue-router": "^0.10.9",
|
||||
"unplugin-vue-router": "0.10.9",
|
||||
"vite": "5.1.5",
|
||||
"vite-plugin-electron": "0.28.8",
|
||||
"vite-plugin-electron-renderer": "0.14.6",
|
||||
@ -70,8 +70,8 @@
|
||||
"vue": "3.4.21",
|
||||
"vue-command": "35.2.1",
|
||||
"vue-i18n": "9.13.1",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-screen": "^2.4.0",
|
||||
"vue-router": "4.5.0",
|
||||
"vue-screen": "2.4.2",
|
||||
"which": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"common.yes": "Yes",
|
||||
"common.no": "No",
|
||||
"common.cancel": "Cancel",
|
||||
"common.confirm": "Confirm",
|
||||
"common.restart": "Restart",
|
||||
@ -67,6 +69,11 @@
|
||||
"device.status.connected": "Connected",
|
||||
"device.status.offline": "Offline",
|
||||
"device.status.unauthorized": "Unauthorized",
|
||||
"device.battery": "Device Battery",
|
||||
"device.isCharging": "Charging Status",
|
||||
"device.temperature": "Device Temperature",
|
||||
"device.powerSource": "Power Source",
|
||||
"device.voltage": "Device Voltage",
|
||||
|
||||
"device.task.name": "Scheduled Task",
|
||||
"device.task.tips": " Note: Please ensure that your computer stays awake, otherwise scheduled tasks will not be executed properly.",
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"common.yes": "Да",
|
||||
"common.no": "Нет",
|
||||
"common.cancel": "Отмена",
|
||||
"common.confirm": "Подтвердить",
|
||||
"common.restart": "Перезапустить",
|
||||
@ -67,6 +69,11 @@
|
||||
"device.status.connected": "Подключено",
|
||||
"device.status.offline": "Не в сети",
|
||||
"device.status.unauthorized": "Не авторизован",
|
||||
"device.battery": "Уровень заряда устройства",
|
||||
"device.isCharging": "Состояние зарядки",
|
||||
"device.temperature": "Температура устройства",
|
||||
"device.powerSource": "Источник питания",
|
||||
"device.voltage": "Напряжение устройства",
|
||||
|
||||
"device.task.name": "Запланированная задача",
|
||||
"device.task.tips": "Примечание: Пожалуйста, убедитесь, что ваш компьютер не переходит в спящий режим, иначе запланированные задачи не будут выполнены правильно.",
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"common.yes": "是",
|
||||
"common.no": "否",
|
||||
"common.cancel": "取消",
|
||||
"common.confirm": "确认",
|
||||
"common.restart": "重启",
|
||||
@ -67,6 +69,11 @@
|
||||
"device.status.offline": "已离线",
|
||||
"device.status.unauthorized": "未授权",
|
||||
"device.status.connected": "已连接",
|
||||
"device.battery": "设备电量",
|
||||
"device.isCharging": "充电状态",
|
||||
"device.temperature": "设备温度",
|
||||
"device.powerSource": "驱动来源",
|
||||
"device.voltage": "设备电压",
|
||||
|
||||
"device.task.name": "计划任务",
|
||||
"device.task.tips": " 注意:请确保你的计算机保持唤醒状态,否则计划任务将无法被正常执行。",
|
||||
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"common.yes": "是",
|
||||
"common.no": "否",
|
||||
"common.cancel": "取消",
|
||||
"common.confirm": "確認",
|
||||
"common.restart": "重新啟動",
|
||||
@ -67,6 +69,11 @@
|
||||
"device.status.connected": "已連接",
|
||||
"device.status.offline": "已離線",
|
||||
"device.status.unauthorized": "未授權",
|
||||
"device.battery": "設備電量",
|
||||
"device.isCharging": "充電狀態",
|
||||
"device.temperature": "設備溫度",
|
||||
"device.powerSource": "驅動來源",
|
||||
"device.voltage": "設備電壓",
|
||||
|
||||
"device.task.name": "計劃任務",
|
||||
"device.task.tips": "注意:請確保您的電腦保持唤醒状态,否則計劃任務將無法正常執行。",
|
||||
|
@ -2,31 +2,113 @@
|
||||
<el-popover
|
||||
ref="popoverRef"
|
||||
placement="right"
|
||||
:width="450"
|
||||
trigger="hover"
|
||||
:width="500"
|
||||
trigger="click"
|
||||
popper-class="!p-0 !overflow-hidden !rounded-xl"
|
||||
@before-enter="onBeforeEnter"
|
||||
@hide="onHide"
|
||||
>
|
||||
<template #reference>
|
||||
<el-link type="primary" :underline="false" icon="InfoFilled" class="mr-1"></el-link>
|
||||
</template>
|
||||
|
||||
<el-descriptions class="!w-full" border label-width="80">
|
||||
<el-descriptions-item :label="$t('device.id')">
|
||||
{{ device.id }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-loading="loading" :element-loading-text="$t('common.loading')" :class="connectFlag ? 'h-60' : ''" class="flex items-stretch p-2 space-x-2">
|
||||
<div v-if="connectFlag" class="flex-none pb-1">
|
||||
<el-image :src="deviceInfo.screencap" :preview-src-list="[deviceInfo.screencap]" class="!h-full !overflow-hidden !rounded-xl !shadow" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 w-0 overflow-auto">
|
||||
<el-descriptions border :column="1" class="el-descriptions--custom">
|
||||
<el-descriptions-item :label="$t('device.id')">
|
||||
{{ deviceInfo.id }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<template v-if="deviceInfo.battery">
|
||||
<el-descriptions-item :label="$t('device.battery')">
|
||||
{{ deviceInfo.battery.batteryPercentage ? `${deviceInfo.battery.batteryPercentage}%` : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.isCharging')">
|
||||
{{ deviceInfo.battery.isCharging ? $t('common.yes') : $t('common.no') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.temperature')">
|
||||
{{ deviceInfo.battery.temperatureCelsius ? `${deviceInfo.battery.temperatureCelsius}℃` : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.powerSource')">
|
||||
{{ deviceInfo.battery.powerSource || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('device.voltage')">
|
||||
{{ deviceInfo.battery.voltageV ? `${deviceInfo.battery.voltageV}v` : '-' }}
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getDictLabel } from '$/dicts/helper'
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const deviceInfo = ref({
|
||||
screencap: void 0,
|
||||
battery: void 0,
|
||||
})
|
||||
|
||||
const connectFlag = computed(() => ['device'].includes(props.device.status))
|
||||
|
||||
const screencapTimer = ref()
|
||||
|
||||
async function onBeforeEnter() {
|
||||
Object.assign(deviceInfo.value, { ...props.device })
|
||||
|
||||
if (!connectFlag.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
await Promise.allSettled([getScreencap(), getBattery()])
|
||||
|
||||
screencapTimer.value = setInterval(() => {
|
||||
getScreencap()
|
||||
getBattery()
|
||||
}, 5 * 1000)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function getScreencap() {
|
||||
const screencap = await window.adb.screencap(props.device.id, { returnBase64: true })
|
||||
|
||||
Object.assign(deviceInfo.value, { screencap: `data:image/png;base64,${screencap}` })
|
||||
}
|
||||
|
||||
async function getBattery() {
|
||||
const battery = await window.adb.battery(props.device.id)
|
||||
|
||||
Object.assign(deviceInfo.value, { battery: battery.computed })
|
||||
}
|
||||
|
||||
function onHide() {
|
||||
clearInterval(screencapTimer.value)
|
||||
|
||||
deviceInfo.value = {}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
:deep() .el-descriptions--custom .el-descriptions__label {
|
||||
@apply !truncate !w-0;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user