全部文章

<p>最近发现米家 app 里面的小米电视一直显示为离线状态,搜索了一番,终于解决了。</p> <p>解决方案为:在小米电视安装“米家连接服务” app,然后在米家 app 里面添加小米电视。</p> <h1>参考</h1> <p><a href="https://mbd.baidu.com/newspage/data/dtlandingsuper?nid=dt_5001179647010634607&sourceFrom=search_a">小米电视无法添加米家APP?4步解决</a></p>
详情
<p>在 macOS 中,可以通过以下步骤查看当前连接的 WiFi 是 2.4GHz 还是 5GHz 频段:</p> <p>1、连接上目标 WiFi。</p> <p>2、按住键盘上的 Option 键。</p> <p>3、用鼠标点击屏幕顶部的 WiFi 图标,下拉菜单将显示更多信息。</p> <p>4、在下拉菜单中,找到当前连接的WiFi网络名称,向下移动列表,直到看到“频道”这一项。</p> <p>5、在“频道”右侧,会显示连接的通道号,2.4GHz 频段的通道号通常在 1 到 11 之间,而 5GHz 频段的通道号通常在 12 到 165 之间。</p>
详情
<p>最近我的 C 盘由蓝色变为红色(磁盘可用空间只有15GB),心里很郁闷,我最近没有安装新软件,磁盘为什么就会满,经过了一番排查后,发现有一个软件会偷偷截屏并存为图片文件,而且几乎每隔两分钟就截屏一次,非常恶心。</p> <p>为了解决这个问题:</p> <p>1、需要监控某个文件夹,如果文件夹里面新增了文件,则删除。</p> <p>2、服务需要运行在后台。</p> <p>3、服务需要开机自启动。</p> <p>恰好我最近在学 Rust,所以我就使用 Rust 来练练手。</p> <h1>监控文件夹</h1> <pre><code class="language-rust">// 监控的文件夹路径 let folder_to_watch = Path::new("D:\\test"); // 创建一个通道用于接收文件系统事件 let (tx, rx) = channel(); // 创建一个文件监控器 let mut watcher = RecommendedWatcher::new(tx, NotifyConfig::default()).expect("无法创建文件监控器"); // 监听指定文件夹的更改事件 watcher.watch(folder_to_watch, RecursiveMode::NonRecursive).expect("无法监听文件夹"); // 进入事件监听循环 loop { match rx.recv() { Ok(event) => match event { Ok(event) => { if event.kind == EventKind::Create(CreateKind::Any) { for path in event.paths { remove_file(&path); } } } Err(e) => error!("错误: {}", e), }, Err(e) => error!("监控文件错误: {}", e), } } fn remove_file(path: &PathBuf) { if path.is_file() { // 尝试删除文件 sleep(Duration::from_secs(1)); match fs::remove_file(&path) { Ok(_) => info!("成功删除文件: {:?}", path), Err(e) => { error!("删除文件失败: {:?}, 错误: {}", path, e) } } } }</code></pre> <p><code>info!</code> 和 <code>error!</code> 宏来自 <code>log</code> crate。</p> <h1>实现服务</h1> <p>实现服务依赖了 <a href="https://crates.io/crates/windows-service">windows-service</a></p> <p>服务的主体结构是这样的:</p> <pre><code class="language-rust">use log::error; use std::ffi::OsString; use windows_service::service_dispatcher; #[macro_use] extern crate windows_service; define_windows_service!(ffi_service_main, my_service_main); const SERVICE_NAME: &str = "FolderMonitor"; fn my_service_main(arguments: Vec<OsString>) { if let Err(e) = run_service(arguments) { error!("运行服务错误:{:?}", e); } } fn run_service(_args: Vec<OsString>) -> Result<(), windows_service::Error> { // 服务入口 // @TODO 监控文件夹 // @TODO 服务运行时,把服务状态改为运行中 // @TODO 服务停止运行时,退出事件监听循环、停止监控文件夹、把服务状态改为已停止 Ok(()) } fn main() -> Result<(), windows_service::Error> { service_dispatcher::start(SERVICE_NAME, ffi_service_main)?; Ok(()) }</code></pre> <p>完整的<code>run_service</code>代码如下</p> <pre><code class="language-rust">fn run_service(_args: Vec<OsString>) -> Result<(), windows_service::Error> { // 初始化日志系统,将日志写入 LOG_FILE let log_file = OpenOptions::new() .create(true) .append(true) .open(LOG_FILE) .expect("无法打开日志文件"); // 自定义日志格式:包含日期和时间 let config = ConfigBuilder::new().set_time_format_rfc3339().build(); CombinedLogger::init(vec![ TermLogger::new( LevelFilter::Warn, Config::default(), TerminalMode::Mixed, ColorChoice::Auto, ), WriteLogger::new(LevelFilter::Info, config, log_file), ]) .unwrap(); info!("Windows 服务已启动"); // 用于控制主循环的退出 let running = Arc::new(AtomicBool::new(true)); let running_clone = Arc::clone(&running); // 监控的文件夹路径 let folder_to_watch = Path::new(MONITOR_PATH); // 创建一个通道用于接收文件系统事件 let (tx, rx) = channel(); // 创建一个文件监控器 let mut watcher = RecommendedWatcher::new(tx, NotifyConfig::default()).expect("无法创建文件监控器"); // 监听指定文件夹的更改事件 watcher .watch(folder_to_watch, RecursiveMode::NonRecursive) .expect("无法监听文件夹"); // 设定 Windows 服务控制处理器 let status_handle = service_control_handler::register(SERVICE_NAME, move |control| match control { ServiceControl::Stop => { info!("收到服务停止命令"); running_clone.store(false, Ordering::Relaxed); // 更新状态,让主循环退出 return ServiceControlHandlerResult::NoError; } ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, }) .unwrap(); status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP, exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .unwrap(); info!("开始监控文件夹: {}", folder_to_watch.to_string_lossy()); // 进入事件监听循环 while running.load(Ordering::Relaxed) { match rx.recv_timeout(Duration::from_secs(1)) { Ok(event) => match event { Ok(event) => { if event.kind == EventKind::Create(CreateKind::Any) { for path in event.paths { remove_file(&path); } } } Err(e) => error!("错误: {}", e), }, Err(_) => {} } } // 停止监听文件夹变动 if let Err(e) = watcher.unwatch(folder_to_watch) { error!("停止监控文件夹失败: {}", e); } else { info!("已停止监控文件夹: {}", folder_to_watch.to_string_lossy()); } status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .unwrap(); info!("服务已停止"); Ok(()) }</code></pre> <h1>安装服务</h1> <p>以管理员身份运行<code>命令行提示符</code>应用</p> <pre><code class="language-cmd">sc create FolderMonitor binPath= "D:\Program Files\folder-monitor\folder_monitor_service.exe" start= auto # 创建服务并设置为自动启动 sc start FolderMonitor # 启动服务 sc stop FolderMonitor # 停止服务</code></pre> <h1>参考</h1> <p>注:以上研究成果是在<code>ChatGPT</code>帮助下完成。</p> <p><a href="https://crates.io/crates/notify">notify</a></p> <p><a href="https://crates.io/crates/windows-service">windows-service</a></p> <p><a href="https://crates.io/crates/log">log</a></p> <p><a href="https://crates.io/crates/simplelog">simplelog</a></p>
详情
<p><img src="/uploads/rust-module.png" alt="" /></p>
详情
<p>如果你还不了解 Web Bluetooth,请先看我之前写的 <a href="http://pcdeng.com/web-bluetooth.html">《通过 Web Bluetooth 驱动打印机打印》</a></p> <p>Characteristic 可以翻译为特征、特性,本文一律用特征表示 Characteristic,单词太长,不好记,简写为 chatt。</p> <p>这篇文章假设你已经知道怎样连接蓝牙打印机、怎样连接 GATT 服务器,怎样获取 service、怎样获取写特征。</p> <p>开始前,你需要知道服务的 UUID、写特征的 UUID、读特征的 UUID、如果没有读特征的 UUID,要知道通知特征的 UUID。</p> <p>如果不知道,可以通过 LightBlue (iOS app) 连接上蓝牙打印机查看。</p> <p>我手上这台打印机没有读特征。</p> <p>大体步骤如下:</p> <p>1、请求设备 navigator.bluetooth.requestDevice(过滤条件),得到设备 selectedDevice</p> <p>2、连接到 GATT 服务器(打印机)selectedDevice.gatt.connect(),得到 server</p> <p>3、获取主服务 server.getPrimaryService(服务 UUID),得到 service</p> <p>4、获取写特征 service.getCharacteristic(写特征 UUID),得到 writeChatt</p> <p>5、获取通知特征 service.getCharacteristic(通知特征 UUID),得到 notifyChatt</p> <p>读数据的交互流程:</p> <p>1、向打印机写命令,比如 <code>GET MODEL NAME</code>,这不是真实的打印机指令,只是一个例子,具体的指令需要参考自己打印机支持的指令。</p> <p>2、启动通知</p> <p>3、监听 notifyChatt 的 <code>characteristicvaluechanged</code> 事件</p> <p>4、<code>characteristicvaluechanged</code> 事件处理函数处理接收到的数据</p> <p>4、<code>characteristicvaluechanged</code> 事件处理函数最后一步停止通知</p> <p>封装读取函数如下:</p> <pre><code class="language-js">const read = (cmd) => { return new Promise(async (resolve, reject) => { await send(cmd); let timer; const handler = async (evt) => { if (timer) { clearTimeout(timer); } const value = evt.target.value; resolve(value); await notifyChatt.stopNotifications(); }; await notifyChatt.startNotifications(); notifyChatt.addEventListener("characteristicvaluechanged", handler); timer = setTimeout(() => { reject(new Error("Timeout")); }, 5 * 1000); }); };</code></pre>
详情
<p>最近在被一个问题困扰很久,我遇到的场景是这样的。</p> <p>我的应用支持的打印指令包括:ESC/POS、TSPL、ZPL。每当用户通过 USB 连接上打印机后,他需要选择打印机支持的打印指令。如果选错了,可能会乱码或会把打印指令当成内容打印出来。</p> <p>这样就会显得这个应用很不智能,增加了用户心智负担。</p> <p>理想的情况是,连接上打印机后,可以通过一条指令查询出当前打印机支持的指令。而本文介绍的就是这条指令。</p> <p>涉及的关键字有:</p> <ul> <li>USB 控制指令</li> <li>controlTransferIn</li> <li>GET DEVICE ID</li> </ul> <p>话不多说,马上进入正题。</p> <p>前提:这次的示例是基于之前的文章 <a href="/webusb-escpos.html">通过 WebUSB 驱动打印机打印</a>,如果你还没看过,<a href="/webusb-escpos.html">先看</a>再回来看这篇文章。</p> <p>关键函数如下:</p> <pre><code class="language-js">const getInfo = async () => { if (!selectedDevice) { return Promise.reject("未连接打印机"); } const setupParams = { requestType: "class", // 请求类型 request: 0x00, // 是 GET DEVICE ID 的请求代码(通常根据你的设备文档确定)。 value: 0x0000, // 请求值 index: 0x0000, // 接口索引 recipient: "interface", }; const { data, status } = await selectedDevice.controlTransferIn( setupParams, 64 ); if (status != "ok" || !data) { return Promise.reject("读取打印机信息失败"); } const decoder = new TextDecoder(); const deviceId = decoder.decode(data) || ""; console.log(`deviceId ${deviceId}`); // 输出,如:1MFG:Printer;MDL:POS-80;CMD:ESC/POS;CLS:PRINTER; return deviceId; };</code></pre> <p><code>deviceId</code> 是一个字符串,每个字段用<code>;</code>分割。每个字段是<code>KEY:VALUE</code>的形式。不同品牌的打印机,这个 <code>deviceId</code> 可能会返回不同格式的内容,要看具体的打印机返回信息。</p> <p>各个字段的含义:</p> <p>1MFG: 1 可能是版本号或其它的含义。MFG 是 manufacturer 的缩写,代表的是制造商名称。</p> <p>MDL: MDL 是 model 的缩写,代表的是打印机的型号。</p> <p>CMD: CMD 是 command 的缩写,代表的是打印机支持的指令。这是我们最感兴趣的字段。</p> <p>CLS: CLS 是 class 的缩写,代表的是USB 类型。PRINTER 代表的是打印机。</p> <p>通过解析出 CMD 字段,我们就可以拿到打印支持的指令,就不需要用户再选一次打印机支持的指令了。</p> <h1>参考</h1> <p><a href="https://developer.mozilla.org/en-US/docs/Web/API/USBDevice/controlTransferIn">https://developer.mozilla.org/en-US/docs/Web/API/USBDevice/controlTransferIn</a></p> <p>chatgpt, 关键字 <code>webusb 发送 ctl GET DEVICE ID</code></p>
详情
<p>其实这是一个很低级的错误,但它花费了我将近一个小时,重启电脑5次,所以简单记一下。</p> <p>我最近在做一个工具,功能差不多完成了,所以就想着把默认的图标换一下。</p> <h1>准备图标</h1> <p>准备一张 512px * 512px 的 png 图片或 1024px * 1024px 的 png 图片,并命名为 app-icon.png,图片放在项目根目录。</p> <h1>生成图标</h1> <p>在项目根目录执行</p> <pre><code class="language-bash">pnpm tauri icon</code></pre> <p>查看 icon 命令的更多选项</p> <pre><code class="language-bash">$ pnpm tauri icon --help > ble-demo@0.1.0 tauri D:\projects\tauri\ble-demo > tauri "icon" "--help" Generate various icons for all major platforms Usage: pnpm run tauri icon [OPTIONS] [INPUT] Arguments: [INPUT] Path to the source icon (squared PNG or SVG file with transparency) [default: ./app-icon.png] Options: -o, --output <OUTPUT> Output directory. Default: 'icons' directory next to the tauri.conf.json file -v, --verbose... Enables verbose logging -p, --png <PNG> Custom PNG icon sizes to generate. When set, the default icons are not generated --ios-color <IOS_COLOR> The background color of the iOS icon - string as defined in the W3C's CSS Color Module Level 4 <https://www.w3.org/TR/css-color-4/> [default: #fff] -h, --help Print help -V, --version Print version</code></pre> <h1>重新打包</h1> <pre><code class="language-bash">pnpm tauri build</code></pre> <h1>重新安装</h1> <p>发现图标不更新。</p> <h1>解决办法</h1> <p>build 之前需要清除之前的 build 缓存</p> <pre><code class="language-bash">cd src-tauri cargo clean cd .. pnpm tauri build</code></pre>
详情
<p>今天远程帮助一个用户处理应用闪退的问题,昨天已经远程过一次,但是没有解决问题。</p> <p>一、背景。</p> <p>这个应用是一个 Windows 桌面端的应用,基于 Electron 构建的,Electron 应用集成了 crashReporter,也集成了electron-log 用来记录日志。</p> <p>用户的系统是 Windows 7, 64bit。</p> <p>现象是:在桌面上双击打开应用,应用闪一下,然后直接退出。</p> <p>二、第一次远程。</p> <p>第一次远程没有定位到原因。我看了日志,只记录了:发现了一个新版本。看了崩溃日志,发现有一个崩溃 report(C:\Users[登录用户]\AppData\Roaming[应用名称]\Crashpad\reports)。</p> <p>因为发现了崩溃日志,我就以为是由于系统差异而导致的崩溃,但询问客户后,客户反馈说,前几天是可以正常使用的。我就建议客户重启系统,重启系统后远程就断了,之后也无法联系用户了(到下班时间了)。</p> <p>三、第二次远程。</p> <p>自上次远程之后用户一直没有反馈消息,我还以为重启大法生效了。直到下午四点多,用户突然又要求远程协助。心里咯噔一下,我也不知道怎样排查了,难道要分析 dmp 文件(crashReporter 生成的文件)?那就很麻烦了。</p> <p>远程连接上之后,按照昨天的排查思路排查了一遍,依然没有头绪。</p> <p>重启不行,那就重装,得到用户的同意后,就重新装了应用。重装后打开应用,还是闪退。</p> <p>覆盖安装不行,那就彻底删除后再安装。但是,还是闪退。</p> <p>彻底没头绪了,和同事说了一嘴,他回应说:会不会是权限问题?我回答说,用户之前是可以用的,应该不会是权限问题。然后他又说,会不会是被安全软件拦截了?然后我就排查用户的电脑是不是安装了安全软件,看了一下,发现用户的电脑安装了 360 安全卫士。我说拦截也会有提示吧?针对这块没有写日志,要怎样验证?</p> <p>同事说,那简单,在命令行执行这个 .exe 看命令行提示就知道了。</p> <p>在桌面对着应用右键,选择“打开文件所在位置”,然后在地址栏输入 cmd 打开 Windows 终端,在命令行输入 xxx.exe 然后命令提示了: xxxxx,看到提示后知道是权限不够。然后另一个同事说,要不先把 360 安全卫士关了试一试。排查:先关掉 360 安全卫士,再打开软件,能正常打开了,欢天喜地,定位到问题了。</p> <p>解决:360 安全卫士里把这个应用添加到可信任列表。</p> <p>至此,问题圆满解决。</p>
详情
<p>最近在 QQ 群里看到一个问题:</p> <p><img src="/uploads/qq-node-chart.png" alt="" /></p> <p>这个问题我之前也想过,但是实际的项目没有用到就搁置了。趁今天有时间,就好好研究一下。</p> <h1>选型</h1> <h2>Node.js 如何实现画布?</h2> <p>Node.js 有 npm 包 <a href="https://www.npmjs.com/package/canvas">node-canvas</a></p> <h2>Node.js 如何实现图表?</h2> <ul> <li>用 <a href="https://f2-v3.antv.vision/zh/docs/tutorial/node-env">F2</a>,建议用 v3 版本,因为 v4 之后用 jsx 描述图表,需要配置 babel 转译一下 jsx,有点麻烦。</li> <li>用 <a href="https://g2.antv.antgroup.com/manual/extra-topics/ssr">G2</a></li> </ul> <h1>实现</h1> <p>图表的实现,我选用的是 <code>F2</code>。</p> <p>按照<a href="https://f2-v3.antv.vision/zh/docs/tutorial/node-env">文档</a>一步一步实现。</p> <p>我遇到的问题是: 1、渲染出的 jpg 图片背景色是黑色的,而我需要的是白色背景。</p> <p><img src="/uploads/pie-dark.jpg" alt="" /></p> <p>阅读 F2 的 API 文档,发现可以通过添加一个组来实现,然后把组的 <code>zIndex</code> 设为 <code>-1</code>,然后在组添加一个和画布一样大小的白色的矩形充当白色背景。</p> <p>我把它抽取成一个方法,如下:</p> <pre><code class="language-js">// chart 是一个图表实例 const addBg = (chart) => { const canvas = chart.get("canvas"); const bgGroup = canvas.addGroup({ zIndex: -1, }); const bg = new F2.G.Shape.Rect({ attrs: { x: 0, y: 0, height: canvas.getWidth(), width: canvas.getHeight(), lineWidth: 0, fill: "#fff", strokeStyle: "#000", radius: 0, }, }); bgGroup.add(bg); };</code></pre> <p>调用代码:</p> <pre><code class="language-js">// 省略了一些代码,可以参考官方示例 const chart = new F2.Chart({ context: canvas.getContext("2d"), width, height, animate: false, pixelRatio, }); addBg(chart);</code></pre> <p>最终效果:</p> <p><img src="/uploads/pie-white.jpg" alt="" /></p>
详情
<h1>encodeURI</h1> <p>对统一资源标识符(URI)进行编码。</p> <p>以下字符不会被编码:</p> <table> <thead> <tr> <th>类型</th> <th>包含</th> </tr> </thead> <tbody> <tr> <td>保留字符</td> <td>; , / ? : @ & = + $</td> </tr> <tr> <td>非转义的字符</td> <td>字母 数字 - _ . ! ~ * ' ( )</td> </tr> <tr> <td>数字</td> <td>#</td> </tr> </tbody> </table> <h2>例子</h2> <pre><code class="language-js">const uri = "http://username:password@www.example.com:80/path/to/file.php?foo=316&bar=this+has+spaces#anchor" const encodedUri = encodeURI(uri);</code></pre> <h1>参考</h1> <p><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/encodeURI">encodeURI()</a></p>
详情
<h1>不可变引用</h1> <p>引用 (Reference) – 引用其他数据的不可变指针。</p> <p>当引用指向某个值,我们称为“借用”这个值,因为是被不可变的借用,所以不能被修改,也不能移动。</p> <p>借用一直持续到生命周期结束,即离开作用域。</p> <pre><code class="language-rust">fn main() { let mut a = 4; // 定义变量 a 并赋值为 4 let ref_a: &i32 = &a; // ref_a 借用了 a,ref_a 指向 a。 println!("{}", a); // `a` 还可以继续使用(可读) println!("{}", *ref_a); // 解引用(获取值) a = 3; // 编译报错,因为 a 已被 ref_a 借走了(不可写) *ref_a = 6; // 编译报错,因为 ref_a 是不可变引用 }</code></pre> <h1>可变引用</h1> <p>可变引用 (Mutable reference)。</p> <p>当一个变量被可变地借用时,也不可使用。</p> <pre><code class="language-rust">fn main() { let mut a = 4; // 定义变量 a 并赋值为 4 let ref_a: &mut i32 = &mut a; // ref_a 借用了 a,ref_a 指向 a。之后 a 不可用 println!("{}", a); // 编译报错,a 被可变借用。 println!("{}", *ref_a); // 解引用(获取值) a = 3; // 编译报错,因为 a 已被 ref_a 可变借走了 *ref_a = 6; // 解引用并重新赋值 println!("{}", *ref_a); // 解引用(获取值) }</code></pre> <h1>参考</h1> <p><a href="https://learnxinyminutes.com/docs/zh-cn/rust-cn/">Y分钟速成X其中 X=Rust</a></p>
详情
<p>接上一篇<a href="/web-print-requirement.html">Web 打印服务之需求篇</a>,这篇主要关注实现过程。</p> <h1>1、用最熟悉的技术栈 Electron 初探</h1> <p>一开始是用 Electron 来做,做完之后发现有几个问题:</p> <ul> <li>安装包太大</li> <li>很容易被抄袭(源码无法保护)</li> </ul> <h1>2、追求安装包最小 Slint</h1> <p>后来尝试用 slint(Slint is a declarative GUI toolkit to build native user interfaces for Rust, C++, or JavaScript apps.)</p> <p>遇到的问题:</p> <ul> <li>界面是写出来了,但是无法实现需求:“应用关闭时不时退出,而是最小化到托盘。”</li> <li>无法实现需求:“实现启动、关闭 WebSocket 服务”</li> </ul> <h1>3、试试 Tauri</h1> <p>后来尝试用 Tauri,遇到的问题:</p> <ul> <li>由于 Rust 不熟,靠 ChatGPT 东拼西凑没能实现启动、关闭 WebSocket 服务,卡在这里两个多星期。</li> <li>在 Windows 系统,Tauri 运行占用的内存比 Electron 还高,诧异之余,也不明所以。</li> </ul> <h1>总结</h1> <p>最后在谷歌和 ChatGPT 的帮助下,用 Tauri 实现了一个应用。</p> <h2>成果</h2> <ul> <li>MacOS 10.15.7 打包出的 dmg 只有 3.1MB</li> </ul>
详情