这个葱花鸡蛋煎饼,材料简单,容易做,适合作为早餐。

参考小高姐的视频:【小高姐】鸡蛋煎饼 三样食材的美味家常早餐饼

1. 材料

这个份量能煎6个饼,适合两人吃。

  • 鸡蛋,2个
  • 面粉(中筋或高筋),35g
  • 水,125g
  • 盐,2g
  • 葱花,适量

2. 做法

  • 鸡蛋、面粉和水混合并搅匀。重点要把鸡蛋搅散。可以先把鸡蛋搅散再混合,或者混合后再搅。
  • 放入盐和葱花,混合均匀。
  • 不粘锅刷薄油,开小火(约100°C),倒入1/6的份量,煎1~2分钟,翻面再煎1分钟,即可出锅。

3. 补充

  • 葱花增加香味,没有或者不喜的,可不放。
  • 可以作为卷饼的皮。

蒸水蛋是很常见的家常菜,这里主要记录材料配比和烹饪时间。

1. 材料

  • 鸡蛋,2只
  • 常温清水,150g
  • 盐,小许
  • 酱油,适量
  • 葱花,小许

2. 做法

  • 鸡蛋与清水的比例是1:1.5。按一个鸡蛋50g,所以这里清水150g。
  • 鸡蛋、清水、盐,一起搅拌均匀,再倒入碟子。水烧开后,放进去蒸。
  • 建议去除表面气泡。对口感要求高的,可以过一下筛子。蒸的时候用另一个碟子盖住,可以避免蒸汽水(倒汗水)滴下。
  • 不同材质的碟子,需要调整蒸的时间。不锈钢碟子大概蒸5~6分钟。
  • 蒸好后撒上葱花,有条件可以淋上热油激发香味。再淋上酱油调味。

3. 补充

  • 这个份量的材料上,可加入虾米 10g。虾米最好浸泡一下,洗净,再均匀放入打好的蛋液。
  • 可以加入白贝,增加鲜味。但是一定要挑选没有泥沙的。
  • 可以加入瑶柱,增加鲜味。个人不喜。

红豆是广东乃至全国甜品的常用食材。红豆沙除了是一种甜品馅料,也是一种甜汤的名称。配合刚上市的栗子,就是秋天的甜品了。加入“黑米”,纯粹误以为是“紫米”(黑糯米),不过增加了嚼劲。

1. 材料

这是两人份量。但是我喜欢刚煮好吃一半,留一半第二天再吃。

  • 红豆,100g
  • 栗子,5~10颗
  • 陈皮,一片(约25平方厘米)
  • 冰糖,适量

2. 做法

  • 红豆清洗后,加水浸泡4个小时以上。红豆比绿豆硬,泡久一点更容易“出沙”(煮烂)。
  • 红豆沥水后倒入高压锅,加3~4倍的水,加入陈皮,放入60%~70%的冰糖,压40分钟。
  • 栗子去壳后,最好切粒备用。最好先煮熟(水煮15分钟左右),特别是整颗。如果一起放高压锅里煮,容易煮烂,所以最好分开煮。
  • 高压锅泄压后倒入另一个锅,加入栗子再煮。大概煮15分钟,调整粘稠度(太稠就加水)。最后调整甜味(加冰糖),即可出锅。

3. 补充

  • 去掉栗子,就是比较传统的陈皮红豆沙了。
  • 我加入黑米 50g增加口感。最好浸泡一下,并与红豆一起放入高压锅。
  • 红豆沙加西米、汤圆等,都是比较常见的吃法。

臭草海带绿豆沙,是广东人夏天必须吃的甜品。中医认为绿豆性寒,具有清热解毒、消暑止渴的功效,适合夏天食用。但是海带和绿豆常有,臭草不常有(缺少臭草感觉没了“灵魂”)。刚好家里种了几棵,可以煲一锅。另外,近年来喜欢加上夏天特有的苹婆,给这道甜品增加口感。

1. 材料

这是一人份量。刚煮好,热的吃一半,留一半放冰箱冷藏,第二天再吃。

  • 绿豆,100g
  • 干海带,2g
  • 苹婆,5~10颗
  • 广东臭草(芸香)的叶子,若干
  • 冰糖,适量

