
[{"content":"","date":"2026-03-04","externalUrl":null,"permalink":"/","section":"Kydin's Blog","summary":"","title":"Kydin's Blog","type":"page"},{"content":"","date":"2026-03-04","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"先说结论：使用 Cloudflare Tunnel 实现内网穿透是目前花费最低（一年 5 元人民币）且网速最好、最安全也是最简单的。\n前期准备 # 一台群晖或者任何你想要内网穿透的 Windows、Mac、Linux 一个 Cloudflare 的账号，直接注册即可，要添加支付方式，可以是信用卡 一个域名，如果你手头上有闲置的就直接用，如果没有就买一个 6 位纯数字的 xyz 域名，我直接在 Cloudflare 里买的，一年 0.85$ 网络架构 # 假设我现在要访问群晖的网页（Synology DSM），我的操作流程是这样的：\n浏览器访问 你的域名 ↓ Cloudflare Access 页面 ↓ 输入邮箱 ↓ 邮箱收到验证码（只有提前设置好权限的邮箱才会收到验证码） ↓ 输入验证码 ↓ 进入 DSM 可以看到，整个流程中，我并不需要在当前设备安装任何网络工具，也不需要做什么设置，非常简单\n此时的网络架构是这样的：\n浏览器 ↓ https://nas.example.com（假设这是你访问的域名） ↓ Cloudflare Edge ↓ Tunnel ↓ cloudflared ↓ https://192.168.1.100:5001（假设这是你家里群晖的网页地址） ↓ DSM 得益于 Cloudflare 全球的网络基础设施，我们实际访问的速度还是很快的，不愧为赛博活菩萨\n搭建 Tunnel # 在 https://dash.cloudflare.com/ 中搜索 Tunnels 快速到达\n点击 创建隧道 -\u0026gt; 输入隧道名称-\u0026gt;点击创建 -\u0026gt; 选择对应的平台，复制下方的命令行\n我使用的是 Docker，其他平台安装也是相同的道理，然后 SSH 进入群晖的后台，粘贴刚刚复制的命令行就可，过一会就可以看到成功链接\n点击刚刚链接的隧道，然后点击Add route-\u0026gt;选择Published application\nSubdomain：子域名，随意填写，例如`dsm` Domain：选择你的主域名，这里我直接用 xyz 域名 Service URL：填写你群晖的端口，假设 DSM 端口为 5001，就写：https://localhost:5001 点击添加即可，添加后，访问dsm.域名正常情况下会显示Bad gateway错误，Error code 502。\n这是因为 群晖默认 DSM 证书是自签名证书，所以 cloudflared 默认会 验证失败，需要特别处理。但是此时 Tunnel 是已经创建成功了。\n添加访问规则 # 解决 502 错误 # 解决这个错误的方式有很多，这里只讲最简单的一种： 进入 Cloudflare Zero Trust -\u0026gt; 网络 -\u0026gt; 连接器 -\u0026gt; 编辑 -\u0026gt; 已发布应用程序路由 -\u0026gt; 编辑 -\u0026gt; 其他应用程序设置 -\u0026gt; TLS -\u0026gt; 无 TLS 验证开启 -\u0026gt; 保存\n此时再次访问dsm.域名，即可正常访问群晖 DSM 页面\n防止暴露在公网中 # 经过上面的配置，我们已经可以正常通过浏览器随时随地访问我们家里的群晖了，但是这样有个安全性的问题就是，内网设备一直处于暴露在公网上的状态，我们可以通过添加Cloudflare Access来增加安全性\n在 Cloudflare Zero Trust -\u0026gt; 访问控制 -\u0026gt; 策略 -\u0026gt; 可重用策略 -\u0026gt; 添加策略 -\u0026gt; 策略名称叫做：Allow Login，操作选择：Allow，添加允许登录的邮箱地址\n然后再次添加策略 -\u0026gt; 策略名称叫做：Block Other，操作选择 Block，选择器为 Everyone\n现在我们已经创建好了两条策略，只有你允许的邮箱才可以登录，现在我们将这两条策略应用到我们的 Tunnel 中即可。\n点击 Cloudflare Zero Trust -\u0026gt; 访问控制 -\u0026gt; 应用程序 -\u0026gt; 添加应用程序 -\u0026gt; 选择自托管 -\u0026gt; 在基本信息中添加公共主机名，子域为 Tunnel 使用的子域名dsm，域为 Tunnel 使用的域名 -\u0026gt; Access 策略选择现有策略，需要注意的是，Allow Login 在前，Block Other 在后 -\u0026gt; 保存\n现在，当你访问域名时，就被Cloudflare Access拦住，只有允许的邮箱才可以进入啦\n","date":"2026-03-04","externalUrl":null,"permalink":"/posts/4fc06c83/","section":"Posts","summary":"","title":"使用 CloudflareTunnel 实现群晖内网穿透","type":"posts"},{"content":" 主题色 # 配色方案直接用的参考文章里面的颜色，在 assets/css/custom.css 中添加：\n/* 主题色 */ :root { --color-neutral: 255, 255, 255; --color-neutral-50: 255, 255, 255; --color-neutral-100: 255, 255, 255; --color-neutral-200: 214, 219, 222; --color-neutral-300: 172, 183, 188; --color-neutral-400: 129, 146, 154; --color-neutral-500: 92, 107, 115; --color-neutral-600: 74, 86, 92; --color-neutral-700: 56, 65, 70; --color-neutral-800: 38, 44, 47; --color-neutral-900: 19, 23, 24; --color-primary-50: 255, 255, 255; --color-primary-100: 255, 255, 255; --color-primary-200: 255, 255, 255; --color-primary-300: 242, 211, 223; --color-primary-400: 225, 151, 181; --color-primary-500: 208, 92, 138; --color-primary-600: 199, 60, 115; --color-primary-700: 170, 49, 97; --color-primary-800: 138, 40, 79; --color-primary-900: 106, 31, 61; --color-secondary-50: 255, 255, 255; --color-secondary-100: 255, 255, 255; --color-secondary-200: 255, 255, 255; --color-secondary-300: 255, 242, 219; --color-secondary-400: 255, 215, 143; --color-secondary-500: 255, 188, 66; --color-secondary-600: 255, 174, 25; --color-secondary-700: 239, 155, 0; --color-secondary-800: 199, 128, 0; --color-secondary-900: 158, 102, 0; } 字体颜色 # 也是参考文章，修改主题色后夜间模式下字体颜色比较暗，一起做了调整，在 assets/css/custom.css 中添加：\n/* 调整正文字体颜色，使其更白更易读 */ .dark .prose { color: #d1d5db; /* 稍微降低亮度，拉开与加粗的对比 */ } .dark .prose strong, .dark .prose h1, .dark .prose h2, .dark .prose h3, .dark .prose h4, .dark .prose h5, .dark .prose h6 { color: #f2f2f2; } 顶部阅读进度条 # 在 assets/css/custom.css 中添加：\n/* 顶部阅读进度条 */ .top-scroll-bar { position: fixed; top: 0; left: 0; z-index: 9999; display: none; width: 0; height: 3px; background: #d05c8a; } 然后在 layouts/partials/ 下创建 extend-footer.html，内容为：\n\u0026lt;!-- 进度条逻辑 --\u0026gt; \u0026lt;script\u0026gt; window.addEventListener(\u0026#39;scroll\u0026#39;, () =\u0026gt; { const bar = document.querySelector(\u0026#39;.top-scroll-bar\u0026#39;); if (!bar) return; const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight; const width = (scrollTop / scrollHeight) * 100; bar.style.width = width + \u0026#39;%\u0026#39;; bar.style.display = \u0026#39;block\u0026#39;; }); \u0026lt;/script\u0026gt; 最后在layouts/partials/ 下创建 extend-head.html来使用这个进度条：\n\u0026lt;!-- 进度条 --\u0026gt; \u0026lt;div class=\u0026#34;top-scroll-bar\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 文字模糊效果 # 用来写一些可能剧透的文字还是挺有用的，在layouts/shortcodes下创建blur.html：\n\u0026lt;span class=\u0026#34;blur\u0026#34;\u0026gt;{{.Inner | markdownify}}\u0026lt;/span\u0026gt; \u0026lt;style\u0026gt; /* 文本高斯模糊 */ .blur { filter: blur(4px); transition: filter 0.3s ease; } .blur:hover { filter: blur(0); } \u0026lt;/style\u0026gt; 使用方法是：\n{{\u0026lt; blur \u0026gt;}}想要高斯模糊的文本{{\u0026lt; /blur \u0026gt;}} 效果如下：这就是模糊的效果 参考资料 # Hugo | 记录 Blowfish 主题美化过程\nBlowfish 主题魔改\n","date":"2026-03-03","externalUrl":null,"permalink":"/posts/3ea7ea88/","section":"Posts","summary":"","title":"Blowfish 主题美化","type":"posts"},{"content":"本文介绍了如何在芯源的 CW32F03 系列 MCU 中移植 FreeRTOS。\n先说一个暴论：所有主频在 100MHz 下的 MCU，都没有必要使用 FreeRTOS，直接跑裸机程序即可。\n但折腾是一种乐趣，所以我们就开始吧！\n本次使用的芯片是武汉芯源的 CW32F030F8，规格如下：\n内核：ARM® Cortex®-M0+，最高主频 64MHz Flash：64KB 片上存储 SRAM：8KB 由于只是移植 FreeRTOS，其它的外设就不过多介绍了，有需要了解的可以去看看数据手册。\n⚠️注意：本文旨在用最少的步骤快速进行 FreeRTOS 的移植，不涉及 FreeRTOS 的参数优化\n本文完整的代码已经上传 GitHub：CW32F03-FreeRTOS\n前置准备 # 下载 FreeRTOS 代码\n我使用的版本是FreeRTOSv202411.00，GitHub 下载地址\n开发环境 # 代码移植 # 通用层 # 当前项目结构如下：\n├─App ├─BSP ├─Core ├─MDK-ARM │ ├─Listings │ ├─Objects │ └─RTE │ ├─CMSIS │ └─Device │ └─CW32F030F8 │ └─Middlewares └─Third_Party 在工程文件夹Middlewares\\Third_Party\\下新建文件夹，命名为FreeRTOS 将FreeRTOSv202411.00\\FreeRTOS\\Source\\中所有 .c 文件复制到FreeRTOS\\中 将FreeRTOSv202411.00\\FreeRTOS\\Source\\include整个文件夹复制到FreeRTOS\\中 此时项目结构如下：\n├─App ├─BSP ├─Core ├─MDK-ARM │ ├─Listings │ ├─Objects │ └─RTE │ ├─CMSIS │ └─Device │ └─CW32F030F8 │ └─Middlewares └─Third_Party └─FreeRTOS │ croutine.c │ event_groups.c │ FreeRTOSConfig.h │ list.c │ queue.c │ stream_buffer.c │ tasks.c │ timers.c │ └─include atomic.h croutine.h deprecated_definitions.h event_groups.h FreeRTOS.h list.h message_buffer.h mpu_prototypes.h mpu_syscall_numbers.h mpu_wrappers.h newlib-freertos.h picolibc-freertos.h portable.h projdefs.h queue.h semphr.h StackMacros.h stack_macros.h stream_buffer.h task.h timers.h 移植层 # 查阅 MCU 手册可以得知，我使用的 CW32F030x6/x8 属于 ARM Cortex-M0 系列\n在FreeRTOS\\目录下新建文件夹portable 复制FreeRTOSv202411.00\\FreeRTOS\\Source\\portable中的MemMang\\文件夹到FreeRTOS\\portable\\下 复制FreeRTOSv202411.00\\FreeRTOS\\Source\\portable中的RVDS\\ARM_CM0\\文件夹到FreeRTOS\\portable\\下 此时项目结构如下：\n├─App ├─BSP ├─Core ├─MDK-ARM │ ├─Listings │ ├─Objects │ └─RTE │ ├─CMSIS │ └─Device │ └─CW32F030F8 └─Middlewares └─Third_Party └─FreeRTOS │ croutine.c │ event_groups.c │ FreeRTOSConfig.h │ list.c │ queue.c │ stream_buffer.c │ tasks.c │ timers.c │ ├─include │ atomic.h │ croutine.h │ deprecated_definitions.h │ event_groups.h │ FreeRTOS.h │ list.h │ message_buffer.h │ mpu_prototypes.h │ mpu_syscall_numbers.h │ mpu_wrappers.h │ newlib-freertos.h │ picolibc-freertos.h │ portable.h │ projdefs.h │ queue.h │ semphr.h │ StackMacros.h │ stack_macros.h │ stream_buffer.h │ task.h │ timers.h │ └─portable ├─MemMang │ heap_1.c │ heap_2.c │ heap_3.c │ heap_4.c │ heap_5.c │ └─RVDS └─ARM_CM0 port.c portmacro.h 内存分配策略的选择 # 上一步我们将FreeRTOSv202411.00\\FreeRTOS\\Source\\portable\\MemMang\\ 复制过来，这个文件夹用于实现 FreeRTOS 的不同内存分配策略，它们的区别如下：\n特性 heap_1 heap_2 heap_3 heap_4 heap_5 碎片化 无 有 无 (外部管理) 低 低 (多区域) 内存释放 不支持 支持 支持 支持 支持 内存合并 不支持 不支持 不支持 支持 支持 多区域 不支持 不支持 不支持 不支持 支持 确定性 高 中等 依赖外部 中等 中等 代码复杂度 简单 中等 简单 中等 较复杂 适用场景 简单应用 较小项目 与标准库集成 通用 复杂内存布局 大部分情况，选择 heap_4\n添加配置文件 # 从FreeRTOSv202411.00\\FreeRTOS\\Source\\examples\\template_configuration\\中，复制FreeRTOSConfig.h到FreeRTOS\\下\nFreeRTOSConfig.h 是 FreeRTOS 的核心配置文件，它允许用户在不修改 FreeRTOS 内核源代码的情况下，自定义系统的所有关键参数和行为。\n修改点 1 # 在 44 行后添加\n#include \u0026#34;cw32f030.h\u0026#34; // 根据 MCU 型号添加主头文件 // 针对不同的编译器调用不同的 stdint.h 文件 #if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__) #include \u0026lt;stdint.h\u0026gt; extern uint32_t SystemCoreClock; // 声明 SystemCoreClock 变量已存在，修改点 2 需要用到 #endif 修改点 2 # 将 54 行的\n#define configCPU_CLOCK_HZ ( ( unsigned long ) 20000000 ) 改为\n#define configCPU_CLOCK_HZ ( SystemCoreClock ) 含义：系统主频。一般 SystemCoreClock 会和系统主频相等，不直接设置主频数。\n修改点 3 # 将 78 行的\n#define configTICK_RATE_HZ 100 改为\n#define configTICK_RATE_HZ 1000 含义：嘀嗒计时频率，原本的 100Hz 太慢了，改为 1000 后也就是 1ms 的节拍频率。\n修改点 4 # 将 135 行的\n#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_64_BITS 改为\n#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS 含义：ARM Cortex-M0 (CW32F030) 是一个 32 位架构，它只支持 16 位或 32 位的 tick 类型，不支持 64 位。使用 64 位编译时报错：\n..\\Middlewares\\Third_Party\\FreeRTOS\\portable\\RVDS\\ARM_CM0\\portmacro.h(73): error: #35: #error directive: configTICK_TYPE_WIDTH_IN_BITS set to unsupported tick type width. 修改点 5 # 将 357 行的\n#define configCHECK_FOR_STACK_OVERFLOW 2 改为\n#define configCHECK_FOR_STACK_OVERFLOW 0 含义：堆栈溢出检测功能，0 为关闭堆栈溢出检测功能，如果要开启，还需要自己实现对应的钩子函数，这里就直接关掉了，反正暂时也没什么用。如果不关闭会报错：\n.\\Objects\\freertos.axf: Error: L6218E: Undefined symbol vApplicationStackOverflowHook (referred from tasks.o). 中断服务函数（重点） # SysTick 中断服务函数是一个非常重要的函数，FreeRTOS 所有跟时间相关的事情都在里面处理，SysTick 就是 FreeRTOS 的一个心跳时钟，驱动着 FreeRTOS 的运行。\nFreeRTOS 接管了如下三个中断：SVC_Handler、PendSV_Handler、SysTick_Handler\n其中PendSV_Handler与SVC_Handler已经实现了，对应port.c中的xPortPendSVHandler与vPortSVCHandler。所以我们现在要做的是：\n注释掉 CW32 官方实现的PendSV_Handler与SVC_Handler函数，因为我们要使用 FreeRTOS 的对应的函数。 自己实现一个SysTick_Handler函数 注释原有的官方函数 # CW32 的中断函数定义都在interrupts_cw32f030.c中，只需要找到以下函数直接注释掉即可\n/** * @brief This function handles System service call via SWI instruction. * @note */ void SVC_Handler(void) { /* USER CODE BEGIN SVCall_IRQn */ /* USER CODE END SVCall_IRQn */ } /** * @brief This function handles Pendable request for system service. * @note */ void PendSV_Handler(void) { /* USER CODE BEGIN PendSV_IRQn */ /* USER CODE END PendSV_IRQn */ } ⚠️注意，在 CW32F03 中，启动文件定义了向量表中的函数名，因此需要修改 startup_cw32f030.s 中的相关字段，否则会导致程序在SysTick_Handler中卡死\n修改点 1 # 修改startup_cw32f030.s的 53 行\nDCD SVC_Handler ;\u0026lt; -5 System Service Call via SVC instruction 改为\nDCD vPortSVCHandler ;\u0026lt; -5 System Service Call via SVC instruction (FreeRTOS) 修改点 2 # 修改startup_cw32f030.s的 56 行\nDCD PendSV_Handler ;\u0026lt; -2 Pendable request for system service 改为\nDCD xPortPendSVHandler ;\u0026lt; -2 Pendable request for system service (FreeRTOS) 修改点 3 # 将startup_cw32f030.s的 105 行\nIMPORT SystemInit IMPORT __main LDR R1, =0x0 改为\nIMPORT SystemInit IMPORT __main IMPORT vPortSVCHandler IMPORT xPortPendSVHandler LDR R1, =0x0 实现 SysTick_Handler 函数 # 在一个你认为合适的地方写上以下代码，我写在BSP/clock/clock.c中\n#include \u0026#34;FreeRTOS.h\u0026#34; #include \u0026#34;task.h\u0026#34; extern void xPortSysTickHandler(void); /** * @brief This function handles System tick timer. */ void SysTick_Handler(void) { #if (INCLUDE_xTaskGetSchedulerState == 1) if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { #endif xPortSysTickHandler(); #if (INCLUDE_xTaskGetSchedulerState == 1) } #endif } 添加到工程 # 建议在 keil 中新建一个分组，将 FreeRTOS\\*.c FreeRTOS\\portable\\MemMang\\heap_4.c FreeRTOS\\portable\\RVDS\\ARM_CM0\\port.c 这些 c 文件添加到工程中，此时你的项目看上去应该是这样的：\n并添加头文件路径：\n测试代码 # 在 main.c 中写了一段简单的测试代码：创建两个任务，一个任务每隔 300ms 通过串口发送一段字符串，一个任务每隔 500ms 通过串口发送一段字符串。其中系统初始化和串口发送是根据我使用的 CW32F03 写的，这里要灵活变通。\n// FreeRTOS 头文件 #include \u0026#34;FreeRTOS.h\u0026#34; #include \u0026#34;task.h\u0026#34; // 开发板硬件 bsp 头文件 #include \u0026#34;bsp.h\u0026#34; #include \u0026#34;uart.h\u0026#34; #define MAX_TASK 2 static TaskHandle_t AppTask_Handle[MAX_TASK]; // 任务句柄 static BaseType_t xReturn[MAX_TASK]; // 创建信息返回值 static void AppTask1(void *parameter) { char data[] = \u0026#34;task1 running\u0026#34;; while (1) { uart_send((uint8_t *)data, sizeof(data)); vTaskDelay(300); } } static void AppTask2(void *parameter) { char data[] = \u0026#34;task2 running\u0026#34;; while (1) { uart_send((uint8_t *)data, sizeof(data)); vTaskDelay(500); } } int main(void) { /* 开发板硬件初始化 */ bsp_init(); uint8_t msg[] = \u0026#34;hello world\u0026#34;; uart_send(msg, sizeof(msg)); /* 创建两个任务 */ xReturn[0] = xTaskCreate((TaskFunction_t)AppTask1, // 任务入口函数 (const char *)\u0026#34;AppTask1\u0026#34;, // 任务名字 (uint16_t)128, // 任务栈大小 (void *)NULL, // 任务入口函数参数 (UBaseType_t)2, // 任务的优先级 (0~最大值 -1 有效) (TaskHandle_t *)\u0026amp;AppTask_Handle[0]); // 任务控制块指针 xReturn[1] = xTaskCreate((TaskFunction_t)AppTask2, // 任务入口函数 (const char *)\u0026#34;AppTask2\u0026#34;, // 任务名字 (uint16_t)128, // 任务栈大小 (void *)NULL, // 任务入口函数参数 (UBaseType_t)2, // 任务的优先级 (0~最大值 -1 有效) (TaskHandle_t *)\u0026amp;AppTask_Handle[1]); // 任务控制块指针 /* 启动任务调度 */ for (int i = 0; i \u0026lt; MAX_TASK; i++) { if (xReturn[i] != pdPASS) { return -1; // 如果有创建失败的任务，就结束程序 } } vTaskStartScheduler(); // 启动任务，开启调度 while (1) ; // 正常不会执行到这里 } 编译后 0 警告 0 错误，直接烧写到板上\n烧写后，程序运行效果如下，自此，CW32F03 移植 FreeRTOS 成功：\n参考链接 # HC32F460 freeRTOS 移植\n超详细的 FreeRTOS 移植全教程——基于 srm32\n","date":"2026-01-19","externalUrl":null,"permalink":"/posts/069bc222/","section":"Posts","summary":"","title":"CW32 移植 FreeRTOS","type":"posts"},{"content":" 关于这个博客 # 本博客使用 Hugo 驱动，主题是 Blowfish\n托管在 Cloudflare 中\n关于我 # Kydin，读音可以是「肯丁」\n不知名的菜鸟程序员\n主要做嵌入式开发，C/C++、Qt，可以写一些 React。准备学学 Vue\n可以访问我的在线简历\n喜欢猫，犬子张西西，是一只英短快乐小猫\n二次元浓度 10% 吧，看的都是经典番，最喜欢的是「进击的巨人」\nLOL 退役中…只打大乱斗，最近都在玩王国之泪\n","date":"2025-06-03","externalUrl":null,"permalink":"/about/","section":"Kydin's Blog","summary":"","title":"About","type":"page"},{"content":" 前情提要 # 笔者最近在开发一个项目的生产软件，用来测试产品的功能，产品在出厂的最后一步需要烧写 MCU（芯源）和 FPGA（高云）的固件，因此需要给测试人员讲解 keil 和高云的软件怎么烧写固件，增加了学习成本。因此萌生出在生产软件中集成烧写 MCU 和 FPGA 固件的想法。\nFPGA 芯片使用的是高云的 GW1N-4D，在高云官方提供的烧录软件中，提供了一个 cli 的可执行文件，于是我们就可以使用 QProcess 来调用这个软件，实现集成烧写 FPGA 固件的功能。\n用到的指令有：\n.\\programmer_cli.exe --scan-cables // 列出当前所有已连接的下载器 .\\programmer_cli.exe --scan // 列出当前所有已连接的设备 .\\programmer_cli.exe --operation_index 6 --device GW1N-4D --fsFile 固件路径 // 烧写固件 其中 --operation_index 6 代表embFlash Erase,Program,Verify，你可以根据实际情况填写 --device GW1N-4D 代表指定要烧写的芯片型号，你应该根据实际情况填写 --fsFile 固件路径 代表需要烧写的固件，应该使用.fs 结尾的文件\n还有更多的参数可以查阅高云提供的《SUG502-1.6_Gowin_Programmer 用户指南.pdf》\n开发环境 # Qt6.9 MSVC 2019 Windows 11 24H2 Gowin_Programmer Version 1.9.9 (64-bit) build(31129) 在 Qt 中实现调用 Gowin_Programmer # 在 Qt 中，调用另一个可执行文件，我们可以通过 QProcess，可以转入我们想要的参数，并在回调函数中获取可执行文件的输出。\n获取所有连接的下载器 # 由于 Gowin_Programmer 不支持输出 json 格式，我直接使用字符串解析来获取它的返回值。\n核心代码：\nvoid FpgaUpdateInterface::ScanProbe(QString gowin_cli_path) { emit DebugMessage(\u0026#34;Scanning probes...\u0026#34;); action_ = Fpga_Proces_Action_ScanProbes; process_-\u0026gt;start(gowin_cli_path, QStringList() \u0026lt;\u0026lt; \u0026#34;--scan-cables\u0026#34;); } void FpgaUpdateInterface::ParseProbeList(const QString\u0026amp; raw_output) { qDebug() \u0026lt;\u0026lt; \u0026#34;Raw probe scan output:\u0026#34; \u0026lt;\u0026lt; raw_output; /* PS C:\u0026gt; .\\programmer_cli.exe --scan-cables Cable found: Gowin USB Cable(FT2CH)/0/786/null (USB location:786) Cost 0.05 second(s) */ for (const QString\u0026amp; line : raw_output.split(\u0026#34;\\r\\n\u0026#34;)) { if (line.contains(\u0026#34;Cable found:\u0026#34;)) { QString probe = line.mid(QString(\u0026#34;Cable found:\u0026#34;).length() + 1).trimmed(); probe_list_.append(probe); } } } 获取所有连接的设备 # 同样也是使用字符串解析来获取它的返回值。\n核心代码：\nvvoid FpgaUpdateInterface::ScanDevice(QString gowin_cli_path) { emit DebugMessage(\u0026#34;Scanning device...\u0026#34;); action_ = Fpga_Proces_Action_ScanDevice; process_-\u0026gt;start(gowin_cli_path, QStringList() \u0026lt;\u0026lt; \u0026#34;--scan\u0026#34;); } void FpgaUpdateInterface::ParseDeviceList(const QString\u0026amp; raw_output) { qDebug() \u0026lt;\u0026lt; \u0026#34;Raw device scan output:\u0026#34; \u0026lt;\u0026lt; raw_output; /* 失败 PS C:\u0026gt; .\\programmer_cli.exe --scan Scanning! Target Cable: Gowin USB Cable(FT2CH)/0/0/null@2.5MHz Error: No Gowin devices found! Cost 0.54 second(s) 成功 PS C:\u0026gt; .\\programmer_cli.exe --scan Scanning! Target Cable: Gowin USB Cable(FT2CH)/0/0/null@2.5MHz Device Info: Family: GW1NRF Name: GW1N-4D GW1NR-4D GW1N-4B GW1NR-4B GW1NRF-4B (One of them) ID: 0x1100381B 1 device(s) found! Cost 0.54 second(s) */ for (const QString\u0026amp; line : raw_output.split(\u0026#39;\\n\u0026#39;)) { if (line.contains(\u0026#34;ID:\u0026#34;)) { QString id = line.mid(QString(\u0026#34;ID:\u0026#34;).length() + 1).trimmed(); device_list_.append(id); } } } 烧写固件 # 烧写固件的指令就是上面说的那个指令，不过有一点要注意的是，烧写过程中会有两个进度条，先是烧写固件，随后进行校验，并且 Gowin_Programmer 每次输出都是完整的一行，因此可以直接使用正则表达式来解析输出，这点还是比较方便的。\n核心代码：\nvoid FpgaUpdateInterface::StartFirmwareUpdate(FirmwareUpdateParam param) { emit DebugMessage(\u0026#34;Starting Firmware Update\u0026#34;); QStringList arguments; arguments \u0026lt;\u0026lt; \u0026#34;--operation_index\u0026#34; \u0026lt;\u0026lt; QString::number(param.operation_index); if (!param.target_chip.isEmpty()) { arguments \u0026lt;\u0026lt; \u0026#34;--device\u0026#34; \u0026lt;\u0026lt; param.target_chip; } if (!param.firmware_path.isEmpty()) { arguments \u0026lt;\u0026lt; \u0026#34;--fsFile\u0026#34; \u0026lt;\u0026lt; param.firmware_path; } else { emit DebugMessage(\u0026#34;Firmware path is empty!\u0026#34;); return; } qDebug() \u0026lt;\u0026lt; \u0026#34;QProcess run command: [\u0026#34; \u0026lt;\u0026lt; param.gowin_cli_path \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; arguments \u0026lt;\u0026lt; \u0026#34;]\u0026#34;; action_ = Fpga_Proces_Action_InProgress; process_-\u0026gt;setProcessChannelMode(QProcess::MergedChannels); // 设置进程通道模式 process_-\u0026gt;start(param.gowin_cli_path, arguments); } void FpgaUpdateInterface::ParseFirmwareUpdateInfo(const QString\u0026amp; output) { qDebug() \u0026lt;\u0026lt; \u0026#34;Firmware update output:\u0026#34; \u0026lt;\u0026lt; output; // 升级阶段 // gowin/programmer_cli.exe 通常输出类似 \u0026#34;\\rProgramming...: [# ] 4% \u0026#34; 的进度信息 // 校验阶段 // gowin/programmer_cli.exe 通常输出类似 \u0026#34;\\rVerifying...: [# ] 4% \u0026#34; 的进度信息 QRegularExpression progressRegex(R\u0026#34;(\\r(Programming|Verifying)\\s*\\.{0,3}:\\s*\\[(#+)\\s*\\]\\s*(\\d+)%\\s*)\u0026#34;); QRegularExpressionMatch match = progressRegex.match(output); if (match.hasMatch()) { QString stage = match.captured(1); // Programming 或 Verifying int percentage = match.captured(3).toInt(); // 百分比 // 根据阶段处理进度 if (stage == \u0026#34;Programming\u0026#34;) { emit ProgrammingUpdated(percentage); } else if (stage == \u0026#34;Verifying\u0026#34;) { emit VerifyingUpdated(percentage); } } } 运行效果 # 结语 # 集成在生产软件中后，通过傻瓜式的页面操作，降低了测试同事的学习成本，也能够减少工作量，算是很有收获的一次功能更新。\n本文的完整代码和编译好的可执行程序可以在 GitHub 中找到：QGowin\n","date":"2025-04-10","externalUrl":null,"permalink":"/posts/3c671acb/","section":"Posts","summary":"","title":"在 Qt 中实现烧录高云 FPGA 的功能","type":"posts"},{"content":"笔者最近在开发一个项目的生产软件，用来测试产品的功能，产品在出厂的最后一步需要烧写 MCU（芯源）和 FPGA（高云）的固件，因此需要给测试人员讲解 keil 和高云的软件怎么烧写固件，增加了学习成本。因此萌生出在生产软件中集成烧写 MCU 和 FPGA 固件的想法。\nMCU 芯片使用的是芯源的 CW32F030，研究了一下有什么可行的方案，最后选定了 QProcess + pyOCD 的方案。\n开发环境 # Qt6.9 MSVC 2019 Windows 11 24H2 pyOCD 0.40.0 什么是 pyOCD # Python based tool and API for debugging, programming, and exploring Arm Cortex microcontrollers.\n从官网的介绍可以得知，pyOCD 是一个基于 Python 的工具和 API，用于调试、编程和探索 Arm Cortex 微控制器，我们可以通过它来实现烧写 CW32。\n由于 CW32 并不在 pyOCD 的官方支持型号中，我们可以将厂家提供的 pack 包（也就是安装到 keil 中的那个，不同的型号都可以在厂家的官网中下载到）作为参数传入 pyOCD，以此来实现固件的烧写操作，这也是选择 pyOCD 的原因之一，以后可以很方便的增加新的芯片烧写支持。\n用到的指令有：\n.\\pyocd.exe list // 列出当前所有已连接的下载器 .\\pyocd.exe json // 以json形式输出所有已连接的下载器 .\\pyocd.exe flash --erase chip --target CW32F030F8 --pack WHXY.CW32F030_DFP.1.0.4.pack 固件路径 // 烧写固件 其中 --erase chip 代表擦除芯片 --target CW32F030F8 代表指定要烧写的芯片型号，你应该根据实际情况填写 --pack WHXY.CW32F030_DFP.1.0.4.pack 代表指定要烧写芯片的开发包，一般是厂家提供（如果你的芯片是 pyOCD 官方支持的，比如 stm32，就不需要这个配置） 至于固件，pyOCD 原生支持 .axf、.hex 和 .bin 格式的固件。\n在 Qt 中实现调用 pyOCD # 在 Qt 中，调用另一个可执行文件，我们可以通过 QProcess，可以转入我们想要的参数，并在回调函数中获取可执行文件的输出。\n获取所有连接的下载器 # 由于 pyOCD 支持输出 json 格式，我们就可以很方便地解析出所有连接的下载器，我使用的是nlohmann/json 这个 json 库。\n核心代码：\nvoid FirmwareUpdateInterface::ScanProbe(QString pyocd_path) { process_-\u0026gt;start(pyocd_path, QStringList() \u0026lt;\u0026lt; \u0026#34;json\u0026#34;); } void FirmwareUpdateInterface::ParseProbeList(const QString\u0026amp; raw_output) { QStringList probes; json j = json::parse(raw_output.toStdString(), nullptr, false); /* { \u0026#34;pyocd_version\u0026#34;: \u0026#34;0.40.0\u0026#34;, \u0026#34;version\u0026#34;: { \u0026#34;major\u0026#34;: 1, \u0026#34;minor\u0026#34;: 1 }, \u0026#34;status\u0026#34;: 0, \u0026#34;boards\u0026#34;: [ { \u0026#34;unique_id\u0026#34;: \u0026#34;B8D4ECDC00E1\u0026#34;, \u0026#34;info\u0026#34;: \u0026#34;embedfire CMSIS-DAP\u0026#34;, \u0026#34;board_vendor\u0026#34;: null, \u0026#34;board_name\u0026#34;: \u0026#34;Generic\u0026#34;, \u0026#34;target\u0026#34;: \u0026#34;cortex_m\u0026#34;, \u0026#34;vendor_name\u0026#34;: \u0026#34;embedfire\u0026#34;, \u0026#34;product_name\u0026#34;: \u0026#34;CMSIS-DAP\u0026#34; } ] } */ QString pyocd_version = QString::fromStdString(j.value(\u0026#34;pyocd_version\u0026#34;, \u0026#34;unknown\u0026#34;)); if (j.contains(\u0026#34;boards\u0026#34;) \u0026amp;\u0026amp; j[\u0026#34;boards\u0026#34;].is_array()) { for (const auto \u0026amp;board : j[\u0026#34;boards\u0026#34;]) { ProbeInfo probe; probe.id = QString::fromStdString(board.value(\u0026#34;unique_id\u0026#34;, \u0026#34;unknown\u0026#34;)); probe.info = QString::fromStdString(board.value(\u0026#34;info\u0026#34;, \u0026#34;unknown\u0026#34;)); probe.target = QString::fromStdString(board.value(\u0026#34;product_name\u0026#34;, \u0026#34;unknown\u0026#34;)); probe_list_.append(probe); } } } 烧写固件 # 烧写固件的指令就是上面说的那个指令，不过有一点要注意的是，pyOCD 烧写过程中，每一次的输出并不是完整的一行，而是一个个字符，因此在解析成进度条时要特殊处理一下，我的方案是：因为 pyOCD 完成烧写一共会输出 50 个=号，因此只需要记录下已经输出的=的个数，就可以算出烧写完成了百分之多少。\n核心代码：\nvoid FirmwareUpdateInterface::StartFirmwareUpdate(FirmwareUpdateParam param) { emit DebugMessage(\u0026#34;Starting Firmware Update\u0026#34;); QStringList arguments; arguments \u0026lt;\u0026lt; \u0026#34;flash\u0026#34;; if (param.is_erase_chip) { arguments \u0026lt;\u0026lt; \u0026#34;--erase\u0026#34; \u0026lt;\u0026lt; \u0026#34;chip\u0026#34;; } if (!param.target_chip.isEmpty()) { arguments \u0026lt;\u0026lt; \u0026#34;--target\u0026#34; \u0026lt;\u0026lt; param.target_chip; } if (!param.pack_path.isEmpty()) { arguments \u0026lt;\u0026lt; \u0026#34;--pack\u0026#34; \u0026lt;\u0026lt; param.pack_path; } if (!param.firmware_path.isEmpty()) { arguments \u0026lt;\u0026lt; param.firmware_path; } else { emit DebugMessage(\u0026#34;Firmware path is empty!\u0026#34;); return; } qDebug() \u0026lt;\u0026lt; \u0026#34;QProcess run command: [\u0026#34; \u0026lt;\u0026lt; param.pyocd_path \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; arguments \u0026lt;\u0026lt; \u0026#34;]\u0026#34;; action_ = InProgress; process_-\u0026gt;setProcessChannelMode(QProcess::MergedChannels); // 设置进程通道模式 process_-\u0026gt;start(param.pyocd_path, arguments); } void FirmwareUpdateInterface::ParseFirmwareUpdateInfo(const QString\u0026amp; output) { // pyOCD 通常输出类似 \u0026#34;[=== ] 45%\u0026#34; 的进度信息 // 方法：匹配进度条格式 \u0026#34;[======= ]\u0026#34; // 进度条通常有固定宽度，50 个字符 if (output.contains(\u0026#34;=\u0026#34;)) { percentage_ += output.count(\u0026#39;=\u0026#39;); int percentage = (percentage_ * 100) / 50; emit ProgressUpdated(percentage); } } 运行效果 # 我尝试烧写两个不同的固件，它们之间的差异是版本号不同。烧写完成后通过读写寄存器，证实了我们的烧写确实是成功了的。\n结语 # pyOCD 在烧写固件方面确实比 keil 方便不少，集成在生产软件中后，通过傻瓜式的页面操作，降低了测试同事的学习成本，也能够减少工作量，算是很有收获的一次功能更新。\n本文的完整代码和编译好的可执行程序可以在 GitHub 中找到：QPyocd\n","date":"2025-04-07","externalUrl":null,"permalink":"/posts/1c812bc3/","section":"Posts","summary":"","title":"在 Qt 中实现烧录 MCU 的功能","type":"posts"},{"content":"笔者最近收到一个新需求：将工业相机的视频流接入生产软件中。由于以前没接触过这类 uvc 摄像头，特此做一个记录。\n什么是 uvc 摄像头 # USB 视频类（UVC）是一个标准类规范，用于标准化 USB 上的视频流功能。它使网络摄像头、数字摄像机、模拟视频转换器、模拟和数字电视调谐器等设备能够与主机无缝连接。\n简单来说，通过 USB 线，就可以实现摄像头的供电、数据传输，并且在 Windows 10 及以上的操作系统中可以即插即用。\n开发环境 # Qt6.9 MSVC 2019 Windows 11 24H2 代码实现 # 使用 QVideoWidget + QMediaCaptureSession，可以实现非常好的实时性能。\n头文件：\n#pragma once #include \u0026lt;QWidget\u0026gt; #include \u0026lt;QImage\u0026gt; #include \u0026lt;QMediaCaptureSession\u0026gt; class UvcCamera : public QWidget { Q_OBJECT public: UvcCamera(QWidget *parent = nullptr); ~UvcCamera(); signals: void ImageCaptured(QImage\u0026amp; img); private: void SelectCamera(const QCameraDevice\u0026amp; dev); private: QMediaCaptureSession* capture_session_; QImageCapture* image_capture_; QImage current_frame_; // 存储当前帧图像 QTimer* capture_timer_; // 捕获定时器 }; 源文件：\nUvcCamera::UvcCamera(QWidget *parent) : QWidget(parent) { this-\u0026gt;setMinimumSize(800, 600); QWidget* central = new QWidget(this); auto* layout = new QVBoxLayout(central); // 摄像头列表控件 auto* cb_camera_list = new QComboBox(this); layout-\u0026gt;addWidget(cb_camera_list); // 视频显示控件 auto* video_widget = new QVideoWidget(this); layout-\u0026gt;addWidget(video_widget); layout-\u0026gt;setContentsMargins(0, 0, 0, 0); // 添加边距设置 this-\u0026gt;setLayout(layout); // 设置主布局 // 列出可用摄像头 const auto cameras = QMediaDevices::videoInputs(); for (const QCameraDevice\u0026amp; dev : cameras) { cb_camera_list-\u0026gt;addItem(dev.description(), QVariant::fromValue(dev)); } // 创建捕获会话与摄像头 capture_session_ = new QMediaCaptureSession(this); capture_session_-\u0026gt;setVideoOutput(video_widget); // 创建图像捕获对象 image_capture_ = new QImageCapture(capture_session_); capture_session_-\u0026gt;setImageCapture(image_capture_); // 定时器定期更新当前帧 capture_timer_ = new QTimer(this); connect(capture_timer_, \u0026amp;QTimer::timeout, this, [=]() { if (image_capture_-\u0026gt;isReadyForCapture()) { image_capture_-\u0026gt;capture(); } }); // 连接图像捕获完成信号 connect(image_capture_, \u0026amp;QImageCapture::imageCaptured, this, [=](int id, const QImage\u0026amp; image) { Q_UNUSED(id); current_frame_ = image; emit ImageCaptured(current_frame_); }); connect(cb_camera_list, \u0026amp;QComboBox::currentIndexChanged, this, [=](int idx) { if (idx \u0026gt;= 0) { QCameraDevice dev = cb_camera_list-\u0026gt;currentData().value\u0026lt;QCameraDevice\u0026gt;(); SelectCamera(dev); } }); // 选择第一个或者用户选择的设备 if (!cameras.isEmpty()) { SelectCamera(cameras.first()); } else { qDebug() \u0026lt;\u0026lt; \u0026#34;没有找到可用的摄像头设备\u0026#34;; } } UvcCamera::~UvcCamera() { if (capture_timer_-\u0026gt;isActive()) { capture_timer_-\u0026gt;stop(); } } void UvcCamera::SelectCamera(const QCameraDevice\u0026amp; dev) { // 先停止并删除之前的摄像头 if (capture_session_-\u0026gt;camera()) { capture_session_-\u0026gt;camera()-\u0026gt;stop(); delete capture_session_-\u0026gt;camera(); } auto* camera = new QCamera(dev, capture_session_); // capture_session_ 作为父对象，方便生命周期管理 capture_session_-\u0026gt;setCamera(camera); // 定期捕获图像以更新当前帧 connect(camera, \u0026amp;QCamera::activeChanged, this, [=](bool active) { if (active) { if (!capture_timer_-\u0026gt;isActive()) { capture_timer_-\u0026gt;start(1000); // 每秒更新一次当前帧 } } else { if (capture_timer_-\u0026gt;isActive()) { capture_timer_-\u0026gt;stop(); } } }); camera-\u0026gt;start(); // 开始采集 qDebug() \u0026lt;\u0026lt; QStringLiteral(\u0026#34;正在使用相机：%1\u0026#34;).arg(dev.description()); } 记得在 cmake 中添加：\nfind_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Multimedia) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS MultimediaWidgets) target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Multimedia) target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::MultimediaWidgets) 运行效果 # ","date":"2025-03-15","externalUrl":null,"permalink":"/posts/ca02a081/","section":"Posts","summary":"","title":"使用 Qt 显示 uvc 摄像头的视频流","type":"posts"},{"content":" 前言 # 笔者最近尝试在 x86 平台快速搭建一个 arm 平台的测试环境，节省对开发板的依赖，主要是用来测试应用层的软件，所以使用 docker 搭建了一个测试环境。\n前期准备 # 宿主机：Ubuntu 24.04 x86_64 安装了 docekr 环境搭建 # 在 docker 运行跨架构的镜像，我是使用了 qemu-user-static 这个项目，\n首先要开启 docker 的实验性功能，编辑 /etc/docker/daemon.json 文件，如果没有就创建，添加以下字段：\n\u0026#34;experimental\u0026#34;: true 安装工具包\nsudo apt install qemu-user-static 开启 qemu-user-static 容器\ndocker run --privileged multiarch/qemu-user-static --reset -p yes 容器的选择 # 在 dockerhub 的 arm64v8 中，有许多不同镜像可以选择，例如 ubuntu、debian 等主流的 linux 发行版都是有的。\ndocker run -it --platform linux/arm64 -p 65110:80 -v $(pwd)/root:/root --name armbuntu_noble arm64v8/ubuntu:noble /bin/bash 指令解析：\narm64v8/ubuntu:noble：运行一个 arm64v8/ubuntu:noble 的容器，\n\u0026ndash;name armbuntu_noble：名称叫做 armbuntu_noble，\n\u0026ndash;platform linux/arm64：使用 arm64 平台，\n-v $(pwd)/root:/root：将当前目录下的 /root 目录映射为容器的 /root 目录，\n-p 65110:80：将容器的 80 端口映射为主机的 65110 端口。\n效果如下： ","date":"2025-02-20","externalUrl":null,"permalink":"/posts/1b422878/","section":"Posts","summary":"","title":"使用docker在x86平台快速搭建一个arm平台的测试环境","type":"posts"},{"content":" 起因 # 之前的博客采用 hexo 进行驱动，整体风格现在看来也还是挺满意的，但是由于换了电脑，之前博客的源文件全部丢失了。加上看到 hugo 的编译效率比 hexo 快不少，于是干脆直接迁移过来。\n距离上一次部署博客已经过了两年多了，这次部署前大概列了几项需求：\n原则：\n不要友链 不要评论区 支持 RSS 全文输出 文章页侧边有目录栏 加分项：\nAI 生成文章摘要 文章可以设置封面图 随机文章 ID 方便更换字体 关于原则的思考 # 大概是现在的自己越来越封闭，只想要把这个博客作为一个单纯写点东西的平台，想在不缺乏基本功能的前提下做到极简。\n于是新的博客去除了文章评论区和友链页，整个页面干净了许多。如果真有什么问题需要交流，完全可以在关于页面找到我。\nRSS # 首先，能够方便地进行 RSS 全文输出，对于我个人来说是非常必要的。hugo 原生就 支持 RSS 输出，相比 hexo 还需要安装插件，方便了不少。\n需要注意的是，默认输出的 RSS 不是全文输出而是摘要输出，我的做法是： 将主题中的 rss.xml 复制到根目录进行修改， 也就是复制 themes/blowfish/layouts/_default/rss.xml 到 layouts/_default/rss.xml。\n然后将 layouts/_default/rss.xml 中的 \u0026lt;description\u0026gt;{{ .Summary | html }}\u0026lt;/description\u0026gt; 修改为 \u0026lt;description\u0026gt;{{ .Content | html }}\u0026lt;/description\u0026gt; 即可。\n随机文章 ID # 之前在 hexo 中使用了 hexo-abbrlink 插件，能够给每一篇文章生成一个随机的唯一 ID，因为默认的文章 URL 是标题，例如：https://kydins.com/posts/从hexo迁移到hugo/\n这样子做有两个问题，一是太丑了，二是带有中文不利于 SEO。\n在 hugo 中我参考了 Ramen 的Hugo 永久链接 的方案\n永久链接的生成方案是比较简单的，直接对时间 + 文章名生成字符串做一下 md5 然后取任意 4-12 位即可。这样做的话 md5 冲撞概率极小，同时也没有那么大的运算负担。\n而 Hugo 在永久链接中支持下面这个参数：slug。简单来说，我们可以针对每一篇文章指定一个 slug，然后在 config.toml 中配置 permalinks 包含 slug 参数，就可以生成唯一的永久链接。我们的目的就是对每篇文章自动生成一个 slug。\n由于是 2018 年的文章，现在的 hugo 配置已经变更，所以我做了一些改动：\n修改 archetypes/default.md 添加如下一行：\nslug = \u0026#39;{{ substr (md5 (printf \u0026#34;%s%s\u0026#34; .Date (replace .TranslationBaseName \u0026#34;-\u0026#34; \u0026#34; \u0026#34; | title))) 4 8 }}\u0026#39; 然后修改 hugo.toml 添加如下行：\n[permalinks] post = \u0026#34;/post/:slug\u0026#34; 之后每次新建文章都会有一个唯一的 ID。\n字体 # 然后就是字体，方便更换字体也是非常重要的一点，目前本博客采用的字体是 LXGW Bright\n因为使用的是 Blowfish 主题，更换字体请参考 Blowfish-进阶自定义\n目录栏 # 现在使用的主题是 Blowfish，可以很方便得开启侧边目录栏，同时支持响应式布局，如果侧边空间不足，目录栏会显示在文章开头。\n修改 config/_default/params.toml 中的\n[article] showTableOfContents = true # 是否展示文章的目录 即可开启侧边栏\nAI 生成文章摘要 # TODO\n搭建 # 部署 hugo 和应用主题的方式非常简单，直接按照 官方文档 就可以，我是直接使用 git 子仓库的方式应用的主题，方便后期更新\n值得注意的是，hugo 的默认配置文件已经从 config.toml 变为 hugo.toml，在看文档的时候需要注意一下\n在 Cloudflare Pages 中发布 # 原先的博客是托管在GitHub Pages中，勉强能够满足使用但是有些小地方不是很舒服。一是使用GitHub Pages必须使用公开的仓库，二是GitHub Pages的网络在某些地方不够稳定。\n因此决定趁这次机会，将博客托管到Cloudflare Pages中。参考官方文档：deploy-a-hugo-site\n","date":"2025-01-29","externalUrl":null,"permalink":"/posts/976a1e5f/","section":"Posts","summary":"","title":"从hexo迁移到hugo","type":"posts"},{"content":"第一次接触到锤子手机，是在高三的时候，当时我拿着偷偷攒了好几周的生活费，在小白有品上买了一个二手的坚果 3。\n其实我当时并不知道锤子是啥，也不认识罗永浩。\n我买坚果 3 的原因就是因为它是我在商城里能找到最便宜的手机。\n我记得大概是 1100 元这样的价格，也正是通过这部手机，我才开始接触到锤子科技。\n坚果 3 给我的第一印象就是屏幕好大，我的上一部手机还是 iPhone 5，所以我仿佛像一个原始人走出山洞一样。\n它最有特色的一点就是前置摄像头是在 home 键旁边的，当你 180 倒转手机，相机会自动打开并设置成前置模式。\n像素非常垃圾，但是对于我这种不自拍的人，这反而是好事，我获得了一个放到现在我也觉得很大的屏幕。\n很多人提前锤子手机，都会说的三件套：一步、大爆炸、闪念胶囊。\n这几个功能确实是 smartisan os 独居匠心的功能，但是美中不足的是大爆炸功能的识图不是本地处理的。\n这部手机我一直用到我高三毕业的暑假，后来有一天晚上我上厕所的时候，不小心把它掉到马桶里了，但居然没有坏掉，我便保留至今了。\n后来我在大四实习的时候，买了一部绿色的坚果 r2，这是卖给字节跳动后的第一部也是最后一部锤子手机，目前仍然是我的主力机之一。\n我认为我在青少年成长期间，接触到各种各样的电子产品很大一部分构造了我的审美观，我会因为它们出色的工业设计所流连。\n我在有能力攒钱后，买了很多手机，例如 iPhone123456、黑莓、小米、锤子。我是向往那个自由、充满创意和机遇的年代的。\n我最喜欢锤子的几个方面：系统提供了一个通过摄像头来实现不同环境光角度下的图标阴影，这是一个吃力不讨好的功能。\n它牺牲了一部分续航和性能，但换来的只是当你浏览桌面时，显得更好看。这是非常理想主义的功能，我非常喜欢。\n锤子的桌面主题也非常耐看，重新绘制的图标就在每年 618、双十一的时候取代了晦气的横幅，同时整个系统的 UI 是非常拟物好看的。\n也因为锤子的早早倒闭，手机上是没有任何广告推送的，同时居然在系统设置里提供了不显示应用名称、不显示小红点、贴边消除小红点等强迫症功能。\n我也通过咸鱼买了一整套 TNT go，我对于锤子的 TNT 系统还是非常喜欢的，虽然现在已经没有多少应用有适配了。\n但是 wps 和剪映居然完美适配，不得不说轻办公的生产力还是有的。搭配上文件系统管理、Termux、UserLAnd 等安卓上的软件，我觉得还是比 ipad 强一点的。\n对于 TNT go 的屏幕素质，也是一块百元内非常好的便携屏：12 英寸 2K 高清显示屏，分辨率为 2160*1440，最高亮度达到 380 尼特。\n我将它放到机柜中，显示家里的网络设备信息，非常合适。\n以上就是我的锤子们的故事，因为目前缺少摄像设备，没能 po 上照片，过段时间我会整理上来。\n希望我们都能成为圆滑当道的尖锐异类。\n","date":"2024-08-04","externalUrl":null,"permalink":"/posts/76f08b53/","section":"Posts","summary":"","title":"我的锤子们","type":"posts"},{"content":"最近学习了 zsh 的一些用法和 Homebrew，继续给大家推荐一些好用的软件，我写了一个一键安装脚本，持续更新中。\n不过我也是刚刚接触到 Mac 生态，所以还是有很多地方不太清楚，这篇文章也算是自己学习的记录吧！\n如果有什么不对的地方请大家多多指教，希望将来我换一台性能好一点的电脑之后，也能尝试着开始写自己的软件！\n#!/usr/bin/env zsh # 允许安装任意来源的 App sudo spctl --master-disable # 安装 Xcode Command Line Tools xcode-select --install # 安装 Homebrew # 官网：https://brew.sh /bin/bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\u0026#34; brew install cask # 安装 oh-my-zsh # 官网：https://ohmyz.sh sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; # 有时候执行 cd 会卡，这让人难以忍受。原因是 oh-my-zsh 在获取 git 信息，可以将 git 信息隐藏 git config --global oh-my-zsh.hide-status 1 # TAG：开源、完全免费 # 介绍：一个图形化的Homebrew应用商店 # 网站：https://aerolite.dev/applite/index.html brew install --cask applite # TAG：开源、完全免费 # 介绍：一个reminders的插件，可以集成到菜单栏中 # 网站：https://github.com/DamascenoRafael/reminders-menubar brew install --cask reminders-menubar # TAG：闭源、核心功能免费 # 介绍：一个快捷启动工具 # 网站：https://www.raycast.com brew install --cask raycast # TAG：开源、完全免费 # 介绍：记住每个app使用的输入法 # 网站：https://keyboardholder.leavesc.com brew install --cask keyboardholder # TAG：开源、完全免费 # 介绍：一个全能的下载器，支持 HTTP, FTP, BitTorrent, Magnet 等 # 网站：https://motrix.app/zh-CN/ brew install --cask motrix # TAG：开源、完全免费 # 介绍：Mac上的一款终端工具 # 网站：https://iterm2.com brew install --cask iterm2 # TAG：开源、完全免费 # 介绍：窗口管理软件 # 网站：https://github.com/MrKai77/Loop brew install --cask mrkai77/cask/loop # TAG：开源、完全免费 # 介绍：Docker管理软件，比原生的运行快速 # 网站：https://orbstack.dev brew install --cask orbstack # TAG：闭源，付费 # 介绍：Mac图片音视频格式转换工具 brew install --cask permute # TAG：开源，完全免费 # 介绍：控制多台显示器的亮度 # 网站：https://github.com/MonitorControl/MonitorControl brew install --cask MonitorControl # TAG：开源、完全免费 # 介绍：指定某个蓝牙设备离开时锁定Mac，靠近时点亮 # 网站：https://github.com/ts1/BLEUnlock brew install --cask bleunlock # TAG：开源、完全免费 # 介绍：广受好评的Mac鼠标增强插件 # 网站：https://github.com/Caldis/Mos brew install --cask mos # TAG：开源、完全免费 # 介绍：Mac上的统计工具，在菜单栏上显示系统CPU、内存等使用率 brew install --cask stats # TAG：开源、完全免费 # 介绍：升级Mac系统，测试机必备 # 网站：https://github.com/ninxsoft/Mist brew install --cask mist # TAG：开源、完全免费 # 介绍：一个在终端（或编辑器）中打开当前目录的Finder应用的插件 # 网站：https://github.com/Ji4n1ng/OpenInTerminal brew install --cask openinterminal-lite # 不必多说部分 brew install --cask telegram brew install --cask discord brew install --cask steam brew install --cask visual-studio-code 我将这个脚本放到 GitHub 上了，后续有更新的话我会同步更新，你也可以直接使用以下指令直接一键安装：\ncurl -fsSL \u0026#34;https://github.com/HiKydin/init-my-mac/blob/main/init-my-mac.zsh\u0026#34; -o init-my-mac.zsh sudo sh init-my-mac.zsh ","date":"2024-06-27","externalUrl":null,"permalink":"/posts/5a920f12/","section":"Posts","summary":"","title":"当我拿到新的 Mac 时我会做的事情","type":"posts"},{"content":"说起我第一次接触到 iPad，是在小学的时候，当时哥哥有一台 iPad，他教我在上面打炉石传说。 那个暑假我过得非常开心，我唯一学会的就是元素法，但一直为打不过副本的蜘蛛而苦恼。 当时我白天热得不行的时候就跑去找哥哥，他也会慷慨得给予我 1 个小时的游戏时间，晚上的时候我们就一起洗澡，然后出发去买 5 毛钱的香芋味甜筒。 我的同龄人都处在一个游戏百花齐放的时代，手游、单机、网游、页游层出不穷。 当大家都在玩着 cf、洛克王国时，我暗戳戳有种高傲：我已经在 iPad 上玩炉石了。那年是 2012 年。\n后来我就再也没接触过 iPad 了。直到大二的时候，攒了几个月的生活费，在淘宝上买了一台 iPad Pro 10.5 寸，是有 home 键的那个版本。 当时我还配了一根一代笔，就是那个充电得插在平板屁股上的愚蠢设计的一代手写笔。 说实话我当时并不知道我买 iPad 是需要做什么，因为我都是用笔记本电脑。或者说我是被“我想要一台打游戏看视频的平板”的消费主义裹挟了。 在计算机专业的学生中，我几乎没看到有人上课带着一台 iPad 去做所谓的“无纸化学习”。 可能是因为对计算机的学生来说，不需要有这么多写写画画。 同时我也不认为买一台 iPad 的有助于考研，我见过太多去图书馆追剧的人了。 我曾经试着给我的 iPad 配上蓝牙键盘和蓝牙鼠标，但用了不久就放弃——实在是太难用了连接鼠标。后来我的 iPad 就一直放着吃灰，只好低价出给学弟了。\n其实那个时候，苹果就开始让 iPad 变成生产力的标志，或许是因为我不是它的目标人群，直到今天我都觉得 iPad 只是一个娱乐工具。 所以等到我大三的时候，我买了全新的 iPad mini6。 我对于 mini6 的设计是很满意的，完美的屏占比，没有 home 键。 这里我要顺带说一下，我个人是非常讨厌指纹解锁的，因为我的手汗特别多。 我大学的时候用的是 iPhone8 plus，几乎每次我掏出它要展示健康码的时候，我都得很狼狈地输密码。 同时我还买了一根二代笔——我不知道用途是啥，但是每个人都说你需要一根笔，其实我根本用不上。\n虽然 mini6 非常顺手，它适合阅读、适合看剧、适合打游戏。 但是我拿起它的次数并不多，可能是这种需要拿起它的感觉太麻烦了——因为手机就已经长在我手上了。 我相信很多人第一次接触苹果产品，都是通过 ipad。很多家长不会给小孩子玩手机，但是他们会提供一段时间给孩子玩 ipad。 从这点来说，ipad 是一个很好的启蒙工具，它在小时候给我带来关于苹果产品最开心的回忆，但是那个创意游戏井喷的时代已经过去了。 我认为苹果对于 iPad Pro 的生产力探索被束缚了手脚——当然，因为它的市值导致他不可能想乔布斯刚回来一样大刀阔斧地改革。\n不过我还是提出我的愿景：苹果有一天会砍掉 MacBook Air 这条产品线——毕竟现在的 15 寸已经不是那么 Air 了。 然后 iPad Pro 将会替代这个位置，成为 iPad 和 Mac 之间的桥梁，根据苹果的刀法 这只是适用于存储大于等于 1t 的高配版，同时支持 iPadOS 和 MacOS 双系统运行，系统之间的环境是独立的。 这么做的话，既不会很大影响 Mac 产品线的效率，毕竟最终你还是需要一台 MacBook Pro。 但是这需要非常大的魄力，实现的可能性是未知的。 祝好！\n","date":"2024-05-15","externalUrl":null,"permalink":"/posts/04b7539b/","section":"Posts","summary":"","title":"我的那些 iPad 们","type":"posts"},{"content":"使用 Mac 也有一段时间了，推荐一下我电脑上常用的几款软件\nAirBuddy # 官网下载\n一个可以让你的 Mac 像 iPhone 一样显示蓝牙耳机弹窗的小工具，弥补一下 Mac 没有弹窗的遗憾\nAlDente # 官网下载\n管理你的 Mac 电池，我一般就是设置为 70%\nAppCleaner # 官网下载\n一个卸载工具，大小只有 4MB，非常好用\nApplite # GitHub 下载 或者使用 brew install --cask applite\n图形化的 Homebrew 应用商店\nBLEUnlock # GitHub 下载 或者使用brew install bleunlock\n通过蓝牙信号强度来自动锁定/解锁你的 Mac\nCalendr # GitHub 下载\n一个显示在菜单栏的日历工具，支持农历\nDropover # 应用商店下载\n一个非常好用的文件拖动工具\nFork # 官网下载\n功能强大的 git 管理工具\nHidden Bar # 应用商店下载\n可以隐藏掉菜单栏的多余项目\nKeka # 官网免费下载\n非常好用的解压工具\nLoop # GitHub 下载\n窗口管理软件，配合触控板的震动非常准确\nMonitorControl # GitHub 下载\n外接屏幕时可以快速调节屏幕亮度\nReeder # 应用商店下载\n老牌 RSS 阅读器\nReminders MenuBar # GitHub 下载\n最强提醒事项插件！直接通过菜单栏管理你的提醒事项\nScreen Studio # 官网下载\n一款简单易用的录频工具\nTermius # 应用商店下载\n跨平台的 SSH 工具，支持串口\nTypora # 官网下载\n非常简洁好用的写作软件\n","date":"2024-05-03","externalUrl":null,"permalink":"/posts/ed7fc7bb/","section":"Posts","summary":"","title":"Mac 常用软件分享","type":"posts"},{"content":" 2024.05.24 补充 # 最近发现一个很棒的网站，分享给大家 《城市租房生存指南》\n写在前面 # 最近经历了大大小小的一些事情，这里记录一下，作为一个从来没跳过槽的职场新人，你在跳槽前后的一些注意事项。\n跳槽前的准备 # 职业规划 # 略\n个人情况评估 # 在你准备跳槽前，你要清楚的是，自己是要骑驴找马，还是裸辞？是打算在哪个城市工作？需要提前搬家吗？如果有竞业协议怎么办？\n简历 # 写一份适合你要投递岗位的简历非常重要，你应该避免一份简历多次投递\n最好的做法是根据不同的 JD (Job Description) 修改你的简历\n我推荐你使用简单简历，我很喜欢他们的产品设计，简洁美观。或者超级简历 。\n你最好将每一份简历规范命名\n基本公式：姓名 + 应聘岗位 + 电话，如果是发邮件的形式，请不要只发送一个附件，在正文中可以做简单的自我介绍。\n你可以阅读《对于最近招聘市场行情的一些个人理解》\n在离职与入职之间 # 提交辞呈/如何体面地离开 # 一封體面的離職信，讓你和公司好好說再見\n首先需要明确的是，辞职不需要申请《中华人民共和国劳动合同法》（全文）\n劳动者提前三十日以书面形式通知用人单位，可以解除劳动合同。劳动者在试用期内提前三日通知用人单位，可以解除劳动合同。\n所以你的辞呈中不需要出现：请批准、申请等字眼，最好也尽可能少地透露个人情况。\n然后跟你的 leader 或者 hr 核对好离职细节，包括社保减员时间、公司资产归还等。\noffer 的选择 # 略\n体检预约 # 现在绝大部分公立三甲的体检项目都是要预约的，并且一般是周一到周五，周末就算可以体检，一般也得工作日才能拿报告。\n需要注意的是，有的医院有区分专门的入职体检/教师资格证体检/公务员体检/成人体检，不要选错了。\n一般来说你应该尽可能约在九点前，正常来说，医院 9 点半前的抽血结果在当天就能出报告。\n总归，早点总是没错的。\n你可以直接用支付宝搜索 \u0026ldquo;网上挂号\u0026rdquo; ，点击 \u0026ldquo;按科室挂号\u0026rdquo;，直接找出所有可以预约的医院。\n体检前一天晚上 # 考虑到第二天需要抽血，你应该避免在晚上深夜进食，并且在第二天早上保持空腹。\n丁香医生 - 关于抽血检查的那些疑惑\n同时你需要准备一张一寸或者两寸的证件照，需要贴在体检报告上。\n记得早点睡觉。\n体检 # 抵达医院后先去缴费，主动递上自己的社保卡或住院卡，告知预约了入职体检，工作人员会告诉你需要交多少钱。\n然后就可以去体检科等待叫号，之后会领取到你的体检表，只需要按照上面的项目做就可以。\n一般是先去抽血，做完之后下午会出结果，去自助机上领取。\n有的自助机是通过上午做项目给的小票扫码打印的，注意保存。\n之后拿着材料到体检科找医生写总结报告，盖章。就可以去缴费处开发票退余额回家了。\n入职 # 入职当天请注意着装，将所有必要的手续用文件袋装好，在出门前清点是否有遗漏的。\n第一天到公司建议比正常上班时间半小时到公司。 一是你可以趁这段时间，熟悉一下公司的布局，或者给自己挑一个好工位。 二是在同事没有完全到的时候你可以进行一些单点社交，这样的压力会比所有人都到工位上时小很多。\n一般情况下，你入职当天都是需要第二天补卡的，不要忘记。\n社保 # 如果是更换省份工作，可能会需要在当地办一张社保卡，具体情况要根据你所在城市的政策，你可以通过小红书、公众号等查询到。\n湖北省医疗保障局 - 变更参保地后，是否需要重新办理社保卡？\n浙江省人民政府 - 社保卡需要重新办理吗？\n写在最后 # 换工作是一件很累的事情，如果你能找到自己满意的事情，那真是一件最值得欢呼的事情。\n如果你暂时还没找到满意的工作，也不要灰心，是金子总会发光。\n","date":"2024-04-27","externalUrl":null,"permalink":"/posts/52813bdd/","section":"Posts","summary":"","title":"从离职到入职","type":"posts"},{"content":"相信很多人都听说过「双人成行」这款游戏的大名吧！2021 的 TGA 年度游戏，传说中配置要求最高的游戏之一：因为你需要一个朋友。 最近我趁着周末时间，花了三周时间把它通关了，这是我目前玩过最好玩的双人游戏！出色的情节设计、恰到好处的音效、有挑战却不至于太难的操作、脑洞大开的解谜，都是我想为它写一篇点评的因素，但它也不需要我的点评，它已经证明自己是最好玩的双人同屏游戏了。\n出色的情节设计 # 大概的情节就是一对夫妻因为长时间的争吵想要离婚，他们的女儿向「Book of love」这本书许愿希望挽回父母的感情。随后这对夫妻变成了两个粘土小人，玩家需要操控着他们完成哈金博士的要求，一步步在合作中找回激情与爱。 从一开始的吸尘器房间开始，就能感觉到这个游戏对于合作的要求，玩家必须在合适的时机完成配合，才能打败 boss。并且在后续的每一个场景中，场景元素各不相同，用近乎奢侈的方式搭建的画面，很可能就在玩家视角中存在几分钟。同时每个场景获得的全新能力也是脑洞大开，不会出现玩法重复导致的“赶紧通关吧”的想法。 个人最喜欢的是玩具城堡中的 rpg 环节，流畅的第三人称视角转为上帝视角的运镜一点也不突兀，在这其中添加的“刷刷刷”的快感更是恰到好处。\n搭档最喜欢的是冰雪世界环节，冰雪世界可探索的地方非常多，几乎所有场景都可以互动，也是给了非常大的地图供玩家爽滑。\n恰到好处的音效 # 在「双人成行」中，对音乐的使用隐藏在各种地方，比如：场景中出现一家钢琴，从过去就会发出“哆来咪发唆啦西哆”的真实声音、从吉他弦上滑过会发出和弦声。 而在鼹鼠窝中，一切声音都会消失，只剩下安静而可怕的呼吸声，还有玩家的脚步声。这时候你也会情不自禁地屏住呼吸，然后在随后的跑酷中又通过紧张的管弦乐绷紧你的神经，这时候脑子里想的是，run！！！\n还有在 club 中，从一步步调动起全场的气氛，真的是身体也会情不自禁地摆动起来。\n有挑战却不至于太难的操作 # 对于一款双人协作的通关游戏，「双人成行」对于关卡的难度把握可谓是正正好，很多时候只需要你找到战斗的技巧，磨三、四个回合就能成功通关。 我认为最难的还是工具箱 boss，在刚开始上手就遇到这么难的 boss，还是有点难打，但是发现其中的诀窍后重来了三次就通关了。 对于「死亡惩罚」的理解，「双人成行」是通过“只要有一个人活着，就可以马上原地复活”，同时堆砌大量的存档点，来确保玩家不需要在一个地方重复走。 在逃离松鼠窝后期，乘坐松鼠们制作的内裤飞机阶段进行的格斗环节也令人大呼精彩。反正我是想象不到，怎么将一个「拳皇」做成双人游戏并且不能是 2v1 的群殴也不能是轮番上场。在操作飞机方向的同时，搭档在飞机上的打斗也会影响飞机的方向。因此更需要两个人的配合。\n在与太空猴大战中，对于导弹的操控也是令人直呼太好玩了，将空战元素漫不经心地完美融合进来。\n脑洞大开的解谜 # 在每个情景中获得的能力，都是需要两个人互相配合才能通关的，而对不同场景的探索，更是解密的关键。 比如在冰雪世界中对于磁铁两极的运用，围绕这点所搭建的解谜真的是太好玩了。\n还有在小梅的钟表中，场景的时间一次次停止、倒转，每一次看似相同的时间，实际上通关的方式就在其中。在玩的过程中我很少遇到卡关的现象，它的场景引导做的足够好，并且不是炫技般地将大量场景简单粗暴地堆叠到你的面前。而是通过丰富、快速地切换来实现引导。\n写在最后 # 在游玩过程中，早期角色还经常拌嘴，但是到了后期就变成两个人的相互鼓励和默契，这其中的过程只有你自己坐下来游玩才能感受。 我在豆瓣看到有人吐槽它的结局也是不能落俗套，说是「最好的离婚冷静期」，但是如果你以小孩子的视角看，最后的信中提到：你们不要为了我而争吵，我会离开，我的零花钱够买一张公交车票，我带了棒棒糖。 或许，你也会觉得，它就是应该这样结局，因为它是所有小孩子心中的最好结局——哪怕大人不这样认为。\n","date":"2024-03-24","externalUrl":null,"permalink":"/posts/5209c12b/","section":"Posts","summary":"","title":"双人成行——送给所有大人的精彩旅程","type":"posts"},{"content":"事情是这样的，笔者在实现一个下载功能的时候遇到一个问题：服务器对下载速率进行了限制，对于业务上的文件下载，是不确定大小的，只能从服务器请求下载的时候才能知道需要下载的文件大小。如果使用单线程下载的话，用户体验非常差，因此需要实现多线程下载的功能。\n获取文件大小 # #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;curl/curl.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; static size_t get_file_size(char *url) { size_t filesize = 0; CURL *curl_handle; curl_global_init(CURL_GLOBAL_ALL); curl_handle = curl_easy_init(); if (curl_handle) { curl_easy_setopt(curl_handle, CURLOPT_URL, url); curl_easy_setopt(curl_handle, CURLOPT_HEADER, 1); // 设置 HEADER 会得到 header，文件大小信息就在 header 中 curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 1); // 设置 NOBODY 可以避免下载 CURLcode res_code = curl_easy_perform(curl_handle); // 请求 curl_easy_getinfo(curl_handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, \u0026amp;filesize); // 从请求头中获取待下载文件的大小 } curl_global_cleanup(); return filesize; } int main() { size_t file_size = 0, per_thread_size = 0; char url[] = \u0026#34;https://test.com/test.exe\u0026#34;; file_size = get_file_size(url); printf(\u0026#34;file_size=%d\\n\u0026#34;,file_size); return 0; } 使用 gcc 编译的时候需要链接上 curl 库\ngcc curl.c -o curltest -lcurl 输出结果\npureos@pureos:~/Documents$ ./curltest HTTP/1.1 200 OK Server: nginx/1.21.5 Date: Mon, 08 Jan 2024 06:21:35 GMT Content-Type: application/octet-stream Content-Length: 74194069 Connection: keep-alive Accept-Ranges: bytes Content-Security-Policy: block-all-mixed-content ETag: \u0026#34;2bf56162523c85f740adebd078eb3a78\u0026#34; Last-Modified: Tue, 02 Jan 2024 03:02:13 GMT Strict-Transport-Security: max-age=31536000; includeSubDomains Vary: Origin Vary: Accept-Encoding X-Amz-Request-Id: 17A84AF9CDE1356D X-Content-Type-Options: nosniff X-Xss-Protection: 1; mode=block file_size=74194069 检查了一下，获取到的文件大小是正确的，那么就可以直接开始多线程下载了。\n多线程下载 # 这里我使用的是创建多个线程，每个线程下载一部分文件，全部下载完成后再合并为一个文件。\nstatic void create_thread(void *(*routine)(void *), void *arg, const pthread_attr_t *attr) { pthread_t tid; if (!pthread_create(\u0026amp;tid, attr, routine, arg)) { pthread_detach(tid); } else { printf(\u0026#34;create thread fail!\\n\u0026#34;); exit(1); } usleep(300000); return; } 完整代码 # #include \u0026lt;pthread.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;curl/curl.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define THREAD_COUNT 16 typedef struct ThreadData { char *url; char *outputFilename; long startRange; long endRange; char finish; } ThreadData; /** * @description: 获取时间戳函数 * @return {*} */ static long long get_timestamp(void) { long long tmp; struct timeval tv; gettimeofday(\u0026amp;tv, NULL); tmp = tv.tv_sec; tmp = tmp * 1000; tmp = tmp + (tv.tv_usec / 1000); return tmp; } /** * @description: 创建线程 * @param {void} * * @return {*} */ static void create_thread(void *(*routine)(void *), void *arg, const pthread_attr_t *attr) { pthread_t tid; if (!pthread_create(\u0026amp;tid, attr, routine, arg)) { pthread_detach(tid); } else { printf(\u0026#34;create thread fail!\\n\u0026#34;); exit(1); } usleep(300000); return; } static size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream) { size_t written = fwrite(ptr, size, nmemb, (FILE *)stream); return written; } /** * @description: 下载文件的线程 * @param {void} *ptr * @return {*} */ static void *download_part(void *ptr) { ThreadData *data = (ThreadData *)ptr; char range[64]; CURL *curl_handle; FILE *pagefile; CURLcode res = -1; snprintf(range, sizeof(range), \u0026#34;%ld-%ld\u0026#34;, data-\u0026gt;startRange, data-\u0026gt;endRange); curl_global_init(CURL_GLOBAL_ALL); curl_handle = curl_easy_init(); if (curl_handle) { curl_easy_setopt(curl_handle, CURLOPT_URL, data-\u0026gt;url); // curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 1L); /* 这个选项用于开启详细模式（verbose mode）。设置为非零值时，libcurl * 会打印额外的调试信息，这些信息包括发送和接收的数据，如 HTTP 请求和响应头。 *这对于开发和调试非常有用，因为你可以看到发生在传输层的所有事情。*/ // curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1L); /* 但是如果你启用了 CURLOPT_NOPROGRESS 选项并设置为零值，libcurl * 将调用一个进度回调函数（progress function）来显示传输进度。*/ curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_data); curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 600L); curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl_handle, CURLOPT_RANGE, range); /* open the file */ pagefile = fopen(data-\u0026gt;outputFilename, \u0026#34;wb\u0026#34;); if (pagefile) { curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, pagefile); res = curl_easy_perform(curl_handle); fclose(pagefile); } curl_easy_cleanup(curl_handle); } curl_global_cleanup(); if (res == CURLE_OK) { data-\u0026gt;finish = 1; } return NULL; } /** * @description: 合并文件 * @param {char} *final_output * @param {int} num_files * @return {*} */ static int merge_files(const char *final_output, int num_files) { FILE *fout, *ftmp; char buffer[1024]; size_t bytes; // 打开最终输出文件 fout = fopen(final_output, \u0026#34;wb\u0026#34;); if (!fout) { perror(\u0026#34;Error opening final output file\u0026#34;); return -1; } // 循环遍历所有临时文件 for (int i = 0; i \u0026lt; num_files; ++i) { char tmp_file[16] = {0}; snprintf(tmp_file, sizeof(tmp_file), \u0026#34;test%d.exe\u0026#34;, i); // 打开临时文件 ftmp = fopen(tmp_file, \u0026#34;rb\u0026#34;); if (!ftmp) { perror(\u0026#34;Error opening temporary file\u0026#34;); fclose(fout); return -1; } // 读取临时文件内容并写入到最终文件中 while ((bytes = fread(buffer, 1, sizeof(buffer), ftmp)) \u0026gt; 0) { fwrite(buffer, 1, bytes, fout); } // 关闭临时文件 fclose(ftmp); // 删除临时文件 remove(tmp_file); } // 关闭最终文件 fclose(fout); return 0; } /** * @description: 获取待下载文件大小 * @param {char} *url * @return {*} */ static size_t get_file_size(char *url) { size_t filesize = 0; CURL *curl_handle; curl_global_init(CURL_GLOBAL_ALL); curl_handle = curl_easy_init(); if (curl_handle) { curl_easy_setopt(curl_handle, CURLOPT_URL, url); curl_easy_setopt(curl_handle, CURLOPT_HEADER, 1); // 设置 HEADER 会得到 header，文件大小信息就在 header 中 curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 1); // 设置 NOBODY 可以避免下载 CURLcode res_code = curl_easy_perform(curl_handle); // 请求 curl_easy_getinfo(curl_handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, \u0026amp;filesize); // 从请求头中获取待下载文件的大小 printf(\u0026#34;file_size=%d\\n\u0026#34;, filesize); } curl_global_cleanup(); return filesize; } int main() { ThreadData threadData[THREAD_COUNT]; const char *final_output = \u0026#34;test.exe\u0026#34;; long long curr_time, finish_time; size_t file_size = 0, per_thread_size = 0; char url[] = \u0026#34;https://test.com/test.exe\u0026#34;; for (int i = 0; i \u0026lt;= 3; i++) { if (i == 3) { printf(\u0026#34;get file size error!\\n\u0026#34;); exit(1); } if (file_size == 0) { file_size = get_file_size(url); break; } } per_thread_size = file_size / THREAD_COUNT; printf(\u0026#34;per_thread_size=%d\\n\u0026#34;, per_thread_size); curr_time = get_timestamp(); printf(\u0026#34;start download!curr_time=%lld\\n\u0026#34;, curr_time); // 创建并启动线程 for (int i = 0; i \u0026lt; THREAD_COUNT; i++) { char tmp_file[16] = {0}; snprintf(tmp_file, sizeof(tmp_file), \u0026#34;test%d.exe\u0026#34;, i); threadData[i].url = url; threadData[i].outputFilename = tmp_file; // 应该是唯一的临时文件名 threadData[i].startRange = i * per_thread_size; threadData[i].endRange = (i == (THREAD_COUNT - 1)) ? (file_size - 1) : ((i + 1) * per_thread_size - 1); threadData[i].finish = 0; printf(\u0026#34;create download thread!i=%d,file name=%s\\n\u0026#34;, i, tmp_file); create_thread(download_part, \u0026amp;threadData[i], NULL); } printf(\u0026#34;downloading...\\n\u0026#34;); // 等待所有线程完成 while (1) { for (int i = 0; i \u0026lt; THREAD_COUNT; i++) { if (threadData[i].finish == 0) { break; } else if (i \u0026lt; THREAD_COUNT - 1 \u0026amp;\u0026amp; threadData[i].finish) { continue; } else if (i == THREAD_COUNT - 1 \u0026amp;\u0026amp; threadData[i].finish) { // 所有全部下载完成 printf(\u0026#34;download finish!\u0026#34;); goto succ; } } sleep(1); } succ: finish_time = get_timestamp(); printf(\u0026#34;finish_time=%lld\\n\u0026#34;, finish_time); printf(\u0026#34;use time = %lld\\n\u0026#34;, finish_time - curr_time); // 合并文件 if (merge_files(final_output, THREAD_COUNT) != 0) { fprintf(stderr, \u0026#34;Failed to merge files\\n\u0026#34;); return 1; } return 0; } 编译的时候链接上所需库：\ngcc curl.c -o curltest -lcurl -lpthread 执行：\n./curltest 此时查看一下是否有在下载：\npureos@pureos:~/Documents$ ls -lh | grep firmware -rw-r--r-- 1 pureos pureos 1.6M 1月 8 14:55 test0.exe -rw-r--r-- 1 pureos pureos 1.8M 1月 8 14:55 test10.exe -rw-r--r-- 1 pureos pureos 1.6M 1月 8 14:55 test11.exe -rw-r--r-- 1 pureos pureos 1.9M 1月 8 14:55 test12.exe -rw-r--r-- 1 pureos pureos 484K 1月 8 14:53 test13.exe -rw-r--r-- 1 pureos pureos 1.1M 1月 8 14:52 test14.exe -rw-r--r-- 1 pureos pureos 1.6M 1月 8 14:55 test15.exe -rw-r--r-- 1 pureos pureos 1.9M 1月 8 14:55 test1.exe -rw-r--r-- 1 pureos pureos 1.4M 1月 8 14:53 test2.exe -rw-r--r-- 1 pureos pureos 1.7M 1月 8 14:54 test3.exe -rw-r--r-- 1 pureos pureos 1.5M 1月 8 14:55 test4.exe -rw-r--r-- 1 pureos pureos 1.6M 1月 8 14:52 test5.exe -rw-r--r-- 1 pureos pureos 1.9M 1月 8 14:55 test6.exe -rw-r--r-- 1 pureos pureos 2.2M 1月 8 14:55 test7.exe -rw-r--r-- 1 pureos pureos 1.9M 1月 8 14:55 test8.exe -rw-r--r-- 1 pureos pureos 2.1M 1月 8 14:55 test9.exe 可以看到有 16 个文件，并且大小是一直在增长的。那我们就静静等待下载完成就好啦！\n写在最后 # 通过这样一个简单的 Demo 实现了 libcurl 的多线程下载，但是在实际的业务流程中，还需要增加几点功能，提高程序的鲁棒性。如：\n控制线程的 curl 的下载超时时间 增加下载失败监测机制，针对某个包下载失败可以重新单独下载 增加文件下载校验，如引入 MD5 校验，防止因网络传输问题导致包损坏。 参考连接 # 【C++】使用 libcurl 来实现多线程下载的功能\n","date":"2024-01-20","externalUrl":null,"permalink":"/posts/705b3de0/","section":"Posts","summary":"","title":"C 语言中使用 libcurl 实现多线程下载","type":"posts"},{"content":"今天在做显示屏的时候发现，LVGL V8.2 的折线图 y 轴居然不能支持显示浮点数，于是研究了一下，发现还是有一些奇技淫巧来实现的。\n原始代码 # int main(int tiks) { float real = json_object_get_double(val); lv_chart_set_next_value(ui.chart_data, ui.chart_data_dot, real); lv_chart_set_axis_tick(ui.chart_data, LV_CHART_AXIS_PRIMARY_Y, 10, 5, tiks, 2, true, 100); lv_chart_set_range(ui.chart_data, LV_CHART_AXIS_PRIMARY_Y, min_data, max_data); lv_obj_add_event_cb(ui.chart_data, chart_event_cb, LV_EVENT_ALL, NULL); lv_obj_refresh_ext_draw_size(ui.chart_data); lv_chart_set_update_mode(ui.chart_data, LV_CHART_UPDATE_MODE_CIRCULAR); lv_chart_set_zoom_x(ui.chart_data, 700); } 查看图标设置函数定义可以发现，只接受整数\nvoid lv_chart_set_next_value(lv_obj_t * obj, lv_chart_series_t * ser, lv_coord_t value) { LV_ASSERT_OBJ(obj, MY_CLASS); LV_ASSERT_NULL(ser); lv_chart_t * chart = (lv_chart_t *)obj; ser-\u0026gt;y_points[ser-\u0026gt;start_point] = value; invalidate_point(obj, ser-\u0026gt;start_point); ser-\u0026gt;start_point = (ser-\u0026gt;start_point + 1) % chart-\u0026gt;point_cnt; invalidate_point(obj, ser-\u0026gt;start_point); lv_chart_refresh(obj); } #if LV_USE_LARGE_COORD typedef int32_t lv_coord_t; #else typedef int16_t lv_coord_t; #endifc 在网上搜了一下，LVGL 似乎不支持图标直接存放浮点数，但是我们可以通过乘以 100 来存入，读出来的时候再除以 100。\n实现浮点数显示 # 首先，将 lv_conf.h 中的浮点数支持开启\n#define LV_SPRINTF_USE_FLOAT 1 这样 lv_snprintf 函数才能支持%f，存入图表的时候乘以 100 来存入。\nint main(int tiks) { float real = json_object_get_double(val) * 100; lv_chart_set_next_value(ui.chart_data, ui.chart_data_dot, real); lv_chart_set_axis_tick(ui.chart_data, LV_CHART_AXIS_PRIMARY_Y, 10, 5, tiks, 2, true, 100); lv_chart_set_range(ui.chart_data, LV_CHART_AXIS_PRIMARY_Y, min_data, max_data); lv_obj_add_event_cb(ui.chart_data, chart_event_cb, LV_EVENT_ALL, NULL); lv_obj_refresh_ext_draw_size(ui.chart_data); lv_chart_set_update_mode(ui.chart_data, LV_CHART_UPDATE_MODE_CIRCULAR); lv_chart_set_zoom_x(ui.chart_data, 700); } lv_chart_set_axis_tick 中，label_en 表示是否显示坐标轴，我们需要将坐标轴以浮点数形式显示。\n修改 lvgl/src/extra/widgets/chart/lv_chart.c 的 draw_y_ticks 函数，你的路径可能跟我不一样，但最终要改的函数都是 draw_y_ticks 这个函数。\n这个函数用于绘制 y 轴坐标，我们需要将存入的乘以 100 的坐标再改为正常值。\n将 lv_snprintf(buf, sizeof(buf), \u0026#34;%\u0026#34; LV_PRId32, tick_value); 改为 lv_snprintf(buf, sizeof(buf), \u0026#34;%.2f\u0026#34;, (float)tick_value / 100.0f); 现在显示的页面就是正常的了，但是我还有一个点击事件的需要修改，这是为了让用户在点击或者拖动折线图的时候，点位的上方可以出现一个标签显示该点位的值。先看一下我之前的写法：\n/** * @description: 折线图点击、拖动事件 * @param {lv_event_t} *e * @return {*} */ static void chart_event_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t *chart = lv_event_get_target(e); if (code == LV_EVENT_VALUE_CHANGED) { lv_obj_invalidate(chart); } if (code == LV_EVENT_REFR_EXT_DRAW_SIZE) { lv_coord_t *s = lv_event_get_param(e); *s = LV_MAX(*s, 20); } else if (code == LV_EVENT_DRAW_POST_END) { int32_t id = lv_chart_get_pressed_point(chart); if (id == LV_CHART_POINT_NONE) return; LV_LOG_USER(\u0026#34;Selected point %d\u0026#34;, (int)id); lv_chart_series_t *ser = lv_chart_get_series_next(chart, NULL); while (ser) { lv_point_t p; lv_chart_get_point_pos_by_id(chart, ser, id, \u0026amp;p); lv_coord_t *y_array = lv_chart_get_y_array(chart, ser); lv_coord_t value = y_array[id]; if (value == LV_CHART_POINT_NONE) return; char buf[32]; lv_snprintf(buf, sizeof(buf), LV_SYMBOL_DUMMY \u0026#34;%d\u0026#34;, value); lv_draw_rect_dsc_t draw_rect_dsc; lv_draw_rect_dsc_init(\u0026amp;draw_rect_dsc); draw_rect_dsc.bg_color = lv_color_black(); draw_rect_dsc.bg_opa = LV_OPA_50; draw_rect_dsc.radius = 3; draw_rect_dsc.bg_img_src = buf; draw_rect_dsc.bg_img_recolor = lv_color_white(); lv_area_t a; a.x1 = chart-\u0026gt;coords.x1 + p.x - 20; a.x2 = chart-\u0026gt;coords.x1 + p.x + 20; a.y1 = chart-\u0026gt;coords.y1 + p.y - 30; a.y2 = chart-\u0026gt;coords.y1 + p.y - 10; lv_draw_ctx_t *draw_ctx = lv_event_get_draw_ctx(e); lv_draw_rect(draw_ctx, \u0026amp;draw_rect_dsc, \u0026amp;a); ser = lv_chart_get_series_next(chart, ser); } } else if (code == LV_EVENT_RELEASED) { lv_obj_invalidate(chart); } } 现在也是一样的，要先除以 100 获取到原本的值再显示：\n将 lv_snprintf(buf, sizeof(buf), LV_SYMBOL_DUMMY \u0026#34;%d\u0026#34;, value); 改为 lv_snprintf(buf, sizeof(buf), LV_SYMBOL_DUMMY \u0026#34;%.2f\u0026#34;, (float)value / 100.0f); 说在最后 # 查资料的时候发现 LVGL 9 要推出了，希望新版本能提供原生支持吧。\n参考链接 # https://forum.lvgl.io/t/chart-display-floats-temperature-values-with-decimal-places/9097/2\n","date":"2024-01-13","externalUrl":null,"permalink":"/posts/e636d2c0/","section":"Posts","summary":"","title":"让你的 LVGL 折线图支持浮点数显示","type":"posts"},{"content":"笔者在使用 mwan3 的过程中遇到了一个非常低级的问题，深感自己的网络基础知识还是不够扎实，特此记录。\n问题描述 # 在 OpenWrt 系统中使用 mwan3 软件进行链路管理，设备连接 Wi-Fi 和 USB 模块进行拨号上网。\n需设置移动网络的优先级为高，Wi-Fi 的优先级低，若此时移动网络断开连接，应无缝切换到 Wi-Fi。\n测试指标为 ping 外网的过程中不应断开。\n实际测试中发现，断开移动网络，无法实现无缝切换，且 Wi-Fi 接口已经连接的情况下，无法手动 ping 通外网。\n问题排查 # 首先看一下我的配置文件吧：\nroot@openwrt:~# cat /etc/config/mwan3 config globals \u0026#39;globals\u0026#39; option mmx_mask \u0026#39;0x3F00\u0026#39; option rtmon_interval \u0026#39;5\u0026#39; config interface \u0026#39;wan\u0026#39; option enabled \u0026#39;0\u0026#39; option family \u0026#39;ipv4\u0026#39; option reliability \u0026#39;1\u0026#39; option count \u0026#39;1\u0026#39; option timeout \u0026#39;4\u0026#39; option interval \u0026#39;10\u0026#39; option down \u0026#39;3\u0026#39; option up \u0026#39;2\u0026#39; option seamless \u0026#39;1\u0026#39; list track_ip \u0026#39;8.8.8.8\u0026#39; list flush_conntrack \u0026#39;ifup\u0026#39; list flush_conntrack \u0026#39;ifdown\u0026#39; config interface \u0026#39;wwan\u0026#39; option family \u0026#39;ipv4\u0026#39; option reliability \u0026#39;1\u0026#39; option count \u0026#39;1\u0026#39; option timeout \u0026#39;4\u0026#39; option interval \u0026#39;10\u0026#39; option down \u0026#39;3\u0026#39; option up \u0026#39;2\u0026#39; option seamless \u0026#39;1\u0026#39; option enabled \u0026#39;1\u0026#39; option add_track_route \u0026#39;0\u0026#39; list track_ip \u0026#39;8.8.8.8\u0026#39; list flush_conntrack \u0026#39;ifup\u0026#39; list flush_conntrack \u0026#39;ifdown\u0026#39; config interface \u0026#39;wwlan\u0026#39; option family \u0026#39;ipv4\u0026#39; option reliability \u0026#39;1\u0026#39; option count \u0026#39;1\u0026#39; option timeout \u0026#39;4\u0026#39; option interval \u0026#39;10\u0026#39; option down \u0026#39;3\u0026#39; option up \u0026#39;2\u0026#39; option seamless \u0026#39;1\u0026#39; option enabled \u0026#39;1\u0026#39; list track_ip \u0026#39;8.8.8.8\u0026#39; option add_track_route \u0026#39;0\u0026#39; list flush_conntrack \u0026#39;ifup\u0026#39; list flush_conntrack \u0026#39;ifdown\u0026#39; config member \u0026#39;wan_mw\u0026#39; option interface \u0026#39;wan\u0026#39; option metric \u0026#39;1\u0026#39; option weight \u0026#39;3\u0026#39; config member \u0026#39;wwan_mw\u0026#39; option interface \u0026#39;wwan\u0026#39; option metric \u0026#39;2\u0026#39; option weight \u0026#39;3\u0026#39; config member \u0026#39;wwlan_mw\u0026#39; option interface \u0026#39;wwlan\u0026#39; option metric \u0026#39;3\u0026#39; option weight \u0026#39;3\u0026#39; config policy \u0026#39;default\u0026#39; option last_resort \u0026#39;default\u0026#39; list use_member \u0026#39;wan_mw\u0026#39; list use_member \u0026#39;wwan_mw\u0026#39; list use_member \u0026#39;wwlan_mw\u0026#39; config rule \u0026#39;https\u0026#39; option sticky \u0026#39;1\u0026#39; option dest_port \u0026#39;443\u0026#39; option proto \u0026#39;tcp\u0026#39; option use_policy \u0026#39;default\u0026#39; config rule \u0026#39;default_rule\u0026#39; option dest_ip \u0026#39;0.0.0.0/0\u0026#39; option use_policy \u0026#39;default\u0026#39; 我配置了优先走移动网络，移动网络断口后无缝切换为 Wi-Fi，但发现断开移动网络后，网络直接就断开了。\n此时查看路由表发现，没有默认路由：\nroot@openwrt:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 192.168.0.0 0.0.0.0 255.255.255.0 U 11 0 0 wlan0 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 这就奇怪了，且禁用移动网络，重启 Wi-Fi 试试看：\nroot@openwrt:~# ifup wwlan root@openwrt:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 11 0 0 wlan0 192.168.0.0 0.0.0.0 255.255.255.0 U 11 0 0 wlan0 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 是有默认路由的，那开启移动网络，再次查看路由表：\nroot@openwrt:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.137.155.2 0.0.0.0 UG 11 0 0 usb0 10.137.155.2 0.0.0.0 255.255.255.254 U 11 0 0 usb0 192.168.0.0 0.0.0.0 255.255.255.0 U 11 0 0 wlan0 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 发现居然 Wi-Fi 的默认路由没了？？？\n原来是移动网络和 Wi-Fi 的跃点相同了，都是 11，导致只会存在 1 个默认路由。\n什么是跃点？ # 引用自百度百科：\n跃点：即路由。一个路由为一个跃点。传输过程中需要经过多个网络，每个被经过的网络设备点（有能力路由的）叫做一个跃点，地址就是它的 ip。跃点数是经过了多少个跃点的累加器，为了防止无用的数据包在网上流散。为路由指定所需跃点数的整数值（范围是 1 ~ 9999），它用来在路由表里的多个路由中选择与转发包中的目标地址最为匹配的路由。所选的路由具有最少的跃点数。跃点数能够反映跃点的数量、路径的速度、路径可靠性、路径吞吐量以及管理属性。\n按笔者自己的理解，可以约等于权重，跃点越大权重越低，优先走跃点小的。\n因此，将移动网络的网关跃点修改为 10，Wi-Fi 接口的网关跃点保持 11 不变。\n查看路由表\nroot@openwrt:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.108.180.39 0.0.0.0 UG 10 0 0 usb0 0.0.0.0 192.168.0.1 0.0.0.0 UG 11 0 0 wlan0 10.108.180.38 0.0.0.0 255.255.255.254 U 10 0 0 usb0 192.168.0.0 0.0.0.0 255.255.255.0 U 11 0 0 wlan0 192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 可以看到，现在已经正常有两个默认路由了，并且移动网络的默认路由是在 Wi-Fi 上面的。这也符合我 mwan3 配置的需求：先走移动网络，没有移动网络后走 Wi-Fi。\n此时在后台 ping 外网，然后断开移动网络，可以看到 ping 是不会断的，自动走 Wi-Fi 出去了。\n参考资料 # 百度百科 - 跃点数\nOpenWrt-mwan3\n","date":"2024-01-09","externalUrl":null,"permalink":"/posts/3802befb/","section":"Posts","summary":"","title":"在 OpenWrt 中使用 mwan3 设置链路切换","type":"posts"},{"content":"最近开始接触了 PT，各种网络黑话、专有名词研究了半天，特写此文总结一下从网上收集到的信息。\n一、什么是 PT？ # PT（Private Tracker）是一种改良自 BitTorrent 协议的 P2P 下载方式。Private Tracker指私有种子服务器，与 BT 最大的不同点分别为可进行私密范围下载，及可统计每个用户的上载及下载量。\n二、PT 怎么玩？ # PT 站是在私密范围内下载\n只允许本站用户下载，不允许用户将种子公开上传 PT 站需要内部邀请或者捐赠的形式获得邀请码 统计上传量和下载量\n网站会统计每一个用户的下载量和上传量，下载量和上传量在一定程度上决定着用户的等级，有没有权限下载文件 每一个用户注册后会得到一个 passkey，用户从网站里面下载的种子里面包含了私人的 passkey 通过 passkey 识别每一个用户，统计每一个用户的下载、上传和做种时间 PT 站是「人人为我，我为人人」的资源共享 Team 新手的话可以先从一些小站开始玩起，将自己的数据养好之后再去各类论坛找大佬求邀请码。\n三、对于新手应该需要知道的 # 1、魔力值：相当于货币，一般可以通过坚持每日签到/做种获得。可以在站内购买上传量/下载量/邀请名额。\n1 个魔力值 * 你的做种数 (做种数最多计 7 个)\n每小时获得的魔力值点数由下面的公式给出：\n式中简言之：为做种人数少、文件体积大的种子做种能获得更多魔力值。\n**A **为中间变量 Ti 为第 **i **个种子的生存时间，即自种子发布起到现在所经过的时间，单位是周 **T0 **为参数。T0 = 8 **Si **为第 i 个种子的大小，单位是 GB Ni 为第 i 个种子当前的做种者数 **N0 **为参数。N0 = 7 Wi 为第 i 个种子的权重系数，默认为 1，零魔种子为 0.2 做种每小时将得到如下的魔力值\n**B **为 1 小时中用户获得的做种魔力值点数 B0 为参数，代表用户 1 小时获得魔力值的上限。B0 = 100 **L **为参数。L = 300 2、上传量：就是你做种上传了多少 G 的资源\n3、下载量：你从 pt 站下载了多少 G 的资源\n4、分享率：上传量/下载量，区分你的整体分享率和独立分享率是很重要的。整体分享率关注的是自从你加入站点以来，账号的整体上传与下载量。而独立分享率则针对每一个你正在下载或做种的文件。\n5、你应该保持一个良好的分享率，防止被当作吸血鬼 t 掉，所以你需要了解什么是 [Free] [2x免费] [50%免费]\n[Free]：这个资源下载是免费的 (不计入下载量)，1 倍上传量。\n[2x 免费]：这个资源下载是免费的 (不计入下载量)，2 倍上传量。\n[50% 免费]：这个资源 0.5 倍下载量，1 倍上传量。\n新手度过考核期可以一定不要放过这类资源，前期可以快速将上传量刷上去。\n四、软件的选择 # 一般来说每个 pt 站对于允许使用的软件都是有要求的，这里需要大家根据情况选择。\n我一般会使用开源软件 Transmission，注意，请不要使用迅雷！\n","date":"2023-12-29","externalUrl":null,"permalink":"/posts/e3632a97/","section":"Posts","summary":"","title":"PT 入坑指北","type":"posts"},{"content":"事情是这样的，项目需要一个串口采集 1032 协议电压的功能。在实现中还是遇到不少问题，由于是第一次使用，遂做下一些记录。\n报文内容 # 使用串口发送 ModBus 报文时，需要解析收到的报文\n01 03 04 00 00 41 40 CB 93\n这是请求报文：\n01：设备地址，表示要访问的 Modbus 设备的地址为 1。 03：功能码，表示要读取保持寄存器。 00 00：起始地址，表示要读取的保持寄存器的起始地址为 0。 00 02：寄存器数量，表示要读取的保持寄存器数量为 2。 C4 0B：CRC 校验码，用于验证报文的正确性。 返回报文：\n01：设备地址，表示返回的报文是来自地址为 1 的 Modbus 设备。 03：功能码，表示返回的报文是读取保持寄存器的响应报文。 04：字节数，表示返回的数据字节数为 4。 00 00：寄存器值，表示起始地址为 0 的第一个保持寄存器的值。 41 40：寄存器值，表示起始地址为 1 的第二个保持寄存器的值。 CB 93：CRC 校验码，用于验证报文的正确性。 解析返回报文 # 由于我只需要采集单路 ModBus 所以我的请求报文是固定的。因此我的返回报文的头部内容也是固定的。\n所以我们需要解析的数据就是\n00 00 41 40\n注意：这是 IEEE 754 浮点数\n这就是我们需要的电压数据。其实很简单，只需要做一个十六进制转 float 就可以。\n细心的朋友可能发现了，我是使用 41400000 转换的，这是为什么呢？\n这是因为我的 float 格式为 CDAB（这是等到最后才发现的）\nC 语言代码实现 # 使用共用体进行类型转换 # #include \u0026lt;stdio.h\u0026gt; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; int main(int argc, char** argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; return 0; } 首先要明确的是，直接使用强制类型转换是不行的。直接看代码：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; typedef union { uint32_t u32; float f; } float_union_t; static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; } int main(int argc, char **argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; float value = utils_get_float_at(rsp, 0); printf(\u0026#34;value = %f\\n\u0026#34;, value); return 0; } 输出结果如下：\npureos@pureos:~$ gcc 1.c -o 1 pureos@pureos:~$ ./1 value = 3.015625 可以看到我们按照正常的字节序转换出来的浮点数是错误的，将这个值转换为十六进制为：\n40 41 00 00\n寻找正确的 12.000000 # 所以正确的12.000000 应该是多少呢？\n使用以下函数查看一下：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; typedef union { uint32_t u32; float f; } float_union_t; static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; } static inline void utils_set_float_at(void *data, int pos, float value) { *((uint32_t *)((uint8_t *)(data) + pos)) = *(uint32_t *)(\u0026amp;value); } int main(int argc, char **argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; float value = utils_get_float_at(rsp, 0); printf(\u0026#34;value = %f\\n\u0026#34;, value); uint8_t float_to_hex_buf[4]; float float_num = 12.000000; bzero(float_to_hex_buf, sizeof(float_to_hex_buf)); utils_set_float_at(float_to_hex_buf, 0, float_num); printf(\u0026#34;float %f to hex = \u0026#34;, float_num); for (int i = 0; i \u0026lt; 4; i++) { printf(\u0026#34;%.02hx \u0026#34;, float_to_hex_buf[i]); } printf(\u0026#34;\\n\u0026#34;); return 0; } 代码输出结果：\npureos@pureos:~$ gcc 1.c -o 1 pureos@pureos:~$ ./1 value = 3.015625 float 12.000000 to hex = 00 00 40 41 现在已经非常清晰了，在我当前的环境中，需要将每位寄存器上的数据位互换位置。也就是两两之间互换。\n原始数据：00 00 41 40\n正确数据：00 00 40 41\n然后将 uint8 的数据转为 uint16，这个时候就可以获取正确的数据了\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; typedef union { uint32_t u32; float f; } float_union_t; static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; } static inline void utils_set_float_at(void *data, int pos, float value) { *((uint32_t *)((uint8_t *)(data) + pos)) = *(uint32_t *)(\u0026amp;value); } int main(int argc, char **argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; float value = utils_get_float_at(rsp, 0); printf(\u0026#34;value = %f\\n\u0026#34;, value); uint8_t float_to_hex_buf[4]; float float_num = 12.000000; bzero(float_to_hex_buf, sizeof(float_to_hex_buf)); utils_set_float_at(float_to_hex_buf, 0, float_num); printf(\u0026#34;float %f to hex = \u0026#34;, float_num); for (int i = 0; i \u0026lt; 4; i++) { printf(\u0026#34;%.02hx \u0026#34;, float_to_hex_buf[i]); } printf(\u0026#34;\\n\u0026#34;); uint16_t dest[4]; bzero(dest, sizeof(dest)); int rc = 2; int offset = 1; for (int i = 0; i \u0026lt; rc; i++) { /* shift reg hi_byte to temp OR with lo_byte */ dest[i] = (rsp[(i \u0026lt;\u0026lt; 1)] \u0026lt;\u0026lt; 8) | rsp[offset + (i \u0026lt;\u0026lt; 1)]; } for (int i = 0; i \u0026lt; 2; i++) { printf(\u0026#34;%.04hx \u0026#34;, dest[i]); } printf(\u0026#34;\\n\u0026#34;); value = utils_get_float_at(dest, 0); printf(\u0026#34;value = %f\\n\u0026#34;, value); return 0; } 输出为：\npureos@pureos:~$ gcc 1.c -o 1 pureos@pureos:~$ ./1 value = 3.015625 float 12.000000 to hex = 00 00 40 41 0000 4140 value = 12.000000 可以看到，我们已经正确解析出了电压值。\n完整代码 # #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; typedef union { uint32_t u32; float f; } float_union_t; static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; } // true is little endian, flase is big endian static int check_cpu() { union w { int a; char b; } c; c.a = 1; return (c.b == 1); } static void reverse(uint16_t data[], int num) { int i, j; uint16_t temp; for (i = 0, j = num - 1; i \u0026lt; j; i++, j--) { temp = data[i]; data[i] = data[j]; data[j] = temp; } } int main(int argc, char **argv) { int rc = 2; int offset = 1; uint8_t rsp[] = {0x01, 0x03, 0x04, 0x00, 0x00, 0x41, 0x40, 0xCB, 0x93}; uint16_t dest[2]; bzero(dest, sizeof(dest)); for (int i = 0; i \u0026lt; rc; i++) { /* shift reg hi_byte to temp OR with lo_byte */ dest[i] = (rsp[offset + 2 + (i \u0026lt;\u0026lt; 1)] \u0026lt;\u0026lt; 8) | rsp[offset + 3 + (i \u0026lt;\u0026lt; 1)]; } if (!check_cpu()) { reverse(dest, 2); } float value = utils_get_float_at(dest, 0); printf(\u0026#34;value = %f\\n\u0026#34;, value); return 0; } 最后还加入了一个对 cpu 的大小端序判断，这样可用性和可移植性会更强。\n","date":"2023-10-08","externalUrl":null,"permalink":"/posts/b790c721/","section":"Posts","summary":"","title":"ModBus 报文解析实战","type":"posts"},{"content":"前几天新购买了一台小鸡，在首次使用时进行了一些配置上的修改，在这里与大家分享。按照本文章的操作，可以大大降低成为肉鸡的概率。\n本文系统：Ubuntu 18.04\n注：本文代码块前的 $ 代表在终端中的输入指令，复制时请勿输入！\n修改 root 密码 # 如果你的 VPS 提供商的机子没有 root 密码，一定一定要马上修改一个密码。\n$ passwd Enter new UNIX password: Retype new UNIX password: passwd: password updated successfully 创建管理员账号 # 这里的管理员是指，能够运行 sudo 命令的用户，为了安全，在正常使用中禁止使用 root 账号操作。\n$ adduser \u0026lt;username\u0026gt; Enter new UNIX password: Retype new UNIX password: 这里输入密码后可能还会提示需要输入用户的信息，回车默认即可 $ usermod -aG sudo \u0026lt;username\u0026gt; # 为用户添加 sudo 权限 删除默认用户和组 # 可以使用\n$ cat /etc/passwd $ cat /etc/group 来分别查看用户列表和组列表\n删除多余的用户\n$ userdel sync \u0026amp;\u0026amp; userdel shutdown \u0026amp;\u0026amp; userdel halt \u0026amp;\u0026amp; userdel uucp \u0026amp;\u0026amp; userdel operator \u0026amp;\u0026amp; userdel games \u0026amp;\u0026amp; userdel gopher 删除多余组\n$ groupdel adm \u0026amp;\u0026amp; groupdel games \u0026amp;\u0026amp; groupdel lp \u0026amp;\u0026amp; groupdel dip 删除了不必要的用户和组后，我们将用户管理的权限关闭\n$ chattr +i /etc/passwd $ chattr +i /etc/shadow $ chattr +i /etc/group $ chattr +i /etc/gshadow i 属性代表这个文件不允许被修改，删除。这样我们就无法给系统新建用户。需要新建用户的时候使用 -i 还原\n修改 SSH 配置 # 添加 ssh 私钥 # 在你的 windows 终端下生成一对私钥对，个人喜好使用 GitHub 推荐的 Ed25519 算法\n$ ssh-keygen -t ed25519 可以自己指定文件保存的路径，一般来说默认即可。之后询问是否输入密码，这里建议还是输入一个密码。\n现在我们需要的私钥上传到服务器中，我直接上传到 home 目录下了\n#创建 .ssh 文件夹 $ mkdir ~/.ssh $ chmod 700 ~/.ssh $ mv id_ed25519 ~/.ssh/authorized_keys $ chmod 400 ~/.ssh/authorized_keys 禁用 root 登录及密码登录 # 在上传了 ssh 公钥后，我们需要关闭 root 账号的登录权限\n$ sudo vi /etc/ssh/sshd_config 将PermitRootLogin yes 改为 PermitRootLogin no 关闭 root 登录 将PasswordAuthentication yes 改为 PasswordAuthentication no 关闭密码登录\n在配置文件最后新增一行\nClientAliveInterval 60 修改 ssh 端口 # 同上一步，将 Port 22 改为任意没有被占用的端口，建议改成小众一点的\n全部修改完成后，重启 ssh 服务\n$ sudo service sshd restart 可以重新连接一下，看是否修改成功\n安装 Fail2Ban # Fail2Ban 是一款入侵防御软件，将尝试爆破 ssh 密码的 ip 封停，可以保护服务器免受暴力攻击。\n$ sudo apt install fail2ban 安装长亭雷池 WAF # 雷池 是一款足够简单、足够好用、足够强的免费 WAF。基于业界领先的语义引擎检测技术，作为反向代理接入，保护你的网站不受黑客攻击。\n核心检测能力由智能语义分析算法驱动，专为社区而生，不让黑客越雷池半步。\n配置需求 # 操作系统：Linux 指令架构：x86_64 软件依赖：Docker 20.10.6 版本以上 软件依赖：Docker Compose 2.0.0 版本以上 最小化环境：1 核 CPU / 1 GB 内存 / 10 GB 磁盘 一键安装 # $ bash -c \u0026#34;$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/setup.sh)\u0026#34; 更多安装方式请参考 安装雷池\n登录 # 浏览器打开后台管理页面 https://\u0026lt;waf-ip\u0026gt;:9443。根据界面提示，使用 支持 TOTP 的认证软件 扫描二维码，然后输入动态口令登录。\n禁止系统响应任何从外部/内部来的 ping 请求 # $ echo 1 \u0026gt; /proc/sys/net/ipv4/icmp_echo_ignore_all 参考链接 # https://zhuanlan.zhihu.com/p/371611071\nhttps://www.logcg.com/archives/884.htmlrchives/884.html\n","date":"2023-10-03","externalUrl":null,"permalink":"/posts/d6cba860/","section":"Posts","summary":"","title":"入手 VPS 后要做的几件事","type":"posts"},{"content":"前几天使用 OpenWrt 测试 GRE 功能时发现，在 4G 网络和有线网之间切换会导致掉线，遂简单排查了一下问题所在。\n情景复现 # 本机\n系统：OpenWrt\n有线网卡（wan）：eth1，IP：192.168.0.194，子网掩码：255.255.255.0，网关：192.168.0.1\n有线网卡（lan）：br-lan，IP：192.168.3.1，子网掩码：255.255.255.0\n无线网卡（wwan）：usb0，IP：10.221.139.224，子网掩码：255.255.255.0，网关：10.221.139.1\n无线网卡使用中国移动 4G 卡上网\nGRE 对端\n系统：OpenWrt\n有线网卡（wan）：eth1，IP：192.168.0.242，子网掩码：255.255.255.0，网关：192.168.0.1\n有线网卡（lan）：br-lan，IP：192.168.2.218，子网掩码：255.255.255.0\n本机 GRE 配置\ncat /etc/config/network config interface \u0026#39;gresbksg\u0026#39; option disabled \u0026#39;0\u0026#39; option peeraddr \u0026#39;192.168.0.242\u0026#39; // 对端IP option proto \u0026#39;gre\u0026#39; option mtu \u0026#39;1280\u0026#39; option peerlocalip \u0026#39;192.168.2.0\u0026#39; // 对端 option peerlocalmask \u0026#39;255.255.255.0\u0026#39; option zone \u0026#39;wan\u0026#39; config interface \u0026#39;gresbksg_grestatic\u0026#39; option ifname \u0026#39;@gresbksg\u0026#39; option disabled \u0026#39;0\u0026#39; option ipaddr \u0026#39;192.168.5.1\u0026#39; // GRE隧道IP，对端为192.168.5.2 option netmask \u0026#39;255.255.255.0\u0026#39; option proto \u0026#39;static\u0026#39; option zone \u0026#39;wan\u0026#39; GRE 正常建立，流量走 eth1，此时的路由表：\nroot@pg-2049671F7524:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 10 0 0 eth1 0.0.0.0 10.221.139.1 0.0.0.0 UG 12 0 0 usb0 10.221.139.0 0.0.0.0 255.255.255.0 U 12 0 0 usb0 192.168.0.0 0.0.0.0 255.255.255.0 U 10 0 0 eth1 192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 192.168.3.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 此时本机可以直接 ping 通对端的 lan 口，\nroot@pg-2049671F7524:~# ping 192.168.2.218 PING 192.168.2.218 (192.168.2.218): 56 data bytes 64 bytes from 192.168.2.218: seq=0 ttl=64 time=1.481 ms 64 bytes from 192.168.2.218: seq=1 ttl=64 time=4.312 ms 64 bytes from 192.168.2.218: seq=2 ttl=64 time=2.344 ms 64 bytes from 192.168.2.218: seq=3 ttl=64 time=1.579 ms 64 bytes from 192.168.2.218: seq=4 ttl=64 time=3.409 ms --- 192.168.2.218 ping statistics --- 5 packets transmitted, 5 packets received, 0% packet loss round-trip min/avg/max = 1.481/2.625/4.312 ms 路由追踪：\nroot@pg-2049671F7524:~# traceroute 192.168.2.218 traceroute to 192.168.2.218 (192.168.2.218), 30 hops max, 38 byte packets 1 192.168.2.218 (192.168.2.218) 1.173 ms 2.722 ms 2.603 ms 拔掉 wan 口网线后，此时正常情况下应该无法 ping 通，因为 GRE 在内网中，路由表：\nroot@pg-2049671F7524:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.77.102.1 0.0.0.0 UG 12 0 0 usb0 10.77.102.0 0.0.0.0 255.255.255.0 U 12 0 0 usb0 192.168.0.242 10.77.102.1 255.255.255.255 UGH 12 0 0 usb0 192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 192.168.3.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 将网线接回去后，还是无法 ping 通，此时的路由表：\nroot@pg-2049671F7524:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 10 0 0 eth1 0.0.0.0 10.77.102.1 0.0.0.0 UG 12 0 0 usb0 10.77.102.0 0.0.0.0 255.255.255.0 U 12 0 0 usb0 192.168.0.0 0.0.0.0 255.255.255.0 U 10 0 0 eth1 192.168.0.242 10.77.102.1 255.255.255.255 UGH 12 0 0 usb0 192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 192.168.3.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 可以发现，我们在拔掉网线后，路由表内自动添加了一条 host 路由\n192.168.0.242 10.77.102.1 255.255.255.255 UGH 12 0 0 usb0 但是由于我们的 GRE 对端是建立在内网中的，4G 网络肯定是无法访问的，所有这条 host 路由是错误的。\n当我们将网线接回后，由于已经存在了一条 host 路由，所有访问 192.168.0.242 的流量会从 usb0 出去，导致 GRE 隧道无法建立。\n怎么使系统正确添加 host 路由？ # 思路一：关闭自动添加 host 路由 # 查看 OpenWrt 源码中的 gre 脚本（openwrt/package/network/config/gre/files/gre.sh）\ngre_setup() { local cfg=\u0026#34;$1\u0026#34; local mode=\u0026#34;$2\u0026#34; local remoteip local ipaddr peeraddr peerlocalip peerlocalmask json_get_vars df ipaddr peeraddr tunlink nohostroute peerlocalip peerlocalmask [ -z \u0026#34;$peeraddr\u0026#34; ] \u0026amp;\u0026amp; { proto_notify_error \u0026#34;$cfg\u0026#34; \u0026#34;MISSING_PEER_ADDRESS\u0026#34; proto_block_restart \u0026#34;$cfg\u0026#34; exit } remoteip=$(resolveip -t 10 -4 \u0026#34;$peeraddr\u0026#34;) if [ -z \u0026#34;$remoteip\u0026#34; ]; then proto_notify_error \u0026#34;$cfg\u0026#34; \u0026#34;PEER_RESOLVE_FAIL\u0026#34; exit fi for ip in $remoteip; do peeraddr=$ip break done if [ \u0026#34;${nohostroute}\u0026#34; != \u0026#34;1\u0026#34; ]; then ( proto_add_host_dependency \u0026#34;$cfg\u0026#34; \u0026#34;$peeraddr\u0026#34; \u0026#34;$tunlink\u0026#34; ) fi [ -z \u0026#34;$ipaddr\u0026#34; ] \u0026amp;\u0026amp; { local wanif=\u0026#34;$tunlink\u0026#34; if [ -z $wanif ] \u0026amp;\u0026amp; ! network_find_wan wanif; then proto_notify_error \u0026#34;$cfg\u0026#34; \u0026#34;NO_WAN_LINK\u0026#34; exit fi if ! network_get_ipaddr ipaddr \u0026#34;$wanif\u0026#34;; then proto_notify_error \u0026#34;$cfg\u0026#34; \u0026#34;NO_WAN_LINK\u0026#34; exit fi } [ -z \u0026#34;$df\u0026#34; ] \u0026amp;\u0026amp; df=\u0026#34;1\u0026#34; case \u0026#34;$mode\u0026#34; in gretapip) gre_generic_setup $cfg $mode $ipaddr $peeraddr \u0026#34;gre4t-$cfg\u0026#34; route add -net \u0026#34;$peerlocalip\u0026#34; netmask \u0026#34;$peerlocalmask\u0026#34; dev \u0026#34;gre4t-$cfg\u0026#34; ;; *) gre_generic_setup $cfg $mode $ipaddr $peeraddr \u0026#34;gre4-$cfg\u0026#34; route add -net \u0026#34;$peerlocalip\u0026#34; netmask \u0026#34;$peerlocalmask\u0026#34; dev \u0026#34;gre4-$cfg\u0026#34; ;; esac } 可以看到，只有在 nohostroute 为非 1 时，才会添加 host 路由\n以下是 ChatGPT 的回答：\nproto_add_host_dependency是 OpenWrt 中的一个函数，它用于向当前接口的依赖列表中添加一个 host 依赖项。\n在 OpenWrt 中，每个接口都有一个依赖列表，用于记录这个接口所依赖的其他接口或主机。当这个接口启动时，系统会检查它的依赖列表，如果依赖项中有任何一个接口或主机不可达，这个接口就无法启动。这种机制可以保证网络的连通性和稳定性。\nproto_add_host_dependency函数用于向当前接口的依赖列表中添加一个 host 依赖项。它接受三个参数：当前接口的配置名称、依赖主机的 IP 地址、依赖主机所在的网络接口名称。例如，以下命令将当前接口eth0添加一个 host 依赖项，依赖主机的 IP 地址是192.168.1.1，所在的网络接口名称是eth1：\nproto_add_host_dependency \u0026#34;eth0\u0026#34; \u0026#34;192.168.1.1\u0026#34; \u0026#34;eth1\u0026#34; 在 GRE 隧道的配置中，proto_add_host_dependency函数通常用于将对端 IP 地址添加到当前隧道的依赖列表中。这样，当对端不可达时，当前隧道就无法启动，从而避免了无效的隧道连接。\n在你的问题中，proto_add_host_dependency函数用于向当前 GRE 隧道的依赖列表中添加对端 IP 地址的 host 依赖项。如果nohostroute字段没有设置为1，系统会自动添加这个 host 路由，使得对端 IP 地址可达。如果nohostroute字段设置为1，则需要手动使用proto_add_host_dependency函数将对端 IP 地址添加到依赖列表中，否则 GRE 隧道无法启动。\nOpenWrt 官方是这样解释这个参数的：\nName Type Required Default Description nohostroute boolean no 0 Do not add routes to ensure the tunnel endpoints are routed via non-tunnel device 于是我尝试了更改 GRE 的配置：\nconfig interface \u0026#39;gresbksg\u0026#39; option disabled \u0026#39;0\u0026#39; option peeraddr \u0026#39;192.168.0.242\u0026#39; option proto \u0026#39;gre\u0026#39; option mtu \u0026#39;1280\u0026#39; option peerlocalip \u0026#39;192.168.2.0\u0026#39; option peerlocalmask \u0026#39;255.255.255.0\u0026#39; option zone \u0026#39;wan\u0026#39; option nohostroute \u0026#39;1\u0026#39; config interface \u0026#39;gresbksg_grestatic\u0026#39; option ifname \u0026#39;@gresbksg\u0026#39; option disabled \u0026#39;0\u0026#39; option ipaddr \u0026#39;192.168.5.1\u0026#39; option netmask \u0026#39;255.255.255.0\u0026#39; option proto \u0026#39;static\u0026#39; option zone \u0026#39;wan\u0026#39; 修改完成后重启 network，发现是可以正常使用的。此时的路由表：\nroot@pg-2049671F7524:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 10 0 0 eth1 0.0.0.0 10.162.134.1 0.0.0.0 UG 12 0 0 usb0 10.162.134.0 0.0.0.0 255.255.255.0 U 12 0 0 usb0 192.168.0.0 0.0.0.0 255.255.255.0 U 10 0 0 eth1 192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 192.168.3.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 将网线拔掉后，路由表已经不会自动添加 host 路由了：\nroot@pg-2049671F7524:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.162.134.1 0.0.0.0 UG 12 0 0 usb0 10.162.134.0 0.0.0.0 255.255.255.0 U 12 0 0 usb0 192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 192.168.3.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 然后再接回网线，查看路由表：\nroot@pg-2049671F7524:~# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 10 0 0 eth1 0.0.0.0 10.162.134.1 0.0.0.0 UG 12 0 0 usb0 10.162.134.0 0.0.0.0 255.255.255.0 U 12 0 0 usb0 192.168.0.0 0.0.0.0 255.255.255.0 U 10 0 0 eth1 192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 192.168.3.0 0.0.0.0 255.255.255.0 U 0 0 0 br-lan 192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 gre4-gresbksg 可以发现因为没有错误的 host 路由，我们的 GRE 隧道又能正常建立连接了。尝试 ping 一下对端的 LAN 口：\nroot@pg-2049671F7524:~# ping 192.168.2.218 PING 192.168.2.218 (192.168.2.218): 56 data bytes 64 bytes from 192.168.2.218: seq=0 ttl=64 time=1.810 ms 64 bytes from 192.168.2.218: seq=1 ttl=64 time=3.701 ms 64 bytes from 192.168.2.218: seq=2 ttl=64 time=6.526 ms 64 bytes from 192.168.2.218: seq=3 ttl=64 time=4.254 ms --- 192.168.2.218 ping statistics --- 4 packets transmitted, 4 packets received, 0% packet loss round-trip min/avg/max = 1.810/4.072/6.526 ms 疑惑：为什么用有线时，不会添加 host 路由？\n思路二：将 GRE 隧道绑定到某个接口上 (未成功) # 可以使用参数tunlink\nName Type Required Default Description tunlink string no (none) Bind the tunnel to the specified interface, OpenWrt 21.02+ 修改 network 中 gre 的配置，将 GRE 隧道绑定到本地网络接口 eth1 上：\n","date":"2023-09-26","externalUrl":null,"permalink":"/posts/ca9be083/","section":"Posts","summary":"","title":"OpenWrt 中 GRE 掉线问题","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"}]