2. 做法

  • 绿豆清洗后,加水浸泡2~3个小时。
  • 绿豆沥水后倒入高压锅,加3~4倍的水,放入60%~70%的冰糖,压40分钟。
  • 干海带浸泡至柔软(大概10分钟)后,切丝备用。
  • 苹婆去壳后,最好切粒备用。整颗的话,比较难煮透,最好先煮熟。
  • 高压锅泄压后倒入另一个锅再煮,主要调整粘稠度(太稠就加水)和加入其它食材。先加入苹婆、臭草叶,煮5分钟。再加入海带煮5分钟。最后调整甜味(加冰糖),即可出锅。

作为孩子的生日礼物,老婆带领全家去香港迪士尼乐园游玩。该乐园从2005年开业至今,差不多20年了,我们仨是第一次进去。但是整个乐园很难察觉20年的岁月痕迹,给人充满活力与快乐的感觉。

1. 总体印象

整个乐园不大,并且得益于香港开通了高铁,能够实现一天游玩不过夜,非常适合周末游。即使看完晚上的烟花,也能坐高铁回去。推荐找个普通周六,游客相对少一点,降低各个项目的排队时间。

对迪士尼的认识,是从迪士尼动画开始。但是迪士尼乐园给我的感觉,不是迪士尼动画乐园,而是具有自己特色的主题游乐场。米奇、唐老鸭,已经不是主角,而是淹没在乐园里众多卡通角色当中。其中“迷离大宅”作为原创故事的园区,利用各种机关和科技手段,展示奇幻的故事。没有熟悉的IP角色,但充满迪士尼的味道。

迪士尼乐园,对比玩过的其它乐园,最大的特点是十分强烈的沉浸感。各个园区主题鲜明,连排队的通道都没有马虎。比如“魔雪奇缘世界”,从户外的街道、城堡,到室内的排队通道、游玩项目,都是“冰雪奇缘”的世界。甚至因为乐园选址在郊外,没有高楼大厦会让游客感到“出戏”。

其游玩项目一般采用轨道小车、小船,以固定的游玩路线,保证游客的体验一致。这个有点西方快餐店的味道,保证各地的出品一致。虽然园区提供了一些刺激的机动游戏,但显然,迪士尼乐园的重点不在此。所以如果只是想玩机动游戏,建议另选其它乐园。

虽然乐园的门票不便宜,但我觉得没必要像“特种兵旅游”那样,一定要玩完所有项目。走到哪玩到哪,好好体会一下这个奇幻的乐园吧。

2. 前期准备

列出需要前期准备的工作。

购买乐园门票

  • 微信小程序“香港迪士尼乐园”可购买门票。
  • 不同时间价格不同。
  • 最好提前截图,便于进园时展示。

预约入园时间

  • 微信小程序“香港迪士尼乐园”可进行预约,包括预约入园时间、餐饮服务、主题酒店等。

开通手机流量

  • 手机开通香港漫游,或者购买香港旅客流量卡。
  • 主要用于移动支付,包括香港地铁刷卡、乐园内外的购物支付等。

规划行程路线

  • 确定交通工具和路线。
  • 建议内地高铁直达香港,再转香港地铁直达乐园。

安装迪士尼乐园手机App

  • 主要查看乐园地图和各个项目的排队等候时长。

3. 开通手机流量

在香港使用手机的移动流量,有两个方案:一是内地手机卡开通香港漫游,二是购买香港旅客流量卡。

3.1 内地SIM卡开启香港漫游

一般流量使用不多,选这个方案即可。主要是方便,开通即可使用,不用换SIM卡。但是内地的电信运营商的收费不一样,开通前最好咨询清楚。

中国联通的计费规则如下(来自联通的短信):

欢迎您抵达中国香港,中国联通伴您出境漫游放心用:
【上网】漫游流量费每天25元封顶,无需办理即可畅享3GB/天高速流量,达量限速。当天使用不足25MB按5MB/5元累加计费(更多优惠请点击https://u.10010.cn/uAbd2,或回复KTGM,根据提示进行更多产品订购)
【通话】接听电话、拨打中国大陆及漫游地均为0.96元/分钟,拨打澳台及其它国家和地区3.86元/分钟(境外拨打中国大陆请加拨+86,键盘长按“0”可拨出“+”号)
【短信】发短信回内地0.36元/条,发港澳台及其他国家和地区1.06元/条
【全球免费客服热线】+8618618610010
精品网络,漫游全球,贴心服务,联通世界。

3.2 购买香港手机卡

我选择了这个方案。淘宝买个“香港旅客流量卡”即可,一般是4G网络的5GB流量(超出流量限制后,应该只是限速),手机插上后,可以通过无线热点分享给同行的其它手机使用。

由于香港也要求手机卡实名制,最好抵达深圳后,在过关前1小时内进行登记。在香港过关后,插上该卡,拨打*103*900#进行开通启用。具体的详细操作,咨询淘宝客服,或查看相关提示短信。

4. 规划行程路线

我们从“广州南站”直达“香港西九龙站”,再转香港地铁(需要转线)到乐园。晚上沿路返回。

乘坐香港地铁时,如果带未成年的小孩,可以分别用“支付宝”和“微信”的乘车卡,一个给自己支付,另一个给孩子支付。

  • 高铁,广州南站 -> 香港西九龙站,约1小时,票价215 RMB。
  • 地铁,香港西九龙站 -> 香港迪士尼乐园,约38分钟,票价约20 RMB。

    • 2号线(屯马线),屯门方向,柯士甸 -> 南昌
    • 4号线(东涌线),东涌方向,南昌 -> 欣澳
    • 迪士尼线,迪士尼方向,欣澳 -> 迪士尼

5. 关于乐园

推荐园区及项目:

  • 魔雪奇缘世界,魔雪奇幻之旅,小船游览,有惊险
  • 迷离庄园,迷离大宅,小车游览
  • 幻想世界,米奇幻想曲,4D电影

午饭:

  • 微信小程序“香港迪士尼乐园”可进行预约相关餐饮。
  • 园区餐厅比较贵,嫌贵可自带干粮。

部署过才发现,使用Docker部署PHP网站,没想象中的简单。顺便总结一下使用Docker Compose部署Typecho的经验。

1. 概述

重点注意:

  • 一般PHP网站的Docker镜像,只提供了网站的PHP源码和php-fpm服务,在生产环境不能单独部署。
  • 生产环境需要前端搭配Nginx或Apache等接收请求,再交给php-fpm服务处理。一般还需要后端连上MySQL、MariaDB、PostgreSQL等数据库。
  • 建议使用Docker Compose部署。比Docker run更好管理,比K8S更省资源。

2. 建立网络

建议先建立一个Docker网络,把相关服务部署到该网络。

  • 所有服务使用service名称作为host名称,配置相互访问时不用填IP。
  • 同一网络内的服务,不用在宿主机暴露端口,各个服务之间也相互访问。一般只需暴露Nginx的端口。

建立Docker网络的参考命令如下。其中网络名称为docker-net

docker network create --driver "bridge" docker-net

3. Nginx服务

Nginx的docker-compose.yaml,路径在/opt/docker_deploy/nginx/,内容如下:

services:
  nginx:
    image: nginx:1.27.0-alpine
    restart: always
    ports:
      - 80:80
      - 443:443
    environment:
      - NGINX_PORT=80
    volumes:
      - ./etc/nginx.conf/:/etc/nginx/nginx.conf:ro
      - ./etc/conf.d:/etc/nginx/conf.d
      - /etc/letsencrypt/archive:/etc/nginx/cert:ro
      - ./logs:/var/log/nginx
      - ./www:/var/www
      - /opt/docker_deploy/typecho/app:/var/www/typecho:ro
    networks:
      - docker-net

networks:
  docker-net:
    external: true

Type的Nginx配置文件,在/opt/docker_deploy/nginx/etc/conf.d/typecho.conf,内容如下:

server {
  server_name blog.xxxx.com;
  listen 80;

  rewrite ^(.*)$  https://$host$1 permanent;
}

server {
  server_name blog.xxxx.com;
  listen 443 ssl;
  http2 on;
  index index.php index.htm index.html;
  root /var/www/typecho;

  ssl_certificate /etc/nginx/cert/blog.xxxx.com/fullchain8.pem;
  ssl_certificate_key /etc/nginx/cert/blog.xxxx.com/privkey8.pem;

  add_header X-Frame-Options SAMEORIGIN;
  add_header Content-Security-Policy "frame-ancestors 'self';";

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    include fastcgi_params;
    #fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param SCRIPT_FILENAME /app$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_pass typecho:9000;
    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
  }

  location / {
    try_files $uri $uri/ /index.php$is_args$query_string;
  }
}

4. Typecho

部署要点:

  • Typecho的Docker部署说明,可参考:https://github.com/typecho/Dockerfile
  • 由于服务器上还有其它网站或系统,需要Nginx做反向代理,所以Typecho的Docker镜像选择了fpm版本。
  • 需要把PHP源码文件映射出来,提供给Nginx访问。

Typecho的docker-compose.yaml,路径在/opt/docker_deploy/typecho/,内容如下:

ervices:
  typecho:
    image: joyqi/typecho:1.2.1-php8.0-fpm-alpine
    restart: always
    #ports:
    #  - "9000:9000"
    environment:
      - "TYPECHO_SITE_URL=https://blog.xxxx.com"
      - "TYPECHO_DB_ADAPTER=Pdo_Mysql"
      - "TYPECHO_DB_HOST=mariadb"
      - "TYPECHO_DB_PORT=3306"
      - "TYPECHO_DB_USER=typecho"
      - "TYPECHO_DB_PASSWORD=123456"
      - "TYPECHO_DB_DATABASE=typecho"
    volumes:
      - ./app:/app
    networks:
      - docker-net

networks:
  docker-net:
    external: true

要注意,如果Nginx的Typecho没有开启SSL,但需要全局https访问Typecho,可以在配置文件config.inc.php添加以下配置:

/** 全站开启https */
define('__TYPECHO_SECURE__', true);

5. MariaDB

MariaDB的docker-compose.yaml,路径在/opt/docker_deploy/mariadb/,内容如下:

ervices:
  mariadb:
    image: mariadb:11.4.2
    restart: always
    #ports:
    #  - 3306:3306
    volumes:
      - ./dbdata:/var/lib/mysql
    environment:
      MARIADB_ROOT_PASSWORD: root123456
      #MYSQL_USER: ${DB_USER}
      #MYSQL_PASSWORD: ${DB_PASSWORD}
      #MYSQL_DATABASE: ${DB_DATABASE}
    networks:
      - docker-net

networks:
  docker-net:
    external: true

记录一下最近选购打气筒的总结。

1. 总结

a) 预算充足,选购电动打气机(充气宝)。

  • 省力,全能。
  • 能显示胎压。
  • 适合家用。用在汽车上的话,可以选购能接入点烟器插头的。

b) 普通家用,选购传统自行车打气筒。

  • 推荐自带压力表(单位psibar),能显示胎压,避免打爆轮胎。
  • 可用于摩托车、电动自行车、普通自行车等的轮胎打气。有的声称汽车轮胎也能用,但没试过。

2. 胎压参考

注意:

  • 胎压仅供参考,特别自行车,最好根据实际情况调整。
  • 汽车如果自身没有显示胎压的,最好去专业的店里打气,避免4个轮的胎压不平衡。

胎压参考值:

a) 两轮摩托车

  • 前轮:28~32 psi,2 bar左右
  • 后轮:30~35 psi,2.5 bar左右

b) 两轮自行车

  • 公路自行车:85~130 psi,5.9~9.0 bar。
  • 山地自行车:30~50 psi,2.1-3.4 bar。
  • BMX自行车:35~70 psi,2.4-4.8 bar。
  • 儿童自行车:25~40 psi,1.7-2.8 bar。

3. 参考资料

虽然家里的迷你服务器(锐角云三角主机)跑了N年,但是一直没有搞定USB硬盘在开机后自动挂载。每次断电重启后,都需要手工执行挂载。终于,通过SystemD的配置,解决了这个问题。

Debian 12上实现开机自动挂载USB磁盘,一般可以在系统启动过程或者系统启动后执行。如果在系统启动过程执行挂载,挂载过程出错,有可能导致系统启动失败(导致不能进入系统)。所以系统启动后执行挂载,相对稳妥。其方案很多,我选择了在fstab文件里通过SystemD的配置实现。

修改/etc/fstab文件,添加以下配置即可。

UUID=7d010a88-bc3c-7a2c-0270-3eae70edd016 /media/ud1 ext4 defaults,auto,nofail,x-systemd.automount,x-systemd.device-timeout=10s 0 0

相关参数解析如下:

  • UUID,填写USB磁盘里需要挂载分区的UUID。可以使用以下其中一条命令查看:

    ls -l /dev/disk/by-uuid/
    lsblk -f
    blkid
  • /media/ud1,设置挂载目录。
  • ext4,分区的文件系统类型。
  • defaults,使用默认选项,即rw,suid,dev,exec,auto,nouser,async

    • rw,挂载该文件系统可读写。
    • suid,从该文件系统执行程序时,遵循用户权限和组权限的设置。
    • dev,解释该文件系统上的字符或块特殊设备。
    • exec,允许执行二进制文件或其它可执行文件。
    • auto,标识mount -a命令可以自动挂载该文件系统,实现开机或重启时自动挂载。
    • nouser,普通用户不能执行挂载。
    • async,该文件系统的所有I/O都应该异步完成。
  • auto,同defaultsauto
  • nofail,设备不存在时,不报告错误信息。
  • x-systemd.automount,为该文件系统创建自动挂载单元(automount unit)。
  • x-systemd.device-timeout=10s,SystemD等待该设备可见(show up)的时间,超过该时间则放弃挂载,避免没有接入该USB磁盘而导致开机等太久。

参考

Windows没有类似Linux的du命令,不能很方便地统计并显示指定路径下各个子项的大小。试过使用CMD的批处理去实现,除了编写代码不方便,实现效也果不好。于是想到使用PowerShell去实现。

具体代码

保存为文件名fold_sum.ps1

param($path)
if ( $path -eq $null ) {
    $path = "."
}
"Path : {0}`n---" -f $path

$totalSize = 0
Get-ChildItem -Force $path | Where-Object { $_.Mode.Substring(5,1) -ne "l" } | ForEach-Object {
    $folderPath = $_.FullName
    if ( $_.PSIsContainer ) {
        $folderSize = (Get-ChildItem -Recurse -Force $folderPath | Select-Object -Property Length | Measure-Object -Property Length -Sum).Sum
    } else {
        $folderSize = (Get-Item -Force $folderPath | Select-Object -Property Length).Length
    }
    $totalSize = $totalSize + $folderSize
    "{0} : {1:N2} MB" -f $_.Name, ($folderSize / 1024 / 1024)
}
# Show total size of current folder
"---`nTotal: {0:N2} MB" -f ($totalSize / 1024 / 1024)

简单说明

  • 使用CMD环境执行时,格式:powershell fold_sum.ps1 d:\path。其中d:\path不填写时默认统计当前路径。
  • 使用PowerShell环境执行时,格式:fold_sum.ps1 d:\path。其中d:\path不填写时默认统计当前路径。
  • Windows 7和Windows 11下都能执行。如果遇到输出结果报错,可以把结果重定向输出到文本文件。
  • PowerShell命令(或者函数)的结果,是基于key-value的格式,能够兼顾人工查看和程序使用。这点,比Linux的Shell要好。
  • 遍历子项时,已经排除了“连接”文件,但仍然会报错某些文件不能访问的问题。需要优化。

参考

1. 背景

由于公司的办公电脑也用于深度学习的模型训练,该电脑就不能同时大型软件(连Chrome也不能多开标签),避免影响训练。于是就想把新手机“红米Note 12 Turbo”利用起来,处理一些“轻办公”的工作。

找到方案是:

  • 方案1:Android系统利用chroot安装完整Linux系统,运行基于X11的图形化软件(例如Chrome),PC端运行X server
  • 方案2:手机运行Android桌面模式,并投屏到PC。

由于方案1运行不流畅、不能方便利用GPU加速、软件兼容不佳等,只能放弃。方案2整理了一下,能达到比较高的实用性。

2. 原理

据说从Android 10开始,Android系统内置了桌面模式。该模式下,App可以自由拖动显示位置,并调整窗口大小(跟PC操作系统一样)。

有趣的是,“开发者选项”里开启了“模拟辅助显示设备”,就可以启用桌面模式,而且基本不影响手机正常显示模式。可以认为同一个Android手机上同时运行“手机模式”和“桌面模式”,App能在这两个模式之间显示。

最后,利用scrcpy把“模拟辅助显示设备”投屏到PC端,就可以使用大屏幕显示“桌面模式”,并且利用鼠标键盘进行输入。

据说手机直连显示器(需要手机直接输出HDMI功能),或者无线连接Miracast,都可以显示“桌面模式”。但是手上没有相关设备,不能验证。

3. 配置

3.1. PC端软件

3.2. Android必要配置

  • 启用“开发者模式”。
  • 进入“设置” -> “系统” -> “开发者选项”,勾选“启用可自由调整的窗口”、“强制使用桌面模式”。

    • 其中“强制使用桌面模式”,就是“模拟辅助显示设备”启用桌面模式。

3.3. 启动命令

在PC端的命令窗口执行以下命令即可启动。Linux Shell脚本参考如下:

#!/bin/bash

# Params
#  $1: SERIAL|HOST[:PORT]
P_SERIAL=$1

PHONE_NAME="My Phone"
CMD_ADB="/opt/android-sdk/platform-tools/adb"
ADB_SERIAL=
CMD_SCRCPY="/opt/scrcpy/scrcpy"
DISPLAY_ID=0
# Log level
declare -A LOG_LEVEL=([i]=INFO [w]=WARN [e]=ERR)

# Log message
# Params
#  $1: log level
#  $2: message
function func_log() {
    lv=${LOG_LEVEL[$1]}
    if [ -z "$lv" ]; then
        lv=NONE
    fi
    echo [$lv] $2 1>&2
    return
}

function func_init() {
    result=n
    
    # Do connect
    is_connected=$(func_connect)
    if [ "$is_connected" == "y" ]; then
        echo y
        return
    fi
    func_log w "Connect failed. SERIAL or HOST is invalid, or need pairing"
    
    # Do pair
    read -p "Need to pair device? Enter "y" for yes, otherwise exit:" need_pair
    if [ "$need_pair" != "y" ]; then
        echo Exit 1>&2
        echo $result
        return
    fi
    is_paired=$(func_pair)
    if [ "$is_paired" != "y" ]; then
        func_log e "Pair failed, exit"
        echo $result
        return
    else
        func_log i "Pair succeed"
    fi
    is_connected=$(func_connect)
    if [ "$is_connected" != "y" ]; then
        func_log e "Connect failed, exit"
        echo $result
        return
    else
        func_log i "Connect succeed"
    fi
    echo y
    return
}

function func_get_serialno() {
    dev_serialno=$($CMD_ADB get-serialno)
    if [ -z "$dev_serialno" ]; then
        return
    fi
    echo $dev_serialno
    return
}

function func_check_serialno() {
    result=n
    dev_serialno=$($CMD_ADB -s "$P_SERIAL" get-serialno)
    if [ "$P_SERIAL" != "$dev_serialno" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_connect() {
    result=n
    
    # Get serialno of default device
    if [ -z $P_SERIAL ]; then
        dev_serialno=$(func_get_serialno)
        if [ -z "$dev_serialno" ]; then
            func_log e "No connected device"
            echo $result
        else
            echo y
            P_SERIAL=$dev_serialno
        fi
        return
    fi
    
    # Check if the device is connected
    is_connected=$(func_check_serialno)
    if [ "$is_connected" == "y" ]; then
        echo y
        return
    fi
    
    # Connect to the device
    dev_connect=$($CMD_ADB connect "$P_SERIAL")
    if [ "${dev_connect:0:9}" != "connected" ] && [ "${dev_connect:0:17}" != "already connected" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_pair() {
    result=n
    echo Enter "HOST[:PORT]" of the paired device:; read pair_host
    echo Enter "PAIRING CODE":; read pair_code
    dev_pair=$($CMD_ADB pair "$pair_host" pair_code)
    if [ "${dev_pair:0:19}" != "Successfully paired" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_tips() {
    echo + Tips ------------------
    echo   - Ctrl + Shift + O, turn ON screen of the connected device
    echo   - Ctrl + O, turn OFF screen of the connected device
    echo + -----------------------
}

function func_start_apps() {
    # --windowingMode <WINDOWING_MODE>: The windowing mode to launch the activity into.
    # --activityType <ACTIVITY_TYPE>: The activity type to launch the activity as.
    $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.LAUNCH -e package cu.axel.smartdock
    # $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.UNFREEZE -e package cu.axel.smartdock
    # sleep 2
    # $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -n cu.axel.smartdock/.activities.MainActivity
}

function func_stop_apps() {
    $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.FREEZE -e package cu.axel.smartdock
}

# main function
function func_main() {
    is_init=$(func_init)
    if [ "$is_init" != "y" ]; then
        return
    fi

    # Turn on auxiliary display device
    $CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices 1920x1080/193
    #$CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices 1920x1008/180

    # Get display-id of Simulate secondary displays
    DISPLAY_ID=$($CMD_SCRCPY $ADB_SERIAL --list-displays | $CMD_ADB $ADB_SERIAL shell "grep -o 'display-id=[1-9][0-9]*' | sed 's/display-id=\([1-9][0-9]*\)/\1/'")
    func_log i "Display id: $DISPLAY_ID"

    # Show tips
    func_tips

    # Run apps
    func_start_apps

    # Run scrcpy
    # $CMD_SCRCPY $ADB_SERIAL --display-id=$DISPLAY_ID --keyboard=uhid --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="$PHONE_NAME - Android Desktop Mode" --window-x=0 --window-y=25
    $CMD_SCRCPY $ADB_SERIAL --display-id=$DISPLAY_ID --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="$PHONE_NAME - Android Desktop Mode" --window-x=0 --window-y=25

    # Stop apps
    func_stop_apps

    # Turn off auxiliary display device
    $CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices null
}

func_main
exit

说明:

  • PHONE_NAME设置手机名称,作为投屏窗口的标题。
  • CMD_ADB设置adb命令所在位置。
  • ADB_SERIAL设置设备序列号,多设备时指定连接的设备。连接无线adb时,可设置IP:端口。可以作为脚本第一个参数传入。
  • CMD_SCRCPY设置scrcpy命令所在位置。
  • DISPLAY_ID是“模拟辅助显示设备”的“display-id”,用于scrcpy投屏。

    • 这里利用了Android自带的grepsed实现字符串匹配提取,避免操作系统缺乏相关命令(例如Windows的CMD)。
    • 如果PC端是Linux设备,可以改为“DISPLAY_ID=$(${CMD_SCRCPY} --list-displays | grep -oP '(?<=display-id=)1-9*')”,简化命令。
  • adb shell settings put global overlay_display_devices用于开启或关闭Android上“模拟辅助显示设备”。

    • 如果在“开发者选项”中开启“模拟辅助显示设备”,没有DPI选项,所以这里通过命令开启。
    • 值为1920x1080/180,设置“模拟辅助显示设备”的分辨率为1920x1080,DPI为180。此值适合针对20吋左右的屏幕。DPI的值,主要影响“模拟辅助显示设备”UI效果,包括字体大小,但不影响手机正常模式。
    • 值为"1920x1080/180\;1920x1080/180",显示两个“模拟辅助显示设备”。多个“模拟辅助显示设备”的配置,需要用双引号(")包裹,并以\;分隔。
    • 值为null,关闭“模拟辅助显示设备”。
    • 对于Windows 11系统,显示器分辨率为1080p时,“模拟辅助显示设备”的分辨率建议为1920x1008。
  • 启动scrcpy

    • 相关参数可以通过scrcpy --help查阅。
  • 关闭“模拟辅助显示设备”。

    • 由于scrcpy运行时,会占着终端,同时暂停了Shell脚本的运行。那么scrcpy停止后,就可以自动执行关闭“模拟辅助显示设备”。

Windows的CMD批处理脚本,参考如下:

@echo off
SETLOCAL ENABLEEXTENSIONS

rem Params
rem  %1: SERIAL|HOST[:PORT]
set P_SERIAL=%1

set PHONE_NAME=My Phone
set CMD_ADB="D:\tools\android-sdk\platform-tools\adb.exe"
set ADB_SERIAL=
set CMD_SCRCPY="D:\tools\scrcpy\scrcpy.exe"
set TEMP_FILE="C:\Users\%username%\AppData\Local\Temp\scrcpy_display_id_%RANDOM%.txt"
set DISPLAY_ID=0

call :func_main
goto end

rem Log message. If error, do exit.
rem Params
rem  %1: log level
rem  %2: message
:func_log
    set lv_index=%~1
    if "%lv_index%" == "i" (
        set lv=INFO
    ) else if "%lv_index%" == "w" (
        set lv=WARN
    ) else if "%lv_index%" == "e" (
        set lv=ERR
    ) else (
        set lv=NONE
    )
    echo [%lv%] %~2
goto :eof

:func_init
    set %~1=n
    
    rem Do connect
    call :func_connect is_connected
    if "%is_connected%" == "y" (
        set %~1=y
        set ADB_SERIAL=-s %P_SERIAL%
        goto :eof
    )
    call :func_log w,"Connect failed. SERIAL or HOST is invalid, or need pairing"
    
    rem Do pair
    set /p need_pair=Need to pair device? Enter "y" for yes, otherwise exit:
    if "%need_pair%" neq "y" (
        echo Exit
        goto :eof
    )
    call :func_pair is_paired
    if "%is_paired%" neq "y" (
        call :func_log e,"Pair failed, exit"
        goto :eof
    ) else (
        call :func_log i,"Pair succeed"
    )
    call :func_connect is_connected
    if "%is_connected%" neq "y" (
        call :func_log e,"Connect failed, exit"
        goto :eof
    ) else (
        call :func_log i,"Connect succeed"
    )
    set ADB_SERIAL=-s %P_SERIAL%
    set %~1=y
goto :eof

:func_get_serialno
    for /f "delims=" %%i in ('%CMD_ADB% get-serialno') do set dev_serialno=%%i
    if "%dev_serialno%" == "" (
        goto :eof
    )
    set %~1=%dev_serialno%
goto :eof

:func_check_serialno
    set %~1=n
    for /f "delims=" %%i in ('%CMD_ADB% -s %P_SERIAL% get-serialno') do set dev_serialno=%%i
    if "%P_SERIAL%" neq "%dev_serialno%" (
        goto :eof
    )
    set %~1=y
goto :eof

:func_connect
    set %~1=n
    
    rem Get serialno of default device
    if "%P_SERIAL%" == "" (
        call :func_get_serialno dev_serialno
    )
    if "%P_SERIAL%%dev_serialno%" == "" (
        call :func_log e,"No connected device"
        goto :eof
    ) else if "%P_SERIAL%%dev_serialno%" == "%dev_serialno%" (
        set %~1=y
        set P_SERIAL=%dev_serialno%
        goto :eof
    )
    
    rem Check if the device is connected
    call :func_check_serialno is_connected
    if "%is_connected%" == "y" (
        set %~1=y
        goto :eof
    )
    
    rem Connect to the device
    for /f "delims=" %%i in ('%CMD_ADB% connect %P_SERIAL%') do set dev_connect=%%i
    if "%dev_connect:~0,9%" == "connected" (
        rem Do nothing
    ) else if "%dev_connect:~0,17%" == "already connected" (
        rem Do nothing
    ) else (
        goto :eof
    )
    set %~1=y
goto :eof

:func_pair
    set %~1=n
    echo Enter "HOST[:PORT]" of the paired device: & set /p pair_host=
    echo Enter "PAIRING CODE": & set /p pair_code=
    for /f "delims=" %%i in ('%CMD_ADB% pair %pair_host% %pair_code%') do set dev_pair=%%i
    if "%dev_pair:~0,19%" neq "Successfully paired" (
        goto :eof
    )
    set %~1=y
goto :eof

:func_tips
    echo + Tips ------------------
    echo   - Ctrl + Shift + O, turn ON screen of the connected device
    echo   - Ctrl + O, turn OFF screen of the connected device
    echo + -----------------------
goto :eof

:func_start_apps
    rem --windowingMode <WINDOWING_MODE>: The windowing mode to launch the activity into.
    rem --activityType <ACTIVITY_TYPE>: The activity type to launch the activity as.
    %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.LAUNCH -e package cu.axel.smartdock
    rem %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.UNFREEZE -e package cu.axel.smartdock
    rem timeout /T 2
    rem %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -n cu.axel.smartdock/.activities.MainActivity
goto :eof

:func_stop_apps
    %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.FREEZE -e package cu.axel.smartdock
goto :eof

rem main function
:func_main
    call :func_init is_init
    if "%is_init%" neq "y" (
        goto :eof
    )
    call :func_log i,"Connected device: %P_SERIAL%"

    rem Turn on auxiliary display device
    rem %CMD_ADB% shell settings put global overlay_display_devices 1920x1080/180
    %CMD_ADB% %ADB_SERIAL% shell settings put global overlay_display_devices 1920x1008/180

    rem Get display id of auxiliary display device
    %CMD_SCRCPY% %ADB_SERIAL% --list-displays | %CMD_ADB% %ADB_SERIAL% shell "grep -o 'display-id=[1-9][0-9]*' | sed 's/display-id=\([1-9][0-9]*\)/\1/'" > %TEMP_FILE%
    set /p DISPLAY_ID=<%TEMP_FILE%
    del %TEMP_FILE%
    call :func_log i,"Display id: %DISPLAY_ID%"

    rem Show tips
    call :func_tips

    rem Run apps
    call :func_start_apps

    rem Run scrcpy
    rem %CMD_SCRCPY% --display-id=%DISPLAY_ID% --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="%PHONE_NAME% - Android Desktop Mode" --window-x=0 --window-y=25
    %CMD_SCRCPY% %ADB_SERIAL% --display-id=%DISPLAY_ID% --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="%PHONE_NAME% - Android Desktop Mode" --window-x=0 --window-y=25

    rem Stop apps
    call :func_stop_apps

    rem Turn off auxiliary display device
    %CMD_ADB% %ADB_SERIAL% shell settings put global overlay_display_devices null
goto :eof

:end