前言 之前尝试过在龙芯3A4000上编译运行EPICS,由于3A4000还是mips64指令集,而3A5000则是龙芯的自主指令集loongarch64,适配起来步骤也会有所不同。
这次使用的是龙博特龙芯3A5000电脑主机。
虽然EPICS官方并没有适配loongarch和mips64,无法做到开箱即用,但只要有gcc、g++、make、perl这些工具,理论上就能编译运行EPICS,在开始编译前,确保你的设备上已经装好了这些工具。
关于如何称呼「龙架构」,龙芯社区也有一些讨论。最初我直接使用loongarch64,后来也使用过la64作为简写,直到我看到如何称呼龙架构?,我觉得有必要和社区保持一致,后续统一使用 loong64 作为架构标识。
diff --git a/categories/epics/index.xml b/categories/epics/index.xml
index b11cc39..987c26b 100644
--- a/categories/epics/index.xml
+++ b/categories/epics/index.xml
@@ -1,4 +1,4 @@
-EPICS on ✨kiraの博客 https://kira-96.github.io/categories/epics/Recent content in EPICS on ✨kiraの博客 Hugo -- gohugo.io zh Copyright © 2019-2024 [kira's blog](/) · Wed, 29 May 2024 14:52:04 +0800 EPICS IOC 访问安全 https://kira-96.github.io/posts/epics-ioc-access-security/Mon, 18 Mar 2024 09:52:05 +0800 https://kira-96.github.io/posts/epics-ioc-access-security/ EPICS IOC 数据访问安全配置 龙芯开发板移植 IgH EtherCAT Master https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/Fri, 23 Feb 2024 12:54:31 +0800 https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/ 龙芯2K0500开发板移植EtherCAT主站程序和EPICS EtherCAT模块 EPICS的MODBUS模块的编译和使用 https://kira-96.github.io/posts/build-epics-module-modbus/Tue, 02 Jan 2024 14:38:38 +0800 https://kira-96.github.io/posts/build-epics-module-modbus/ 交叉编译EPICS的MODBUS模块 交叉编译 ACAI https://kira-96.github.io/notes/cross-compiling-acai/Tue, 26 Dec 2023 19:31:56 +0800 https://kira-96.github.io/notes/cross-compiling-acai/ 交叉编译 ACAI 交叉编译EPICS和IOC https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/Tue, 12 Dec 2023 16:26:35 +0800 https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/ Linux交叉编译EPICS Windows上使用MinGW编译安装EPICS https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/Wed, 29 Nov 2023 18:57:02 +0800 https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/ 需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。
+EPICS on ✨kiraの博客 https://kira-96.github.io/categories/epics/Recent content in EPICS on ✨kiraの博客 Hugo -- gohugo.io zh Copyright © 2019-2024 [kira's blog](/) · Fri, 07 Jun 2024 14:51:36 +0800 EPICS IOC 访问安全 https://kira-96.github.io/posts/epics-ioc-access-security/Mon, 18 Mar 2024 09:52:05 +0800 https://kira-96.github.io/posts/epics-ioc-access-security/ EPICS IOC 数据访问安全配置 龙芯开发板移植 IgH EtherCAT Master https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/Fri, 23 Feb 2024 12:54:31 +0800 https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/ 龙芯2K0500开发板移植EtherCAT主站程序和EPICS EtherCAT模块 EPICS的MODBUS模块的编译和使用 https://kira-96.github.io/posts/build-epics-module-modbus/Tue, 02 Jan 2024 14:38:38 +0800 https://kira-96.github.io/posts/build-epics-module-modbus/ 交叉编译EPICS的MODBUS模块 交叉编译 ACAI https://kira-96.github.io/notes/cross-compiling-acai/Tue, 26 Dec 2023 19:31:56 +0800 https://kira-96.github.io/notes/cross-compiling-acai/ 交叉编译 ACAI 交叉编译EPICS和IOC https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/Tue, 12 Dec 2023 16:26:35 +0800 https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/ Linux交叉编译EPICS Windows上使用MinGW编译安装EPICS https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/Wed, 29 Nov 2023 18:57:02 +0800 https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/ 需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。
这里使用MinGW环境编译EPICS,不使用MSVC编译器。
安装 Strawberry Perl 这里选择 Strawberry Perl 5.38.2.1。
直接安装即可,需要注意的是,安装路径不能有空格和中文,最好放在盘符的根目录下。
diff --git a/categories/index.xml b/categories/index.xml
index a84df6b..a12dff1 100644
--- a/categories/index.xml
+++ b/categories/index.xml
@@ -1 +1 @@
-Categories on ✨kiraの博客 https://kira-96.github.io/categories/Recent content in Categories on ✨kiraの博客 Hugo -- gohugo.io zh Copyright © 2019-2024 [kira's blog](/) · Wed, 29 May 2024 14:52:04 +0800 EPICS https://kira-96.github.io/categories/epics/Mon, 18 Mar 2024 09:52:05 +0800 https://kira-96.github.io/categories/epics/ Hugo https://kira-96.github.io/categories/hugo/Tue, 30 Jan 2024 09:45:30 +0800 https://kira-96.github.io/categories/hugo/ 技术 https://kira-96.github.io/categories/%E6%8A%80%E6%9C%AF/Sat, 11 Nov 2023 15:45:11 +0800 https://kira-96.github.io/categories/%E6%8A%80%E6%9C%AF/ 编程 https://kira-96.github.io/categories/%E7%BC%96%E7%A8%8B/Sun, 19 Feb 2023 10:23:19 +0800 https://kira-96.github.io/categories/%E7%BC%96%E7%A8%8B/ Linux https://kira-96.github.io/categories/linux/Fri, 03 Feb 2023 11:34:14 +0800 https://kira-96.github.io/categories/linux/ DICOM https://kira-96.github.io/categories/dicom/Fri, 30 Apr 2021 10:26:13 +0800 https://kira-96.github.io/categories/dicom/ 分享 https://kira-96.github.io/categories/%E5%88%86%E4%BA%AB/Mon, 22 Mar 2021 10:07:35 +0800 https://kira-96.github.io/categories/%E5%88%86%E4%BA%AB/
\ No newline at end of file
+Categories on ✨kiraの博客 https://kira-96.github.io/categories/Recent content in Categories on ✨kiraの博客 Hugo -- gohugo.io zh Copyright © 2019-2024 [kira's blog](/) · Fri, 07 Jun 2024 14:51:36 +0800 EPICS https://kira-96.github.io/categories/epics/Mon, 18 Mar 2024 09:52:05 +0800 https://kira-96.github.io/categories/epics/ Hugo https://kira-96.github.io/categories/hugo/Tue, 30 Jan 2024 09:45:30 +0800 https://kira-96.github.io/categories/hugo/ 技术 https://kira-96.github.io/categories/%E6%8A%80%E6%9C%AF/Sat, 11 Nov 2023 15:45:11 +0800 https://kira-96.github.io/categories/%E6%8A%80%E6%9C%AF/ 编程 https://kira-96.github.io/categories/%E7%BC%96%E7%A8%8B/Sun, 19 Feb 2023 10:23:19 +0800 https://kira-96.github.io/categories/%E7%BC%96%E7%A8%8B/ Linux https://kira-96.github.io/categories/linux/Fri, 03 Feb 2023 11:34:14 +0800 https://kira-96.github.io/categories/linux/ DICOM https://kira-96.github.io/categories/dicom/Fri, 30 Apr 2021 10:26:13 +0800 https://kira-96.github.io/categories/dicom/ 分享 https://kira-96.github.io/categories/%E5%88%86%E4%BA%AB/Mon, 22 Mar 2021 10:07:35 +0800 https://kira-96.github.io/categories/%E5%88%86%E4%BA%AB/
\ No newline at end of file
diff --git a/index.html b/index.html
index 68f7d28..d3eb845 100644
--- a/index.html
+++ b/index.html
@@ -1,4 +1,4 @@
-✨kiraの博客
+✨kiraの博客
欢迎来到我的博客🎉
音乐 Listen1 音乐播放器 [Windows/Linux/MacOS/Android] YesPlayMusic 高颜值的第三方网易云播放器 [Windows/Linux/MacOS] AIMP [Windows/Android] Dopamine [Windows] 视频 PotPlayer [Windows] MPC-BE [Windows] QQ影音 [Window/MacOS/Android/iOS] MX Player [Android] 美图下载 Pixiviz pixivFANBOX SauceNAO PixEz [Android/iOS] 下载 Flud 种子下载器 [Android] Internet Download Manager [Windows] Free Download Manager [Windows/MacOS/Linux/Android] Motrix [Windows/MacOS/Linux]...
原文:IOC Access Security
@@ -50,10 +50,11 @@
安装EPICS 这里不再写具体步骤了,总之就是非常简单,下载、解压、编译即可。具体步骤可以参考以前的文章。
安装Qt 直接使用终端安装Qt
1 2 3 4 5 sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator sudo apt install qtdeclarative5-dev qttools5-dev # 安装Qt Svg库,编译QWT时需要用到 sudo apt install libqt5svg5-dev 安装QWT Qt EPICS推荐使用Qwt 6.1.4,如果在Ubuntu 20.04上直接通过终端安装也是这个版本。我使用Qwt 6.2.0编译,也是没有问题的,这里以Qwt 6.2.0为例。
+最新测试:Qwt 6.3.0也可以用 ~
先下载Qwt的源码 下载Qwt-6.2.0。 下载完成后解压
1 2 3 4 # 解压tar.bz2 tar -jxvf qwt-6.2.0.tar.bz2 # 解压zip unzip qwt-6.2.0.zip 解压完成后编译Qwt,使用QtCreator或者在终端使用qmake都可以。
然后手动将编译生成的文件复制到以下位置,例:
-1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin....
前言 本来这篇文章应该在上周就写完的,不过突然被安排出差,一直忙到了现在,终于可以静下心来做些其它事情。
+1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin....
前言 本来这篇文章应该在上周就写完的,不过突然被安排出差,一直忙到了现在,终于可以静下心来做些其它事情。
之前和龙芯3A5000主机一起送过来的还有一块龙芯2K500的迷你开发板,整个板子不到巴掌大小。之前只是简单做了上电启动,这次拿到了比较完整的开发资料,可以尝试为开发板编写一些程序了。
由于暂时没有屏幕,只能先试着做一些其它的事情,如通信和IO控制,其中最简单,最基础的就是LED灯的控制。然后我就发现,这个板子居然有一颗可以调节亮度的LED灯!没错,之前做的LED控制都只能进行开关操作,而可以调节亮度,意味着可以做出更多的显示效果,这次我就做了一个呼吸灯的效果。
然后,我也简单了解了一下这种亮度调节的原理,实际上就是通过调节PWM输出的占空比,改变一个周期内输出的高低电平所占的比例,实现控制LED灯亮度的效果。由于引脚输出的电压是固定的,所以不能通过改变电平来控制亮度,而改变高低电平的占空比则是另一种思路,嵌入式设备的屏幕背光亮度调节也是基于同样的原理。
diff --git a/index.json b/index.json
index 71b179c..c45b816 100644
--- a/index.json
+++ b/index.json
@@ -1 +1 @@
-[{"content":"音乐 Listen1 音乐播放器 [Windows/Linux/MacOS/Android]\nYesPlayMusic 高颜值的第三方网易云播放器 [Windows/Linux/MacOS]\nAIMP [Windows/Android]\nDopamine [Windows]\n视频 PotPlayer [Windows]\nMPC-BE [Windows]\nQQ影音 [Window/MacOS/Android/iOS]\nMX Player [Android]\n美图下载 Pixiviz\npixivFANBOX\nSauceNAO\nPixEz [Android/iOS]\n下载 Flud 种子下载器 [Android]\nInternet Download Manager [Windows]\nFree Download Manager [Windows/MacOS/Linux/Android]\nMotrix [Windows/MacOS/Linux]\n截图、录屏 PixPin [Windows/MacOS]\nSnipaste [Windows/MacOS]\nGifCam [Windows]\nVeryCapture [Windows]\nScreenToGif [Windows]\nOBS Studio [Windows/Linux/MacOS]\npaint.net [Windows]\n写作 Visual Studio Code [Windows/MacOS/Linux]\nTypora [Windows/MacOS/Linux] (付费使用)\nMarkText [Window/MacOS/Linux]\nTypedown [Windows]\nJoplin [Windows/MacOS/Linux/Android/iOS]\n其它工具 Firefox [Windows/MacOS/Linux/Android/iOS]\nPowerToys [Windows]\nDevToys [Windows]\nWatt Toolkit [Windows/MacOS/Linux/Android/iOS]\nCrystal Disk Mark [Windows]\nGeek Uninstaller [Windows]\n7-Zip [Windows]\nZArchiver [Android]\nDism++ [Windows]\nLive2DViewerEX [Windows/Android]\nKeePass [Windows/MacOS/Linux/Android/iOS/\u0026hellip;]\nWinSCP [Windows]\nLocalSend [Windows/MacOS/Linux/Android/iOS/\u0026hellip;]\nWireShark [Windows/MacOS/Linux]\nDICOM Dump [Windows]\nInno Setup [Windows]\nOh My Posh [Windows/MacOS/Linux]\nTermius [Windows/MacOS/Linux/Android/iOS]\ntermux [Android]\nAurora Store [Android]\nVentoy [Windows/Linux]\nRufus [Windows]\nputty [Windows/Linux/MacOS]\n模拟器 PPSSPP - A PSP emulator [Windows/MacOS/Linux/Android/iOS]\nYuzu – 任天堂 Switch 模拟器 [Windows | Android | Linux | MacOS | iOS]\nRyujinx - Nintendo Switch Emulator [Windows/Linux/MacOS]\nCemu - Wii U Emulator [Windows]\nDolphin - 任天堂 GameCube 和 Wii 模拟器 [Windows/MacOS/Linux]\nRPCS3 - The PlayStation 3 Emulator [Windows/Linux/MacOS/FreeBSD]\nXenia - Xbox 360 Research Emulator [Windows]\nPS Remote Play [Windows/MacOS/Android/iOS]\nChiaki - Free and Open Source PlayStation Remote Play Client [Windows/Linux/MacOS/Android/Switch]\n开源字体 Fira Code\nCascadia Code\nNerd Fonts\n思源黑体\n思源宋体\nHarmonyOS Sans\nMiSans\nOPPO Sans\n","permalink":"https://kira-96.github.io/posts/%E4%B8%AA%E4%BA%BA%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%88%86%E4%BA%AB/","summary":"音乐 Listen1 音乐播放器 [Windows/Linux/MacOS/Android] YesPlayMusic 高颜值的第三方网易云播放器 [Windows/Linux/MacOS] AIMP [Windows/Android] Dopamine [Windows] 视频 PotPlayer [Windows] MPC-BE [Windows] QQ影音 [Window/MacOS/Android/iOS] MX Player [Android] 美图下载 Pixiviz pixivFANBOX SauceNAO PixEz [Android/iOS] 下载 Flud 种子下载器 [Android] Internet Download Manager [Windows] Free Download Manager [Windows/MacOS/Linux/Android] Motrix [Windows/MacOS/Linux]","title":"个人常用软件分享"},{"content":"原文:IOC Access Security\n功能 访问安全功能用于保护IOC数据库,限制来自未经授权的CA或pvAccess客户端访问。访问安全性基于以下几点:\nWho 客户端的用户ID(Channel Access/pvAccess)。\nWhere 用户登录的主机 ID。客户端运行的主机,但不会分辨用户是本地用户或远程登录到主机的用户。\nWhat 记录的各个字段都受到保护。每条记录都有一个字段包含记录的访问安全组(ASG)。每个字段都有一个访问安全级别(ASL0或ASL1)。安全级别在记录定义文件(.dbd)中定义。\nWhen 访问规则可以包含类似于CALC Record的输入计算。\n定义 ASL 访问安全级别\nASG 访问安全组\nUAG 用户访问组\nHAG 主机访问组\n快速上手 为了启用特定 IOC 的访问安全性,需要完成以下操作:\n创建访问安全文件(.acf) 可能需要修改IOC数据库 记录实例可能需要设置访问安全组ASG字段。如果ASG为空,记录将会使用“DEFAULT”访问安全组。\n访问安全文件可以在iocInit之后通过asSubInit和asSubProcess作为关联的子程序重新加载。将值1写入此记录将导致重新加载。\n必须启动脚本在的iocInit之前包含以下命令:\n1 2 3 4 asSetFilename(\u0026#34;/full/path/to/accessSecurityFile\u0026#34;) /* 下面是一个可选命令 */ /* 使用宏替换 */ asSetSubstitutions(\u0026#34;var1=sub1,var2=sub2,...\u0026#34;) 如果在iocInit之前未执行asSetFilename,就不会启用访问安全限制。\n如果给定asSetFilename,但在首次初始化访问安全性时发生错误,则对该IOC的所有访问都会被拒绝。\n成功启动访问安全性后,尝试重新启动时出现错误,将会保持上次的访问安全配置。\n启动IOC并启用访问安全后,可以通过asSetFilename、asSetSubstitutions和asInit来更改访问安全规则。也可以使用函数asInitialize、asInitFile和asInitFP。\n在启动IOC之后重新初始化访问安全配置操作是“非常昂贵”的操作,尽量不要这样做。\n访问安全配置文件 本节介绍包含用户访问组(UAG)、主机访问组(HAG)和访问安全组(ASG)。IOC会读取访问配置文件(建议使用扩展名.acf)然后创建访问配置数据库。首先给出一个简单的例子,然后是完整的语法描述。\n简单示例\n1 2 3 4 5 6 7 8 9 UAG(uag) {user1,user2} HAG(hag) {host1,host2} ASG(DEFAULT) { RULE(1,READ) RULE(1,WRITE) { UAG(uag) HAG(hag) } } 上面的规则提供了无限制的读权限(READ),而位于主机host1和host2上的用户user1和user2则拥有写权限(WRITE)。\n语法定义\n在以下描述中:\n[] 可选项\n| 备选项\n... 任意数量的定义\n元素\u0026lt;name\u0026gt;、\u0026lt;user\u0026gt;、\u0026lt;host\u0026gt;、\u0026lt;pvname\u0026gt;和\u0026lt;calculation\u0026gt;可以是带引号或不带引号的字符串。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 UAG(\u0026lt;name\u0026gt;) [{ \u0026lt;user\u0026gt; [, \u0026lt;user\u0026gt; ...] }] ... HAG(\u0026lt;name\u0026gt;) [{ \u0026lt;host\u0026gt; [, \u0026lt;host\u0026gt; ...] }] ... ASG(\u0026lt;name\u0026gt;) [{ [INP\u0026lt;index\u0026gt;(\u0026lt;pvname\u0026gt;) ...] RULE(\u0026lt;level\u0026gt;,NONE | READ | WRITE [, NOTRAPWRITE | TRAPWRITE]) { [UAG(\u0026lt;name\u0026gt; [,\u0026lt;name\u0026gt; ...])] [HAG(\u0026lt;name\u0026gt; [,\u0026lt;name\u0026gt; ...])] CALC(\u0026lt;calculation\u0026gt;) } ... }] ... UAG:用户访问组。这是用户名列表,列表可以空。一个用户名可以出现在多个UAG中。用户名必须和运行CA客户端的主机上的用户名相同。对于vxWorks客户端,用户名通常取自引导参数的用户字段。\nHAG:主机访问组。这是主机名列表,列表可以空。同一主机名可以出现在多个HAG中。主机名必须和运行CA客户端的主机主机名相同。对于vxWorks客户端,主机名通常取自引导参数的目标名称。\nASG:访问安全组。DEFAULT是默认的访问安全组。\nINP\u0026lt;index\u0026gt;:index必须是A到L中的一个值。类似于CALC record的INP字段。如果在ASG的规则中定义了CALC字段,则需要INP字段。\nRULE:定义访问权限\u0026lt;level\u0026gt;必须为0或1。级别1字段的权限继承了级别0字段的权限。权限为NONE、READ和WRITE,WRITE也继承了READ权限。标准EPICS记录类型的所有字段除VAL、CMD(命令)和RES(重置)外都设置为1级。可选参数指定是否应捕获写入,如果未给定,则默认为NOTRAPWRITE。\nUAG指定可以访问的用户访问组列表。如果未定义UAG,则允许所有用户访问。\nHAG指定具有访问权限的主机访问组列表。如果未定义HAG,则允许所有主机访问。\nCALC与计算记录的CALC字段类似,但结果必须计算为TRUE或FALSE。只有当计算结果为TRUE才适用该规则(RULE),其中实际测试对于(0.99 \u0026lt; result \u0026lt; 1.01)为TRUE。任何其他结果都被认为FALSE,并将导致该规则被忽略。\n可以为ASG定义多条RULE,相同的RULE级别和访问权限也可以有多个。用于客户端的TRAPWRITE设置由通过规则检查的第一个WRITE规则确定。\n每个记录类型的字段都有一个关联的访问安全级别ASL0或ASL1(默认值)。操作员通常更改的字段被分配为ASL0,其他字段被分配给ASL1。例如,模拟输出记录的VAL字段被分配为ASL0,其他字段分配为ASL1。这是因为在正常操作过程中只应修改VAL字段。\n创建或修改访问配置文件后,可以使用ascheck命令查找语法错误:\n1 ascheck -S \u0026#34;xxx=yyy,...\u0026#34; \u0026lt; \u0026#34;filename\u0026#34; -S表示使用宏替换。此命令会显示语法错误的位置,正确则不会有任何输出。\n实验 首先新建一个示例IOC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ mkdir example $ cd example/ $ makeBaseApp.pl -t example test $ makeBaseApp.pl -i -t example test The following target architectures are available in base: linux-loong64 linux-x86_64 What architecture do you want to use? linux-x86_64 The following applications are available: test What application should the IOC(s) boot? The default uses the IOC\u0026#39;s name, even if not listed above. Application name? test $ make 然后创建访问安全配置文件accessSecurity.acf。\n1 2 cd iocBoot/ioctest/ touch accessSecurity.acf 修改配置文件内容,示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 UAG(read) {deepin} UAG(write) {deepin} HAG(hosts) {LAPTOP-CTDCXXXX} ASG(DEFAULT) { RULE(1,READ) RULE(1,WRITE) { HAG(hosts) } } ASG(deepin) { RULE(1,READ) { UAG(read,write) HAG(hosts) } RULE(1,WRITE,TRAPWRITE) { UAG(write) HAG(hosts) } } 稍微解释一下:\n创建了两个用户访问组(UAG),名称为read和write,两个用户访问组都只包含用户deepin。\n创建了一个主机访问组(HAG),名称为hosts,包含主机名LAPTOP-CTDCXXXX。\n创建了默认(DEFAULT)访问安全组(ASG),不限制读取(READ)权限,只有hosts主机访问组的用户拥有写入(WRITE)权限。\n创建了访问安全组(ASG),名称为deepin,hosts主机访问组所包含主机上的deepin用户才拥有读取(READ)和写入(WRITE)权限。\n可以使用ascheck工具检查一下语法是否正确。\n1 ascheck accessSecurity.acf 然后还可以修改一下db文件,例:\n1 2 cd example/db/ vi dbExample2.db 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 record(ai, \u0026#34;$(user):aiExample$(no)\u0026#34;) { field(DESC, \u0026#34;Analog input No. $(no)\u0026#34;) field(INP, \u0026#34;$(user):calcExample$(no).VAL NPP NMS\u0026#34;) field(EGUF, \u0026#34;10\u0026#34;) field(EGU, \u0026#34;Counts\u0026#34;) field(HOPR, \u0026#34;10\u0026#34;) field(LOPR, \u0026#34;0\u0026#34;) field(HIHI, \u0026#34;8\u0026#34;) field(HIGH, \u0026#34;6\u0026#34;) field(LOW, \u0026#34;4\u0026#34;) field(LOLO, \u0026#34;2\u0026#34;) field(HHSV, \u0026#34;MAJOR\u0026#34;) field(HSV, \u0026#34;MINOR\u0026#34;) field(LSV, \u0026#34;MINOR\u0026#34;) field(LLSV, \u0026#34;MAJOR\u0026#34;) +\tfield(ASG, \u0026#34;deepin\u0026#34;) } alias(\u0026#34;$(user):aiExample$(no)\u0026#34;,\u0026#34;$(user):ai$(no)\u0026#34;) 这里指定$(user):aiExample$(no) record使用 deepin 访问安全组。\n最后,修改st.cmd来启用访问安全配置功能。\n1 2 cd example/iocBoot/ioctest/ vi st.cmd 1 2 3 4 5 6 7 #- Run this to trace the stages of iocInit #-traceIocInit + asSetFilename(\u0026#34;${TOP}/iocBoot/${IOC}/accessSecurity.acf\u0026#34;) cd \u0026#34;${TOP}/iocBoot/${IOC}\u0026#34; iocInit 启动IOC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 cd example/iocBoot/ioctest/ ./st.cmd #!../../bin/linux-x86_64/test \u0026lt; envPaths epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;ioctest\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;/home/deepin/example\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/usr/local/epics/base-7.0.8\u0026#34;) epicsEnvSet(\u0026#34;EPICS_HOST_ARCH\u0026#34;, \u0026#34;linux-x86_64\u0026#34;) cd \u0026#34;/home/deepin/example\u0026#34; ## Register all support components dbLoadDatabase \u0026#34;dbd/test.dbd\u0026#34; test_registerRecordDeviceDriver pdbbase ## Load record instances dbLoadTemplate \u0026#34;db/user.substitutions\u0026#34; dbLoadRecords \u0026#34;db/testVersion.db\u0026#34;, \u0026#34;user=deepin\u0026#34; dbLoadRecords \u0026#34;db/dbSubExample.db\u0026#34;, \u0026#34;user=deepin\u0026#34; asSetFilename(\u0026#34;/home/deepin/example/iocBoot/ioctest/accessSecurity.acf\u0026#34;) cd \u0026#34;/home/deepin/example/iocBoot/ioctest\u0026#34; iocInit Starting iocInit ############################################################################ ## EPICS R7.0.8 ## Rev. 2024-03-01T16:27+0800 ## Rev. Date build date/time: ############################################################################ iocRun: All initialization complete ## Start any sequence programs #seq sncExample, \u0026#34;user=deepin\u0026#34; epics\u0026gt; 然后打开一个新的终端窗口进行测试。\n注意,我这里的主机名是LAPTOP-CTDCXXXX,主机有两个用户deepin和root。\n分别使用两个用户访问使用默认(DEFAULT)和名为deepin访问安全组的变量。\n1 2 3 4 deepin@LAPTOP-CTDCXXXX:~$ caget deepin:circle:angle deepin:circle:angle 186 deepin@LAPTOP-CTDCXXXX:~$ caget deepin:aiExample1 deepin:aiExample1 6 用户deepin对使用不同访问安全组的变量都可以访问。\n1 2 3 4 5 6 7 8 # 使用root用户 deepin@LAPTOP-CTDCXXXX:~$ sudo su root@LAPTOP-CTDCXXXX:/home/deepin# caget deepin:circle:angle deepin:circle:angle 55 root@LAPTOP-CTDCXXXX:/home/deepin# caget deepin:aiExample1 Read operation timed out: some PV data was not read. deepin:aiExample1 *** no read access 用户root可以访问使用默认(DEFAULT)访问安全组的变量,而不可访问使用名为deepin访问安全组的变量。\n测试结果与编写的访问安全配置规则相符合,说明访问安全配置成功。\n不过,此次实验只测试了本机上的不同用户,对于更为复杂的控制系统的数据安全访问权限,则需要做更完善的安全配置。\n","permalink":"https://kira-96.github.io/posts/epics-ioc-access-security/","summary":"原文:IOC Access Security\n功能 访问安全功能用于保护IOC数据库,限制来自未经授权的CA或pvAccess客户端访问。访问安全性基于以下几点:\nWho 客户端的用户ID(Channel Access/pvAccess)。\nWhere 用户登录的主机 ID。客户端运行的主机,但不会分辨用户是本地用户或远程登录到主机的用户。\nWhat 记录的各个字段都受到保护。每条记录都有一个字段包含记录的访问安全组(ASG)。每个字段都有一个访问安全级别(ASL0或ASL1)。安全级别在记录定义文件(.dbd)中定义。\nWhen 访问规则可以包含类似于CALC Record的输入计算。\n定义 ASL 访问安全级别\nASG 访问安全组\nUAG 用户访问组\nHAG 主机访问组\n快速上手 为了启用特定 IOC 的访问安全性,需要完成以下操作:\n创建访问安全文件(.acf) 可能需要修改IOC数据库 记录实例可能需要设置访问安全组ASG字段。如果ASG为空,记录将会使用“DEFAULT”访问安全组。\n访问安全文件可以在iocInit之后通过asSubInit和asSubProcess作为关联的子程序重新加载。将值1写入此记录将导致重新加载。\n必须启动脚本在的iocInit之前包含以下命令:\n1 2 3 4 asSetFilename(\u0026#34;/full/path/to/accessSecurityFile\u0026#34;) /* 下面是一个可选命令 */ /* 使用宏替换 */ asSetSubstitutions(\u0026#34;var1=sub1,var2=sub2,...\u0026#34;) 如果在iocInit之前未执行asSetFilename,就不会启用访问安全限制。\n如果给定asSetFilename,但在首次初始化访问安全性时发生错误,则对该IOC的所有访问都会被拒绝。\n成功启动访问安全性后,尝试重新启动时出现错误,将会保持上次的访问安全配置。\n启动IOC并启用访问安全后,可以通过asSetFilename、asSetSubstitutions和asInit来更改访问安全规则。也可以使用函数asInitialize、asInitFile和asInitFP。\n在启动IOC之后重新初始化访问安全配置操作是“非常昂贵”的操作,尽量不要这样做。\n访问安全配置文件 本节介绍包含用户访问组(UAG)、主机访问组(HAG)和访问安全组(ASG)。IOC会读取访问配置文件(建议使用扩展名.acf)然后创建访问配置数据库。首先给出一个简单的例子,然后是完整的语法描述。\n简单示例\n1 2 3 4 5 6 7 8 9 UAG(uag) {user1,user2} HAG(hag) {host1,host2} ASG(DEFAULT) { RULE(1,READ) RULE(1,WRITE) { UAG(uag) HAG(hag) } } 上面的规则提供了无限制的读权限(READ),而位于主机host1和host2上的用户user1和user2则拥有写权限(WRITE)。","title":"EPICS IOC 访问安全"},{"content":"前言 IgH EtherCAT Master 是一个开源的EtherCAT主站驱动程序,用于管理EtherCAT从站设备,支持Linux操作系统,工控上使用的比较多。\ndls ethercat是由英国钻石光源开发的用于 EPICS 控制系统 EtherCAT 设备的支持程序,基于 IgH Master 主站程序开发,实现对 EtherCAT 总线设备的读写。\n交叉编译环境:Ubuntu\n运行开发板:龙芯2K0500金龙开发板\n内核版本:Linux LS-GD 5.10.0-rt17.lsgd #1 PREEMPT_RT\n相关软件包下载地址 epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov)\nepics-modules/asyn: EPICS module for driver and device support\nepics-modules/busy: APS BCDA synApps module: busy\nepics-modules/autosave: APS BCDA synApps module: autosave\ndls-controls/ethercat: EPICS support to read/write to ethercat based hardware\nIgH EtherCAT Master for Linux\nPREEMPT RT patch\n配置交叉编译环境 关于这一节,之前的文章已经详细讲过,参考配置交叉编译环境。\n如果你使用的是其他开发套件,请按照开发手册安装配置好环境。\n编译 IgH EtherCAT Master 源码一定要下载 stable-1.5 分支的,其他版本我也没有测试!\n打补丁 1 2 3 4 5 6 7 # 先将补丁复制到ethercat驱动源码下 cp ethercat-master/etc/makeDocumentation/configurable-error-suppression.patch ethercat-stable-1.5/ cd ethercat-stable-1.5/ # 打补丁 patch -p1 \u0026lt; configurable-error-suppression.patch # 这里需要注意一下patch的输出结果 由于补丁的时间比较早,跟现有的源码不太匹配,有些地方补丁会失败(FAILED)。比如我这里的报错,可能需要再手动修改一下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 patching file lib/Makefile.am Hunk #1 succeeded at 32 (offset -2 lines). patching file lib/common.c patching file lib/domain.c patching file lib/liberror-documentation.txt patching file lib/liberror.c patching file lib/liberror.h patching file lib/master.c Hunk #1 succeeded at 37 (offset -2 lines). ... Hunk #12 FAILED at 419. Hunk #13 FAILED at 448. ... 2 out of 31 hunks FAILED -- saving rejects to file lib/master.c.rej patching file lib/reg_request.c Hunk #1 succeeded at 39 (offset -2 lines). ... patching file lib/sdo_request.c Hunk #1 succeeded at 39 (offset -2 lines). ... patching file lib/slave_config.c Hunk #1 succeeded at 39 (offset -1 lines). ... patching file lib/voe_handler.c Hunk #1 succeeded at 40 (offset -2 lines). ... 我们参考lib/master.c.rej,手动修改一下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # lib/master.c.rej --- lib/master.c\tTue Feb 12 17:31:08 2013 +0100 +++ lib/master.c\tMon Mar 23 15:52:53 2015 +0000 @@ -419,7 +431,8 @@ if (EC_IOCTL_ERRNO(ret) == EIO \u0026amp;\u0026amp; abort_code) { *abort_code = download.abort_code; } - fprintf(stderr, \u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, + ecrt_errcode = ECRT_ERRMASTERSDODOWNLOAD; + ERRPRINTF(\u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, strerror(EC_IOCTL_ERRNO(ret))); return -EC_IOCTL_ERRNO(ret); } @@ -448,7 +461,8 @@ if (EC_IOCTL_ERRNO(ret) == EIO \u0026amp;\u0026amp; abort_code) { *abort_code = download.abort_code; } - fprintf(stderr, \u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, + ecrt_errcode = ECRT_ERRMASTERSDODOWNLOADCOMPLETE; + ERRPRINTF(\u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, strerror(EC_IOCTL_ERRNO(ret))); return -EC_IOCTL_ERRNO(ret); } 然后这个补丁还有点问题,我们需要手动修改一下lib/liberror.c。这里源文件和头文件变量定义不一致,编译会报错,以头文件为准。\n1 2 3 4 5 6 7 #include \u0026#34;liberror.h\u0026#34; int ecrt_err_to_stderr = 1; // char *ecrt_errstring[ERRSTRING_LEN]; char ecrt_errstring[ERRSTRING_LEN]; int ecrt_errcode; 准备内核源码 由于需要将源码编译成相应的系统驱动,所以这里需要使用内核的源码。\n如果是标准系统,可以直接安装linux-headers。\n1 2 # Ubuntu/Debian sudo apt install linux-headers-$(uname -r) 树莓派系统\n1 sudo apt install raspberrypi-kernel-headers 而对于开发板系统,我们可以使用随开发板提供的内核源码。\n内核的编译步骤请根据开发板的用户手册完成。\n最好再给内核打上 PREEMPT_RT 补丁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 解压内核源码 tar -xvf linux-5.10-2k500-src-f45937d-build.20230721100738.tar.gz # linux-5.10-2k500-cbd-src cd linux-5.10-2k500-cbd-src/ # 为内核打上实时补丁(可选) patch -p1 \u0026lt; patch-5.10-rt17.patch # 配置交叉编译器 ./set_env.sh # 编译内核 make loongson_2k500_defconfig # make menuconfig make uImage # 编译模块 make modules 编译 EtherCAT Master 驱动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cd ethercat-stable-1.5/ # to create the configure script, if downloaded from the repo ./bootstrap # 这里需要注意是否出现报错,需要安装 autoconf、pkg-config 等工具 # 执行configure # --host 指定程序运行的主机架构 # --with-linux-dir 指定源码目录 # 如果是安装的linux-headers,通常在 /usr/src/linux-headers-xxx # 如果直接使用内核源码,则必须通过上述编译步骤! # --prefix 指定安装目录 ./configure --host=loongarch64-linux-gnu CC=loongarch64-linux-gnu-gcc --enable-generic=yes --enable-8139too=no --with-linux-dir=/path/to/linux-5.10-2k500-cbd-src --prefix=/path/to/__install_dir # 编译 make all modules # 或者 # make # make ARCH=loongarch CORSS_COMPILE=loongarch64-linux-gnu- modules # 安装生成的文件 # make install 如果完全按照上述步骤,应该可以编译成功。\n下面整理一下生成的文件。\n1 2 3 4 5 # 复制生成的主程序 cp tool/ethercat /path/to/__install_dir/bin/ # 复制生成的驱动程序 cp device/ec_generic.ko /path/to/__install_dir/modules/ cp master/ec_master.ko /path/to/__install_dir/modules/ 这是我整理的文件,后面需要将这些文件下载到开发板。\n__install_dir ├─ bin │ └─ ethercat (主程序) ├─ etc (配置) ├─ include (头文件) ├─ lib (libethercat.so) ├─ modules (驱动目录) │ ├─ ec_master.ko │ └─ ec_generic.ko ├─ sbin │ └─ ethercatctl └─ share └─ bash-completion/ (bash自动补全) 编译 EPICS ethercat 模块 以下步骤需要先安装好EPICS Base!\n编译 asyn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cd asyn touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # SSCAN模块路径 # SSCAN=$(SUPPORT)/sscan # CALC模块路径 # CALC=$(SUPPORT)/calc # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 autosave 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cd autosave touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 busy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cd busy touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # ASYN模块路径 ASYN=$(SUPPORT)/asyn # AUTOSAVE模块路径 AUTOSAVE=$(SUPPORT)/autosave # BUSY模块路径 BUSY=$(SUPPORT)/busy # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 ethercat 这一步可以说是最麻烦,问题最多的。编译出什么问题都需要去找到相应的Makefile修改。\n由于该软件包已经长时间无人维护,建议使用我修改过的版本。\n首先需要安装所需的包libxml2-dev。但我们实际上并不需要用这个软件包,我们只需要它的头文件。\n软件依赖的动态库(.so)文件,我们则需要从开发板系统中拷贝出来,无法直接用编译电脑的动态库。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 cd ethercat-master/ # 创建 3rd 目录,用于放置所需的头文件和动态库 mkdir 3rd mkdir 3rd/include mkdir 3rd/lib # 安装 libxml2-dev sudo apt install libxml2-dev # 从系统目录中复制出libxml2的头文件 # 因为还需要做一些修改,不能直接使用 cp -r /usr/include/libxml2/ ./3rd/include/ # 复制刚刚编译生成的 libethercat cp /path/to/__install_dir/lib/libethercat.so* ./3rd/lib/ # 从开发板系统复制所需要的动态库 # libxml2 依赖 libz 和 liblzma scp root@192.168.1.10:/usr/lib/libxml2.so.2.9.12 ./3rd/lib/ scp root@192.168.1.10:/usr/lib/libz.so.1.2.11 ./3rd/lib/ scp root@192.168.1.10:/usr/lib/liblzma.so.5.2.5 ./3rd/lib/ # 手动创建一下链接 cd 3rd/lib/ ln -s libxml2.so.2.9.12 libxml2.so.2 ln -s libxml2.so.2 libxml2.so ln -s liblzma.so.5.2.5 liblzma.so.5 ln -s liblzma.so.5 liblzma.so ln -s libz.so.1.2.11 libz.so.1 ln -s libz.so.1 libz.so 然后需要修改一下libxml2的头文件,不然编译的时候会报错。\n报错信息:\n1 2 3 4 5 6 7 8 9 10 In file included from ../../../libxml2/libxml/parser.h:812, from ../../../libxml2/libxml/globals.h:18, from ../../../libxml2/libxml/threads.h:35, from ../../../libxml2/libxml/xmlmemory.h:218, from ../../../libxml2/libxml/tree.h:1307, from ../parser.c:10: ../../../libxml2/libxml/encoding.h:31:10: fatal error: unicode/ucnv.h: No such file or directory #include \u0026lt;unicode/ucnv.h\u0026gt; ^~~~~~~~~~~~~~~~ compilation terminated. 解决方法:\n1 2 3 4 5 6 # 修改 xmlversion.h vi 3rd/include/libxml2/libxml/xmlversion.h # 找到下面行,禁用 LIBXML_ICU_ENABLED #define LIBXML_ICU_ENABLED # 将 #if 1 改为 #if 0 1 2 3 4 5 6 7 8 9 /** * LIBXML_ICU_ENABLED: * * Whether icu support is available */ - #if 0 + #if 1 #define LIBXML_ICU_ENABLED #endif 做好上面的准备工作,还需要修改源码中的路径配置,然后才能正常编译。\n下面都是我踩坑留下的记录。\n修改 configure/RELEASE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd ethercat-master/ # 首先修改 configure/RELEASE vi configure/RELEASE # 需要修改的有4项 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # ASYN模块路径 ASYN=$(SUPPORT)/asyn # BUSY模块路径 BUSY=$(SUPPORT)/busy # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 修改 ethercatApp/scannerSrc/Makefile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cd ethercat-master/ # 修改 ethercatApp/scannerSrc/Makefile vi ethercatApp/scannerSrc/Makefile # 需要修改 EtherCAT Master 源码相关路径、 # libxml2头文件路径、动态库(.so)路径 # 修改 ETHERLAB 源码路径 ETHERLAB=/path/to/ethercat-stable-1.5 ETHERLABPREFIX=$(ETHERLAB) USR_INCLUDES += -I$(ETHERLABPREFIX)/include # 修改动态库路径 USR_LDFLAGS += -L$(TOP)/3rd/lib -Wl,-rpath=$(TOP)/3rd/lib # 修改 libxml2 头文件路径 USR_INCLUDES += -I$(TOP)/3rd/include/libxml2 USR_SYS_LIBS += ethercat xml2 # 下面类似 scanner_INCLUDES += -I$(ETHERLAB)/lib serialtool_INCLUDES += -I$(ETHERLAB)/master get-slave-revisions_INCLUDES += -I$(ETHERLAB)/master 修改 ethercatApp/src/Makefile,与上面类似。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 cd ethercat-master/ # 修改 ethercatApp/src/Makefile vi ethercatApp/src/Makefile # 需要修改 EtherCAT Master 源码相关路径、 # libxml2头文件路径、动态库(.so)路径 # 修改 ETHERLAB 源码路径 ETHERLAB=/path/to/ethercat-stable-1.5 # ecAsyn_INCLUDES += -I$(ETHERLAB)/src/ethercat-$(subst -,.,$(VERSION))/include # gadc_INCLUDES += -I$(ETHERLAB)/src/ethercat-$(subst -,.,$(VERSION))/include ecAsyn_INCLUDES += -I$(ETHERLAB)/include gadc_INCLUDES += -I$(ETHERLAB)/include # 修改 libxml2 头文件路径 USR_INCLUDES += -I$(TOP)/3rd/include/libxml2 # 添加动态库路径 USR_LDFLAGS += -L$(TOP)/3rd/lib -Wl,-rpath=$(TOP)/3rd/lib USR_SYS_LIBS += xml2 修改源码 由于ethercat-master的源码原本是为x86_64架构编写的,编译到LoongArch架构的设备上运行可能会出现一些奇怪的错误。\n例如,在运行slaveinfo时会出现Segmentation fault错误,而这通常是空指针导致内存访问出错。\n原因分析:\nslaveinfo在运行时会根据程序自己的路径寻找slave-types.txt文件,问题就出在这里。来看源码 ethercatApp/scannerSrc/slave-list-path.c。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // ethercatApp/scannerSrc/slave-list-path.c int get_root_dir_index(const char *program_name) { // Search for the binary path char binary_dir[] = \u0026#34;bin/linux-x86_64/\u0026#34;; // Find the binary path in the program path and return the pointer char *found = strstr(program_name, binary_dir); // Handle the case where it is not found if (found == NULL) { return -1; } // Calculate the difference in the pointers to get the index return found - program_name; } 这个get_root_dir_index函数是用于计算当前程序(slaveinfo)所在的根目录。这里向上查找的路径为bin/linux-x86_64,当然不能找到LoongArch的目录bin/linux-loong64了。所以,此函数只在路径为bin/linux-x86_64/slaveinfo时才能正常运行,其它情况都返回-1。(第一次见到这样写的,真无语了……)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // ethercatApp/scannerSrc/slave-list-path.c char *get_slave_list_filename(const char *program_path) { char relative_path[] = \u0026#34;etc/scripts/slave-types.txt\u0026#34;; char *slave_list_filename = NULL; // Get absolute path of application char *real_path = calloc(PATH_MAX, sizeof(char)); get_app_path(program_path, real_path); // Get root directory int root_dir_index = get_root_dir_index(real_path); if (root_dir_index != -1) { slave_list_filename = calloc(root_dir_index + strlen(relative_path) + 1, sizeof(char)); strncpy(slave_list_filename, real_path, root_dir_index); } // Append relative path strcat(slave_list_filename, relative_path); // Check file struct stat fstat; int result = stat(slave_list_filename, \u0026amp;fstat); if (result) { printf(\u0026#34;Could not find slave list file at %s\\n\u0026#34;, slave_list_filename); } // Cleanup free(real_path); return slave_list_filename; } 而在get_slave_list_filename函数中,只处理了get_root_dir_index正常返回的情况。当get_root_dir_index返回-1时,slave_list_filename始终为NULL,这就导致后续操作出错。\n其实这个问题直接把程序放到bin/linux-x86_64目录下运行就可以了,不过既然找到了问题,索性就改一改。\n现在已经知道了出错的地方,该如何修改呢?这里我本着尽量少改动源码的原则,在get_root_dir_index查找根目录出错时,直接使用当前目录。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 char *get_slave_list_filename(const char *program_path) { char relative_path[] = \u0026#34;etc/scripts/slave-types.txt\u0026#34;; char *slave_list_filename = NULL; // Get absolute path of application char *real_path = calloc(PATH_MAX, sizeof(char)); get_app_path(program_path, real_path); // Get root directory int root_dir_index = get_root_dir_index(real_path); if (root_dir_index != -1) { slave_list_filename = calloc(root_dir_index + strlen(relative_path) + 1, sizeof(char)); strncpy(slave_list_filename, real_path, root_dir_index); } + else + { + slave_list_filename = calloc(PATH_MAX, sizeof(char)); + if (NULL != getcwd(slave_list_filename, PATH_MAX)) + { + strcat(slave_list_filename, \u0026#34;/\u0026#34;); + } + } // Append relative path strcat(slave_list_filename, relative_path); // Check file struct stat fstat; int result = stat(slave_list_filename, \u0026amp;fstat); if (result) { printf(\u0026#34;Could not find slave list file at %s\\n\u0026#34;, slave_list_filename); } // Cleanup free(real_path); return slave_list_filename; } 目前只发现了这个问题,希望后面没有坑了。\n编译 最后终于可以开始编译了。\n1 2 3 cd ethercat-master/ # 执行交叉编译 make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 到这里,编译过程还可能会出错,不过已经可以编译出我们所需要的东西了。\n最终编译得到scanner、slaveinfo程序就可以了。\n以下是我编译时的输出:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... Installing library ../../../lib/linux-loong64/libscannerlib.a ... Installing created executable ../../../bin/linux-loong64/serialtool Installing created executable ../../../bin/linux-loong64/get-slave-revisions Installing created executable ../../../bin/linux-loong64/scanner Installing created executable ../../../bin/linux-loong64/slaveinfo Installing created executable ../../../bin/linux-loong64/parsertest ... Installing shared library ../../../lib/linux-loong64/libecAsyn.so Installing library ../../../lib/linux-loong64/libecAsyn.a Installing created executable ../../../bin/linux-loong64/parsertest ... Installing template file ../../../db/EK1100.template ... make -C ./protocol install make[2]: 进入目录“/home/deepin/ethercat-master/ethercatApp/protocol” make[2]: *** 没有规则可制作目标“install”。 停止。 make[2]: 离开目录“/home/deepin/ethercat-master/ethercatApp/protocol” make[1]: *** [/usr/local/epics/base-7.0.8/configure/RULES_DIRS:85:protocol.install] 错误 2 make[1]: 离开目录“/home/deepin/ethercat-master/ethercatApp” make: *** [/usr/local/epics/base-7.0.8/configure/RULES_DIRS:85:ethercatApp.install] 错误 2 最后的错误,我直接忽略了,因为已经得到了需要的可执行程序。\n整理一下编译生成的文件:\nethercat-master ├─ bin │ └─ linux-loong64 ├─ db ├─ dbd └─ lib └─ linux-loong64 测试运行 将整理好的文件下载到开发板后,我们测试运行一下。\n测试安装 EtherCAT 主站驱动程序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@LS-GD modules]# modinfo ec_generic.ko filename: /root/__install/modules/ec_generic.ko version: 1.5.2 unknown license: GPL description: EtherCAT master generic Ethernet device module author: Florian Pose \u0026lt;fp@igh-essen.com\u0026gt; srcversion: 848BB80F1C588A2FDA42EDB depends: ec_master name: ec_generic vermagic: 5.10.0-rt17.lsgd preempt_rt mod_unload modversions LOONGARCH 64BIT [root@LS-GD modules]# insmod ec_master.ko [root@LS-GD modules]# insmod ec_generic.ko [root@LS-GD modules]# lsmod Module Size Used by ec_generic 6427 0 ec_master 464125 1 ec_generic 测试ethercat主程序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 [root@LS-GD tool]# ./ethercat Please specify a command! Usage: ethercat \u0026lt;COMMAND\u0026gt; [OPTIONS] [ARGUMENTS] Commands (can be abbreviated): alias Write alias addresses. config Show slave configurations. crc CRC error register diagnosis. cstruct Generate slave PDO information in C language. data Output binary domain process data. debug Set the master\u0026#39;s debug level. domains Show configured domains. download Write an SDO entry to a slave. eoe Display Ethernet over EtherCAT statictics. foe_read Read a file from a slave via FoE. foe_write Store a file on a slave via FoE. graph Output the bus topology as a graph. master Show master and Ethernet device information. pdos List Sync managers, PDO assignment and mapping. reg_read Output a slave\u0026#39;s register contents. reg_write Write data to a slave\u0026#39;s registers. rescan Rescan the bus. sdos List SDO dictionaries. sii_read Output a slave\u0026#39;s SII contents. sii_write Write SII contents to a slave. slaves Display slaves on the bus. soe_read Read an SoE IDN from a slave. soe_write Write an SoE IDN to a slave. states Request application-layer states. upload Read an SDO entry from a slave. version Show version information. xml Generate slave information XML. Global options: --master -m \u0026lt;master\u0026gt; Comma separated list of masters to select, ranges are allowed. Examples: \u0026#39;1,3\u0026#39;, \u0026#39;5-7,9\u0026#39;, \u0026#39;-3\u0026#39;. Default: \u0026#39;-\u0026#39; (all). --force -f Force a command. --quiet -q Output less information. --verbose -v Output more information. --help -h Show this help. Numerical values can be specified either with decimal (no prefix), octal (prefix \u0026#39;0\u0026#39;) or hexadecimal (prefix \u0026#39;0x\u0026#39;) base. Call \u0026#39;ethercat \u0026lt;COMMAND\u0026gt; --help\u0026#39; for command-specific help. Send bug reports to fp@igh.de. 测试scanner主程序。\n1 2 3 [root@LS-GD linux-loong64]# chmod +x scanner [root@LS-GD linux-loong64]# ./scanner usage: scanner [-m master_index] [-s] [-q] scanner.xml socket_path 可以看到,驱动程序和主程序都能在开发板上运行,说明已经编译完成了。\n安装 EtherCAT 主站到开发板系统 首先将编译好的EtherCAT Master下载到开发板系统,然后将各个目录下的文件放到相应的系统目录下。(这里我还以__install_dir的目录结构为例。)\n原文件/目录 系统文件/目录 bin/ethercat /usr/bin/ethercat etc/init.d/ethercat /etc/init.d/ethercat etc/sysconfig/ethercat /etc/sysconfig/ethercat etc/ethercat.conf /etc/ethercat.conf include/ - lib/libethercat.so* /usr/lib/libethercat.so* modules/ /lib/modules/5.10.0-rt17.lsgd/ sbin/ethercatctl /sbin/ethercatctl share/bash-completion/ /usr/share/bash-completion/ 注意:如果系统目录存在/lib/modules/{内核版本}目录,则可以将modules目录下的ec_master.ko和ec_generic.ko复制到该目录下,然后在终端执行depmod命令。否则,可以按照下面的步骤做相应修改。\n例如,将modules下的驱动文件放到开发板文件系统的/root/modules/目录下。\n修改/etc/init.d/ethercat和/sbin/ethercatctl脚本文件。\n例:/etc/init.d/ethercat\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 LSMOD=/sbin/lsmod MODPROBE=/sbin/modprobe + INSMOD=/sbin/insmod RMMOD=/sbin/rmmod MODINFO=/sbin/modinfo - ETHERCAT=/home/loongson/__install_dir/bin/ethercat + ETHERCAT=/usr/bin/ethercat MASTER_ARGS= + MODULE_DIR=/root/modules start) echo -n \u0026#34;Starting EtherCAT master 1.5.2 \u0026#34; ... # load master module - if ! ${MODPROBE} ${MODPROBE_FLAGS} ec_master \u0026#34;${MASTER_ARGS}\u0026#34; \\ + if ! ${INSMOD} ${MODULE_DIR}/ec_master.ko \u0026#34;${MASTER_ARGS}\u0026#34; \\ main_devices=\u0026#34;${DEVICES}\u0026#34; backup_devices=\u0026#34;${BACKUPS}\u0026#34;; then exit_fail fi # check for modules to replace for MODULE in ${DEVICE_MODULES}; do ECMODULE=ec_${MODULE} - if ! ${MODINFO} \u0026#34;${ECMODULE}\u0026#34; \u0026gt; /dev/null; then - continue # ec_* module not found - fi if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ]; then if ${LSMOD} | grep \u0026#34;^${MODULE} \u0026#34; \u0026gt; /dev/null; then if ! ${RMMOD} \u0026#34;${MODULE}\u0026#34;; then exit_fail fi fi fi - if ! ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${ECMODULE}\u0026#34;; then + if ! ${INSMOD} \u0026#34;${MODULE_DIR}/${ECMODULE}.ko\u0026#34;; then if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ]; then ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${MODULE}\u0026#34; # try to restore fi exit_fail fi done exit_success ;; 例:/sbin/ethercatctl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 LSMOD=/sbin/lsmod MODPROBE=/sbin/modprobe + INSMOD=/sbin/insmod RMMOD=/sbin/rmmod MODINFO=/sbin/modinfo IP=/bin/ip - ETHERCAT=/home/loongson/__install_dir/bin/ethercat + ETHERCAT=/usr/bin/ethercat + MODULE_DIR=/root/modules #------------------------------------------------------------------------------ - ETHERCAT_CONFIG=/home/loongson/__install_dir/etc/ethercat.conf + ETHERCAT_CONFIG=/etc/ethercat.conf start) ... # load master module - if ! ${MODPROBE} ${MODPROBE_FLAGS} ec_master \\ + if ! ${INSMOD} ${MODULE_DIR}/ec_master.ko \\ main_devices=\u0026#34;${DEVICES}\u0026#34; backup_devices=\u0026#34;${BACKUPS}\u0026#34;; then exit 1 fi LOADED_MODULES=ec_master # check for modules to replace for MODULE in ${DEVICE_MODULES}; do ECMODULE=ec_${MODULE} - if ! ${MODINFO} \u0026#34;${ECMODULE}\u0026#34; \u0026gt; /dev/null; then - continue # ec_* module not found - fi if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;ccat\u0026#34; ]; then # unload standard module and check if unloading was successful ${RMMOD} \u0026#34;${MODULE}\u0026#34; 2\u0026gt; /dev/null || true if ${LSMOD} | grep \u0026#34;^${MODULE} \u0026#34; \u0026gt; /dev/null; then # could not unload module ${RMMOD} ${LOADED_MODULES} exit 1 fi fi - if ! ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${ECMODULE}\u0026#34;; then + if ! ${INSMOD} \u0026#34;${MODULE_DIR}/${ECMODULE}.ko\u0026#34;; then if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;ccat\u0026#34; ]; then ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${MODULE}\u0026#34; # try to restore fi ${RMMOD} ${LOADED_MODULES} exit 1 fi LOADED_MODULES=\u0026#34;${ECMODULE} ${LOADED_MODULES}\u0026#34; done exit 0 ;; 修改 EtherCAT Master 配置文件。\n例:/etc/sysconfig/ethercat和/etc/ethercat.conf\n1 2 3 4 5 6 MASTER0_DEVICE=\u0026#34;00:11:22:33:44:55\u0026#34; #MASTER1_DEVICE=\u0026#34;\u0026#34; #MASTER0_BACKUP=\u0026#34;\u0026#34; DEVICE_MODULES=\u0026#34;generic\u0026#34; MASTER\u0026lt;X\u0026gt;_DEVICE配置网卡的物理地址(MAC),可通过ifconfig命令查看。\nDEVICE_MODULES配置使用的模块名称,这里仅使用通用网卡驱动generic。\n运行 EtherCAT Master 启动EtherCAT主站程序。\n1 2 3 /etc/init.d/ethercat start # 或者 /sbin/ethercatctl start 如果一切正常,可以看到/dev目录下有EtherCAT0设备文件。\n查看主站信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [root@LS-GD ~]# ethercat master Master0 Phase: Idle Active: no Slaves: 1 Ethernet devices: Main: 00:11:22:33:44:55 (attached) Link: UP Tx frames: 370862 Tx bytes: 22319896 Rx frames: 370861 Rx bytes: 22319836 Tx errors: 0 Tx frame rate [1/s]: 125 125 125 Tx rate [KByte/s]: 7.3 7.3 7.3 Rx frame rate [1/s]: 125 125 125 Rx rate [KByte/s]: 7.3 7.3 7.3 Common: Tx frames: 1108336 Tx bytes: 66704720 Rx frames: 1108307 Rx bytes: 66702980 Lost frames: 29 Tx frame rate [1/s]: 125 125 125 Tx rate [KByte/s]: 7.3 7.3 7.3 Rx frame rate [1/s]: 125 125 125 Rx rate [KByte/s]: 7.3 7.3 7.3 Loss rate [1/s]: 0 0 0 Frame loss [%]: 0.0 0.0 0.0 Distributed clocks: Reference clock: Slave 0 DC reference time: 0 Application time: 0 2000-01-01 00:00:00.000000000 查看从站设备\n1 2 [root@LS-GD ~]# ethercat slaves 0 0:0 PREOP + XB6-EC0002(Modules/Slots and MDP) 生成从站信息XML文件\n1 [root@LS-GD ~]# ethercat xml \u0026gt; scanner.xml 其他ethercat命令\n使用ethercat -h命令查看其他命令的使用方法。\n参考 IgH EtherCAT Master Building and installing IgH EtherCAT Master IgH EtherCAT Master for Linux EtherCAT DRIVER AND TOOLS FOR EPICS AND LINUX AT PSI INTEGRATION OF EtherCAT HARDWARE INTO THE EPICS BASED DISTRIBUTED CONTROL SYSTEM AT iThemba LABS 在“福珑2.0”主机上编译EPICS Ehtercat驱动软件的体验 ","permalink":"https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/","summary":"前言 IgH EtherCAT Master 是一个开源的EtherCAT主站驱动程序,用于管理EtherCAT从站设备,支持Linux操作系统,工控上使用的比较多。\ndls ethercat是由英国钻石光源开发的用于 EPICS 控制系统 EtherCAT 设备的支持程序,基于 IgH Master 主站程序开发,实现对 EtherCAT 总线设备的读写。\n交叉编译环境:Ubuntu\n运行开发板:龙芯2K0500金龙开发板\n内核版本:Linux LS-GD 5.10.0-rt17.lsgd #1 PREEMPT_RT\n相关软件包下载地址 epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov)\nepics-modules/asyn: EPICS module for driver and device support\nepics-modules/busy: APS BCDA synApps module: busy\nepics-modules/autosave: APS BCDA synApps module: autosave\ndls-controls/ethercat: EPICS support to read/write to ethercat based hardware\nIgH EtherCAT Master for Linux\nPREEMPT RT patch","title":"龙芯开发板移植 IgH EtherCAT Master"},{"content":"Hugo原生支持GoAT,可以直接使用goat代码块。\n1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · a b a b a b a b \u0026amp; A M S i o i q f b B x u j o e a ( - x d r a \u0026gt; e f R \u0026gt; c o C n u o b ( n r ) ) d n e e J d r o s i n N o R D t o i u a a n g d d l o i t n e D i a g o n a l s C V u e r r v t e i d c a l n o t A N C : o u l d r r i A a / I v n s i n e e h s t d - e - t r l B i h i i s i o n ' s r e q n . u * o o b t t o e l a s d ' * l i n e D o n S e e ? a r c 3 h 本博客还支持Mermaid.js作图。\nsequenceDiagram participant Alice participant Bob Alice-\u003e\u003eJohn: Hello John, how are you? loop Healthcheck John-\u003e\u003eJohn: Fight against hypochondria end Note right of John: Rational thoughts prevail! John--\u003e\u003eAlice: Great! John-\u003e\u003eBob: How about you? Bob--\u003e\u003eJohn: Jolly good! gantt dateFormat YYYY-MM-DD title Adding GANTT diagram to mermaid excludes weekdays 2014-01-10 section A section Completed task :done, des1, 2014-01-06,2014-01-08 Active task :active, des2, 2014-01-09, 3d Future task : des3, after des2, 5d Future task2 : des4, after des3, 5d gitGraph commit commit branch develop commit commit commit checkout main commit commit xychart-beta title \"Sales Revenue\" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis \"Revenue (in $)\" 4000 --\u003e 11000 bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] 参考\nDiagrams | Hugo Hugo博客添加mermaid作图 Render ASCII art as SVG diagrams Mermaid 入门文档 ","permalink":"https://kira-96.github.io/notes/hugo-diagrams/","summary":"Hugo原生支持GoAT,可以直接使用goat代码块。\n1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · a b a b a b a b \u0026amp; A M S i o i q f b B x u j o e a ( - x d r a \u0026gt; e f R \u0026gt; c o C n u o b ( n r ) ) d n e e J d r o s i n N o R D t o i u a a n g d d l o i t n e D i a g o n a l s C V u e r r v t e i d c a l n o t A N C : o u l d r r i A a / I v n s i n e e h s t d - e - t r l B i h i i s i o n ' s r e q n .","title":"Hugo Diagrams"},{"content":"前言 MODBUS是一种应用层消息传递协议,通常用于 I/O 系统通信和可编程逻辑控制器(PLC)通信。\n链接类型 描述 MODBUS TCP TCP/IP 使用502端口 MODBUS RTU RTU通常通过串行通信链路运行,即RS-232、 RS-422 或 RS-485。RTU 使用额外的 CRC 进行数据包检查。协议直接将每个字节作为 8 个数据位传输,因此使用“二进制” 而不是 ASCII 编码。使用串行链路开始和结束时,消息帧是按时间而不是按特定字符检测的。 MODBUS ASCII 串行协议,通常在串行通信链路上运行,即 RS-232、RS-422 或 RS-485。串行 ASCII 使用额外的 LRC 数据包检查。该协议将每个字节编码为 2 个 ASCII 字符。消息帧的开始和结束由特定字符检测 (“:” 开始消息,CR/LF 结束消息)。该协议效率低于 Modbus RTU,但在某些环境中可能更可靠。 Modbus 提供对以下 4 种类型的数据的访问:\n主表 对象类型 访问 说明 离散输入 1bit 只读 这种类型的数据可以由 I/O 系统提供。 线圈 1bit 读写 此类数据可由应用程序更改。 输入寄存器 16位字(2字节) 只读 这种类型的数据可以由 I/O 系统提供。 保持寄存器 16位字(2字节) 读写 此类数据可由应用程序更改。 Modbus 通信由从 Modbus 客户端发送到 Modbus 服务器的请求消息组成。服务器使用响应消息进行回复。Modbus 请求消息包含:\n描述数据传输类型的 Modbus 功能码(1字节)。 Modbus 地址(2字节),用于描述从服务器中读取或写入数据的地址。 对于写入操作,则需要传输写入的数据。 Modbus模块 支持以下 9 个 Modbus 功能码:\n访问 功能说明 功能码 1bit 读取线圈 1 1bit 读取离散输入 2 1bit 写入单线圈 5 1bit 写入多个线圈 15 16位字访问(2字节) 读取输入寄存器 4 16位字访问(2字节) 读取保持寄存器 3 16位字访问(2字节) 写入单个寄存器 6 16位字访问(2字节) 写入多个寄存器 16 16位字访问(2字节) 读/写多个寄存器 23 Modbus读取操作仅限于传输125个16位字或2000 bit。Modbus写入操作仅限于传输123个16位字或1968 bit。\n编译MODBUS模块 使用到的模块下载地址 epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov)\nepics-modules/asyn: EPICS module for driver and device support\nepics-modules/modbus: EPICS support for communication with Programmable Logic Controllers (PLCs) and other devices via the Modbus protocol over TCP, serial RTU, and serial ASCII links epics-modules/sscan: APS BCDA synApps module: sscan\nepics-modules/calc: APS BCDA synApps module: calc\nepics-modules/ipac: IPAC Carrier and Communication Module Drivers\nsequencer / Download and Installation — EPICS Sequencer Version 2.2 (bessy.de)\n以下步骤需要先安装好EPICS Base.\n编译 SSCAN(可选) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cd sscan touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 CALC(可选) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd calc touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # SSCAN模块路径 SSCAN=$(SUPPORT)/sscan # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 asyn(必需) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cd asyn touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # SSCAN模块路径 SSCAN=$(SUPPORT)/sscan # CALC模块路径 CALC=$(SUPPORT)/calc # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 modbus 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd modbus touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # ASYN模块路径 ASYN=$(SUPPORT)/asyn # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译完成后,可以看到bin\\\u0026lt;EPICS_HOST_ARCH\u0026gt;路径下生成了可执行程序modbusApp,它就是与Modbus设备通信的主程序了。\n使用 MODBUS 程序 在Modbus模块的iocBoot\\iocTest目录下,可以看到很多示例程序。这里总结一下,我们使用时主要需要编写两部分内容。\n用于配置设备连接和通信的.cmd文件 用于使用模板解析数据的.substitutions文件 这里给出示例并做简要说明。\nenvPaths文件:用于配置程序运行时的环境变量路径。\n这里需要配置好base、asyn、modbus模块的路径。\n1 2 3 4 5 6 7 8 9 # envPaths epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;app\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;..\u0026#34;) epicsEnvSet(\u0026#34;SUPPORT\u0026#34;,\u0026#34;/root/modules\u0026#34;) epicsEnvSet(\u0026#34;ASYN\u0026#34;,\u0026#34;/root/modules/asyn\u0026#34;) epicsEnvSet(\u0026#34;MODBUS\u0026#34;,\u0026#34;/root/modules/modbus\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/root/base\u0026#34;) # epicsEnvSet(\u0026#34;EPICS_CAS_SERVER_PORT\u0026#34;, 9001) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 # AMSAMOTION.cmd \u0026lt; envPaths dbLoadDatabase(\u0026#34;$(MODBUS)/dbd/modbusApp.dbd\u0026#34;) modbusApp_registerRecordDeviceDriver(pdbbase) # MODBUS TCP 配置 # Use the following commands for TCP/IP #drvAsynIPPortConfigure(const char *portName, # const char *hostInfo, # unsigned int priority, # int noAutoConnect, # int noProcessEos); drvAsynIPPortConfigure(\u0026#34;AMSAMOTION\u0026#34;,\u0026#34;192.168.xxx.xxx:502\u0026#34;,0,0,1) #asynSetOption(\u0026#34;AMSAMOTION\u0026#34;,0, \u0026#34;disconnectOnReadTimeout\u0026#34;, \u0026#34;Y\u0026#34;) # MODBUS RTU配置 #drvAsynSerialPortConfigure(const char *portName, # const char *ttyName, # unsigned int priority, # int noAutoConnect, # int noProcessEos); # drvAsynSerialPortConfigure(\u0026#34;Koyo1\u0026#34;, \u0026#34;/dev/ttyS1\u0026#34;, 0, 0, 0) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;baud\u0026#34;,\u0026#34;38400\u0026#34;) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;parity\u0026#34;,\u0026#34;none\u0026#34;) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;bits\u0026#34;,\u0026#34;8\u0026#34;) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;stop\u0026#34;,\u0026#34;1\u0026#34;) # Modbus ASCII 还需配置其他项 # asynOctetSetOutputEos(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;\\r\\n\u0026#34;) # asynOctetSetInputEos(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;\\r\\n\u0026#34;) # 超时设置 #modbusInterposeConfig(const char *portName, # modbusLinkType linkType, # int timeoutMsec, # int writeDelayMsec) # Modbus Link Type: 0 = TCP/IP,1 = RTU,2 = ASCII modbusInterposeConfig(\u0026#34;AMSAMOTION\u0026#34;,0,5000,0) # 读取/写入配置 #drvModbusAsynConfigure(portName, # tcpPortName, # slaveAddress, # modbusFunction, # modbusStartAddress, # modbusLength, # dataType, # pollMsec, # plcType); drvModbusAsynConfigure(\u0026#34;AMSA:AI\u0026#34;, \u0026#34;AMSAMOTION\u0026#34;, 1, 4, 0, 6, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:AO1\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 6, 0, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:AO2\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 6, 1, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:AOSta\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;,1, 3, 0, 2, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DI\u0026#34;, \u0026#34;AMSAMOTION\u0026#34;, 1, 2, 0, 8, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO1\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 0, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO2\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 1, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO3\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 2, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO4\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 3, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO5\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 4, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO6\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 5, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO7\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 6, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO8\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 7, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DOSta\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;,1, 1, 0, 8, 0, 100, \u0026#34;AMSA\u0026#34;) # Enable ASYN_TRACEIO_HEX on modbus server asynSetTraceIOMask(\u0026#34;AMSAMOTION\u0026#34;,0,4) # Dump up to 512 bytes in asynTrace asynSetTraceIOTruncateSize(\u0026#34;AMSAMOTION\u0026#34;,0,512) dbLoadTemplate(\u0026#34;AMSAMOTION.substitutions\u0026#34;) iocInit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 # AMSAMOTION.substitutions # asyn record for the underlying asyn octet port file \u0026#34;$(ASYN)/db/asynRecord.db\u0026#34; { pattern {P, R, PORT, ADDR, IMAX, OMAX} {AMSAMOTION: OctetAsyn, AMSAMOTION, 0, 80, 80} } file \u0026#34;$(TOP)/db/ai.template\u0026#34; { pattern {P, R, PORT, OFFSET, BITS, EGUL, EGUF, PREC, SCAN} {AMSAMOTION:, AI1, AMSA:AI, 0, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI2, AMSA:AI, 1, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI3, AMSA:AI, 2, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI4, AMSA:AI, 3, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI5, AMSA:AI, 4, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI6, AMSA:AI, 5, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} } file \u0026#34;$(TOP)/db/ao.template\u0026#34; { pattern {P, R, PORT, OFFSET, BITS, EGUL, EGUF, PREC} {AMSAMOTION: AO1, AMSA:AO1, 0, 0xFFFF, 0, 65535, 0} {AMSAMOTION: AO2, AMSA:AO2, 0, 0xFFFF, 0, 65535, 0} } file \u0026#34;$(TOP)/db/ai.template\u0026#34; { pattern {P, R, PORT, OFFSET, BITS, EGUL, EGUF, PREC, SCAN} {AMSAMOTION:, AO1:STATE, AMSA:AOSta, 0, 0xFFFF, 0, 65535, 0, \u0026#34;1 second\u0026#34;} {AMSAMOTION:, AO2:STATE, AMSA:AOSta, 1, 0xFFFF, 0, 65535, 0, \u0026#34;1 second\u0026#34;} } file \u0026#34;$(TOP)/db/bi_bit.template\u0026#34; { pattern {P, R, PORT, OFFSET, ZNAM, ONAM, ZSV, OSV, SCAN} {AMSAMOTION: DI1, AMSA:DI, 0, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI2, AMSA:DI, 1, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI3, AMSA:DI, 2, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI4, AMSA:DI, 3, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI5, AMSA:DI, 4, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI6, AMSA:DI, 5, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI7, AMSA:DI, 6, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI8, AMSA:DI, 7, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} } file \u0026#34;$(TOP)/db/bi_bit.template\u0026#34; { pattern {P, R, PORT, OFFSET, ZNAM, ONAM, ZSV, OSV, SCAN} {AMSAMOTION: DO1:STATE, AMSA:DOSta, 0, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO2:STATE, AMSA:DOSta, 1, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO3:STATE, AMSA:DOSta, 2, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO4:STATE, AMSA:DOSta, 3, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO5:STATE, AMSA:DOSta, 4, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO6:STATE, AMSA:DOSta, 5, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO7:STATE, AMSA:DOSta, 6, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO8:STATE, AMSA:DOSta, 7, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} } file \u0026#34;$(TOP)/db/bo_bit.template\u0026#34; { pattern {P, R, PORT, OFFSET, ZNAM, ONAM} {AMSAMOTION: DO1, AMSA:DO1, 0, OFF, ON} {AMSAMOTION: DO2, AMSA:DO2, 0, OFF, ON} {AMSAMOTION: DO3, AMSA:DO3, 0, OFF, ON} {AMSAMOTION: DO4, AMSA:DO4, 0, OFF, ON} {AMSAMOTION: DO5, AMSA:DO5, 0, OFF, ON} {AMSAMOTION: DO6, AMSA:DO6, 0, OFF, ON} {AMSAMOTION: DO7, AMSA:DO7, 0, OFF, ON} {AMSAMOTION: DO8, AMSA:DO8, 0, OFF, ON} } 最后运行程序,在终端执行:\n1 /path/to/modbus/bin/\u0026lt;EPICS_HOST_ARCH\u0026gt;/modbusApp AMSAMOTION.cmd 或者在.cmd文件第一行添加下面一行:\n1 #!../bin/\u0026lt;EPICS_HOST_ARCH\u0026gt;/modbusApp 然后直接执行.cmd脚本。\n1 2 chmod +x AMSAMOTION.cmd ./AMSAMOTION.cmd 参考\nOverview of Modbus Creating a modbus port driver EPICS Process Database Concepts ","permalink":"https://kira-96.github.io/posts/build-epics-module-modbus/","summary":"前言 MODBUS是一种应用层消息传递协议,通常用于 I/O 系统通信和可编程逻辑控制器(PLC)通信。\n链接类型 描述 MODBUS TCP TCP/IP 使用502端口 MODBUS RTU RTU通常通过串行通信链路运行,即RS-232、 RS-422 或 RS-485。RTU 使用额外的 CRC 进行数据包检查。协议直接将每个字节作为 8 个数据位传输,因此使用“二进制” 而不是 ASCII 编码。使用串行链路开始和结束时,消息帧是按时间而不是按特定字符检测的。 MODBUS ASCII 串行协议,通常在串行通信链路上运行,即 RS-232、RS-422 或 RS-485。串行 ASCII 使用额外的 LRC 数据包检查。该协议将每个字节编码为 2 个 ASCII 字符。消息帧的开始和结束由特定字符检测 (“:” 开始消息,CR/LF 结束消息)。该协议效率低于 Modbus RTU,但在某些环境中可能更可靠。 Modbus 提供对以下 4 种类型的数据的访问:\n主表 对象类型 访问 说明 离散输入 1bit 只读 这种类型的数据可以由 I/O 系统提供。 线圈 1bit 读写 此类数据可由应用程序更改。 输入寄存器 16位字(2字节) 只读 这种类型的数据可以由 I/O 系统提供。 保持寄存器 16位字(2字节) 读写 此类数据可由应用程序更改。 Modbus 通信由从 Modbus 客户端发送到 Modbus 服务器的请求消息组成。服务器使用响应消息进行回复。Modbus 请求消息包含:","title":"EPICS的MODBUS模块的编译和使用"},{"content":"关于 ACAI ACAI 是一个C++封装的Channel Access协议应用开发接口(API),提供异步通道访问接口。\nACAI Channel Access Interface\nEPICS Qt依赖ACAI提供的Channel Access接口。\n前置步骤 这篇笔记是交叉编译EPICS和IOC内容的补充。\n在进行下面步骤前,请完成配置交叉编译环境和编译 EPICS Base。\n这里依旧以龙芯架构为例。\nEPICS base 编译完成后,可以看到bin目录下有linux-loong64、linux-x86_64两个目录,linux-x86_64目录下比linux-loong64目录多出了许多perl脚本,我们需要把这些脚本复制到龙架构的目录下,下面编译需要用到。\n1 $ cp ./bin/linux-x86_64/*.pl ./bin/linux-loong64/ 编译 在EPICS-Qt安装中已经介绍过编译ACAI。这次是使用交叉编译方式,步骤略有不同。\n1 2 3 4 5 6 7 8 9 10 11 12 cd ~/loongson/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改交叉编译的目标架构,和EPICS base中保持一致 EPICS_HOST_ARCH=linux-loong64 # 修改EPICS_BASE路径,例: EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ # 等待编译完成 编译完成后可以在lib/linux-loong64/目录下找到libacai.so。\n","permalink":"https://kira-96.github.io/notes/cross-compiling-acai/","summary":"关于 ACAI ACAI 是一个C++封装的Channel Access协议应用开发接口(API),提供异步通道访问接口。\nACAI Channel Access Interface\nEPICS Qt依赖ACAI提供的Channel Access接口。\n前置步骤 这篇笔记是交叉编译EPICS和IOC内容的补充。\n在进行下面步骤前,请完成配置交叉编译环境和编译 EPICS Base。\n这里依旧以龙芯架构为例。\nEPICS base 编译完成后,可以看到bin目录下有linux-loong64、linux-x86_64两个目录,linux-x86_64目录下比linux-loong64目录多出了许多perl脚本,我们需要把这些脚本复制到龙架构的目录下,下面编译需要用到。\n1 $ cp ./bin/linux-x86_64/*.pl ./bin/linux-loong64/ 编译 在EPICS-Qt安装中已经介绍过编译ACAI。这次是使用交叉编译方式,步骤略有不同。\n1 2 3 4 5 6 7 8 9 10 11 12 cd ~/loongson/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改交叉编译的目标架构,和EPICS base中保持一致 EPICS_HOST_ARCH=linux-loong64 # 修改EPICS_BASE路径,例: EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ # 等待编译完成 编译完成后可以在lib/linux-loong64/目录下找到libacai.so。","title":"交叉编译 ACAI"},{"content":"前言 之前已经讲过在龙芯3A5000(loongarch64)上编译运行EPICS,不过这种情况只适用于有完整开发环境的情况下进行编译。一些时候,我们只有编译器,而缺少make,perl等工具,比如一些开发板厂商提供的开发套件。这种情况下,就需要通过交叉编译(cross-compiling)的方式来编译EPICS。\n这里以龙芯金龙2K500先锋开发板为例,我们使用Ubuntu-20.04作为构建系统,详细讲解如何构建出可以在开发板上运行的EPICS工具包,并部署在开发板上。\n由于开发板上没有开发环境,即使编译出目标平台的EPICS Base,我们依然不能直接在开发板上创建和编译IOC。所以,我们还是使用Ubuntu-20.04作为构建系统,创建并编译IOC,最后在开发板上运行。\n配置交叉编译环境 关于这一节,之前的文章已经详细讲过,参考配置交叉编译环境。\n如果你使用的是其他开发套件,请按照开发手册安装配置好环境。\n编译 EPICS Base 首先,下载、解压Base,参考以前的文章。\n在龙芯3A5000(loongarch64)上编译运行EPICS中我已经详细讲解了如何在龙架构上编译EPICS,这次,需要在原来对源码修改的基础上,再增加对交叉编译的支持。\n添加configure/os/CONFIG.linux-x86_64.linux-loong64\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # CONFIG.linux-x86_64.linux-loong64 # # Definitions for linux-x86_64 host - linux-loong64 target builds # Sites may override these in CONFIG_SITE.linux-x86_64.linux-loong64 #------------------------------------------------------- VALID_BUILDS = Ioc Command GNU_TARGET = loongarch64-linux-gnu # prefix of compiler tools CMPLR_SUFFIX = CMPLR_PREFIX = $(addsuffix -,$(GNU_TARGET)) # Provide a link-time path for readline if needed OP_SYS_INCLUDES += $(READLINE_DIR:%=-I%/include) READLINE_LDFLAGS = $(READLINE_DIR:%=-L%/lib) RUNTIME_LDFLAGS_READLINE_YES_NO = $(READLINE_DIR:%=-Wl,-rpath,%/lib) RUNTIME_LDFLAGS += \\ $(RUNTIME_LDFLAGS_READLINE_$(LINKER_USE_RPATH)_$(STATIC_BUILD)) SHRLIBDIR_LDFLAGS += $(READLINE_LDFLAGS) PRODDIR_LDFLAGS += $(READLINE_LDFLAGS) # Library flags STATIC_LDFLAGS_YES= -Wl,-Bstatic STATIC_LDFLAGS_NO= STATIC_LDLIBS_YES= -Wl,-Bdynamic STATIC_LDLIBS_NO= 添加configure/os/CONFIG_SITE.linux-x86_64.linux-loong64\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # CONFIG_SITE.linux-x86_64.linux-loong64 # # Site specific definitions for linux-x86_64 host - linux-loong64 target builds #------------------------------------------------------- # Set GNU crosscompiler target name GNU_TARGET = loongarch64-linux-gnu # Set GNU tools install path # Examples is the installation at the APS: GNU_DIR = /opt/loongson-gnu-toolchain-8.3-x86_64-loongarch64-linux-gnu-rc1.2 # If cross-building shared libraries and the paths on the target machine are # different than on the build host, you should uncomment the lines below to # disable embedding compile-time library paths into the generated files. # You will need to provide another way for programs to find their shared # libraries at runtime, such as by setting LD_LIBRARY_PATH or (better) using # mechanisms related to /etc/ld.so.conf #SHRLIBDIR_RPATH_LDFLAGS_YES_NO = #PRODDIR_RPATH_LDFLAGS_YES_NO = # However it is usually simpler to set STATIC_BUILD=YES here and not # try to use shared libraries at all when cross-building, like this: #STATIC_BUILD=YES #SHARED_LIBRARIES=NO # To use libreadline, point this to its install prefix #READLINE_DIR = $(GNU_DIR) #READLINE_DIR = /tools/cross/linux-x86.linux-loong64/readline # See CONFIG_SITE.Common.linux-loong64 for other COMMANDLINE_LIBRARY values #COMMANDLINE_LIBRARY = READLINE GNU_DIR需要改为安装交叉编译工具链的路径。\nSTATIC_BUILD和SHARED_LIBRARIES可以设置是否为非静态编译,即是否生成动态库.so,两个设置项必须一起修改,这里的设置会覆盖掉configure/CONFIG_SITE中的设置。由于动态库在编译一些其他工具时还会用到,所以这里我选择生成动态库。\n1 2 # STATIC_BUILD=YES # SHARED_LIBRARIES=NO 然后,需要设置交叉编译的目标架构。\n新增configure/CONFIG_SITE.local,或者直接修改configure/CONFIG_SITE(不推荐)。\n1 CROSS_COMPILER_TARGET_ARCHS=linux-loong64 最后,进行编译即可。(确保构建系统上有make和perl,应该都有吧。)\n1 2 3 # 到源码目录下 $ cd ~/loongson/base-7.0.8 $ make -j8 等待编译完成即可。\n编译完成后,可以看到bin和lib目录下,都有linux-loong64、linux-x86_64两个目录,其中linux-loong64目录下就是我们要在开发板上运行的EPICS工具包了。linux-x86_64目录下的则是编译生成的本机的EPICS工具包,待会儿我们还会用到。\n由于开发板的存储空间很小,只有几百兆,所以,我们只能单独将龙架构的内容下载到板子上。\n目录如下:\nbase ├─ bin │ └─ linux-loong64 ├─ db ├─ dbd ├─ include (可选) └─ lib └─ linux-loong64 将目录中的内容全部打包下载到开发板即可。\n1 2 3 4 5 # 在开发板上运行 $ cd base/bin/linux-loong64 # 运行软IOC测试 $ ./softIoc epics\u0026gt; ※ 如果使用非静态编译(生成动态库)的方式编译,运行时可能会提示找不到动态库。需要将动态库添加到系统的动态库路径。\n如下(根据实际情况修改路径):\n1 $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/base/lib/linux-loong64 ※ include 目录不是EPICS运行必需的,但如果要基于EPICS进行开发,可能需要用到EPICS的头文件。\n例如:交叉编译 ACAI\n编译 IOC 前面已经讲了,我们需要在构建主机上交叉编译IOC,具体步骤和直接创建编译IOC基本一样。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ cd ~ # 创建目录 $ mkdir test $ cd test/ # 替换成自己编译EPICS的目录,注意是使用linux-x86_64目录下的脚本 $ ~/loongson/base-7.0.8/bin/linux-x86_64/makeBaseApp.pl -t example test $ ~/loongson/base-7.0.8/bin/linux-x86_64/makeBaseApp.pl -i -t example test The following target architectures are available in base: linux-loong64 linux-x86_64 What architecture do you want to use? linux-loong64 The following applications are available: test What application should the IOC(s) boot? The default uses the IOC\u0026#39;s name, even if not listed above. Application name? test 这里唯一多的步骤就是选择目标架构,输入linux-loong64即可。\n然后修改编译设置,这里就不用生成动态库了,直接使用静态编译。\n添加configure/CONFIG_SITE.local,启用静态编译。\n1 2 3 4 5 6 7 8 9 10 11 # Build shared libraries (DLLs on Windows). # Must be either YES or NO. Definitions in the target-specific # os/CONFIG.Common.\u0026lt;target\u0026gt; and os/CONFIG_SITE.Common.\u0026lt;target\u0026gt; files may # override this setting. On Windows only these combinations are valid: # SHARED_LIBRARIES = YES and STATIC_BUILD = NO # SHARED_LIBRARIES = NO and STATIC_BUILD = YES SHARED_LIBRARIES=NO # Build client objects statically. # Must be either YES or NO. STATIC_BUILD=YES 最后编译。\n1 2 $ cd ~/test $ make 等待编译完成即可。然后将生成的可执行文件下载到开发板,这里依旧是只下载运行IOC所必需的内容。\n目录如下:\ntest ├─ bin │ └─ linux-loong64 ├─ db ├─ dbd └─ iocBoot └─ ioctest 在把IOC复制到开发板上之后,还需要根据实际情况修改IOC运行的环境变量。\n修改iocBoot/ioctest/envPaths\n1 2 3 4 epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;ioctest\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;/root/test\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/root/base\u0026#34;) epicsEnvSet(\u0026#34;EPICS_HOST_ARCH\u0026#34;,\u0026#34;linux-loong64\u0026#34;) 最后在开发板上运行IOC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # 在开发板上运行 $ cd ~/test/iocBoot/ioctest # 添加可执行权限 $ chmod +x st.cmd $ ./st.cmd #!../../bin/linux-loong64/test \u0026lt; envPaths epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;ioctest\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;/root/test\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/root/base\u0026#34;) epicsEnvSet(\u0026#34;EPICS_HOST_ARCH\u0026#34;,\u0026#34;linux-loong64\u0026#34;) cd \u0026#34;/root/test\u0026#34; ## Register all support components dbLoadDatabase \u0026#34;dbd/test.dbd\u0026#34; test_registerRecordDeviceDriver pdbbase Warning: IOC is booting with TOP = \u0026#34;/root/test\u0026#34; but was built with TOP = \u0026#34;/home/ubuntu/test\u0026#34; ## Load record instances dbLoadTemplate \u0026#34;db/user.substitutions\u0026#34; dbLoadRecords \u0026#34;db/testVersion.db\u0026#34;, \u0026#34;user=lsgd\u0026#34; dbLoadRecords \u0026#34;db/dbSubExample.db\u0026#34;, \u0026#34;user=lsgd\u0026#34; cd \u0026#34;/root/test/iocBoot/ioctest\u0026#34; iocInit Starting iocInit ############################################################################ ## EPICS R7.0.8 ## Rev. 2023-12-30T14:06+0800 ## Rev. Date build date/time: ############################################################################ iocRun: All initialization complete ## Start any sequence programs #seq sncExample, \u0026#34;user=lsgd\u0026#34; epics\u0026gt; 这里可以看到启动时会有一个Warning,警告IOC运行时和编译时的TOP路径不一致,但实际上并不影响IOC运行,忽略即可。\n此时可以使用dbl、dbpr等命令查看变量。或者在其他终端运行camonitor程序。\n","permalink":"https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/","summary":"前言 之前已经讲过在龙芯3A5000(loongarch64)上编译运行EPICS,不过这种情况只适用于有完整开发环境的情况下进行编译。一些时候,我们只有编译器,而缺少make,perl等工具,比如一些开发板厂商提供的开发套件。这种情况下,就需要通过交叉编译(cross-compiling)的方式来编译EPICS。\n这里以龙芯金龙2K500先锋开发板为例,我们使用Ubuntu-20.04作为构建系统,详细讲解如何构建出可以在开发板上运行的EPICS工具包,并部署在开发板上。\n由于开发板上没有开发环境,即使编译出目标平台的EPICS Base,我们依然不能直接在开发板上创建和编译IOC。所以,我们还是使用Ubuntu-20.04作为构建系统,创建并编译IOC,最后在开发板上运行。\n配置交叉编译环境 关于这一节,之前的文章已经详细讲过,参考配置交叉编译环境。\n如果你使用的是其他开发套件,请按照开发手册安装配置好环境。\n编译 EPICS Base 首先,下载、解压Base,参考以前的文章。\n在龙芯3A5000(loongarch64)上编译运行EPICS中我已经详细讲解了如何在龙架构上编译EPICS,这次,需要在原来对源码修改的基础上,再增加对交叉编译的支持。\n添加configure/os/CONFIG.linux-x86_64.linux-loong64\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # CONFIG.linux-x86_64.linux-loong64 # # Definitions for linux-x86_64 host - linux-loong64 target builds # Sites may override these in CONFIG_SITE.linux-x86_64.linux-loong64 #------------------------------------------------------- VALID_BUILDS = Ioc Command GNU_TARGET = loongarch64-linux-gnu # prefix of compiler tools CMPLR_SUFFIX = CMPLR_PREFIX = $(addsuffix -,$(GNU_TARGET)) # Provide a link-time path for readline if needed OP_SYS_INCLUDES += $(READLINE_DIR:%=-I%/include) READLINE_LDFLAGS = $(READLINE_DIR:%=-L%/lib) RUNTIME_LDFLAGS_READLINE_YES_NO = $(READLINE_DIR:%=-Wl,-rpath,%/lib) RUNTIME_LDFLAGS += \\ $(RUNTIME_LDFLAGS_READLINE_$(LINKER_USE_RPATH)_$(STATIC_BUILD)) SHRLIBDIR_LDFLAGS += $(READLINE_LDFLAGS) PRODDIR_LDFLAGS += $(READLINE_LDFLAGS) # Library flags STATIC_LDFLAGS_YES= -Wl,-Bstatic STATIC_LDFLAGS_NO= STATIC_LDLIBS_YES= -Wl,-Bdynamic STATIC_LDLIBS_NO= 添加configure/os/CONFIG_SITE.","title":"交叉编译EPICS和IOC"},{"content":"需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。\n这里使用MinGW环境编译EPICS,不使用MSVC编译器。\n安装 Strawberry Perl 这里选择 Strawberry Perl 5.38.2.1。\n直接安装即可,需要注意的是,安装路径不能有空格和中文,最好放在盘符的根目录下。\n例:D:\\Strawberry\n安装完成后检查系统环境变量,查看系统Path环境变量是否有Strawberry Perl的路径。没有则手动添加,以安装在D盘为例。\n1 2 3 D:\\Strawberry\\c\\bin D:\\Strawberry\\perl\\site\\bin D:\\Strawberry\\perl\\bin 其中D:\\Strawberry\\c\\bin就是MinGW环境的路径。\n查看Perl版本,检查一下是不是装好了。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026gt; perl -v This is perl 5, version 38, subversion 2 (v5.38.2) built for MSWin32-x64-multi-thread Copyright 1987-2023, Larry Wall Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5 source kit. Complete documentation for Perl, including FAQ lists, should be found on this system using \u0026#34;man perl\u0026#34; or \u0026#34;perldoc perl\u0026#34;. If you have access to the Internet, point your browser at https://www.perl.org/, the Perl Home Page. 注意:perl 5.32.1.1 之后的版本可能会提示\nLocale \u0026lsquo;Chinese (Simplified)_China.936\u0026rsquo; is unsupported, and may crash the interpreter.\n需要设置环境变量LC_ALL=C。\n编译安装EPICS Base 修改base源码目录下的startup/windows.bat文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rem The location of Strawberry Perl (pathname). If empty, Strawberry Perl rem is assumed to already be in PATH and will not be added. If nonempty, rem Strawberry Perl will be added to PATH. rem 设置Strawberry Perl安装路径 set _strawberry_perl_home=D:\\Strawberry rem The EPICS host architecture specification for EPICS_HOST_ARCH rem (\u0026lt;os\u0026gt;-\u0026lt;arch\u0026gt;[-\u0026lt;toolset\u0026gt;] as defined in configure/CONFIG_SITE). rem 设置编译主机架构,这里使用mingw set _epics_host_arch=windows-x64-mingw rem The install location of EPICS Base (pathname). If nonempty and rem _auto_path_append is yes, it will be used to add the host architecture rem bin directory to PATH. set _epics_base=C:\\EPICS\\base-7.0.8 rem Set the environment for Microsoft Visual Studio rem 使用 rem 注释掉下面一行 rem call \u0026#34;%_visual_studio_home%\\VC\\Auxiliary\\Build\\vcvarsall.bat\u0026#34; x64 注意,编译需要使用命令行工具cmd,不能用powershell。\n注意,这里使用的是 perl 自带的编译工具,如果要使用其他版本的MinGW,则需要自行配置环境变量。\n比如这里使用 MinGW 12.2.0,则需要设置环境变量。\n1 set Path=C:\\WINDOWS;C:\\WINDOWS\\System32;C:\\WINDOWS\\System32\\Wbem;D:\\Strawberry\\perl\\bin;D:\\Tools\\mingw1220_64\\bin;D:\\Tools\\mingw1220_64\\libexec\\gcc\\x86_64-w64-mingw32\\12.2.0 1 2 3 4 5 6 7 cd base-7.0.8 .\\startup\\windows.bat # 根据使用的编译工具执行命令 # perl 环境的make命名为gmake gmake -j16 # mingw32-make -j16 等待编译完成。\n编译完成后的工具在bin\\windows-x64-mingw目录下。\n测试使用:\n1 2 3 cd bin\\windows-x64-mingw softIoc.exe epics\u0026gt; ","permalink":"https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/","summary":"需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。\n这里使用MinGW环境编译EPICS,不使用MSVC编译器。\n安装 Strawberry Perl 这里选择 Strawberry Perl 5.38.2.1。\n直接安装即可,需要注意的是,安装路径不能有空格和中文,最好放在盘符的根目录下。\n例:D:\\Strawberry\n安装完成后检查系统环境变量,查看系统Path环境变量是否有Strawberry Perl的路径。没有则手动添加,以安装在D盘为例。\n1 2 3 D:\\Strawberry\\c\\bin D:\\Strawberry\\perl\\site\\bin D:\\Strawberry\\perl\\bin 其中D:\\Strawberry\\c\\bin就是MinGW环境的路径。\n查看Perl版本,检查一下是不是装好了。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026gt; perl -v This is perl 5, version 38, subversion 2 (v5.38.2) built for MSWin32-x64-multi-thread Copyright 1987-2023, Larry Wall Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5 source kit.","title":"Windows上使用MinGW编译安装EPICS"},{"content":"前言 Linux 内核提供了丰富的设备驱动接口,其中GPIO和LED属于是最基本的一类了。之前就已经讲过用户空间下的GPIO读写操作,LED设备的操作也基本相同。其实完全可以使用GPIO驱动去控制LED,但LED的驱动针对LED提供了更多的功能,一起来看一下吧。\n配置设备树 设备树中的LED节点配置,例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* kernel/arch/arm/boot/dts/imx6ul-14x14-evk-c-emmc.dts */ leds { compatible = \u0026#34;gpio-leds\u0026#34;; pinctrl-names = \u0026#34;default\u0026#34;; status = \u0026#34;okay\u0026#34;; led1{ label = \u0026#34;led1\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led2{ label = \u0026#34;led2\u0026#34;; gpios = \u0026lt;\u0026amp;gpio1 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led3{ label = \u0026#34;heartbeat\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 5 GPIO_ACTIVE_LOW\u0026gt;; linux,default-trigger = \u0026#34;heartbeat\u0026#34;; }; }; 节点属性说明:\nlabel:LED设备的名字,名字必须是唯一的。如果没有设置,则会使用节点的名字。\ngpios:GPIO的编号,以及高低电平设置,GPIO_ACTIVE_LOW低电平点亮,GPIO_ACTIVE_HIGH高电平点亮。\ndefault-state:默认状态,on/off。\nlinux,default-trigger:设置LED的触发器。backlight-背光灯,heartbeat-心跳灯,timer-定时,default-on-默认开状态,disk-activity-硬盘状态,gpio,none。\n用户空间下的LED操作 注意:以下操作都需要root权限!\n用户空间下的GPIO文件系统接口在/sys/class/leds/{label}目录下。\nLED节点有以下属性可以配置:\ntrigger\n设置LED的触发器。\n1 echo heartbeat \u0026gt; /sys/class/leds/led1/trigger brightness\n设置LED的开关或者亮度。\n1 2 3 4 # 关闭LED echo 0 \u0026gt; /sys/class/leds/led1/brightness # 打开LED echo 1 \u0026gt; /sys/class/leds/led1/brightness 对于写入的任何非0值,都会是打开LED的操作,可写入值范围为0~255。\n而对于使用PWM控制的LED灯,brightness才可以控制灯的亮度。参考呼吸灯的实现。\n程序控制LED灯 示例读取:\n1 2 3 4 5 6 7 8 9 10 11 quint8 readLed(const QString \u0026amp;led) { QFile file(QString(\u0026#34;/sys/class/leds/%1/brightness\u0026#34;).arg(led)); if (!file.open(QIODevice::ReadOnly)) return 0; QByteArray ba = file.readAll(); file.close(); return (quint8)QString(ba).toInt(); } 示例写入:\n1 2 3 4 5 6 7 8 9 bool writeLed(const QString \u0026amp;led, quint8 value) { QFile file(QString(\u0026#34;/sys/class/leds/%1/brightness\u0026#34;).arg(led)); if (!file.open(QIODevice::WriteOnly)) return false; QByteArray ba = QString::number(value).toLatin1(); return file.write(ba) == ba.size(); } 参考\nDocumentation/devicetree/bindings/leds/common.txt (v4.13) LED子系统详解 ","permalink":"https://kira-96.github.io/posts/linux-led%E5%AD%90%E7%B3%BB%E7%BB%9F/","summary":"前言 Linux 内核提供了丰富的设备驱动接口,其中GPIO和LED属于是最基本的一类了。之前就已经讲过用户空间下的GPIO读写操作,LED设备的操作也基本相同。其实完全可以使用GPIO驱动去控制LED,但LED的驱动针对LED提供了更多的功能,一起来看一下吧。\n配置设备树 设备树中的LED节点配置,例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* kernel/arch/arm/boot/dts/imx6ul-14x14-evk-c-emmc.dts */ leds { compatible = \u0026#34;gpio-leds\u0026#34;; pinctrl-names = \u0026#34;default\u0026#34;; status = \u0026#34;okay\u0026#34;; led1{ label = \u0026#34;led1\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led2{ label = \u0026#34;led2\u0026#34;; gpios = \u0026lt;\u0026amp;gpio1 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led3{ label = \u0026#34;heartbeat\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 5 GPIO_ACTIVE_LOW\u0026gt;; linux,default-trigger = \u0026#34;heartbeat\u0026#34;; }; }; 节点属性说明:","title":"Linux LED子系统"},{"content":"前言 本文主要记录了EPICS Qt在Linux上的安装步骤。这里以loongnix操作系统为例,Ubuntu系统上编译安装步骤类似。\nEPICS Qt是一个基于Qt的分层框架,使用Channel Access (CA) and PV Access(PVA)访问EPICS数据。它是为快速开发控制系统图形界面而设计的,最初是在澳大利亚同步加速器开发的。\n安装EPICS 这里不再写具体步骤了,总之就是非常简单,下载、解压、编译即可。具体步骤可以参考以前的文章。\n安装Qt 直接使用终端安装Qt\n1 2 3 4 5 sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator sudo apt install qtdeclarative5-dev qttools5-dev # 安装Qt Svg库,编译QWT时需要用到 sudo apt install libqt5svg5-dev 安装QWT Qt EPICS推荐使用Qwt 6.1.4,如果在Ubuntu 20.04上直接通过终端安装也是这个版本。我使用Qwt 6.2.0编译,也是没有问题的,这里以Qwt 6.2.0为例。\n先下载Qwt的源码 下载Qwt-6.2.0。 下载完成后解压\n1 2 3 4 # 解压tar.bz2 tar -jxvf qwt-6.2.0.tar.bz2 # 解压zip unzip qwt-6.2.0.zip 解压完成后编译Qwt,使用QtCreator或者在终端使用qmake都可以。\n然后手动将编译生成的文件复制到以下位置,例:\n1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin.so /usr/lib/loongarch64-linux-gnu/qt5/plugins/designer/ # 复制qwt头文件 sudo mkdir /usr/include/qwt sudo cp qwt-6.2.0/src/*.h /usr/include/qwt 安装ACAI ACAI Channel Access Interface\nEPICS Qt依赖ACAI提供的Channel Access接口。\n1 2 3 4 5 6 7 8 cd /usr/local/epics/modules/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改EPICS_BASE路径,例: # EPICS_BASE=/usr/local/epics/base-7.0.7 make -j8 # 等待编译完成 安装google protobuf 如果需要EPICS Qt支持EPICS Archiver Appliance,需要安装google protobuf。\n1 sudo apt install protobuf-compiler libprotobuf-dev EPICS Qt 首先克隆EPICS Qt的两个代码仓库。\n1 2 3 4 # framework and support libraries git clone https://github.com/qtepics/qeframework.git # QEGui display manager git clone https://github.com/qtepics/qegui.git 这里我将代码都放在~/QtEpics目录。\n在开始编译前,需要先配置一些环境变量(根据自己的实际情况设置)。具体可以参考 EPICS Qt Environment Variables\n1 2 3 4 5 6 7 8 9 10 11 12 export EPICS_HOST_ARCH=linux-loong64 export EPICS_BASE=/usr/local/epics/base-7.0.7 export ACAI=/usr/local/epics/modules/acai export QWT_INCLUDE_PATH=/usr/include/qwt export QWT_ROOT=/usr/lib/loongarch64-linux-gnu export QE_FRAMEWORK=\u0026#34;$HOME/QtEpics/qeframework\u0026#34; # 支持PV Access export QE_PVACCESS_SUPPORT=YES # 支持Archiver Appliance export QE_ARCHAPPL_SUPPORT=YES export PROTOBUF_INCLUDE_PATH=/usr/include/google/protobuf export PROTOBUF_LIB_DIR=/usr/lib/loongarch64-linux-gnu 如果环境变量设置了支持Archiver Appliance,需要先编译archapplDataSup。\n1 2 cd ~/QtEpics/qeframework/archapplDataSup/ make 编译完成后,可以看到~/QtEpics/qeframework/lib/linux-loong64目录下有libarchapplData.a、libarchapplData.so两个文件。\n然后依次编译 qeframework qeplugin qegui。EPICS Qt文档说明需要修改configure/RELEASE文件,但我这里修改后似乎没有生效,可能是使用了Qt Creator的原因,只能通过上面的环境变量设置。\n注意:这里设置完环境变量,需要直接通过终端打开Qt Creator。\n编译qeframework $HOME/QtEpics/qeframework/qeframeworkSup/project/framework.pro 编译qeplugin $HOME/QtEpics/qeframework/qepluginApp/project/qeplugin.pro 编译qegui $HOME/QtEpics/qegui/qeguiApp/project/QEGuiApp.pro 最后将编译生成的文件复制到以下位置,例:\n1 2 3 sudo cp ~/QtEpics/qeframework/lib/linux-loong64/libarchapplData.so /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loong64/libQEFramework.so /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loong64/designer/libQEPlugin.so /usr/lib/loongarch64-linux-gnu/qt5/plugins/designer/ 运行QEGuiApp\n1 2 cd ~/epics/qtepics/qegui/bin/linux-loong64 ./qegui 运行测试 运行时环境变量设置,例:\n1 2 3 export QE_ARCHIVE_TYPE=ARCHAPPL export QE_ARCHIVE_LIST=\u0026#34;http://192.168.1.2:17665/mgmt/bpl\u0026#34; export EPICS_CA_ADDR_LIST=\u0026#34;192.168.1.2:5732 192.168.1.3:6666\u0026#34; 问题汇总 编译过程中可能会遇到一些问题\n找不到Qwt的头文件 解决办法: 修改qeframework/qeframeworkSup/project/common/common.pri\n1 2 INCLUDEPATH += $$PWD +INCLUDEPATH += $$(QWT_INCLUDE_PATH) 找不到QEFramework的头文件 解决办法: 修改对应项目的项目文件\n1 +INCLUDEPATH += $$(QE_FRAMEWORK)/include Windows上编译踩坑\n我这里使用的是MinGW编译器,仅供参考。Windows编译安装EPICS,可以参考我的笔记。\nACAI编译报错,这个是由于平台函数的差异导致的报错。\n我已经提交了修复补丁,参考 fix build error on windows。\n编译 google protobuf\n网上有很多关于windows编译google protobuf的文章。请自行搜索解决。\n编译 QEFramework\n这里需要修改configure/RELEASE.local,示例:\n1 2 3 4 5 6 7 8 9 10 EPICS_HOST_ARCH=windows-x64-mingw EPICS_BASE=C:\\Users\\YourName\\.epics\\base-7.0.8 ACAI=D:\\source\\acai QE_FRAMEWORK=D:\\source\\qeframework QE_PVACCESS_SUPPORT=YES QE_ARCHAPPL_SUPPORT=YES PROTOBUF_INCLUDE_PATH=D:\\source\\protobuf-3.21.12\\src PROTOBUF_LIB_DIR=D:\\source\\protobuf-3.21.12\\build QWT_INCLUDE_PATH=D:\\source\\qwt-6.2.0\\src QWT_ROOT=D:\\source\\build-qwt-Desktop_Qt_5_15_2_MinGW_64_bit-Release Windows上运行程序\n将编译过程中生成的DLL全部复制到qegui程序目录下,大致汇总一下有:\nqwt.dll,libprotobuf.dll,acai.dll,QEFramework.dll,archapplData.dll(这个是在Windows上才会生成,在qeframework\\archapplDataSup\\src\\O.windows-x64-mingw\\目录下可以找到)\ndesigner/QEPlugin.dll,这个也很重要,不然qegui加载.ui文件会显示空白。\nEPICS相关的DLL:\nca.dll,Com.dll,nt.dll,pvAccess.dll,pvData.dll\n以及Qt相关的DLL。\n参考链接 EPICS Qt at GitHub EPICS Qt Getting Started Archiver Appliance Support for EPICS Qt ","permalink":"https://kira-96.github.io/posts/epics-qt%E5%AE%89%E8%A3%85/","summary":"前言 本文主要记录了EPICS Qt在Linux上的安装步骤。这里以loongnix操作系统为例,Ubuntu系统上编译安装步骤类似。\nEPICS Qt是一个基于Qt的分层框架,使用Channel Access (CA) and PV Access(PVA)访问EPICS数据。它是为快速开发控制系统图形界面而设计的,最初是在澳大利亚同步加速器开发的。\n安装EPICS 这里不再写具体步骤了,总之就是非常简单,下载、解压、编译即可。具体步骤可以参考以前的文章。\n安装Qt 直接使用终端安装Qt\n1 2 3 4 5 sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator sudo apt install qtdeclarative5-dev qttools5-dev # 安装Qt Svg库,编译QWT时需要用到 sudo apt install libqt5svg5-dev 安装QWT Qt EPICS推荐使用Qwt 6.1.4,如果在Ubuntu 20.04上直接通过终端安装也是这个版本。我使用Qwt 6.2.0编译,也是没有问题的,这里以Qwt 6.2.0为例。\n先下载Qwt的源码 下载Qwt-6.2.0。 下载完成后解压\n1 2 3 4 # 解压tar.bz2 tar -jxvf qwt-6.2.0.tar.bz2 # 解压zip unzip qwt-6.2.0.zip 解压完成后编译Qwt,使用QtCreator或者在终端使用qmake都可以。\n然后手动将编译生成的文件复制到以下位置,例:\n1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin.","title":"EPICS Qt安装"},{"content":"前言 本来这篇文章应该在上周就写完的,不过突然被安排出差,一直忙到了现在,终于可以静下心来做些其它事情。\n之前和龙芯3A5000主机一起送过来的还有一块龙芯2K500的迷你开发板,整个板子不到巴掌大小。之前只是简单做了上电启动,这次拿到了比较完整的开发资料,可以尝试为开发板编写一些程序了。\n由于暂时没有屏幕,只能先试着做一些其它的事情,如通信和IO控制,其中最简单,最基础的就是LED灯的控制。然后我就发现,这个板子居然有一颗可以调节亮度的LED灯!没错,之前做的LED控制都只能进行开关操作,而可以调节亮度,意味着可以做出更多的显示效果,这次我就做了一个呼吸灯的效果。\n然后,我也简单了解了一下这种亮度调节的原理,实际上就是通过调节PWM输出的占空比,改变一个周期内输出的高低电平所占的比例,实现控制LED灯亮度的效果。由于引脚输出的电压是固定的,所以不能通过改变电平来控制亮度,而改变高低电平的占空比则是另一种思路,嵌入式设备的屏幕背光亮度调节也是基于同样的原理。\n开发板上电启动、连接串口终端 由于暂时没有屏幕,想要和开发板进行交互就只能通过终端的方式,通常开发板都会有调试串口,我们先通过串口终端登录设备,配置好网口IP地址后,再通过网络连接登录设备。\n串口的连接方式如下图:\n将绿、白、黑三色线以图中方式接好(红线不用接),USB端插入到电脑,应该不需要装驱动,电脑可以直接识别出串口设备。\n打开串口终端工具,比如Windows MobaXterm,linux minicom等,我比较喜欢用putty。\n配置好端口,设置\n波特率:115200\n数据位:8位\n停止位:1位\n校验:无\n硬件流控:无\n然后给开发板接通电源,就可以看到调试输出信息了。\n查看系统信息,可以看到运行的是安装了PREEMPT_RT补丁的实时操作系统。\n1 2 3 [root@LS-GD ~]# uname -a Linux LS-GD 5.10.0.lsgd-g434b00a6badf #1 PREEMPT Wed Sep 14 12:57:58 CST 2022 loongarch64 GNU/Linux [root@LS-GD ~]# 配置交叉编译环境 2K500开发板是loongarch64架构的嵌入式板卡。下载好对应的交叉编译工具链后,解压到系统/opt/目录下。按手册来就好~\n1 $ sudo tar -xf toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18.tar.xz -C /opt/ 然后我们需要将交叉编译器添加到系统路径,方便我们接下来使用。\n这里我直接将配置写成脚本,方便下次使用。\n1 2 3 4 5 6 7 8 $ cd /opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 # 创建脚本 $ sudo touch environment-setup-loongarch64-linux-gnu # 添加可执行权限 $ sudo chmod +x environment-setup-loongarch64-linux-gnu # 修改脚本内容 $ sudo vi environment-setup-loongarch64-linux-gnu # 内容如下~ 1 2 3 4 5 6 7 8 # environment-setup-loongarch64-linux-gnu CC_PREFIX=/opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 export PATH=$CC_PREFIX/bin:$PATH export LD_LIBRARY_PATH=$CC_PREFIX/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=$CC_PREFIX/loongarch64-linux-gnu/lib64:$LD_LIBRARY_PATH export ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu- 接下来测试一下交叉编译环境\n1 2 3 4 5 6 7 8 9 10 $ cd ~ $ . /opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18/environment-setup-loongarch64-linux-gnu $ loongarch64-linux-gnu-gcc -v Using built-in specs. COLLECT_GCC=loongarch64-linux-gnu-gcc COLLECT_LTO_WRAPPER=/opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18/bin/../libexec/gcc/loongarch64-linux-gnu/8.3.0/lto-wrapper Target: loongarch64-linux-gnu Configured with: /dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/src/gcc/configure --build=x86_64-redhat-linux --host=x86_64-redhat-linux --target=loongarch64-linux-gnu --program-prefix=loongarch64-linux-gnu- --prefix=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross --libdir=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross/lib --with-gxx-include-dir=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross/sysroot/usr/include/c++ --with-sysroot=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross/sysroot --with-native-system-header-dir=/usr/include --with-arch=loongarch64 --with-abi=lp64 --with-multilib-list=lp64d,lp64s --with-pkgversion=\u0026#39;LoongArch\\ GNU\\ toolchain\\ vec.32-rc2\u0026#39; --disable-linker-build-id --with-newlib --without-headers --disable-shared --enable-threads=posix --enable-tls --enable-languages=c,c++,fortran --enable-__cxa_atexit --enable-libquadmath-support --disable-gcov --disable-libcc1 --enable-initfini-array --disable-nls --disable-bootstrap --with-glibc-version=2.28 Thread model: posix gcc version 8.3.0 (LoongArch GNU toolchain vec.32-rc2) 可以看到loongarch交叉编译器的版本信息,配置交叉编译环境完成。\n编写程序 控制LED灯的亮度,Linux系统已经有驱动实现了,我们要做的操作就是向相应的文件中写入数值即可,剩下的都是系统的事情。\n对于一般的LED灯,只有开关两个选项,写入0为关闭,写入非0值打开。\n而对于PWM控制的LED灯,需要写入具体数值来控制灯的亮度,同样,0为关闭,写入数值越大LED灯就越亮,当然,这是有上限的。这里经过测试,写入255后,LED灯达到最亮。\n而这次写的呼吸灯程序,则是逐渐改变LED灯的亮度,实现LED灯缓慢闪烁的效果。\n这里我将亮度分为10个级别,从0到255(2^8 - 1),每100ms改变一下LED灯的亮度,一个周期刚好为2秒(从灭到最亮,然后从最亮到灭)。\n至于为什么是分为这10个级别,而不是从0~255变化,大家可以自己试试,看一下效果。\n以下是完整程序:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * file: led-pwm.c */ #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; /** * @brief 呼吸灯效果 * @param fd 设备文件 */ void breath(int fd); int main(int argc, char *argv[]) { if (argc \u0026lt; 2) { printf(\u0026#34;Please input LED device.\\n\u0026#34;); return -1; } int fd; char file[64]; sprintf(file, \u0026#34;%s/brightness\u0026#34;, argv[1]); /* 打开设备文件 */ fd = open(file, O_WRONLY); if (fd \u0026lt; 0) { printf(\u0026#34;Error open file: %s\\n\u0026#34;, file); return fd; } breath(fd); /* 关闭设备文件 */ close(fd); return 0; } void breath(int fd) { const int values[15] = { 0, 1, 2, 4, 8, 16, 32, 64, 96, 128, 160, 192, 224, 255, 255 }; char buf[5]; while (1) { for (int i = 0; i \u0026lt; 30; i++) { int j = i \u0026lt; 15 ? i : (29 - i); sprintf(buf, \u0026#34;%d\u0026#34;, values[j]); write(fd, buf, sizeof(buf)); usleep(100000); // 休眠100ms } } } 然后编译,得到在开发板上运行可执行程序。\n1 $ loongarch64-linux-gnu-gcc led-pwm.c -o led-pwm 下载程序到开发板 运行前的最后一步,需要将编译好可执行程序复制到开发板上,我通常是使用scp命令将文件复制到开发板。\n需要先在串口终端通过ifconfig设置开发板的网口IP,第一次使用scp前,需要先用ssh登录到开发板。\n系统默认账户为root,默认密码为123\n由于之前不知道默认密码是什么,所以先用passwd命令改了密码😅。\n1 2 3 4 5 6 7 8 # 例:将可执行程序复制到开发板 $ scp ./led-pwm root@192.168.0.10:~/ # 使用 ssh 登录开发板 $ ssh root@192.168.0.10 # 为程序添加可执行权限(在开发板操作) ~ $ chmod +x ./led-pwm # 运行程序 ~ $ ./led-pwm /sys/class/leds/led1-pwm 实际运行效果 由于开发板的LED2默认是心跳模式[heartbeat],所以会一直一闪一闪的,在运行呼吸灯程序前,可以先把LED2改为[none]模式。\n1 ~ $ echo none \u0026gt; /sys/class/leds/led2/trigger 执行编写的呼吸灯程序\n1 ~ $ ./led-pwm /sys/class/leds/led1-pwm 实际运行效果如下:\n\u003c!DOCTYPE HTML\u003e ","permalink":"https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF2k500%E5%BC%80%E5%8F%91%E6%9D%BF%E4%B8%8A%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%91%BC%E5%90%B8%E7%81%AF%E6%95%88%E6%9E%9C/","summary":"前言 本来这篇文章应该在上周就写完的,不过突然被安排出差,一直忙到了现在,终于可以静下心来做些其它事情。\n之前和龙芯3A5000主机一起送过来的还有一块龙芯2K500的迷你开发板,整个板子不到巴掌大小。之前只是简单做了上电启动,这次拿到了比较完整的开发资料,可以尝试为开发板编写一些程序了。\n由于暂时没有屏幕,只能先试着做一些其它的事情,如通信和IO控制,其中最简单,最基础的就是LED灯的控制。然后我就发现,这个板子居然有一颗可以调节亮度的LED灯!没错,之前做的LED控制都只能进行开关操作,而可以调节亮度,意味着可以做出更多的显示效果,这次我就做了一个呼吸灯的效果。\n然后,我也简单了解了一下这种亮度调节的原理,实际上就是通过调节PWM输出的占空比,改变一个周期内输出的高低电平所占的比例,实现控制LED灯亮度的效果。由于引脚输出的电压是固定的,所以不能通过改变电平来控制亮度,而改变高低电平的占空比则是另一种思路,嵌入式设备的屏幕背光亮度调节也是基于同样的原理。\n开发板上电启动、连接串口终端 由于暂时没有屏幕,想要和开发板进行交互就只能通过终端的方式,通常开发板都会有调试串口,我们先通过串口终端登录设备,配置好网口IP地址后,再通过网络连接登录设备。\n串口的连接方式如下图:\n将绿、白、黑三色线以图中方式接好(红线不用接),USB端插入到电脑,应该不需要装驱动,电脑可以直接识别出串口设备。\n打开串口终端工具,比如Windows MobaXterm,linux minicom等,我比较喜欢用putty。\n配置好端口,设置\n波特率:115200\n数据位:8位\n停止位:1位\n校验:无\n硬件流控:无\n然后给开发板接通电源,就可以看到调试输出信息了。\n查看系统信息,可以看到运行的是安装了PREEMPT_RT补丁的实时操作系统。\n1 2 3 [root@LS-GD ~]# uname -a Linux LS-GD 5.10.0.lsgd-g434b00a6badf #1 PREEMPT Wed Sep 14 12:57:58 CST 2022 loongarch64 GNU/Linux [root@LS-GD ~]# 配置交叉编译环境 2K500开发板是loongarch64架构的嵌入式板卡。下载好对应的交叉编译工具链后,解压到系统/opt/目录下。按手册来就好~\n1 $ sudo tar -xf toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18.tar.xz -C /opt/ 然后我们需要将交叉编译器添加到系统路径,方便我们接下来使用。\n这里我直接将配置写成脚本,方便下次使用。\n1 2 3 4 5 6 7 8 $ cd /opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 # 创建脚本 $ sudo touch environment-setup-loongarch64-linux-gnu # 添加可执行权限 $ sudo chmod +x environment-setup-loongarch64-linux-gnu # 修改脚本内容 $ sudo vi environment-setup-loongarch64-linux-gnu # 内容如下~ 1 2 3 4 5 6 7 8 # environment-setup-loongarch64-linux-gnu CC_PREFIX=/opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 export PATH=$CC_PREFIX/bin:$PATH export LD_LIBRARY_PATH=$CC_PREFIX/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=$CC_PREFIX/loongarch64-linux-gnu/lib64:$LD_LIBRARY_PATH export ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu- 接下来测试一下交叉编译环境","title":"龙芯2K500开发板上实现的呼吸灯效果"},{"content":"使用weston-screenshooter\n但必须启用weston桌面--debug选项,否则会出现以下错误:\n1 2 3 [root@RK356X:/]# weston-screenshooter [02:41:05.145] libwayland: error in client communication (pid 776) weston_screenshooter@5: error 0: screenshooter failed: permission denied. Debug protocol must be enabled 以RK3568开发板,buildroot系统为例,修改/etc/init.d/S50launcher,找到weston所在行,添加--debug选项。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ...... # Uncomment to disable mirror mode # unset WESTON_DRM_MIRROR export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/var/run} export QT_QPA_PLATFORM=${QT_QPA_PLATFORM:-wayland} weston --tty=2 --debug --idle-time=0\u0026amp; { # Wait for weston ready while [ ! -e ${XDG_RUNTIME_DIR}/wayland-0 ]; do sleep .1 done /usr/bin/QLauncher \u0026amp; }\u0026amp; ...... forlinx开发板使用的yocto系统也类似,修改/lib/systemd/system/weston.service,在weston后添加--debug选项。\n1 2 3 $ vi /lib/systemd/system/weston.service # 修改如下 # ExecStart=/usr/bin/weston --debug --log=${XDG_RUNTIME_DIR}/weston.log $OPTARGS 然后重启系统,之后就可以使用weston-screenshooter截取屏幕了。\n链接\nwayland-project/weston weston.ini配置文件 ","permalink":"https://kira-96.github.io/notes/weston-screenshot/","summary":"使用weston-screenshooter\n但必须启用weston桌面--debug选项,否则会出现以下错误:\n1 2 3 [root@RK356X:/]# weston-screenshooter [02:41:05.145] libwayland: error in client communication (pid 776) weston_screenshooter@5: error 0: screenshooter failed: permission denied. Debug protocol must be enabled 以RK3568开发板,buildroot系统为例,修改/etc/init.d/S50launcher,找到weston所在行,添加--debug选项。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ...... # Uncomment to disable mirror mode # unset WESTON_DRM_MIRROR export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/var/run} export QT_QPA_PLATFORM=${QT_QPA_PLATFORM:-wayland} weston --tty=2 --debug --idle-time=0\u0026amp; { # Wait for weston ready while [ !","title":"weston桌面系统截屏方法"},{"content":"前言 之前尝试过在龙芯3A4000上编译运行EPICS,由于3A4000还是mips64指令集,而3A5000则是龙芯的自主指令集loongarch64,适配起来步骤也会有所不同。\n这次使用的是龙博特龙芯3A5000电脑主机。\n虽然EPICS官方并没有适配loongarch和mips64,无法做到开箱即用,但只要有gcc、g++、make、perl这些工具,理论上就能编译运行EPICS,在开始编译前,确保你的设备上已经装好了这些工具。\n关于如何称呼「龙架构」,龙芯社区也有一些讨论。最初我直接使用loongarch64,后来也使用过la64作为简写,直到我看到如何称呼龙架构?,我觉得有必要和社区保持一致,后续统一使用 loong64 作为架构标识。\n下载 base 这里我们就以目前最新版本7.0.7为例,其它版本的Base也类似。\n1 2 3 $ cd ~/下载/ $ wget https://epics.anl.gov/download/base/base-7.0.7.tar.gz $ tar -xzvf base-7.0.7.tar.gz 你可以在你觉得合适的位置编译安装Base,这里按我们的习惯,放在/usr/local/epics目录下。\n1 2 $ mkdir /usr/local/epics $ mv base-7.0.7 /usr/local/epics/ 编译 按照一般步骤,现在就可以开始编译了,我们可以先尝试一下,看看是什么结果。\n1 2 3 $ cd /usr/local/epics/base-7.0.7/ # 执行 `make` 命令 $ make 不出所料,果然失败了,输出的错误和在3A4000上编译时的错误也有一些不同。\n下面是在3A4000上编译时输出的错误:\n下面一行报错是差不多的,在loongarch64上编译却多了上面一行报错,意思就是没有识别出loongarch64架构。\n但是先不要慌,这里同时也给出了报错的位置,让我们看看EpicsHostArch.pl里写了些什么。\n1 $ vi ./src/tools/EpicsHostArch.pl 它其实就是一个perl脚本,用来判断当前的系统和cpu架构,而loongarch64显然没有做适配,所以就出现了上面错误。\n\u0026ldquo;Architecture \u0026rsquo;loongarch64-linux-gnu-thread-multi\u0026rsquo; not recognized\u0026rdquo;\n既然识别不了loongarch64,那我们就手动添加一行,让它可以识别就行了,即使看不太懂上面的脚本也没关系,看个半懂就行了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 sub HostArch { my $arch = $Config{archname}; for ($arch) { return \u0026#39;linux-x86_64\u0026#39; if m/^x86_64-linux/; return \u0026#39;linux-x86\u0026#39; if m/^i[3-6]86-linux/; return \u0026#39;linux-arm\u0026#39; if m/^arm-linux/; return \u0026#39;linux-aarch64\u0026#39; if m/^aarch64-linux/; return \u0026#39;linux-ppc64\u0026#39; if m/^powerpc64-linux/; return \u0026#39;linux-loong64\u0026#39; if m/^loongarch64-linux/; return \u0026#39;windows-x64\u0026#39; if m/^MSWin32-x64/; return \u0026#39;win32-x86\u0026#39; if m/^MSWin32-x86/; return \u0026#34;cygwin-x86_64\u0026#34; if m/^x86_64-cygwin/; return \u0026#34;cygwin-x86\u0026#34; if m/^i[3-6]86-cygwin/; return \u0026#39;solaris-sparc\u0026#39; if m/^sun4-solaris/; return \u0026#39;solaris-x86\u0026#39; if m/^i86pc-solaris/; my ($kernel, $hostname, $release, $version, $cpu) = uname; if (m/^darwin/) { for ($cpu) { return \u0026#39;darwin-x86\u0026#39; if m/^x86_64/; return \u0026#39;darwin-aarch64\u0026#39; if m/^arm64/; } die \u0026#34;$0: macOS CPU type \u0026#39;$cpu\u0026#39; not recognized\\n\u0026#34;; } die \u0026#34;$0: Architecture \u0026#39;$arch\u0026#39; not recognized\\n\u0026#34;; } } 我们在上面位置添加一行内容,来让它可以识别loongarch64架构。\n1 2 3 4 return \u0026#39;linux-aarch64\u0026#39; if m/^aarch64-linux/; return \u0026#39;linux-ppc64\u0026#39; if m/^powerpc64-linux/; + return \u0026#39;linux-loong64\u0026#39; if m/^loongarch64-linux/; return \u0026#39;windows-x64\u0026#39; if m/^MSWin32-x64/; 此时我们再执行一下make命令。\n可以看到,现在已经可以识别出loongarch64-linux了,报错和在3A4000上编译时也基本一样了。\n以下步骤同样适用于在3A4000(mips64)上编译EPICS,只需要将loong64全部替换为mips64\n剩下的报错就是,没有找到对应的编译配置项,我们同样可以仿照已经做了适配的架构来改写,直接按照下面步骤来就可以了。\n添加 CONFIG.Common.linux-loong64 1 2 3 4 $ cd configure/os/ # 添加 CONFIG.Common.linux-loong64 $ cp CONFIG.Common.linux-aarch64 CONFIG.Common.linux-loong64 $ vi CONFIG.Common.linux-loong64 修改成如下内容:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # CONFIG.Common.linux-loong64 # # Definitions for linux-loong64 target builds # Override these settings in CONFIG_SITE.Common.linux-loong64 #------------------------------------------------------- # Include definitions common to all Linux targets include $(CONFIG)/os/CONFIG.Common.linuxCommon ARCH_CLASS = loongarch ARCH_DEP_CFLAGS = $(GNU_ARCH_CFLAGS) $(GNU_TUNE_CFLAGS) ARCH_DEP_CFLAGS += $(GNU_DEP_CFLAGS) 添加 CONFIG.linux-loong64.Common 1 2 3 # 添加 CONFIG.linux-loong64.Common $ cp CONFIG.linux-aarch64.Common CONFIG.linux-loong64.Common $ vi CONFIG.linux-loong64.Common 修改成如下内容(内容没有变化,可以不修改):\n1 2 3 4 5 6 7 8 # CONFIG.linux-loong64.Common # # Definitions for linux-loong64 host builds # Sites may override these definitions in CONFIG_SITE.linux-loong64.Common #------------------------------------------------------- # Include definitions common to unix hosts include $(CONFIG)/os/CONFIG.UnixCommon.Common 添加 CONFIG.linux-loong64.linux-loong64 1 2 3 # 添加 CONFIG.linux-loong64.linux-loong64 $ cp CONFIG.linux-aarch64.linux-aarch64 CONFIG.linux-loong64.linux-loong64 $ vi CONFIG.linux-loong64.linux-loong64 修改成如下内容(内容没有变化,可以不修改):\n1 2 3 4 5 6 7 8 # CONFIG.linux-loong64.linux-loong64 # # Definitions for native linux-loong64 builds # Override these definitions in CONFIG_SITE.linux-loong64.linux-loong64 #------------------------------------------------------- # Include common gnu compiler definitions include $(CONFIG)/CONFIG.gnuCommon 添加 CONFIG_SITE.Common.linux-loong64 1 2 3 # 添加 CONFIG_SITE.Common.linux-loong64 $ cp CONFIG_SITE.Common.linux-aarch64 CONFIG_SITE.Common.linux-loong64 $ vi CONFIG_SITE.Common.linux-loong64 修改成如下内容。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # CONFIG_SITE.Common.linux-loong64 # # Site Specific definitions for all linux-loong64 targets #------------------------------------------------------- # NOTE for SHARED_LIBRARIES: In most cases if this is set to YES the # shared libraries will be found automatically. However if the .so # files are installed at a different path to their compile-time path # then in order to be found at runtime do one of these: # a) LD_LIBRARY_PATH must include the full absolute pathname to # $(INSTALL_LOCATION)/lib/$(EPICS_HOST_ARCH) when invoking base # executables. # b) Add the runtime path to SHRLIB_DEPLIB_DIRS and PROD_DEPLIB_DIRS, which # will add the named directory to the list contained in the executables. # c) Add the runtime path to /etc/ld.so.conf and run ldconfig # to inform the system of the shared library location. # Depending on your version of Linux you\u0026#39;ll want one of the following # lines to enable command-line editing and history in iocsh. If you\u0026#39;re # not sure which, start with the top one and work downwards until the # build doesn\u0026#39;t fail to link the readline library. If none of them work, # comment them all out to build without readline support. # No other libraries needed (recent Fedora, Ubuntu etc.): #COMMANDLINE_LIBRARY = READLINE # Needs -lncurses (RHEL 5 etc.): #COMMANDLINE_LIBRARY = READLINE_NCURSES # Needs -lcurses (older versions) #COMMANDLINE_LIBRARY = READLINE_CURSES # Readline is broken or you don\u0026#39;t want use it: #COMMANDLINE_LIBRARY = EPICS # WARNING: Variables that are set in $(CONFIG)/CONFIG.gnuCommon cannot be # overridden in this file for native builds, e.g. variables such as # OPT_CFLAGS_YES, WARN_CFLAGS, SHRLIB_LDFLAGS # They must be set in CONFIG_SITE.linux-loong64.linux-loong64 or for # cross-builds in CONFIG_SITE.\u0026lt;host-arch\u0026gt;.linux-loong64 instead. # Tune GNU compiler output for a specific cpu-type # (e.g. loongarch64, la264, la364, la464 etc.) GNU_ARCH_CFLAGS = -march=loongarch64 GNU_TUNE_CFLAGS = -mtune=loongarch64 # Enable soft-float feature (e.g. none, 32, 64) GNU_DEP_CFLAGS = -mfpu=none 添加 CONFIG_SITE.linux-loong64.linux-loong64 1 2 3 # 添加 CONFIG_SITE.linux-loong64.linux-loong64 $ cp CONFIG_SITE.linux-aarch64.linux-aarch64 CONFIG_SITE.linux-loong64.linux-loong64 $ vi CONFIG_SITE.linux-loong64.linux-loong64 修改成如下内容:\n1 2 3 4 5 6 7 8 9 10 11 # CONFIG_SITE.linux-loong64.linux-loong64 # # Site specific definitions for native linux-loong64 builds #------------------------------------------------------- # It makes sense to include debugging symbols even in optimized builds # in case you want to attach gdb to the process or examine a core-dump. # This does cost disk space, but not memory as debug symbols are not # loaded into RAM when the binary is loaded. #OPT_CFLAGS_YES += -g #OPT_CXXFLAGS_YES += -g 这里是对编译器的优化选项,暂时不知道怎么改,我就直接注释掉了。\n重新编译 到这里就全部改好了,其实最主要修改的就是第一个文件。下面就可以尝试编译了,这次应该没问题了。\n1 2 3 $ cd /usr/local/epics/base-7.0.7/ # 执行 `make` 命令 $ make -j8 接下来就静静等待编译完成。\n编译完后查看编译输出目录bin/linux-loong64/(截图比较旧)。\n添加到PATH 为了方便以后使用,我们将编译输出的可执行文件目录添加到PATH。\n1 2 3 4 5 $ cd ~ $ mkdir .epics $ cd .epics/ $ touch env $ vi env 编辑 env 如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/sh # EPICS base shell setup export EPICS_BASE=\u0026#34;/usr/local/epics/base-7.0.7\u0026#34; export EPICS_HOST_ARCH=linux-loong64 # affix colons on either side of $PATH to simplify matching case \u0026#34;:${PATH}:\u0026#34; in *:\u0026#34;${EPICS_BASE}/bin/${EPICS_HOST_ARCH}\u0026#34;:*) ;; *) # Prepending path in case a system-installed epics needs to be overridden export PATH=\u0026#34;${EPICS_BASE}/bin/${EPICS_HOST_ARCH}:$PATH\u0026#34; ;; esac 然后修改 .bashrc\n1 2 $ cd ~ $ vi .bashrc 在文件最后添加一行\n1 . \u0026#34;$HOME/.epics/env\u0026#34; 执行下面命令,使添加PATH生效\n1 $ . .bashrc 运行EPICS IOC 做完上面的步骤,EPICS base的配置就完成了,我们来尝试运行一下。\n在终端执行softIoc。\n运行正常,大功告成!\n只要base可以成功运行,其它的一些模块应该也没有问题,后续我会继续尝试在龙芯上安装EPICS其它的模块。\n链接 EPICS - Experimental Physics and Industrial Control System (anl.gov) epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov) ","permalink":"https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF3a5000loongarch64%E4%B8%8A%E7%BC%96%E8%AF%91%E8%BF%90%E8%A1%8Cepics/","summary":"前言 之前尝试过在龙芯3A4000上编译运行EPICS,由于3A4000还是mips64指令集,而3A5000则是龙芯的自主指令集loongarch64,适配起来步骤也会有所不同。\n这次使用的是龙博特龙芯3A5000电脑主机。\n虽然EPICS官方并没有适配loongarch和mips64,无法做到开箱即用,但只要有gcc、g++、make、perl这些工具,理论上就能编译运行EPICS,在开始编译前,确保你的设备上已经装好了这些工具。\n关于如何称呼「龙架构」,龙芯社区也有一些讨论。最初我直接使用loongarch64,后来也使用过la64作为简写,直到我看到如何称呼龙架构?,我觉得有必要和社区保持一致,后续统一使用 loong64 作为架构标识。\n下载 base 这里我们就以目前最新版本7.0.7为例,其它版本的Base也类似。\n1 2 3 $ cd ~/下载/ $ wget https://epics.anl.gov/download/base/base-7.0.7.tar.gz $ tar -xzvf base-7.0.7.tar.gz 你可以在你觉得合适的位置编译安装Base,这里按我们的习惯,放在/usr/local/epics目录下。\n1 2 $ mkdir /usr/local/epics $ mv base-7.0.7 /usr/local/epics/ 编译 按照一般步骤,现在就可以开始编译了,我们可以先尝试一下,看看是什么结果。\n1 2 3 $ cd /usr/local/epics/base-7.0.7/ # 执行 `make` 命令 $ make 不出所料,果然失败了,输出的错误和在3A4000上编译时的错误也有一些不同。\n下面是在3A4000上编译时输出的错误:\n下面一行报错是差不多的,在loongarch64上编译却多了上面一行报错,意思就是没有识别出loongarch64架构。\n但是先不要慌,这里同时也给出了报错的位置,让我们看看EpicsHostArch.pl里写了些什么。\n1 $ vi ./src/tools/EpicsHostArch.pl 它其实就是一个perl脚本,用来判断当前的系统和cpu架构,而loongarch64显然没有做适配,所以就出现了上面错误。\n\u0026ldquo;Architecture \u0026rsquo;loongarch64-linux-gnu-thread-multi\u0026rsquo; not recognized\u0026rdquo;\n既然识别不了loongarch64,那我们就手动添加一行,让它可以识别就行了,即使看不太懂上面的脚本也没关系,看个半懂就行了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 sub HostArch { my $arch = $Config{archname}; for ($arch) { return \u0026#39;linux-x86_64\u0026#39; if m/^x86_64-linux/; return \u0026#39;linux-x86\u0026#39; if m/^i[3-6]86-linux/; return \u0026#39;linux-arm\u0026#39; if m/^arm-linux/; return \u0026#39;linux-aarch64\u0026#39; if m/^aarch64-linux/; return \u0026#39;linux-ppc64\u0026#39; if m/^powerpc64-linux/; return \u0026#39;linux-loong64\u0026#39; if m/^loongarch64-linux/; return \u0026#39;windows-x64\u0026#39; if m/^MSWin32-x64/; return \u0026#39;win32-x86\u0026#39; if m/^MSWin32-x86/; return \u0026#34;cygwin-x86_64\u0026#34; if m/^x86_64-cygwin/; return \u0026#34;cygwin-x86\u0026#34; if m/^i[3-6]86-cygwin/; return \u0026#39;solaris-sparc\u0026#39; if m/^sun4-solaris/; return \u0026#39;solaris-x86\u0026#39; if m/^i86pc-solaris/; my ($kernel, $hostname, $release, $version, $cpu) = uname; if (m/^darwin/) { for ($cpu) { return \u0026#39;darwin-x86\u0026#39; if m/^x86_64/; return \u0026#39;darwin-aarch64\u0026#39; if m/^arm64/; } die \u0026#34;$0: macOS CPU type \u0026#39;$cpu\u0026#39; not recognized\\n\u0026#34;; } die \u0026#34;$0: Architecture \u0026#39;$arch\u0026#39; not recognized\\n\u0026#34;; } } 我们在上面位置添加一行内容,来让它可以识别loongarch64架构。","title":"龙芯3A5000(LoongArch64)上编译运行EPICS"},{"content":"前言 由于换了新的工作,我的工作方向也有了很大的变化,之前基本上是单纯的写代码,现在则经常需要和硬件设备交互,开发平台也转到了Linux+Qt。硬件设备的控制,其中最基本的就是LED灯以及一些开关继电器的操作,其本质就是GPIO的操作。考虑到系统的精简和成本控制,最好是可以直接通过Linux系统去控制,当然也有其它替代方案,比如使用支持Modbus协议的IO模块。关于Modbus的使用,后面有空再讲,这里就记录一下最简单的Linux系统下的GPIO控制,用户空间下的GPIO文件系统接口。\n在此之前,有必要再了解一下GPIO的概念。\n“通用输入/输出”(GPIO)是一种灵活的软件控制数字信号。它们由多种芯片提供,对于使用嵌入式和定制硬件的Linux开发人员来说很熟悉。每个GPIO代表一个连接到特定引脚的位,即球栅阵列(BGA)封装上的“球”。电路板示意图显示了哪些外部硬件连接到哪些GPIO。驱动程序可以通用地编写,以便板设置代码将这样的引脚配置数据传递给驱动程序。\nA “General Purpose Input/Output” (GPIO) is a flexible software-controlled digital signal. They are provided from many kinds of chip, and are familiar to Linux developers working with embedded and custom hardware. Each GPIO represents a bit connected to a particular pin, or “ball” on Ball Grid Array (BGA) packages. Board schematics show which external hardware connects to which GPIOs. Drivers can be written generically, so that board setup code passes such pin configuration data to drivers.\n在单片机上,我们可以很方便的控制GPIO,但在嵌入式Linux上则不一样,通常GPIO对于用户来说是不可见的,不过Linux系统也提供了相应的接口供用户控制GPIO,每个非专用的引脚都可以用作GPIO。\n配置IO多路复用器(IOMUXC) 将需要复用的IO添加到pinctrl_hog节点,例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* kernel/arch/arm64/boot/dts/freescale/OK8MP-C.dts */ \u0026amp;iomuxc { pinctrl-names = \u0026#34;default\u0026#34;; pinctrl-0 = \u0026lt;\u0026amp;pinctrl_hog\u0026gt;; pinctrl_hog: hoggrp { fsl,pins = \u0026lt; MX8MP_IOMUXC_HDMI_DDC_SCL__HDMIMIX_HDMI_SCL\t0x400001c3 MX8MP_IOMUXC_HDMI_DDC_SDA__HDMIMIX_HDMI_SDA\t0x400001c3 MX8MP_IOMUXC_HDMI_HPD__HDMIMIX_HDMI_HPD\t0x40000019 MX8MP_IOMUXC_HDMI_CEC__HDMIMIX_HDMI_CEC\t0x40000019 /* GPIO */ MX8MP_IOMUXC_GPIO1_IO07__GPIO1_IO07\t0x159 MX8MP_IOMUXC_GPIO1_IO09__GPIO1_IO09\t0x159 MX8MP_IOMUXC_GPIO1_IO12__GPIO1_IO12\t0x159 MX8MP_IOMUXC_ECSPI2_MOSI__GPIO5_IO11\t0x159 MX8MP_IOMUXC_ECSPI2_MISO__GPIO5_IO12\t0x159 MX8MP_IOMUXC_ECSPI2_SS0__GPIO5_IO13\t0x159 \u0026gt;; }; } 具体的GPIO名字要参照xxxx-pinfunc.h里面的定义,配置为GPIO时,一定要使用IOMUXC_xxxx_xxxx__GPIOn_IOxx的宏定义。\n然后是后面的上下拉配置,具体的计算方法和参数意义如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PAD_CTL_HYS (1 \u0026lt;\u0026lt; 16) /* Hysteresis 滞后使能*/ PAD_CTL_PUS_100K_DOWN (0 \u0026lt;\u0026lt; 14) /* 100KOhm Pull Down */ PAD_CTL_PUS_47K_UP (1 \u0026lt;\u0026lt; 14) /* 47KOhm Pull Up */ PAD_CTL_PUS_100K_UP (2 \u0026lt;\u0026lt; 14) /* 100KOhm Pull Up */ PAD_CTL_PUS_22K_UP (3 \u0026lt;\u0026lt; 14) /* 22KOhm Pull Up */ PAD_CTL_PUE (1 \u0026lt;\u0026lt; 13) /* Pull / Keep Enable */ PAD_CTL_PKE (1 \u0026lt;\u0026lt; 12) /* Pull / Keep Select 0: Keeper 1: Pull */ PAD_CTL_ODE (1 \u0026lt;\u0026lt; 11) /* Open Drain Enable 漏极开路 */ PAD_CTL_SPEED_LOW (1 \u0026lt;\u0026lt; 6) /* 带宽配置 */ PAD_CTL_SPEED_MED (2 \u0026lt;\u0026lt; 6) PAD_CTL_SPEED_HIGH (3 \u0026lt;\u0026lt; 6) PAD_CTL_DSE_DISABLE (0 \u0026lt;\u0026lt; 3) /* Drive Strength Field 驱动能力 */ PAD_CTL_DSE_240ohm (1 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_120ohm (2 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_80ohm (3 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_60ohm (4 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_48ohm (5 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_40ohm (6 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_34ohm (7 \u0026lt;\u0026lt; 3) PAD_CTL_SRE_FAST (1 \u0026lt;\u0026lt; 0) /* Slew Rate Field 压摆率 */ PAD_CTL_SRE_SLOW (0 \u0026lt;\u0026lt; 0) 注意:不要直接设置为0,没有任何作用。可以使用0x80000000,它表示“我不知道,保持默认值”。\n用户空间下的GPIO读写操作 用户空间下的GPIO文件系统接口在/sys/class/gpio/目录下。\n注意:以下操作都需要root权限!\n使能GPIO 在此之前,需要先知道GPIO对应的编号数值,计算方法如下:\nGPIOn_IOx = (n - 1) × 32 + x\n例:GPIO5_IO13 = (5 - 1) × 32 + 13 = 141\n执行命令\n1 2 3 # echo N \u0026gt; /sys/class/gpio/export # N为GPIO对应的编号,例: echo 141 \u0026gt; /sys/class/gpio/export 如果需要取消使能GPIO,则执行命令\n1 2 3 # echo N \u0026gt; /sys/class/gpio/unexport # N为GPIO对应的编号,例: echo 141 \u0026gt; /sys/class/gpio/unexport GPIO配置 使能GPIO之后,/sys/class/gpio/目录下就多出来了相应的GPIO节点目录。例:gpio141\nGPIO节点有以下属性可以配置:\n/sys/class/gpio/gpioN/\ndirection\n读取为in或者out。通常可以写入此值。写入out默认输出为低。为了确保操作无误,可以写入值“low”和“high”将GPIO配置为具有该初始值的输出。\n请注意,如果内核不支持更改GPIO的方向,或者该属性是由内核代码导出的,而内核代码没有明确允许用户空间重新配置该GPIO方向,则该属性将不存在。\nvalue\n读取为0(low)或1(high)。如果GPIO被配置为输出,则可以写入该值;任何非零值都被视为高。\n如果引脚可以配置为中断生成,并且它已经配置为生成中断(请参阅“edge”的描述),那么您可以对该文件进行轮询(poll),每当触发中断时,轮询(poll)将返回。如果使用poll,请设置事件为POLLPRI和POLLERR。如果使用select,则将文件描述符设置为exceptfds。轮询返回后,要么lseek到sysfs文件的开头并读取新值,要么关闭文件并重新打开以读取值。\nedge\n读取为none、rising、falling或者both。编写这些字符串以选择将对“value”文件返回进行轮询的信号边缘。\n仅当引脚可以配置为中断生成输入引脚时,此属性才存在。\nactive_low\n读取为0(假)或1(真)。写入任何非零值以反转读取和写入的值属性。现有和后续轮询支持通过边缘属性配置“rising”和“falling”边缘将遵循此设置。\n例:\n将GPIO配置为输出\n1 echo out \u0026gt; /sys/class/gpio/gpio{N}/direction 将GPIO配置为输入,上升沿触发中断\n1 2 echo in \u0026gt; /sys/class/gpio/gpio{N}/direction echo rising \u0026gt; /sys/class/gpio/gpio{N}/edge GPIO读写 GPIO配置为输入(in)时,只能读取输入值,不能写入\nGPIO配置为输出(out)时,可以读取当前值和写入新值\n1 2 3 4 5 # 读取时 cat /sys/class/gpio/gpio{N}/value # 写入时 echo 0 \u0026gt; /sys/class/gpio/gpio{N}/value echo 1 \u0026gt; /sys/class/gpio/gpio{N}/value 程序控制GPIO 示例读取:\n1 2 3 4 5 6 7 8 9 10 11 int readGpio(unsigned short io) { QFile file(QString(\u0026#34;/sys/class/gpio/gpio%1/value\u0026#34;).arg(io)); if (!file.open(QIODevice::ReadOnly)) return 0; QByteArray ba = file.readAll(); file.close(); return QString(ba).toInt(); } 示例写入:\n1 2 3 4 5 6 bool writeGpio(unsigned short io, unsigned char value) { char buf[128]; sprintf(buf, \u0026#34;echo %d \u0026gt; /sys/class/gpio/gpio%d/value\u0026#34;, value, io); return ::system(buf) == 0; } 参考\nLegacy GPIO Interfaces GPIO Sysfs Interface for Userspace Definitive GPIO guide ","permalink":"https://kira-96.github.io/posts/linux-gpio%E6%93%8D%E4%BD%9C%E5%85%B6%E4%B8%80/","summary":"前言 由于换了新的工作,我的工作方向也有了很大的变化,之前基本上是单纯的写代码,现在则经常需要和硬件设备交互,开发平台也转到了Linux+Qt。硬件设备的控制,其中最基本的就是LED灯以及一些开关继电器的操作,其本质就是GPIO的操作。考虑到系统的精简和成本控制,最好是可以直接通过Linux系统去控制,当然也有其它替代方案,比如使用支持Modbus协议的IO模块。关于Modbus的使用,后面有空再讲,这里就记录一下最简单的Linux系统下的GPIO控制,用户空间下的GPIO文件系统接口。\n在此之前,有必要再了解一下GPIO的概念。\n“通用输入/输出”(GPIO)是一种灵活的软件控制数字信号。它们由多种芯片提供,对于使用嵌入式和定制硬件的Linux开发人员来说很熟悉。每个GPIO代表一个连接到特定引脚的位,即球栅阵列(BGA)封装上的“球”。电路板示意图显示了哪些外部硬件连接到哪些GPIO。驱动程序可以通用地编写,以便板设置代码将这样的引脚配置数据传递给驱动程序。\nA “General Purpose Input/Output” (GPIO) is a flexible software-controlled digital signal. They are provided from many kinds of chip, and are familiar to Linux developers working with embedded and custom hardware. Each GPIO represents a bit connected to a particular pin, or “ball” on Ball Grid Array (BGA) packages. Board schematics show which external hardware connects to which GPIOs. Drivers can be written generically, so that board setup code passes such pin configuration data to drivers.","title":"Linux GPIO 操作其一"},{"content":"问题描述 在使用 WSL 更新软件包的时候经常会遇到这样一个报错\n1 /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link 意思是说 /usr/lib/wsl/lib/libcuda.so.1 不是一个符号链接。\n问题分析 通过名字可以判断这应该是nVidia显卡驱动相关的库,进入 /usr/lib/wsl/lib/ 目录,可以看到有 libcuda.so、libcuda.so.1、libcuda.so.1.1 三个文件,都是文件形式,而通过报错我们知道 libcuda.so、libcuda.so.1 应该是符号链接文件。\n它们关系应该是:\nlibcuda.so -\u0026gt; libcuda.so.1 -\u0026gt; libcuda.so.1.1\n知道原因就好解决了,把 libcuda.so、libcuda.so.1 删掉,再重新创建符号链接就可以了。\n1 2 ubuntu@dell:/usr/lib/wsl/lib$ sudo rm libcuda.so rm: 无法删除 \u0026#39;libcuda.so\u0026#39;: 只读文件系统 很遗憾,这样是不行的。最后经过多方查找,终于找到了解决方案。\n解决方法 解决方法就是上面的方法,但不是在 WSL 中操作。\n使用管理员权限执行 cmd 命令:\n1 2 3 4 5 C:\u0026gt;cd C:\\Windows\\System32\\lxss\\lib C:\\Windows\\System32\\lxss\\lib\u0026gt;del /s /q \u0026#34;libcuda.so\u0026#34; C:\\Windows\\System32\\lxss\\lib\u0026gt;del /s /q \u0026#34;libcuda.so.1\u0026#34; C:\\Windows\\System32\\lxss\\lib\u0026gt;mklink libcuda.so.1 libcuda.so.1.1 C:\\Windows\\System32\\lxss\\lib\u0026gt;mklink libcuda.so libcuda.so.1 或者在Powershell中执行:\n1 2 3 4 5 6 cd C:\\Windows\\System32\\lxss\\lib rm libcuda.so rm libcuda.so.1 wsl -e /bin/bash ln -s libcuda.so.1.1 libcuda.so.1 ln -s libcuda.so.1.1 libcuda.so 然后在 wsl 中执行:\n1 $ sudo ldconfig 参考\nldconfig: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link libcuda.so.1 is not a symbolic link #5548 ","permalink":"https://kira-96.github.io/posts/wsl-libcuda.so.1-is-not-a-symbolic-link-%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/","summary":"问题描述 在使用 WSL 更新软件包的时候经常会遇到这样一个报错 1 /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link 意思是说 /usr/lib/wsl/lib/libcuda.so.1 不是一个符号链接。 问题分析 通过名字可以判断这应该是nVidi","title":"WSL libcuda.so.1 is not a symbolic link 的解决方法"},{"content":"前言 由于这半年来一直在做嵌入式Linux系统软件开发工作,所以经常和嵌入式设备打交道,最早接触的嵌入式Linux应该就是树莓派了,而我的树莓派一般也不接屏幕,基本上都使用VNC远程连接,所以就想着能不能把VNC也移植到嵌入式设备上,最后找到了x11vnc。\nVNC(虚拟网络计算)是一种非常有用的网络图形协议(应用程序在一台计算机上运行,但在另一台计算机上显示其窗口),但与X不同,查看端非常简单,不保持任何状态。它是一种远程帧缓冲区(RFB)协议。\nx11vnc允许用户通过任何VNC viewer远程查看并与real X显示器(即与物理监视器、键盘和鼠标相对应的显示器)交互。\n准备工作 需要先用git克隆下面两个仓库,libvncserver和x11vnc。 x11vnc是基于libvncserver的服务端程序。\nGitHub - LibVNC/libvncserver: LibVNCServer/LibVNCClient are cross-platform C libraries that allow you to easily implement VNC server or client functionality in your program. GitHub - LibVNC/x11vnc: a VNC server for real X displays 编译 libvncserver 编译x11vnc需要libvncserver,libvncserver按照 README 编译即可\n1 2 3 4 mkdir build cd build cmake .. cmake --build . 编译完成后将build文件夹下生成的.so文件复制到 sysroot\n例:\n1 2 3 4 5 6 7 # 复制 .so 文件 $ sudo cp ./libvncclient.so* /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/lib/ # 复制 pkgconfig 文件 $ sudo cp ./libvnc*.pc /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/lib/pkgconfig/ # 复制头文件 $ sudo cp -r ../rfb /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/include/ $ sudo cp ./rfb/rfbconfig.h /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/include/rfb/ 编译 x11vnc x11vnc 需要使用 autoconf 和 automake 生成 configure 和 makefile\n1 2 3 4 5 6 7 8 9 10 11 12 13 $ cd x11vnc-master # 使用aclocal工具生成aclocal.m4 $ aclocal # 使用autoconf工具生成configure文件 $ autoconf # 使用autoheader工具生成config.h.in文件 $ autoheader # 使用automake生成Makefile.in文件 $ automake --add-missing # configure 配置交叉编译,如果libvncserver没有正确编译安装,这里会提示找不到libvncserver $ ./configure CC=arm-poky-linux-gnueabi-gcc AR=arm-poky-linux-gnueabi-ar AS=arm-poky-linux-gnueabi-as LD=arm-poky-linux-gnueabi-ld --host=arm-poky-linux --prefix=/home/ubuntu/ CFLAGS=\u0026#34;-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7 --sysroot=/opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi\u0026#34; # 编译安装 $ make install 安装完成后在 /home/ubuntu/ (configure 步骤设置的 \u0026ndash;prefix)目录下出现了bin和 share两个目录,bin目录下的就是 x11vnc 的可执行文件。\n安装运行 将 libvncserver 编译得到的 *.so 文件和 x11vnc 可执行文件复制到开发板即可。\n1 2 3 4 # 复制可执行文件 $ scp -r /home/ubuntu/bin root@192.168.10.7:~/ # 复制 libvncserver $ scp /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/lib/libvnc*.so.0.9.13 root@192.168.10.7:/usr/lib/ ssh登录开发板\n1 2 3 4 5 6 7 8 9 10 11 12 # 创建符号链接 root@imx6ulevk:~# ln -s /usr/lib/libvncclient.so.0.9.13 /usr/lib/libvncclient.so.1 root@imx6ulevk:~# ln -s /usr/lib/libvncclient.so.1 /usr/lib/libvncclient.so root@imx6ulevk:~# ln -s /usr/lib/libvncserver.so.0.9.13 /usr/lib/libvncserver.so.1 root@imx6ulevk:~# ln -s /usr/lib/libvncserver.so.1 /usr/lib/libvncserver.so # 重命名文件夹 root@imx6ulevk:~# mv ~/bin ~/x11vnc root@imx6ulevk:~# cd ~/x11vnc root@imx6ulevk:~# chmod +x ./x11vnc # 运行 x11vnc # x11vnc -display :0 root@imx6ulevk:~# ./x11vnc 然后在电脑上启动 VNC Viewer, 输入开发板的ip地址就可以通过远程桌面访问设备了。\n实际运行效果如图\n参考\nautoconf / automake工具使用介绍 给imx6嵌入式平台移植x11vnc搭建远程控制环境 ","permalink":"https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E9%80%82%E7%94%A8%E4%BA%8E-imx6ul-%E7%9A%84-x11vnc/","summary":"前言 由于这半年来一直在做嵌入式Linux系统软件开发工作,所以经常和嵌入式设备打交道,最早接触的嵌入式Linux应该就是树莓派了,而我的树莓","title":"交叉编译适用于 iMX6UL 的 x11vnc"},{"content":"开始一份新工作 好久没有更新博客了,最近几个月都在忙新工作的事情,虽然只是短短3个月,但回想起来已经是很久之前了。从4月份面试,然后体检,提交各种材料证明,到5月份工作交接,6月份正式离职进入新单位,搬家等等,这几个月真是忙的有点喘不过气。即使到了新的单位,也没有闲下来的时间,来了之后就要上手做项目,到这周为止,项目总算初步完成,这才有时间来自我整理一下。即使5月份买了PS5游戏机也没怎么有时间玩,没什么游戏是一方面,忙也是一方面。\n为什么要换工作 其实这个问题我也一直在思考,包括在到了新单位之后,看到到手少得可怜的工资,我也在问自己,为什么要换工作?\n总的来说,上一家公司给我的印象还是挺不错的,大学毕业后,当时没有找到一份好的工作,是它接纳了我,这三年来,我也在公司学到了很多,和同事相处也还都不错,所以决定从那里离开,我也是下了很大的决心。\n最主要的原因还是我渴望做一些更有创造性的内容,而不是整天的修修补补,虽然修bug也是程序员的一项重要工作,但每天都面对着相同的内容,难免会让人心生厌倦。很多时候我都会想,自己是不是会一直这样待下去,在同一家公司一直工作,直到退休,这可能是个不错的选择,但我不想做一个整天闲着的人,性格使然,我这一生可能注定是忙碌的,我渴望接触和学习新的知识,而不是一直待在安乐窝里,所以我选择走出去,见识一下更广阔的天地。\n关于新工作 新工作在上海,一个研究所里,除了工资,其它都还可以,但出来打工,工资不才是最重要的吗?目前工作了刚好一个月,工资到手4k多,据说还是两个月的,这让我真的难以接受,要不是自己还有点存款,日子真是没法混了。\n以上是吐槽,6月份从上一家公司离职,休息了一天就到新单位报到,第二个星期就直接上手做项目,可以说是赶得很紧了。而关于工作的内容,目前是在做嵌入式Linux方向的软件,之前没有做过Linux平台的软件开发,一直都是Windows平台,但由于有了一些工作经验,所以上手其实很快,总之就是一边学习一边做项目,结合自己以前的开发经验,其实动手做起来并不难。中间也有遇到一些问题,因为是做嵌入式开发,所以难免会涉及到如何控制芯片管脚输出,通过管脚输出来控制外围设备,这些涉及到硬件领域的内容我其实并不在行,虽然大学时候学过单片机相关的课程,但在Linux系统中控制芯片和直接编程控制芯片引脚还是有很大的不同的,所以自己也在周末抽时间学习相关的内容。\n总的来说,这一个月来学到的新东西还是很多的,每天都忙碌着学习和工作,还是很充实的,同时新的工作环境,也让我接触到了更多领域的人,这又何尝不是一种进步呢?\n但是,新工作是真的累,这一个月的项目做下来,感觉自己有点吃不消,虽然有安排组员一起工作,但我对他们的工作成果并不满意,以后有必要进行一些指导,总是一个人做会很累很累,而且现在项目也比较多,人手也不够。\n个人感觉在新单位还是有很大的发展空间的,因为这里使用的技术都相对要落后,自己可以发挥以前学到的一些项目管理的经验,积攒更多的经验,对于技术人员来说才是成长。\n最后 由于工作的方向换了,所以以后可能会写一些嵌入式开发相关的内容,还有Qt之类的一些学习经验,总之,随学随记,不定期更新。\n","permalink":"https://kira-96.github.io/posts/%E5%BC%80%E5%A7%8B%E4%B8%80%E4%BB%BD%E6%96%B0%E5%B7%A5%E4%BD%9C/","summary":"开始一份新工作 好久没有更新博客了,最近几个月都在忙新工作的事情,虽然只是短短3个月,但回想起来已经是很久之前了。从4月份面试,然后体检,提交","title":"开始一份新工作"},{"content":"计算切片间距的方法:\n对于CT扫描出的断层图像,没有存储Spacing Between Slices信息,但可以利用位置信息计算得到。\n需要先读取相邻两层切片的位置信息,假设为pos1和pos2,然后计算两个位置的距离即为切片间距。\n1 2 3 4 5 // double pos1[3], pos2[3]; double spacing = sqrt( (pos1[0] - pos2[0]) * (pos1[0] - pos2[0]) + (pos1[1] - pos2[1]) * (pos1[1] - pos2[1]) + (pos1[2] - pos2[2]) * (pos1[2] - pos2[2])); ","permalink":"https://kira-96.github.io/notes/calculate-spacing-between-slices/","summary":"计算切片间距的方法: 对于CT扫描出的断层图像,没有存储Spacing Between Slices信息,但可以利用位置信息计算得到。 需要先读取相邻两层切片的","title":"Calculate Spacing Between Slices"},{"content":"前言 The Medical Imaging Interaction Toolkit (MITK)是一个免费的开源软件,用于开发交互式医学影像处理软件。最近突然安排我做相关的一些工作,首先就要从编译开始,当然官网也有编译好的版本,可以直接下载使用。本来在Windows上编译这种开源的软件就很麻烦,在加上github上的东西下载巨慢,常常出错,折腾了好久才编译完成,这里就记录一下踩过的那些坑。\n准备工作 Visual Studio 2017 CMake (\u0026gt;=3.19) Qt 5.12.10 (\u0026gt;=5.12.9) Python3 Git OpenSSL 安装包 Doxygen MITK MITK-Diffusion 我这里使用的是Visual Studio 2017版本,2019应该也可以。\nQt需要安装5.12.9以上的版本,官方编译似乎用的5.12.10,所以我这里也选择5.12.10版本,安装过程中尽量把所有组件都选上,因为编译时会使用到很多组件。\nCMake,Python,OpenSSL都选择64位版本安装。\n最后就是用git克隆下MITK和MITK-Diffusion的仓库。\n由于MITK在编译的过程中会下载一些第三方的软件包,所以要能克隆github上的仓库才行,最好有梯子,我就是这里卡了很久。\nCMake 相关配置 在MITK仓库目录下新建build文件夹(名字可以随意),然后打开cmake GUI工具,填写source code和build文件夹路径 点击Configure按钮,第一次会弹出对话框选择编译器,根据需要配置即可,这里选择msvc-2017,x64 此时会出现错误提示,找不到Qt,需要手动设置一下Qt5的路径,找到Qt5_DIR项,在Value中填写路径,例:D:/Qt/Qt5.12.10/msvc2017_64/lib/cmake/Qt5,再次点击Configure按钮 检查是否所有的安装路径都正确被检测到(Qt,OpenSSL),确保没有选项是红色 如果只需要编译MITK,那么就可以直接跳到第9步 找到MITK_EXTENSION_DIRS选项,在Value中填写MITK-Diffusion仓库的路径,再次点击Configure按钮 找到MITK_BUILD_CONFIGURATION选项,Value设置为DiffusionRelease,再次点击Configure按钮 此时下方输出会报错,提示找不到NumPy,需要先安装python库NumPy,打开终端,执行pip3 install --user NumPy,完成后再次点击Configure按钮,确保没有错误,没有选项是红色 点击Generate按钮,下方输出Generating done之后,点击Open Project按钮 附上我的配置:\n编译 直接编译ALL_BUILD项目即可,但此时编译可能会有一堆莫名奇妙的错误,可以先到MITK仓库目录下找到CMakeExternals目录,然后将里面所有的文件换行符改为Windows下的换行符(CRLF),可以使用VS Code或者Notepad++等工具,过程会有点枯燥。\n确保网络可用,最好有国外朋友帮忙,因为编译过程中会下载很多github上的仓库,没有国外朋友帮忙很容易出错。编译ALL_BUILD项目,第一次编译会非常慢,通常需要几个小时,建议先去忙点其它事情。\n编译过程中经常会遇到警告被视为错误,没有生成object,导致编译出错,双击错误,打开错误文件,然后再找到错误文件的位置,用记事本打开,选择文件→另存为,选择保存编码为Unicode,再次编译。\n在编译MITK-Diffusion的过程中,可能会遇到一些类型转换的报错,如无法将itk::Point转换为mitk::PointSet::PointType等,可能一些编译器能通过,但msvc会报错,只需要稍微修改一下,将出错的参数强制转换成对应类型即可,如:mitk::PointSet::PointType(itkPoint)。\n重复3、4若干次,直到编译成功。\n编译后的可执行文件在build/MITK-build/bin目录下。\n","permalink":"https://kira-96.github.io/posts/build-mitk-on-windows/","summary":"前言 The Medical Imaging Interaction Toolkit (MITK)是一个免费的开源软件,用于开发交互式医学影像处理软件。最近突然安排我做相关的一些工作,首先就要从编译开始,当然官网","title":"在 Windows 上编译 MITK"},{"content":"前言 今天偶然看到一个新的开源的git服务软件Gitea,一看到界面,瞬间就爱了,因为之前我自己用的是gitblit,界面比较简单,主要是用来管理公司的一些小项目。今天看到Gitea之后,就决定迁移到过去,简单折腾了一下,配置起来比gitblit要简单一些,但界面却更加漂亮了,总体上看起来比较像github,并且还支持主题系统,很合我的胃口。\n下载二进制包 首先去下载对应系统的二进制包,可以去github或者官网下载最新的发布版本。\n我是在windows下配置的,所以选择下载windows版的可执行程序。\n开启服务 下载后不需要安装,直接就能运行,但直接运行的话会有一个控制台显示在桌面,所以可以考虑将程序作为一个系统服务在后台运行。\n由于不需要安装,可以直接将下载的可执行程序放在自己想要安装的目录,eg: D:\\gitea\\gitea.exe\n以管理员方式打开cmd或者powershell,执行命令:\n1 sc create gitea start= auto binPath= \u0026#34;\\\u0026#34;D:\\gitea\\gitea.exe\\\u0026#34; web --config \\\u0026#34;D:\\gitea\\custom\\conf\\app.ini\\\u0026#34;\u0026#34; 打开系统的服务管理界面,找到gitea,此时是已停止状态,按下鼠标右键,在弹出的菜单选择开始,然后可以看到状态变为正在启动。\n打开浏览器访问 http://localhost:3000,应该就能看到Gitea的界面了,点击页面上的探索(explore),还需要进行一些配置才能正常运行,数据库可以直接使用sqlite,这样就不需要再安装其它的数据库了,然后就是一些目录配置,根据需要选择目录就行。\n最后可以创建管理员账号,也可以不用设置,完成安装后第一个注册的账号会自动成为管理员。\n到这里安装和配置就完成了,此时再看服务中的gitea的状态,已经变为正在运行。\n迁移仓库到 Gitea Gitea 自带迁移外部仓库的功能,但我一直导入失败,提示没有导入本地仓库的权限。后来只能将本地的仓库推送到Gitea,不过效果是一样的。\n首先在Gitea中新建一个同名的仓库,复制仓库的git链接。\n修改本地仓库的origin\n1 $ git remote origin set-url http://localhost:3000/user/myrepo.git 或者直接修改.git/config文件中的origin url。\n推送本地仓库到Gitea\n1 $ git push origin main 静静等待推送完成,然后刷新gitea的仓库页面,就可以看到所有的提交记录了。\n启用 SSH 编辑 D:\\gitea\\custom\\conf\\app.ini,在[server]部分新增一项配置\n1 2 [server] START_SSH_SERVER = true 保存后,在服务中重启 gitea 即可。\n在gitea的账号管理中新增SSH密钥,然后就可以使用ssh的方式管理账户所拥有的仓库了。\n最后 总的来看,Gitea的配置十分简单,基本上下载后就可以使用,没有其它的依赖,gitblit则需要安装java运行环境,界面十分漂亮,功能也比较完善,自用完全没有问题。\n","permalink":"https://kira-96.github.io/posts/install-gitea/","summary":"前言 今天偶然看到一个新的开源的git服务软件Gitea,一看到界面,瞬间就爱了,因为之前我自己用的是gitblit,界面比较简单,主要是用来","title":"Gitea 安装使用"},{"content":"首先需要安装Docker\n安装完成后,拉取以下3个仓库\n1 2 3 $ docker pull dcm4che/slapd-dcm4chee:2.4.56-23.1 $ docker pull dcm4che/postgres-dcm4chee:13.1-23 $ docker pull dcm4che/dcm4chee-arc-psql:5.23.1 拉取完成后,就可以启动服务了,这里有两种方式,一种是使用Docker命令行依次启动上面3个服务,不过比较麻烦,也容易出错,另一种是直接使用Docker Copmose,相对来说要简单很多。这里就直接使用 Docker Compose的方式。\n创建以下两个文件:\ndocker-compose.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 version: \u0026#34;3\u0026#34; services: ldap: image: dcm4che/slapd-dcm4chee:2.4.56-23.1 logging: driver: json-file options: max-size: \u0026#34;10m\u0026#34; ports: - \u0026#34;389:389\u0026#34; env_file: docker-compose.env volumes: - /var/local/dcm4chee-arc/ldap:/var/lib/openldap/openldap-data - /var/local/dcm4chee-arc/slapd.d:/etc/openldap/slapd.d db: image: dcm4che/postgres-dcm4chee:13.1-23 logging: driver: json-file options: max-size: \u0026#34;10m\u0026#34; ports: - \u0026#34;5432:5432\u0026#34; env_file: docker-compose.env volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/local/dcm4chee-arc/db:/var/lib/postgresql/data arc: image: dcm4che/dcm4chee-arc-psql:5.23.1 logging: driver: json-file options: max-size: \u0026#34;10m\u0026#34; ports: - \u0026#34;8080:8080\u0026#34; - \u0026#34;8443:8443\u0026#34; - \u0026#34;9990:9990\u0026#34; - \u0026#34;9993:9993\u0026#34; - \u0026#34;11112:11112\u0026#34; - \u0026#34;2762:2762\u0026#34; - \u0026#34;2575:2575\u0026#34; - \u0026#34;12575:12575\u0026#34; env_file: docker-compose.env environment: WILDFLY_CHOWN: /opt/wildfly/standalone /storage WILDFLY_WAIT_FOR: ldap:389 db:5432 depends_on: - ldap - db volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/local/dcm4chee-arc/wildfly:/opt/wildfly/standalone - /var/local/dcm4chee-arc/storage:/storage docker-compose.env\n1 2 3 4 5 TZ=Asia/Shanghai STORAGE_DIR=/storage/fs1 POSTGRES_DB=pacsdb POSTGRES_USER=pacs POSTGRES_PASSWORD=pacs 其中TZ是用来设置时区的。\n启动\n1 $ docker-compose -p dcm4chee up -d 停止\n1 $ docker-compose -p dcm4chee stop 重新启动\n1 $ docker-compose -p dcm4chee start 删除\n1 $ docker-compose -p dcm4chee down 参考\nRun minimum set of archive services on a single host\n","permalink":"https://kira-96.github.io/notes/running-dcm4chee-arc-light-on-docker/","summary":"首先需要安装Docker 安装完成后,拉取以下3个仓库 1 2 3 $ docker pull dcm4che/slapd-dcm4chee:2.4.56-23.1 $ docker pull dcm4che/postgres-dcm4chee:13.1-23 $ docker pull dcm4che/dcm4chee-arc-psql:5.23.1 拉取完成后,就可以启动服务了,这里有两种方式,一种是使用D","title":"Running Dcm4chee Arc Light on Docker"},{"content":"GDI+绘制椭圆时只支持输入一个矩形范围,无法绘制倾斜的椭圆。\n绘制椭圆的 API:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 摘要: 绘制边界 System.Drawing.RectangleF 定义的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // rect: System.Drawing.RectangleF 结构,它定义椭圆的边界。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, RectangleF rect); // // 摘要: 绘制一个由边框(该边框由一对坐标、高度和宽度指定)定义的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // x: 定义椭圆的边框的左上角的 X 坐标。 // y: 定义椭圆的边框的左上角的 Y 坐标。 // width: 定义椭圆的边框的宽度。 // height: 定义椭圆的边框的高度。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, float x, float y, float width, float height); // // 摘要: 绘制边界 System.Drawing.Rectangle 结构指定的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // rect: System.Drawing.Rectangle 结构,它定义椭圆的边界。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, Rectangle rect); // // 摘要: 绘制一个由边框定义的椭圆,该边框由矩形的左上角坐标、高度和宽度指定。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // x: 定义椭圆的边框的左上角的 X 坐标。 // y: 定义椭圆的边框的左上角的 Y 坐标。 // width: 定义椭圆的边框的宽度。 // height: 定义椭圆的边框的高度。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, int x, int y, int width, int height); 可以看到,绘制椭圆的API,都只支持输入一个矩形区域,长和宽都只能是水平或者竖直的,因此,绘制出椭圆的长轴和短轴也是水平或竖直的,而无法绘制一个旋转过的椭圆,如下图:\n给定椭圆的4个顶点,绘制出椭圆。虽然不能直接调用API绘制椭圆,但是可以通过绘制4条连续的贝塞尔曲线来闭合成一个椭圆。\n1 2 3 4 5 6 7 8 9 10 // // 摘要: 用 System.Drawing.PointF 结构数组绘制一系列贝塞尔样条。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // points: System.Drawing.PointF 结构的数组,这些结构表示确定曲线的点。 // 此数组中的点数应为 3 的倍数加 1,如 4、7 或 10。 // // 异常: // T:System.ArgumentNullException: pen 为 null。- 或 -points 为 null。 public void DrawBeziers(Pen pen, PointF[] points); 使用这种方法实际上只需要知道椭圆的中心点,两个轴的长度以及旋转角度即可,而这些都可以通过4个顶点计算得到。\n根据以上的信息,依次计算出连续贝塞尔曲线的13个控制点,然后调用DrawBeziers绘制椭圆。\n各个点的位置如图:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /** * C# */ // MAGICAL CONSTANT to map ellipse to beziers // 2/3*(sqrt(2)-1) const double Ellipse2Beziers = 0.2761423749154; // GDI Bitmap using var bitmap = new System.Drawing.Bitmap(width, height); using var graphics = System.Drawing.Graphics.FromImage(bitmap); // 椭圆的4个顶点 PointF[] ellipse = new PointF[4] { point1, point2, point3, point4 }; // 两个轴的长度 double r1 = ellipse[2].DistanceTo(ellipse[3]); double r2 = ellipse[0].DistanceTo(ellipse[1]); // 旋转角度 double angle = -SysMath.Atan2(ellipse[2].Y - ellipse[3].Y, ellipse[2].X - ellipse[3].X); double sin = SysMath.Sin(angle); double cos = SysMath.Cos(angle); // 贝塞尔曲线控制点相对于中心点的偏移长度 SizeF offset = new SizeF((float)(r1 * Ellipse2Beziers), (float)(r2 * Ellipse2Beziers)); // 椭圆中心点 PointF center = new PointF((ellipse[0].X + ellipse[1].X) / 2f, (ellipse[0].Y + ellipse[1].Y) / 2f); // 贝塞尔曲线的控制点 PointF[] beziers = new PointF[13] { new PointF((float)(center.X - r1 / 2.0), center.Y), new PointF((float)(center.X - r1 / 2.0), center.Y - offset.Height), new PointF(center.X - offset.Width, (float)(center.Y - r2 / 2.0)), new PointF(center.X, (float)(center.Y - r2 / 2.0)), new PointF(center.X + offset.Width, (float)(center.Y - r2 / 2.0)), new PointF((float)(center.X + r1 / 2.0), center.Y - offset.Height), new PointF((float)(center.X + r1 / 2.0), center.Y), new PointF((float)(center.X + r1 / 2.0), center.Y + offset.Height), new PointF(center.X + offset.Width, (float)(center.Y + r2 / 2.0)), new PointF(center.X, (float)(center.Y + r2 / 2.0)), new PointF(center.X - offset.Width, (float)(center.Y + r2 / 2.0)), new PointF((float)(center.X - r1 / 2.0), center.Y + offset.Height), new PointF((float)(center.X - r1 / 2.0), center.Y) }; // 旋转变换 double offsetX = center.X - center.X * cos - center.Y * sin; double offsetY = center.Y + center.X * sin - center.Y * cos; for (int j = 0; j \u0026lt; beziers.Length; j++) { beziers[j] = new PointF( (float)(beziers[j].X * cos + beziers[j].Y * sin + offsetX), (float)(beziers[j].Y * cos - beziers[j].X * sin + offsetY)); } // 绘制曲线 graphics.DrawBeziers(new Pen(Brushes.White, 1f)/* Pen */, beziers); 通过上面代码就可以完美的绘制出一个旋转任意角度的椭圆了。\n参考\nMFC上如何绘制一个可以旋转的椭圆\nDrawing Rotated and Skewed Ellipses\n","permalink":"https://kira-96.github.io/notes/draw-rotate-ellipse-with-gdi/","summary":"GDI+绘制椭圆时只支持输入一个矩形范围,无法绘制倾斜的椭圆。 绘制椭圆的 API: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30","title":"GDI+绘制旋转的椭圆"},{"content":"规则\n私有 Tag (gggg, xxxx)\nGroup number (gggg) 必须为奇数 (odd),并且 (0001, xxxx),(0003, xxxx),(0005, xxxx),(0007, xxxx),(FFFF, xxxx) 不允许使用。\n(gggg, 0000) were Group Length Elements, which have been retired.(已弃用)\n(gggg, 0001-000F),(gggg, 0100-0FFF) 不允许使用。\n(gggg, 0010-00FF) 供私有tag创建者(Private Creator)使用,用于在该group中插入一个未使用的标识码(identification code),私有标识码的VR应该为LO (Long String),VM应该为1。\n(gggg, 1000-FFFF) 为 Data Element。\nPrivate Creator 和 Data Element 的对应关系为:\n例:\nData Element (0029, 1000-10FF) 的 Private Creator 是 (0029, 0010)\nData Element (0029, 1100-11FF) 的 Private Creator 是 (0029, 0011)\nData Element (0029, 1200-12FF) 的 Private Creator 是 (0029, 0012)\n……\nData Element (0029, FF00-FFFF) 的 Private Creator 是 (0029, 00FF)\n标准\nhttp://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.8.html\n","permalink":"https://kira-96.github.io/notes/private-dicom-element/","summary":"规则 私有 Tag (gggg, xxxx) Group number (gggg) 必须为奇数 (odd),并且 (0001, xxxx),(0003, xxxx),(0005, xxxx),(0007, xxxx),(FFF","title":"Private Dicom Element"},{"content":"说明 这里是个人工作时常用的一些git命令,现在越来越多了,小本本都快记不下了,这里稍微做一下整理。\n工具下载 首先是git的下载地址:\n官网:https://git-scm.com/ taobao镜像:https://npm.taobao.org/mirrors/git-for-windows/ 由于官网的下载速度很慢,推荐使用taobao镜像的下载地址。\n设置用户名和邮箱 全局配置\n1 2 $ git config --global user.name [name] $ git config --global user.email [email] 配置当前仓库\n1 2 $ git config user.name [name] $ git config user.email [email] 查看用户名和邮箱\n1 2 $ git config user.name $ git config user.email 生成SSH密钥 1 $ ssh-keygen -t rsa -C \u0026#34;[email]\u0026#34; 执行完毕和用户目录下就会生成一个**.ssh**文件夹。\n拉取远程代码库 可以直接clone远程代码仓库(推荐)\n1 $ git clone https://github.com/libgit2/libgit2 mylibgit 先初始化仓库,再拉取代码\n1 2 3 4 5 $ cd ./mylibgit $ git init $ git remote add origin git@github.com:libgit2/libgit2.git $ git pull origin master $ git push -u origin master 提交代码变更 查看工作区变更\n1 $ git status 添加文件到暂存区\n1 $ git add README.md 添加所有变更文件到暂存区\n1 $ git add . 提交到本地仓库\n1 $ git commit -m \u0026#39;Update README\u0026#39; 推送到远程代码仓库(main是分支)\n1 $ git push origin main 版本切换 查看版本/提交记录\n1 $ git log 查看版本/提交简介\n1 $ git log --pertty=oneline 撤销提交,保留代码变更 撤销上次提交\n1 $ git reset --soft HEAD^ 撤销前n次提交\n1 $ git reset --soft HEAD~n 回退到某次提交记录\n1 $ git reset --soft commit-id 版本回退,不保留代码变更 回退到当前最新提交\n1 $ git reset --hard HEAD 回退到上一版本\n1 $ git reset --hard HEAD^ 回退到之前的第n个版本\n1 $ git reset --hard HEAD~n 回退到某个版本/重新切换回未来版本\n1 $ git reset --hard commit-id 强制推送:在已经推送到远程的记录又被修改的情况下\n1 $ git push origin main --force 1 $ git push -f origin main 分支管理 查看当前仓库分支\n1 $ git branch 查看当前仓库以及远程仓库所有分支\n1 $ git branch -a 在当前分支的基础上创建新分支\n1 $ git checkout -b 分支名 删除已合并的本地分支\n1 $ git branch -d 分支名 删除未合并的本地分支\n1 $ git branch -D 分支名 删除远程仓库分支\n1 $ git push origin -d 分支名 或\n1 $ git push origin :分支名 删除远程已经删除的分支\n1 $ git remote prune origin 标签-tag tag和分支操作类似\n查看tag\n1 $ git tag 给当前版本添加tag\n1 $ git tag 标签名 给某一版本添加tag\n1 $ git tag 标签名 commit-id 删除标签\n1 $ git tag -d 标签名 删除远程标签\n1 $ git push origin -d 标签名 推送标签到远程仓库\n1 $ git push origin 标签名 子模块(submodule) 当前仓库添加子模块\n1 $ git submodule add \u0026lt;url\u0026gt; \u0026lt;path\u0026gt; 拉取仓库后拉取子模块\n1 2 $ git submodule init $ git submodule update 1 $ git submodule update --init 或\n1 $ git clone --recurse-submodules 更新子模块\n1 $ git submodule update --remote \u0026lt;submodule\u0026gt; 删除子模块\n1 2 $ git submodule deinit \u0026lt;submodule\u0026gt; $ git rm --cached \u0026lt;submodule\u0026gt; 保持fork之后的仓库和上游同步 遇到一些好的代码仓库,有时候会fork一份到自己的账号,但一旦原来的代码仓库有了新的提交,如何保持自己的仓库和源仓库代码同步呢?\n一种方式是通过远程仓库向fork之后的仓库提交一个PR,但这样会导致提交记录不一致,非常EP,在网上搜罗了好久,终于找到一个完美的方法。\n首先需要将fork之后的仓库clone到本地。\n然后设置本地仓库的上游仓库地址为源仓库\n1 $ git remote add upstream git@github.com:kira-96/myblog.git 同步上游仓库变更\n1 2 3 $ git fetch upstream $ git checkout main $ git merge upstream/main 推送到远程仓库\n1 $ git push origin main 参考\ngit思维导图 保持fork之后的项目和上游同步 ","permalink":"https://kira-96.github.io/posts/%E4%B8%80%E4%BA%9B%E5%B8%B8%E7%94%A8%E7%9A%84git%E5%91%BD%E4%BB%A4/","summary":"说明 这里是个人工作时常用的一些git命令,现在越来越多了,小本本都快记不下了,这里稍微做一下整理。 工具下载 首先是git的下载地址: 官网:ht","title":"一些常用的git命令"},{"content":" 食用方法 Step 1\n将ChineseSimplified.isl放到Inno Setup安装目录下的\u0026quot;Languages\u0026quot;文件夹里面\nStep 2\n如果你是通过新建脚本的方式创建脚本,在Languages选项勾选Chinese Simplified即可:\n如果你需要在现有脚本中添加简体中文支持 直接在你的脚本的[Languages]部分添加下面一行即可\n1 Name: \u0026#34;chinesesimplified\u0026#34;; MessagesFile: \u0026#34;compiler:Languages\\ChineseSimplified.isl\u0026#34; 示例:\n1 2 3 [Languages] Name: \u0026#34;english\u0026#34;; MessagesFile: \u0026#34;compiler:Default.isl\u0026#34; Name: \u0026#34;chinesesimplified\u0026#34;; MessagesFile: \u0026#34;compiler:Languages\\ChineseSimplified.isl\u0026#34; 注意:此翻译版本支持 Inno Setup 6.1.0+ 的软件,Inno Setup 5 的翻译文件在这里\n查看6.1.0+和6.0.0+的区别\n查看6.0.3+和6.0.0+的区别\n链接 Inno Setup issrc ","permalink":"https://kira-96.github.io/Inno-Setup-Chinese-Simplified-Translation/","summary":"食用方法 Step 1 将ChineseSimplified.isl放到Inno Setup安装目录下的\u0026quot;Languages\u0026quot;文件夹里","title":"Inno Setup 简体中文语言包"},{"content":"简介 Prism是一个用于WPF、Xamarin Forms、WinUI等的MVVM框架,刚刚学习,这里只是个人总结的一些知识点笔记。\nIoC IContainerProvider\n1 2 3 4 protected override Window CreateShell() { return Container.Resolve\u0026lt;MainWindow\u0026gt;(); } 1 2 3 4 5 6 public void OnInitialized(IContainerProvider containerProvider) { var regionManager = containerProvider.Resolve\u0026lt;IRegionManager\u0026gt;(); var viewA = containerProvider.Resolve\u0026lt;ViewA\u0026gt;(); ... } IContainerRegistry\n1 2 3 4 5 6 7 8 9 // App.xaml.cs protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.Register\u0026lt;IApplicationCommands, ApplicationCommands\u0026gt;(); containerRegistry.RegisterDialog\u0026lt;NotificationDialog, NotificationDialogViewModel\u0026gt;(); containerRegistry.RegisterForNavigation\u0026lt;Page1\u0026gt;(); containerRegistry.RegisterForNavigation\u0026lt;Page2\u0026gt;(); ... } Module IModule\n1 2 3 4 5 6 7 8 9 10 public class SimpleModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { } public void RegisterTypes(IContainerRegistry containerRegistry) { } } 使用App.config加载模块\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!-- App.config --\u0026gt; \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;configSections\u0026gt; \u0026lt;section name=\u0026#34;modules\u0026#34; type=\u0026#34;Prism.Modularity.ModulesConfigurationSection, Prism.Wpf\u0026#34; /\u0026gt; \u0026lt;/configSections\u0026gt; \u0026lt;startup\u0026gt; \u0026lt;/startup\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module assemblyFile=\u0026#34;Simple.dll\u0026#34; moduleType=\u0026#34;Simple.SimpleModule, Simple, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\u0026#34; moduleName=\u0026#34;SimpleModule\u0026#34; startupLoaded=\u0026#34;True\u0026#34; /\u0026gt; \u0026lt;/modules\u0026gt; \u0026lt;/configuration\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override IModuleCatalog CreateModuleCatalog() { return new ConfigurationModuleCatalog(); } ... } 直接引用加载模块\n1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule\u0026lt;SimpleModule\u0026gt;(); } ... } 指定模块文件夹\n1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = @\u0026#34;.\\Modules\u0026#34; }; } ... } 使用ModuleCatalog加载模块\n1 2 3 4 5 6 7 8 \u0026lt;m:ModuleCatalog xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:m=\u0026#34;clr-namespace:Prism.Modularity;assembly=Prism.Wpf\u0026#34;\u0026gt; \u0026lt;m:ModuleInfo ModuleName=\u0026#34;Simple\u0026#34; ModuleType=\u0026#34;Simple.SimpleModule, Simple, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\u0026#34; /\u0026gt; \u0026lt;/m:ModuleCatalog\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override IModuleCatalog CreateModuleCatalog() { return ModuleCatalog.CreateFromXaml(new Uri(\u0026#34;/Modules;component/ModuleCatalog.xaml\u0026#34;, UriKind.Relative)); } ... } Command DelegateCommand\n1 2 3 4 5 6 7 8 9 10 11 12 public DelegateCommand ExecuteDelegateCommand { get; } public DelegateCommand\u0026lt;string\u0026gt; ExecuteGenericDelegateCommand { get; } public DelegateCommand DelegateCommandObservesProperty { get; } public DelegateCommand DelegateCommandObservesCanExecute { get; } ExecuteDelegateCommand = new DelegateCommand(Execute, CanExecute); DelegateCommandObservesProperty = new DelegateCommand(Execute, CanExecute).ObservesProperty(() =\u0026gt; IsEnabled); DelegateCommandObservesCanExecute = new DelegateCommand(Execute).ObservesCanExecute(() =\u0026gt; IsEnabled); ExecuteGenericDelegateCommand = new DelegateCommand\u0026lt;string\u0026gt;(ExecuteGeneric).ObservesCanExecute(() =\u0026gt; IsEnabled); CompositeCommand\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public CompositeCommand SampleCommand { get; } = new CompositeCommand(true); ... DelegateCommand UpdateCommand = new DelegateCommand(Update).ObservesCanExecute(() =\u0026gt; CanUpdate); SampleCommand.RegisterCommand(UpdateCommand); ... private void OnIsActiveChanged() { UpdateCommand.IsActive = IsActive; IsActiveChanged?.Invoke(this, new EventArgs()); } Event To Command\n1 2 3 4 5 6 \u0026lt;i:Interaction.Triggers\u0026gt; \u0026lt;i:EventTrigger EventName=\u0026#34;SelectionChanged\u0026#34;\u0026gt; \u0026lt;prism:InvokeCommandAction Command=\u0026#34;{Binding PersonSelectedCommand}\u0026#34; CommandParameter=\u0026#34;{Binding ElementName=ListOfPerson, Path=SelectedItem}\u0026#34; /\u0026gt; \u0026lt;/i:EventTrigger\u0026gt; \u0026lt;/i:Interaction.Triggers\u0026gt; BindableBase 1 2 3 4 public class ViewAViewModel : BindableBase, IActiveAware { ... } ViewModelLocator AutoWireViewModel\n1 2 3 4 \u0026lt;Window x:Class=\u0026#34;Demo.Views.MainWindow\u0026#34; ... xmlns:prism=\u0026#34;http://prismlibrary.com/\u0026#34; prism:ViewModelLocator.AutoWireViewModel=\u0026#34;True\u0026#34;\u0026gt; 更改命名约定\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // App.xaml.cs public partial class App : PrismApplication { ... protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =\u0026gt; { var viewName = viewType.FullName.Replace(\u0026#34;.ViewModels.\u0026#34;, \u0026#34;.CustomNamespace.\u0026#34;); var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName; var viewModelName = $\u0026#34;{viewName}ViewModel, {viewAssemblyName}\u0026#34;; return Type.GetType(viewModelName); }); } ... } 自定义ViewModel注册\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // App.xaml.cs public partial class App : PrismApplication { ... protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); // type / type ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel)); // type / factory ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () =\u0026gt; Container.Resolve\u0026lt;CustomViewModel\u0026gt;()); // generic factory ViewModelLocationProvider.Register\u0026lt;MainWindow\u0026gt;(() =\u0026gt; Container.Resolve\u0026lt;CustomViewModel\u0026gt;()); // generic type ViewModelLocationProvider.Register\u0026lt;MainWindow, CustomViewModel\u0026gt;(); } ... } EventAggregator IEventAggragator\n1 2 3 4 public interface IEventAggregator { TEventType GetEvent\u0026lt;TEventType\u0026gt;() where TEventType : EventBase; } 创建消息事件类\n1 2 3 public class SimpleMessageEvent : PubSubEvent\u0026lt;string\u0026gt; { } 订阅事件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 private readonly IEventAggregator eventAggregator; public MainPageViewModel(IEventAggregator ea) { eventAggregator = ea; ea.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Subscribe(ShowMessage); // Subscribing on the UI Thread // ea.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Subscribe(ShowMessage, ThreadOption.UIThread); } public void ShowMessage(string payload) { // TODO } 发布消息\n1 eventAggregator.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Publish(\u0026#34;Hello!\u0026#34;); 筛选订阅\n1 ea.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Subscribe(ShowMessage, ThreadOption.UIThread, keepSubscriberReferenceAlive, x =\u0026gt; x.Contains(\u0026#34; \u0026#34;)); 取消订阅\n1 eventAggregator.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Unsubscribe(ShowMessage); RegionManager 1 2 3 4 5 6 7 8 9 \u0026lt;Window x:Class=\u0026#34;Regions.Views.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:prism=\u0026#34;http://prismlibrary.com/\u0026#34; Title=\u0026#34;Shell\u0026#34;\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;ContentControl prism:RegionManager.RegionName=\u0026#34;ContentRegion\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; 1 2 3 // IContainerProvider containerProvider var regionManager = containerProvider.Resolve\u0026lt;IRegionManager\u0026gt;(); regionManager.RegisterViewWithRegion(\u0026#34;ContentRegion\u0026#34;, typeof(ViewA)); 1 2 3 4 5 6 7 // IContainerProvider containerProvider var regionManager = containerProvider.Resolve\u0026lt;IRegionManager\u0026gt;(); var region = regionManager.Regions[\u0026#34;ContentRegion\u0026#34;]; region.Add(containerProvider.Resolve\u0026lt;ViewA\u0026gt;()); region.Add(containerProvider.Resolve\u0026lt;ViewB\u0026gt;()); region.Add(containerProvider.Resolve\u0026lt;ViewC\u0026gt;()); RegionNavigation 1 2 // IRegionManager regionManager regionManager.RequestNavigate(regionName: \u0026#34;NavigateRegion\u0026#34;, source: \u0026#34;Page1\u0026#34;); Navigation Callback\n1 2 3 4 5 6 7 // IRegionManager regionManager regionManager.RequestNavigate(regionName: \u0026#34;NavigateRegion\u0026#34;, source: \u0026#34;Page1\u0026#34;, navigationCallback: NavigationComplete); private void NavigationComplete(NavigationResult result) { dialogService.ShowDialog(\u0026#34;NotificationDialog\u0026#34;, new DialogParameters($\u0026#34;message=Navigate to {result.Context.Uri} complete.\u0026#34;), null); } Navigation Parameters\n1 2 3 4 5 6 var parameters = new NavigationParameters { { \u0026#34;content\u0026#34;, \u0026#34;Hello!\u0026#34; } }; // IRegionManager regionManager regionManager.RequestNavigate(regionName: \u0026#34;NavigateRegion\u0026#34;, source: \u0026#34;Page1\u0026#34;, navigationParameters: parameters); INavigationAware\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Page1ViewModel : BindableBase, INavigationAware { ... public bool IsNavigationTarget(NavigationContext navigationContext) { return true; } public void OnNavigatedFrom(NavigationContext navigationContext) { } public void OnNavigatedTo(NavigationContext navigationContext) { if (navigationContext.Parameters[\u0026#34;content\u0026#34;] is string content) { // TODO } } ... } IConfirmNavigationRequest\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Page1ViewModel : BindableBase, IConfirmNavigationRequest { ... public void ConfirmNavigationRequest(NavigationContext navigationContext, Action\u0026lt;bool\u0026gt; continuationCallback) { bool result = true; ButtonResult buttonResult = ButtonResult.None; dialogService.ShowDialog(\u0026#34;NotificationDialog\u0026#34;, new DialogParameters($\u0026#34;message=Do you to navigate?\u0026#34;), res =\u0026gt; { buttonResult = res.Result; }); if (buttonResult != ButtonResult.OK) result = false; continuationCallback(result); } ... } IRegionMemberLifetime\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Page1ViewModel : BindableBase, INavigationAware, IRegionMemberLifetime { public bool KeepAlive { get { return false; } } public bool IsNavigationTarget(NavigationContext navigationContext) { return false; } public void OnNavigatedFrom(NavigationContext navigationContext) { } public void OnNavigatedTo(NavigationContext navigationContext) { } } Navigation Journal\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Page1ViewModel : BindableBase, INavigationAware { private IRegionNavigationJournal journal; public DelegateCommand GoForwardCommand { get; } public DelegateCommand GoBackCommand { get; } public Page1ViewModel() { GoForwardCommand = new DelegateCommand(GoForward, CanGoForward); GoBackCommand = new DelegateCommand(GoBack); } ... public bool IsNavigationTarget(NavigationContext navigationContext) { return true; } public void OnNavigatedFrom(NavigationContext navigationContext) { } public void OnNavigatedTo(NavigationContext navigationContext) { journal = navigationContext.NavigationService.Journal; GoForwardCommand.RaiseCanExecuteChanged(); } ... private bool CanGoForward() { return journal != null \u0026amp;\u0026amp; journal.CanGoForward; } private void GoForward() { journal?.GoForward(); } private void GoBack() { journal?.GoBack(); } } DialogService See DOC. Dialog Service\n1 2 3 4 5 6 // DialogServiceModule.cs public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterDialog\u0026lt;NotificationDialog, NotificationDialogViewModel\u0026gt;(); // containerRegistry.RegisterDialogWindow\u0026lt;MyRibbonWindow\u0026gt;(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 // viewmodel private readonly IDialogService dialogService; public MainViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void NavigationComplete(NavigationResult result) { // Show Dialog with parameters. dialogService.ShowDialog(\u0026#34;NotificationDialog\u0026#34;, new DialogParameters($\u0026#34;message=Navigate to {result.Context.Uri} complete.\u0026#34;), null); } 参考 Documentation Prism-Samples-Wpf ","permalink":"https://kira-96.github.io/posts/prism-note/","summary":"简介 Prism是一个用于WPF、Xamarin Forms、WinUI等的MVVM框架,刚刚学习,这里只是个人总结的一些知识点笔记。 IoC IContainerProvider 1 2 3","title":"Prism note"},{"content":"简介 Inno Setup是一个免费的安装包生成软件,完全开源免费,使用起来也非常方便,文档也十分全面。与其它同类软件相比十分的小巧便携,功能也十分全面。\n近期Inno Setup的6.1.0版本也即将发布,也带来了更多的新功能。由于正式版本还没有发布,这里就使用的先行版本。\n下载页面 Inno Setup 6.1版本新增了安装过程中的下载页面,在所有选项准备完毕,正式开始安装之前可以下载需要的文件。官方也给出了下载示例的代码 CodeDownloadFiles.iss。\n[Code] var DownloadPage: TDownloadWizardPage; function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; begin if Progress = ProgressMax then Log(Format(\u0026#39;Successfully downloaded file to {tmp}: %s\u0026#39;, [FileName])); Result := True; end; procedure InitializeWizard; begin DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadProgress); end; function NextButtonClick(CurPageID: Integer): Boolean; begin if CurPageID = wpReady then begin DownloadPage.Clear; DownloadPage.Add(\u0026#39;https://files.jrsoftware.org/is/6/innosetup-6.1.0-dev.exe\u0026#39;, \u0026#39;innosetup-6.1.0-dev.exe\u0026#39;, \u0026#39;\u0026#39;); DownloadPage.Add(\u0026#39;https://jrsoftware.org/download.php/iscrypt.dll\u0026#39;, \u0026#39;ISCrypt.dll\u0026#39;, \u0026#39;2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc\u0026#39;); DownloadPage.Show; try try DownloadPage.Download; Result := True; except SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); Result := False; end; finally DownloadPage.Hide; end; end else Result := True; end; 上面代码在初始化时创建了一个下载页面,并在wpReady之后显示。\nCreateDownloadPage的原型:\nfunction CreateDownloadPage(const ACaption, ADescription: String; const OnDownloadProgress: TOnDownloadProgress): TDownloadWizardPage; 创建一个下载页面用于下载文件并显示进度。\n前两个参数指定页面的标题和页面描述,第3个参数是在下载进度更新后的回调函数TOnDownloadProgress,可以指定为nil(空)。\nTOnDownloadProgress = function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean; CreateDownloadPage返回TDownloadWizardPage类型:\nTDownloadWizardPage = class(TOutputProgressWizardPage) property AbortButton: TNewButton; read; procedure Add(const Url, BaseName, RequiredSHA256OfFile: String); procedure Clear; function Download: Int64; end; 可以看到,TDownloadWizardPage有一个Add的方法,用于新增一个下载任务,它有3个参数:\nUrl:下载链接,BaseName:下载后的文件名称\nRequiredSHA256OfFile:文件的哈希值,用于校验下载文件,值为空时,则忽略校验\nClear方法清空下载任务列表,Download方法开始下载任务。\n具体的使用可以看上面的NextButtonClick函数里的写法。\n另一个方法是使用DownloadTemporaryFile函数:\nfunction DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64; [Code] function OnDownloadProgress(const Url, Filename: string; const Progress, ProgressMax: Int64): Boolean; begin if ProgressMax \u0026lt;\u0026gt; 0 then Log(Format(\u0026#39; %d of %d bytes done.\u0026#39;, [Progress, ProgressMax])) else Log(Format(\u0026#39; %d bytes done.\u0026#39;, [Progress])); Result := True; end; function InitializeSetup: Boolean; begin try DownloadTemporaryFile(\u0026#39;https://jrsoftware.org/download.php/is.exe\u0026#39;, \u0026#39;innosetup-latest.exe\u0026#39;, \u0026#39;\u0026#39;, @OnDownloadProgress); DownloadTemporaryFile(\u0026#39;https://jrsoftware.org/download.php/iscrypt.dll\u0026#39;, \u0026#39;ISCrypt.dll\u0026#39;, \u0026#39;2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc\u0026#39;, @OnDownloadProgress); Result := True; except Log(GetExceptionMessage); Result := False; end; end; 使用起来和前一种方法有所不同,但大致都是类似的,这里不再赘述。\n消息框设计器 软件的工具菜单(Tools)中新增了消息框设计器(MessageBox Designer)工具。\n工具提供了两种消息框,Message Box和Task Dialog Message Box,工具可以设置对话框的图标,按钮和默认选项等。\n将鼠标指针放在需要插入对话框的代码位置,打开MessageBox Designer,完成选项后点击OK即可,然后就可以看到先前鼠标所在的位置插入了一段MessageBox代码。\n1 2 3 [CustomMessages] DownloadComplete=下载完成 DownloadCompleteMessage=下载已完成。 // Display a message box SuppressibleTaskDialogMsgBox(CustomMessage(\u0026#39;DownloadComplete\u0026#39;), CustomMessage(\u0026#39;DownloadCompleteMessage\u0026#39;), mbInformation, MB_OK, [\u0026#39;OK\u0026#39;], 0, IDOK); 链接 Inno Setup 6 Revision History Inno Setup 简体中文翻译 ","permalink":"https://kira-96.github.io/posts/inno-setup-6.1.0-%E6%96%B0%E5%A2%9E%E7%9A%84%E5%8A%9F%E8%83%BD%E4%BD%93%E9%AA%8C/","summary":"简介 Inno Setup是一个免费的安装包生成软件,完全开源免费,使用起来也非常方便,文档也十分全面。与其它同类软件相比十分的小巧便携,功能也十分全","title":"Inno Setup 6.1.0 新增的功能体验"},{"content":"简介 在编码的时候难免会遇到不同编程语言之间的接口调用,其中最通用的就是C的动态链接库,几乎所有语言都可以调用C的接口函数。那么这种关系能否反过来呢?用C调用其它的函数接口,当然也是可以的,只需要将函数指针作为参数传递进去就可以了。\nC#中使用Delegate来表示函数指针。\n准备工作 首先新建一个C++的动态库,随便定义一个函数指针类型,以及一个导出函数。大致内容如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include \u0026#34;pch.h\u0026#34; #include \u0026lt;iostream\u0026gt; using namespace std; #ifdef DYNLIB_EXPORTS #define DLL_API extern \u0026#34;C\u0026#34; __declspec(dllexport) #else #define DLL_API extern \u0026#34;C\u0026#34; __declspec(dllimport) #endif typedef struct { int Left; int Top; int Right; int Bottom; } MyRect, * MyRectPtr; // 函数指针 typedef VOID(CALLBACK* PRINTCALLBACK)(MyRectPtr); BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } // 导出函数,使用上面定义的函数指针类型作为参数 DLL_API VOID Print(MyRectPtr pRect, PRINTCALLBACK callback) { if (callback == NULL) { cout \u0026lt;\u0026lt; \u0026#39;\\t\u0026#39; \u0026lt;\u0026lt; pRect-\u0026gt;Top \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; pRect-\u0026gt;Left \u0026lt;\u0026lt; \u0026#34;\\t\\t\u0026#34; \u0026lt;\u0026lt; pRect-\u0026gt;Right \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#39;\\t\u0026#39; \u0026lt;\u0026lt; pRect-\u0026gt;Bottom \u0026lt;\u0026lt; endl; } else { callback(pRect); } } 准备好了C++的部分,再来写C#部分的代码:\n首先定义一个和C++部分相同的MyRect结构体和函数指针类型delegate\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 对应C++部分的MyRect [StructLayout(LayoutKind.Sequential)] struct MyRect { public int Left; public int Top; public int Right; public int Bottom; } // 对应C++部分的 PRINTCALLBACK [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate void PrintRect(ref MyRect myRect); 这样准备工作就做完了,一定要保证C++部分和C#部分定义的数据类型和接口一致,不然调用时会出问题的。\n调用 导入C++动态库的函数入口\n1 2 [DllImport(\u0026#34;DynLib.dll\u0026#34;, EntryPoint = \u0026#34;#1\u0026#34;, CallingConvention = CallingConvention.Cdecl)] public static extern void PrintInCpp(ref MyRect pRect, PrintRect callback); 然后定义好C#这边的delegate实例,就完成了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void Print(ref MyRect myRect) { Console.WriteLine(\u0026#34;C# Print func called.\u0026#34;); Console.WriteLine(\u0026#34;[({0},{1}),({2},{3})]\u0026#34;, myRect.Left, myRect.Top, myRect.Right, myRect.Bottom); } static void Main() { var rect = new MyRect() { Left = 100, Top = 100, Right = 220, Bottom = 200 }; PrintInCpp(ref rect, null); // callback为null PrintInCpp(ref rect, new PrintRect(Print)); Console.WriteLine(\u0026#34;Press any key exit...\u0026#34;); Console.ReadKey(true); } 这里调用了两次接口函数,为了方便看出区别,在callback参数为NULL时,会由c++打印结果,否则c++会调用外部的函数接口。\n输出结果:\n1 2 3 4 5 6 7 ./delegatefunc 100 100 220 200 C# Print func called. [(100,100),(220,200)] Press any key exit... ","permalink":"https://kira-96.github.io/posts/%E4%BC%A0%E9%80%92%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E5%88%B0c__/","summary":"简介 在编码的时候难免会遇到不同编程语言之间的接口调用,其中最通用的就是C的动态链接库,几乎所有语言都可以调用C的接口函数。那么这种关系能否反","title":"C#传递函数指针到C++"},{"content":"常用图像像素相关的一些Tag Tag VR Keyword (0028,0002) US Samples Per Pixel (0028,0004) CS Photometric Interpretation (0028,0006) US Planar Configuration (0028,0010) US Rows (0028,0011) US Columns (0028,0100) US Bits Allocated (0028,0101) US Bits Stored (0028,0102) US High Bit (0028,0103) US Pixel Representation (7FE0,0010) OB/OW Pixel Data 相关Tag说明 Sample Per Pixel Samples per Pixel (0028,0002) is the number of separate planes in this image. One and three image planes are defined. Other numbers of image planes are allowed, but their meaning is not defined by this Standard.\nFor monochrome (gray scale) and palette color images, the number of planes is 1. For RGB and other three vector color models, the value of this Attribute is 3.\nSamples Per Pixel 指此图像中平面的个数。对于灰度图像,它的值为1,对于RGB等彩色图像,它的值为3。 听起来可能比较拗口,简单解释一下,对于灰度的图像,它只有一个灰度值,所以是1,而彩色的图像通常是由RGB三个通道混合而成,它的值为3。\nPhotometric Interpretation 指定解析图像像素的格式。个人比较习惯叫它图像类型,可以根据这个tag判断图像是灰度还是彩色图像。\nMONOCHROME1\n灰度图,最小值显示为白色,像素值越大就越黑。\nMONOCHROME2\n灰度图,最小值显示为黑色,像素值越大就越亮。这应该是最常用的格式了。\nPALETTE COLOR\n自带调色板的图像,显示出来是彩色的,所以属于彩图。当使用它时,Samples Per Pixel的值必须为1。并且必须要有RGB三种颜色的调色板查找表,它的像素值(Pixel Data)用于查找表。\nThe pixel value is used as an index into each of the Red, Blue, and Green Palette Color Lookup Tables (0028,1101-1103\u0026amp;1201-1203).\nRGB\n彩色图像,每个像素由RGB三种颜色组成,Samples Per Pixel值必须为3。\nYBR_FULL\n通过色度信号来表示颜色的格式,每个像素由一个亮度Y(luminance)和两个色度Cb(蓝色)、Cr(红色)组成。Samples Per Pixel值为3。 $$Y=+0.2990R+0.5870G+0.1140B$$ $$Cb=-0.1687R-0.3313G+0.5000B+128$$ $$Cr=+0.5000R-0.4187G-0.0813B+128$$\nYBR_FULL_422\n类似于YBR_FULL,通过色度信号来表示颜色的格式,每个像素点都有对应的亮度Y(luminance),每两个像素点采集一次色度信号,缺少的色度信息通过内插补点的方式运算得到。 Samples Per Pixel的值应该为3,Planar Configuration的值必须是0,像素存储的格式为:Y, Y, Cb, Cr, ...\nYBR_PARTIAL_422(Retired)\n类似于YBR_FULL_422,通过色度信号来表示颜色的格式,不过亮度和色度的计算方式和YBR_FULL的计算方式不同。 $$Y=+0.2568R+0.5041G+0.0979B+16$$ $$Cb=-0.1482R-0.2910G+0.4392B+128$$ $$Cr=+0.4392R-0.3678G-0.0714B+128$$\nYBR_PARTIAL_420\n类似于YBR_PARTIAL_422,通过色度信号来表示颜色的格式,不同的是,用4:2:2的采样方式时,行方向的色度信息会被丢掉一半,而4:2:0的采样方式,不仅会把行方向的色度信息丢掉一半,列方向的色度信息也会被丢掉一半。色度的采样(Cb,Cr)只有亮度Y(luminance)的 1/4。 Samples Per Pixel的值应该为3,Planar Configuration的值必须是0。\nYBR_ICT\nIrreversible Color Transformation.(不可逆颜色变换)\nYCbCr的计算方式和YBR_FULL一样。Y为0时表示黑色,Cb,Cr都为0时表示没有颜色。 JPEG 2000有损压缩的彩色图像。Samples Per Pixel的值应该为3,Planar Configuration的值必须是0。\nYBR_RCT\nReversible Color Transformation.(可逆颜色变换)\nJPEG 2000无损压缩的彩色图像。Samples Per Pixel的值应该为3。 从RGB转换到YBR_RCT $$Y=floor(\\frac{R+2G+B}{4})$$ $$Cb=B-G$$ $$Cr=R-G$$ 从YBR_RCT转换到RGB $$R=Cr+G$$ $$G=Y-floor(\\frac{Cb+Cr}{4})$$ $$B=Cb+G$$\n不再使用的格式\nHSV、ARGB、CMYK\nPlanar Configuration 指定颜色是按照像素来排列的或是按平面(plane)来排列的。当Samples Per Pixel大于1时应设定此值。 当值为0时表示颜色按像素排列。对于RGB图像,像素的格式为:R1,G1,B1,R2,G2,B2,... 当值为1时表示颜色按平面排列。对于RGB图像,像素的格式为:R1,R2,R3,...Rn,G1,G2,G3,...Gn,B1,B2,B3,...Bn\nRows 图像的行数量,即图像的高(Height)。\nColumns 图像的列数量,即图像的宽(Width)。\nBits Allocated, Bits Stored, High Bit, Pixel Representation Bits Allocated指定每个像素分配多少位(bit)。值应当为1或者8的倍数。而对于图像像素,Bits Allocated的值通常为8或者16,实际上可以理解为每个像素分配多少字节,因为是8的倍数。\nBits Stored指定存储每个像素占用了多少位(bit),值不能大于Bits Allocated。\nHigh Bit则指定了像素的最高位,通常应该是Bits Stored - 1。\nPixel Representation指定了像素数据的类型。值只能为0或者1,对于彩色图像,值只能为0。 值为0时,表示像素为无符号整型(unsigned integer)。 值为1时,表示像素为2的补码,其实就是有符号整型,即允许存在负数。 这里一定要注意,如果不能正确处理负数,全部按照无符号整型来计算的话,就会遇到符号位的问题,即一个负数会变成一个很大的正数,图像上原本是黑色的区域会变得很亮。\nPixel Data 图像像素。一堆二进制数字,通常会存放在Dicom文件最后的位置。\n最后 其实,在写这篇文章之前,一些东西我都还是一知半解的,在写的过程中我也是不断的在查阅资料和源码。其中一些东西难免会掺杂了自己的理解,如果有错误的地方欢迎指正。\n参考\nDICOM Standard Browser 颜色空间 ","permalink":"https://kira-96.github.io/posts/dicom%E5%9B%BE%E5%83%8F%E5%83%8F%E7%B4%A0%E7%9B%B8%E5%85%B3tag%E8%AF%B4%E6%98%8E/","summary":"常用图像像素相关的一些Tag Tag VR Keyword (0028,0002) US Samples Per Pixel (0028,0004) CS Photometric Interpretation (0028,0006) US Planar Configuration (0028,0010) US Rows (0028,0011) US Columns (0028,0100) US Bits Allocated (0028,0101) US Bits Stored (0028,0102) US High Bit (0028,0103) US Pixel Representation (7FE0,0010) OB/OW Pixel Data 相关Tag说明 Sample Per Pixel Samples per Pixel (0028,0002)","title":"DICOM图像像素相关Tag说明"},{"content":"文档 The Rust Programming Language Second edition\nRust 程序设计语言(第二版)简体中文版\nRust by Example\n通过例子学 Rust\nAsync programming in Rust with async-std\nasync-std 中文文档\n其它 Rust Language Cheat Sheet\nRust Fundamentals\nRust语言中文社区\n","permalink":"https://kira-96.github.io/posts/rust-%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%BA%90%E6%94%B6%E9%9B%86/","summary":"文档 The Rust Programming Language Second edition Rust 程序设计语言(第二版)简体中文版 Rust by Example 通过例子学 Rust Async programming in Rust with async-std async-std 中文文档 其它 Rust Language Cheat Sheet Rust Fundamentals Rust语言中文社区","title":"Rust 学习资源收集"},{"content":"前言 由于我是从事医疗行业软件开发的,所以必不可少的会和图像打交道,最近刚刚好在做一个图像旋转相关的功能,借此又复习(预习)了一下线性代数,趁着没忘赶紧做一下笔记。\nDICOM 中与方位计算有关的 Tag 在开始之前,有必要先了解一下DICOM中与图像方位计算有关的几个Tag,主要有3个。\nTag Keyword (0020,0032) Image Position (Patient) (0020,0037) Image Orientation (Patient) 其中Image Position指的是图像左上角的像素在患者坐标系中的位置。 Image Orientation由6个数字组成,分别是图像的行(Row)方向和列(Column)方向的单位向量与x/y/z坐标轴夹角的余弦值(cosine)。\n有了上面两个Tag的值,就可以计算出图像在空间坐标系中的位置和方位了。\n计算法向量 现在我们已经有了图像平面上两个垂直的向量,行和列方向的向量,使用行列式就能计算出图像所在平面的法向量了。\n$$u × v = \\left[\\begin{matrix} i \u0026amp; j \u0026amp; k \\\\ u_1 \u0026amp; u_2 \u0026amp; u_3 \\\\ v_1 \u0026amp; v_2 \u0026amp; v_3 \\end{matrix}\\right]$$\n具体怎么算,可以看这里,讲得很详细。\n代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 double[] vector1 = new[] {orientation[0], orientation[1], orientation[2]}; double[] vector2 = new[] {orientation[3], orientation[4], orientation[5]}; double[] normal = new double[] { 0, 0, 0 }; normal[0] = vector1[1] * vector2[2] - vector1[2] * vector2[1]; normal[1] = vector1[2] * vector2[0] - vector1[0] * vector2[2]; normal[2] = vector1[0] * vector2[1] - vector1[1] * vector2[0]; var temp = Math.Sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); normal[0] /= temp; normal[1] /= temp; normal[2] /= temp; 其中orientation就是Image Orientation中的6个值。计算得到的normal就是图像所在平面的法向量的单位向量。\n图像方位矩阵 在3D图形变换中经常使用的是4维矩阵,把图像的位置信息也放到矩阵中,可以方便的进行位置变换的计算。\n这里分别用u,v,w表示图像的行/列/法线方向向量,S代表图像位置。\n$$u=(u_1,u_2,u_3)$$ $$v=(v_1,v_2,v_3)$$ $$w=(w_1,w_2,w_3)$$ $$S=(s_x,s_y,s_z)$$\n4维矩阵则表示为\n$$matrix=\\left[\\begin{matrix} u_1 \u0026amp; v_1 \u0026amp; w_1 \u0026amp; s_x \\\\ u_2 \u0026amp; v_2 \u0026amp; w_2 \u0026amp; s_y \\\\ u_3 \u0026amp; v_3 \u0026amp; w_3 \u0026amp; s_z \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n图像旋转 先来看特殊情况下的旋转,即矩阵绕坐标轴的旋转。T为变换矩阵。\n绕X轴旋转\n$$T=\\left[\\begin{matrix} 1 \u0026amp; 0 \u0026amp; 0 \u0026amp; 0 \\\\ 0 \u0026amp; cos\\theta \u0026amp; -sin\\theta \u0026amp; 0 \\\\ 0 \u0026amp; sin\\theta \u0026amp; cos\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n绕Y轴旋转\n$$T=\\left[\\begin{matrix} cos\\theta \u0026amp; 0 \u0026amp; sin\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 1 \u0026amp; 0 \u0026amp; 0 \\\\ -sin\\theta \u0026amp; 0 \u0026amp; cos\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n绕Z轴旋转\n$$T=\\left[\\begin{matrix} cos\\theta \u0026amp; -sin\\theta \u0026amp; 0 \u0026amp; 0 \\\\ sin\\theta \u0026amp; cos\\theta \u0026amp; 0 \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 1 \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n直接用$matrix \\times T$就可以得到旋转后的矩阵了。\n但是图像旋转并不一定是绕坐标轴旋转,这里说图像旋转指的是在图像所在平面上的旋转,即图像矩阵绕法线旋转一定角度,不存在其它情况,所以需要一种更加通用的计算方法。角度的正负按右手定则决定。\n经测试,下面的方法并不通用,下面的旋转矩阵适用于点位置的变换,不适用于DICOM中的方位变换\n这里直接给出结果,图像矩阵绕向量$(u,v,w)$旋转$\\theta$的变换矩阵T。\n$$T=\\left[\\begin{matrix} u^2+(1-u^2)cos\\theta \u0026amp; u v(1-cos\\theta)-w sin\\theta \u0026amp; u w(1-cos\\theta)+v sin\\theta \u0026amp; 0 \\\\ u v(1-cos\\theta)+w sin\\theta \u0026amp; v^2+(1-v^2)cos\\theta \u0026amp; v w(1-cos\\theta)-u sin\\theta \u0026amp; 0 \\\\ u w(1-cos\\theta)-v sin\\theta \u0026amp; v w(1-cos\\theta)+u sin\\theta \u0026amp; w^2+(1-w^2)cos\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n代码示例:\n这里用到了MatrixD,主要是用于矩阵的运算。\n这里其实是我想的复杂了,DICOM图像旋转的本质就是两个方向向量的旋转,只需要将两个方向向量绕法向量旋转即可。而这一切fo-dicom都已经为我们做好了。\n1 2 3 4 5 6 7 8 9 10 Vector3D forward = new Vector3D(new[] { orientation[0], orientation[1], orientation[2] }); Vector3D down = new Vector3D(new[] { orientation[3], orientation[4], orientation[5] }); Orientation3D orientation3D = new Orientation3D(forward, down); // 旋转,顺时针为正,逆时针为负 orientation3D.Pitch(angle * Math.PI / 180.0); // orientation3D.Forward // orientation3D.Down 实现起来很简单,浏览Pitch的源码就会发现,其实就是将两个向量绕Right向量(即法向量)旋转,得到新的Forward和Down就是旋转后图像的方位信息。\n这里借一张图来说明问题:\n在创建Orientation3D时的参数Forward和Down就是图像的行方向和列方向的方向向量,Pitch方法将Forward和Down向量旋转一个角度,得到的就是旋转后图像的行和列的方向向量。\n图像翻转 翻转后的方位计算则比较简单,如果是水平翻转,只需要将水平(Row)方向的向量反向即可,竖直翻转将竖直(Column)方向的向量反向即可。\n向量反向只需要将 (u,v,w) 3个值前面添加负号即可。\n1 2 3 4 5 6 7 8 9 10 11 12 // 水平翻转 var newOrientation = new double[6] { -orientation[0], -orientation[1], -orientation[2], orientation[3], orientation[4], orientation[5] }; // 竖直翻转 var newOrientation = new double[6] { orientation[0], orientation[1], orientation[2], -orientation[3], -orientation[4], -orientation[5] }; 不过,像这么奇葩的功能应该不会有人去用吧。\n注意\n对图像矩阵进行旋转或者翻转操作之后,由于图像左上角的像素已经发生变化,所以原有的位置信息也已经改变,需要重新计算才能保证图像在空间中处于正确的位置。对于图像翻转来说或许能够轻易计算出来,不过旋转之后的图像却比较难计算了。\n最后,附上一段计算图像方位的代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // [R] Right - 沿着X方向递减 // [L] Left - 沿着X方向递增 // [A] Anterior - 沿着Y方向递减 // [P] Posterior - 沿着Y方向递增 // [F] Feet - 沿着Z方向递减 // [H] Head - 沿着Z方向递增 static string ComputeOrientation(Vector3D vector) { char x = vector.X \u0026lt; 0 ? \u0026#39;R\u0026#39; : \u0026#39;L\u0026#39;; char y = vector.Y \u0026lt; 0 ? \u0026#39;A\u0026#39; : \u0026#39;P\u0026#39;; char z = vector.Z \u0026lt; 0 ? \u0026#39;F\u0026#39; : \u0026#39;H\u0026#39;; double x1 = Math.Abs(vector.X); double y1 = Math.Abs(vector.Y); double z1 = Math.Abs(vector.Z); string result = \u0026#34;\u0026#34;; for (int i = 0; i \u0026lt; 3; i++) { if (x1 \u0026gt; 0.0001 \u0026amp;\u0026amp; x1 \u0026gt; y1 \u0026amp;\u0026amp; x1 \u0026gt; z1) { result += x; x1 = 0; } else if (y1 \u0026gt; 0.0001 \u0026amp;\u0026amp; y1 \u0026gt; x1 \u0026amp;\u0026amp; y1 \u0026gt; z1) { result += y; y1 = 0; } else if (z1 \u0026gt; 0.0001 \u0026amp;\u0026amp; z1 \u0026gt; x1 \u0026amp;\u0026amp; z1 \u0026gt; y1) { result += z; z1 = 0; } else { break; } } return result; } 这里用到了Vector3D,表示一个3维向量,可以使用数组代替。\n参考\n知乎:如何理解线性代数?\n行列式,快速求出法向量\nMRI的DICOM图像方位算法的研究\n三维空间几何变换矩阵\n图形学 位移,旋转,缩放矩阵变换\nDICOM中几个判断图像方向的tag\nDICOM Standard Browser\n","permalink":"https://kira-96.github.io/posts/%E4%B8%89%E7%BB%B4%E5%9B%BE%E5%BD%A2%E7%9F%A9%E9%98%B5%E5%8F%98%E6%8D%A2/","summary":"前言 由于我是从事医疗行业软件开发的,所以必不可少的会和图像打交道,最近刚刚好在做一个图像旋转相关的功能,借此又复习(预习)了一下线性代数,趁","title":"三维图形矩阵变换"},{"content":"前言 由于我很少用MFC,只有工作上需要的时候才会用到,所以我也是个新手,遇到问题需要到网上找很久资料。这里只是记录一些特殊情景下会用到的技巧,方便以后查找。\n隐藏窗口任务栏图标 这个问题我在网上找了很久,大致有两种方案:\n修改窗口的扩展样式\n1 ModifyStyleEx(WS_EX_APPWINDOW,WS_EX_TOOLWINDOW); 但是这样做会导致整个窗口的样式会变得很难看。\n将一个隐藏窗口设置成主窗口的父窗口\n这样做比较麻烦,而且任务视图下也不能再看到窗口,显然不是我想要的效果。\n最后终于找到了一个比较完美的解决方案,通过COM的方式移除任务栏列表中的图标。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 显示/隐藏任务栏图标(COM方式) bool ShowInTaskbar(HWND hWnd, bool isShow) { CoInitialize(nullptr); ITaskbarList* pTaskbarList; HRESULT hr = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList, (void**)\u0026amp;pTaskbarList); if (SUCCEEDED(hr)) { pTaskbarList-\u0026gt;HrInit(); if (isShow) { pTaskbarList-\u0026gt;AddTab(hWnd); } else { pTaskbarList-\u0026gt;DeleteTab(hWnd); } CoUninitialize(); return true; } CoUninitialize(); return false; } 程序启动时默认隐藏窗口 一种比较简单的方法是在程序启动时将窗口移动到屏幕外的不可见区域。 如果直接在OnInitDialog中设置ShowWindow(SW_HIDE)是无效的,因为此时窗口还没有显示出来,自然也无法隐藏。\n这里的思路是在程序启动的时候先将窗口移动到屏幕外,然后通过另一个线程将窗口隐藏起来,这样做虽然程序启动后窗口还是会一闪即逝,但由于是在屏幕之外,实际上并不能看到,然后在窗口需要显示的时候调用ShowWindow(SW_SHOW)即可。\n1 2 3 4 5 6 7 8 9 10 11 12 // CxxxDlg.h 头文件 #include \u0026lt;future\u0026gt; class CxxxDlg : public CDialogEx { ... ... ... private: std::future\u0026lt;int\u0026gt; hideTask; // 后台隐藏窗口线程 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 BOOL CxxxDlg::OnInitDialog() { ... CRect rcClient; GetWindowRect(\u0026amp;rcClient); // 将窗口移动到屏幕外 MoveWindow(-rcClient.Width(), rcClient.top, rcClient.Width(), rcClient.Height()); // 新建线程,延时1s后隐藏窗口 hideTask = std::async( std::launch::async, [\u0026amp;] { std::this_thread::sleep_for(std::chrono::seconds(1)); // ShowWindow(SW_HIDE); ShowWindowAsync(m_hWnd, SW_HIDE); std::this_thread::sleep_for(std::chrono::seconds(1)); // 延时1s,待窗口完全隐藏后再将窗口居中 CenterWindow(); return 0; }); ... return TRUE; // 除非将焦点设置到控件,否则返回 TRUE } 点击关闭时隐藏窗口 有时候我们希望在点击主窗口的关闭按钮之后将窗口隐藏或者最小化,而不是退出程序。这时只需要拦截掉窗口的关闭消息即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // CxxxDlg.h 头文件 class CxxxDlg : public CDialogEx { ... protected: afx_msg void OnSysCommand(UINT nID, LPARAM lParam); ... } void CxxxDlg::OnSysCommand(UINT nID, LPARAM lParam) { ///////////////////////////////////// // 这里捕获窗口的关闭消息 // 不直接关闭窗口,而是隐藏起来 if (nID == SC_CLOSE) { ShowWindowAsync(m_hWnd, SW_HIDE); } ///////////////////////////////////// else { CDialogEx::OnSysCommand(nID, lParam); } } 这样就可以拦截掉窗口的关闭消息了,这样不仅仅是点击关闭按钮时会隐藏窗口,通过窗口菜单关闭窗口或是使用Alt+F4都不能真正关闭窗口。那么我怎样才能退出程序呢,总不能用任务管理器吧。\n其实很简单,在需要退出程序的时候向窗口发送WM_CLOSE消息即可。\n1 SendMessage(WM_CLOSE); CFileDialog导致CDialogEx“失去焦点”的解决方法 继承自CDialogEx的窗口在使用CFileDialog之后会导致窗口标题栏变成灰色,很像是窗口失去了焦点,此时窗口仍然能够正常操作,但即使鼠标点击在窗口上窗口的标题栏依旧是灰色,无法恢复到窗口激活状态的颜色,即使将窗口属性设置为WS_EX_TOPMOST(置顶)依旧是这样,必须点击窗口外的其它区域或者使用Tab切换一下才能恢复正常。\n而继承自CDialog的窗口则没有这个问题,可以将窗口的基类改成CDialog来避免这个问题。对于我这样的强迫症来说是无法忍受的,所以用尽千方百计终于找到了一个可行的解决方案。\n经过尝试,单纯让窗口获取焦点或者将窗口放到前台的方法都是无效的。\n1 2 3 4 5 6 7 8 9 10 11 12 CFileDialog dlg( TRUE, _T(\u0026#34;ini\u0026#34;), nullptr, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T(\u0026#34;ini(*.ini)|*.ini|TEXT(*.txt)|*.txt|\u0026#34;), this); auto dlgResult = dlg.DoModal(); // 无效的方法 this-\u0026gt;SetFocus(); this-\u0026gt;SetForegroundWindow(); ... 所以只能曲线救国,既然通过手动切换的方式可以恢复到正常状态,不妨先切换到其它窗口再切换回来。\n1 2 3 4 5 6 7 8 9 10 11 12 CFileDialog dlg( TRUE, _T(\u0026#34;ini\u0026#34;), nullptr, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T(\u0026#34;ini(*.ini)|*.ini|TEXT(*.txt)|*.txt|\u0026#34;), this); auto dlgResult = dlg.DoModal(); // 先将焦点放到桌面,再切换回本窗口 ::SetForegroundWindow(::GetDesktopWindow()); this-\u0026gt;SetForegroundWindow(); ... 完美解决了问题。\n窗口启用视觉样式 启用视觉样式之后,可以让程序看起来更加现代化一些,只支持Window XP以后的系统。 可以通过为程序添加清单文件来实现,不过比较麻烦。在VC++ 2005之后,直接添加编译器指令到代码中就可以了。\n在预编译头文件中添加下面代码即可。\n1 2 3 4 5 6 7 8 9 #if defined _M_IX86 #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;x86\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #elif defined _M_IA64 #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;ia64\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #elif defined _M_X64 #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;amd64\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #else #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;*\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #endif 任务栏显示进度 为窗口的任务栏图标添加进度显示,也可以为任务栏添加按钮。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ITaskbarList3* pTaskbar; // 初始化COM组件 CoInitialize(NULL); CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(\u0026amp;pTaskbar)); // TBPF_NOPROGRESS\t= 0, // 正常状态,不显示进度 // TBPF_INDETERMINATE\t= 0x1, // 忙碌状态,不显示进度 // TBPF_NORMAL\t= 0x2, // 正常状态,显示进度(绿色) // TBPF_ERROR\t= 0x4, // 错误状态,显示进度(红色) // TBPF_PAUSED\t= 0x8 // 停止状态,显示进度(黄色) pTaskbar-\u0026gt;SetProgressState(GetSafeHwnd(), TBPF_NORMAL); pTaskbar-\u0026gt;SetProgressValue(GetSafeHwnd(), 60, 100); // 设置提示信息 pTaskbar-\u0026gt;SetThumbnailTooltip(GetSafeHwnd(), TEXT(\u0026#34;Tooltip\u0026#34;)); // 设置覆盖图标 HICON hIcon = AfxGetApp()-\u0026gt;LoadIcon(IDI_ICON_ERR); pTaskbar-\u0026gt;SetOverlayIcon(GetSafeHwnd(), hIcon, _T(\u0026#34;Error\u0026#34;)); // 添加任务栏按钮 THUMBBUTTONMASK dwMask = THB_ICON | THB_TOOLTIP; THUMBBUTTON buttons[3]; buttons[0].iId = 0; buttons[0].dwMask = dwMask; buttons[0].hIcon = hIcon; memcpy(buttons[0].szTip, TEXT(\u0026#34;Tooltip\u0026#34;), sizeof(buttons[0].szTip)); // ... pTaskbar-\u0026gt;ThumbBarAddButtons(GetSafeHwnd(), 3, buttons); // 最后释放COM组件 CoUninitialize(); 未完待续,持续更新中\u0026hellip; 参考\nMFC简单的启动时隐藏界面方式(仅启动时隐藏)\nEnabling Visual Styles\n","permalink":"https://kira-96.github.io/posts/%E4%B8%80%E4%BA%9B%E5%9F%BA%E6%9C%AC%E6%B2%A1%E4%BB%80%E4%B9%88%E7%94%A8%E7%9A%84mfc%E6%8A%80%E5%B7%A7/","summary":"前言 由于我很少用MFC,只有工作上需要的时候才会用到,所以我也是个新手,遇到问题需要到网上找很久资料。这里只是记录一些特殊情景下会用到的技巧","title":"一些基本没什么用的MFC技巧"},{"content":"前言 最近公司的系统也开始陆续向 Windows 10 迁移了,我的办公电脑也终于换上了新系统。为了适应新的开发环境,有时候需要获取一些系统相关的信息,这里就稍微总结一下。\n这篇文章并不只是写如何获取Win10系统主题色,也会包含一些其它的内容,主要是用C++和C#语言,可能会持续更新。\n检测是否为 Windows10 系统 C++ 方式\n在Win10上已经不能直接通过GetVersion或GetVersionEx的方式获取系统信息,单纯使用这两个函数编译时会报错。 这里提供另外一种方式:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool IsWindows10() { typedef void(__stdcall*NTPROC)(DWORD*, DWORD*, DWORD*); HMODULE inst = LoadLibrary(\u0026#34;ntdll.dll\u0026#34;); NTPROC ntProc = reinterpret_cast\u0026lt;NTPROC\u0026gt;(GetProcAddress(inst, \u0026#34;RtlGetNtVersionNumbers\u0026#34;)); DWORD dwMajor, dwMinor, dwBuildNumber; ntProc(\u0026amp;dwMajor, \u0026amp;dwMinor, \u0026amp;dwBuildNumber); FreeLibrary(inst); return dwMajor == 10; } 当然上面的方法也适用于C#,但这里使用C#的方法来检测系统版本。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /// \u0026lt;summary\u0026gt; /// Gets if the Operating System is Windows 10 /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;True if Windows 10\u0026lt;/returns\u0026gt; public static bool IsWindows10 { get { // IMPORTANT: Windows 8.1. and Windows 10 will ONLY admit their real version if your program\u0026#39;s manifest // claims to be compatible. Otherwise they claim to be Windows 8. See the first comment on: // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724833%28v=vs.85%29.aspx // Get Operating system information OperatingSystem os = Environment.OSVersion; // Get the Operating system version information Version vi = os.Version; // Pre-NT versions of Windows are PlatformID.Win32Windows. We\u0026#39;re not interested in those. if (os.Platform == PlatformID.Win32NT) { if (vi.Major == 10) { return true; } } return false; } } 使用上面的方法时需要注意,首先要为你的程序添加清单文件(app.manifest),并且取消对Windows 10系统兼容的注释。 如:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... \u0026lt;compatibility xmlns=\u0026#34;urn:schemas-microsoft-com:compatibility.v1\u0026#34;\u0026gt; \u0026lt;application\u0026gt; \u0026lt;!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的 Windows 版本的列表。取消评论适当的元素, Windows 将自动选择最兼容的环境。 --\u0026gt; \u0026lt;!-- Windows Vista --\u0026gt; \u0026lt;!--\u0026lt;supportedOS Id=\u0026#34;{e2011457-1546-43c5-a5fe-008deee3d3f0}\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!-- Windows 7 --\u0026gt; \u0026lt;supportedOS Id=\u0026#34;{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\u0026#34; /\u0026gt; \u0026lt;!-- Windows 8 --\u0026gt; \u0026lt;!--\u0026lt;supportedOS Id=\u0026#34;{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!-- Windows 8.1 --\u0026gt; \u0026lt;!--\u0026lt;supportedOS Id=\u0026#34;{1f676c76-80e1-4239-95bb-83d0f6d0da78}\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!-- Windows 10 --\u0026gt; \u0026lt;supportedOS Id=\u0026#34;{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\u0026#34; /\u0026gt; \u0026lt;/application\u0026gt; \u0026lt;/compatibility\u0026gt; ... 是否为 Win7 以下版本\n1 2 3 4 5 6 7 8 9 10 11 12 public static bool IsWindows7OrLower { get { Version v = Environment.OSVersion.Version; int versionMajor = v.Major; int versionMinor = v.Minor; double version = versionMajor + (double)versionMinor / 10; return version \u0026lt;= 6.1; } } 获取 Window 10 主题色(Accent Color) 在UWP中可以轻易的获取SystemAccentColor。但随着Win10移动端的失利,UWP基本已被微软宣告死亡。 这里就讲一下如何通过其它方式读取到系统的主题色。\n在Win10上,当系统的主题色发生变化时,系统会给所有窗口都发送主题色变更的消息\n1 #define WM_DWMCOLORIZATIONCOLORCHANGED 0x0320 1 2 3 4 5 6 7 8 9 10 11 12 13 LRESULT CxxxDlg::OnColorizationColorChanged(WPARAM wParam, LPARAM lParam) { DWORD color = 0; BOOL opaque = FALSE; HRESULT hr = DwmGetColorizationColor(\u0026amp;color, \u0026amp;opaque); if (SUCCEEDED(hr)) { // Update the application to use the new color. } return 0; } 但我使用之后发现,并不能正确的获取到系统的主题颜色,而是DWM颜色。 所以这里使用了另一种方式,通过读取注册表的方式获取系统主题色。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\DWM\\AccentColor public static Color GetSystemAccentColor() { using (RegistryKey dwm = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\DWM\u0026#34;, false)) { if (dwm.GetValueNames().Contains(\u0026#34;AccentColor\u0026#34;)) { // 这里不要尝试转换成uint,因为有可能符号位为 1(负数),会导致强制转换错误 // 直接进行下面的位操作即可 int accentColor = (int)dwm.GetValue(\u0026#34;AccentColor\u0026#34;); // 注意:读取到的颜色为 AABBGGRR return Color.FromArgb( (byte)((accentColor \u0026gt;\u0026gt; 24) \u0026amp; 0xFF), (byte)(accentColor \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 8) \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 16) \u0026amp; 0xFF)); } } return SystemParameters.WindowGlassColor; // 近似的系统主题色 } 以上的方式是通过读取注册表的方式,所以理论上任何语言都是通用的,再结合WM_DWMCOLORIZATIONCOLORCHANGED消息,就可以完美做到程序跟随系统主题色。\n同样的,也可以通过读取注册表的方式获取到DWM颜色。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\DWM\\ColorizationColor public static Color GetColorizationColor() { using (RegistryKey dwm = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\DWM\u0026#34;, false)) { if (dwm.GetValueNames().Contains(\u0026#34;ColorizationColor\u0026#34;)) { int accentColor = (int)dwm.GetValue(\u0026#34;ColorizationColor\u0026#34;); // 注意:读取到的颜色为 AARRGGBB return Color.FromArgb( (byte)((accentColor \u0026gt;\u0026gt; 24) \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 16) \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 8) \u0026amp; 0xFF), (byte)(accentColor \u0026amp; 0xFF)); } } return SystemParameters.WindowGlassColor; } 亮色主题还是暗色主题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\\AppsUseLightTheme // 应用是亮色还是暗色 public static bool AppsUseLightTheme() { using (RegistryKey personalize = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\u0026#34;, false)) { if (personalize.GetValueNames().Contains(\u0026#34;AppsUseLightTheme\u0026#34;)) { return (int)personalize.GetValue(\u0026#34;AppsUseLightTheme\u0026#34;) == 1; } } return true; } 对于较高版本的Win10,系统也可以设置亮色/暗色模式,我使用的版本暂且不支持。\n需要读取注册表的路径为\\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\\SystemUsesLightTheme。 读取方式应该和上面差别不大。\n主题色是否应用到窗口标题栏和边框 如果没有在系统个性化设置中设置将主题色应用到窗口标题栏和边框时,Win10窗口的标题栏和边框会始终为白色(对应亮色模式)或黑色(对应暗色模式)。 设置了将主题色应用到窗口标题栏和边框后,窗口的标题栏会跟随系统主题色的变化而变化。 那么我们怎样知道用户是怎样设置的呢?依旧是通过读取注册表的方式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\DWM\\ColorPrevalence public static bool IsWindowPrevalenceAccentColor() { using (RegistryKey dwm = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\DWM\u0026#34;, false)) { if (dwm.GetValueNames().Contains(\u0026#34;ColorPrevalence\u0026#34;)) { int colorPrevalence = (int)dwm.GetValue(\u0026#34;ColorPrevalence\u0026#34;); return colorPrevalence == 1; } } return false; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // c++ 读取注册表 bool IsWindowPrevalenceAccentColor() { LPCTSTR dwm = _T(\u0026#34;Software\\\\Microsoft\\\\Windows\\\\DWM\u0026#34;); HKEY hDwmKey; if (ERROR_SUCCESS != RegOpenKeyEx(HKEY_CURRENT_USER, dwm, 0, KEY_READ, \u0026amp;hDwmKey)) { return false; } DWORD type = REG_DWORD; DWORD value = 0; DWORD cbData = 4; if (ERROR_SUCCESS != RegQueryValueEx(hDwmKey, _T(\u0026#34;ColorPrevalence\u0026#34;), nullptr, \u0026amp;type, (LPBYTE)\u0026amp;value, \u0026amp;cbData)) { RegCloseKey(hDwmKey); return false; } RegCloseKey(hDwmKey); return value == 1; } 根据背景色计算前景色 还有一个遗留的问题就是,窗口的标题栏颜色跟随系统主题色变化时,标题文字的颜色也会动态变换成白色或者黑色,至于什么情况下是白色,什么时候是黑色,暂时还没研究出来。\n最后在网上找到了一些算法,通过背景的颜色计算出前景色,效果还是很理想的。\n方法1:\n1 2 3 4 5 6 7 8 9 10 11 /// \u0026lt;summary\u0026gt; /// 根据背景色计算前景色(白/黑) /// https://github.com/loilo/windows-titlebar-color/blob/master/WindowsAccentColors.js#L53 /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;background\u0026#34;\u0026gt;背景颜色\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;前景颜色(白/黑)\u0026lt;/returns\u0026gt; public static Color GetForegroundColor(Color background) { return (background.R * 2 + background.G * 5 + background.B) \u0026lt;= 1024 /* 8*128 */ ? Colors.White : Colors.Black; } 方法2:\n1 2 3 4 5 6 7 8 9 10 11 12 /// \u0026lt;summary\u0026gt; /// 计算能在任何背景色上清晰显示的前景色 /// https://www.cnblogs.com/walterlv/p/10236517.html /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;background\u0026#34;\u0026gt;背景颜色\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;前景颜色(黑/白)\u0026lt;/returns\u0026gt; public static Color GetReverseForegroundColor(Color background) { double grayLevel = (0.299 * background.R + 0.587 * background.G + 0.114 * background.B) / 255; return grayLevel \u0026gt; 0.5 ? Colors.Black : Colors.White; } 持续更新中\u0026hellip;\n参考\nC++ 获取并判断操作系统版本,解决Win10、 Windows Server 2012 R2 读取失败的方案\nwpf/winform获取windows10系统颜色和主题色\nWM_DWMCOLORIZATIONCOLORCHANGED message\nDwmGetColorizationColor function\n分享一个算法,计算能在任何背景色上清晰显示的前景色\n","permalink":"https://kira-96.github.io/posts/%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96windows10%E4%B8%BB%E9%A2%98%E9%A2%9C%E8%89%B2/","summary":"前言 最近公司的系统也开始陆续向 Windows 10 迁移了,我的办公电脑也终于换上了新系统。为了适应新的开发环境,有时候需要获取一些系统相关的信息,这里就稍微","title":"如何获取Windows10主题颜色"},{"content":"前言 经常看到其它程序在最小化或者窗口隐藏后依旧会在通知栏显示一个托盘图标,像微信、QQ之类的,即使主窗口不显示,程序并不会退出,依旧可以通过托盘图标进行操作。 那么怎样为自己的程序添加一个托盘图标呢?这次就来讲一讲。\n实现(C#) 首先,为项目添加引用System.Windows.Forms。\n这里以WPF为例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 // 主窗口的后台代码 // MainWindow.xaml.cs using System.Windows; using System.Windows.Forms; public partial class MainWindow : Window { private NotifyIcon notifyIcon; // 托盘图标 // 托盘菜单 private System.Windows.Controls.ContextMenu trayIconContextMenu; public MainWindow() { InitializeComponent(); InitializeTrayIcon(); } private void InitializeTrayIcon() { notifyIcon = new NotifyIcon() { Visible = true, Text = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, // 使用嵌入的资源 Icon = new System.Drawing.Icon( System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(\u0026#34;AppNamespace.icon.ico\u0026#34;), System.Windows.Forms.SystemInformation.SmallIconSize) }; notifyIcon.MouseClick += TrayIconMouseClick; // notifyIcon.MouseDoubleClick += TrayIconMouseDoubleClick; trayIconContextMenu = (System.Windows.Controls.ContextMenu)FindResource(\u0026#34;TrayIconContextMenu\u0026#34;); } private void Window_Deactivated(object s, System.EventArgs e) { trayIconContextMenu.IsOpen = false; } private void TrayIconMouseClick(object s, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { // Open the Notify icon context menu trayIconContextMenu.IsOpen = true; // Required to close the Tray icon when Deactivated is called // See: http://copycodetheory.blogspot.be/2012/07/notify-icon-in-wpf-applications.html Activate(); } } } 需要注意的是NotifyIcon的Icon属性必须设置,否则就不能显示托盘图标。而且图标必须是.ico格式,其它格式png,jpg等都是不行的。 我这里是使用的嵌入的资源,也可以使用文件路径的方式:\n1 Icon = new System.Drawing.Icon(\u0026#34;icon.ico\u0026#34;); 上面代码也没有使用NotifyIcon的ContextMenu属性,而是使用的WPF中的ContextMenu,直接在鼠标右键单击托盘图标的时候将trayIconContextMenu显示出来。\n下面是trayIconContextMenu的定义:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!-- MainWindow.xaml --\u0026gt; \u0026lt;Window.Resources\u0026gt; \u0026lt;ContextMenu x:Key=\u0026#34;TrayIconContextMenu\u0026#34; Placement=\u0026#34;MousePoint\u0026#34;\u0026gt; \u0026lt;MenuItem Header=\u0026#34;Show Window\u0026#34; ToolTip=\u0026#34;show main window\u0026#34; Click=\u0026#34;MenuItemShowClick\u0026#34;\u0026gt; \u0026lt;MenuItem.Icon\u0026gt; \u0026lt;Image Source=\u0026#34;pack://application:,,,/Icons/window.png\u0026#34; /\u0026gt; \u0026lt;/MenuItem.Icon\u0026gt; \u0026lt;/MenuItem\u0026gt; \u0026lt;Separator /\u0026gt; \u0026lt;MenuItem Header=\u0026#34;Exit\u0026#34; ToolTip=\u0026#34;exit\u0026#34; Click=\u0026#34;MenuItemExitClick\u0026#34;\u0026gt; \u0026lt;MenuItem.Icon\u0026gt; \u0026lt;Image Source=\u0026#34;pack://application:,,,/Icons/exit.png\u0026#34; /\u0026gt; \u0026lt;/MenuItem.Icon\u0026gt; \u0026lt;/MenuItem\u0026gt; \u0026lt;/ContextMenu\u0026gt; \u0026lt;/Window.Resources\u0026gt; 通过上面的代码就可以为应用程序添加一个系统托盘图标了。\n系统托盘图标还有一个功能就是可以在通知栏显示通知。\n1 notifyIcon.ShowBalloonTip(0, \u0026#34;消息\u0026#34;, \u0026#34;程序正在运行\u0026#34;, ToolTipIcon.Info); 第一个参数是显示超时,不过现在已经没用了。\ntimeout: 气球状提示应显示的时间段,以毫秒为单位。从 Windows Vista 开始,此参数已被否决。 通知显示时间现在基于系统的辅助功能设置。\nBalloonTip在Win7上显示为气球状提示,在Win10上显示为Toast通知。\n其它 经过本人测试,NotifyIcon在控制台程序上也是可以使用的,遗憾的是一部分功能不能正常使用。 BalloonTip可以正常显示。但菜单是无法使用的,因为不能触发鼠标事件。\n","permalink":"https://kira-96.github.io/posts/%E4%B8%BA%E7%A8%8B%E5%BA%8F%E6%B7%BB%E5%8A%A0%E9%80%9A%E7%9F%A5%E6%A0%8F%E5%9B%BE%E6%A0%87/","summary":"前言 经常看到其它程序在最小化或者窗口隐藏后依旧会在通知栏显示一个托盘图标,像微信、QQ之类的,即使主窗口不显示,程序并不会退出,依旧可以通过","title":"为程序添加通知栏图标"},{"content":"简介 通常来说,应用程序可以启动任意数目的实例,前提是你的电脑内存足够大,想启动多少都可以。但有时候我们只希望程序同时只有一个实例在运行,应用程序不会重复运行。这就是应用程序的单例模式。\n实现(WPF) 当然,实现的方法不是唯一的。\n可以在程序启动的时候查找应用程序的窗口名字,如果查找的结果不为空,就说明已经有一个实例正在运行,但如果你的程序碰巧和其它程序的窗口名字重复就不好说了。\n也可以在程序启动时获取应用程序进程的名字,然后再查找系统所有进程,看是否有重复的,原理和上一种类似。\n这里要讲的并不是上面两种,而是通过Mutex来实现,使用比前两种更加简单和有效。\nSystem.Threading.Mutex官方说明是可用于进程间同步的同步基元。。\n使用起来也很简单,重写应用程序的OnStartup方法:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // App.xaml.cs /// \u0026lt;summary\u0026gt; /// App.xaml 的交互逻辑 /// \u0026lt;/summary\u0026gt; public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { Mutex mutex = new Mutex(true, \u0026#34;MutexName\u0026#34;, out bool createNew); if (createNew) { base.OnStartup(e); } else { MessageBox.Show(\u0026#34;程序已在运行中。\u0026#34;, \u0026#34;提示\u0026#34;, MessageBoxButton.OK, MessageBoxImage.Information); Application.Current.Shutdown(); } } } 注意Mutex的第2个参数,可以是任意字符串,越复杂越好,避免和其它程序冲突。 如果createNew为false,就说明已经有一个实例正在运行了,直接退出当前程序。\n注意:必须保证Mutex在程序运行过程中不被垃圾回收,否则就失效了。\n上面的写法是可以的,但如果我们使用的是Caliburn.Micro或者Stylet等框架,程序启动时都是通过Bootstrapper的,在Bootstrapper中的写法会有一些不同。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 使用 Stylet public class Bootstrapper : Bootstrapper\u0026lt;ShellViewModel\u0026gt; { /// \u0026lt;summary\u0026gt; /// 必须定义在类内部,一旦被释放就无效了 /// \u0026lt;/summary\u0026gt; private Mutex mutex; protected override void OnStart() { mutex = new Mutex(true, MUTEX_NAME, out bool createNew); if (!createNew) { MessageBox.Show(\u0026#34;程序已在运行中。\u0026#34;, \u0026#34;提示\u0026#34;, MessageBoxButton.OK, MessageBoxImage.Information); // 退出当前应用程序 // 尽量不要使用 // Application.Shutdown(); // 因为在这里使用会触发主窗口的Closing事件 System.Environment.Exit(0); } base.OnStart(); } } 需要注意的是要将Mutex定义在类的内部,如果定义在OnStartup函数体内,那么在程序运行时它就失效了,就不能使程序以单例模式运行了。\n如果你想要写在其它地方,可以将Mutex定义为static,这样就可以保证它在应用程序运行过程中不会失效了。\n还有就是在这里尽量使用System.Environment.Exit(0)而不是Application.Current.Shutdown(),因为我发现这里会触发主窗口的Closing事件,如果你不希望触发它,那么就使用Environment.Exit(0)。\n补充 关于应用程序单例模式还可以通过WindowsFormsApplicationBase来实现,但我也没试过。使用Mutex已经足够满足要求了。\n最后再加一项功能,我想要在当前程序检测到已经存在正在运行的实例时,退出并激活已经存在的程序窗口。\n这一点可以通过Windows的API来实现,思路就是查找应用程序的主窗口,并将其激活。代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [DllImport(\u0026#34;user32.dll\u0026#34;, EntryPoint = \u0026#34;FindWindow\u0026#34;)] public static extern IntPtr FindWindow(string classname, string windowname); [DllImport(\u0026#34;user32.dll\u0026#34;, EntryPoint = \u0026#34;ShowWindow\u0026#34;)] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport(\u0026#34;user32.dll\u0026#34;, EntryPoint = \u0026#34;SetForegroundWindow\u0026#34;)] public static extern bool SetForegroundWindow(IntPtr hWnd); public const int SW_NORMAL = 1; public const int SW_RESTORE = 9; public static void FindWindowAndActive(string classname, string windowname) { IntPtr hWnd = FindWindow(classname, windowname); ShowWindow(hWnd, SW_NORMAL); SetForegroundWindow(hWnd); } 在应用程序退出之前调用FindWindowAndActive函数即可。\n1 2 3 4 ... FindWindowAndActive(null, \u0026#34;Main Window\u0026#34;); // 激活已经存在的实例窗口 System.Environment.Exit(0); // 退出当前应用程序 ... ","permalink":"https://kira-96.github.io/posts/%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","summary":"简介 通常来说,应用程序可以启动任意数目的实例,前提是你的电脑内存足够大,想启动多少都可以。但有时候我们只希望程序同时只有一个实例在运行,应用","title":"应用程序单例模式"},{"content":"虽然平时很少会用到命令行参数,但有时候可以使用命令行参数来使程序执行不同的行为。\n在写控制台程序的时候,我们可以直接得到程序命令行参数。\n1 2 3 4 static void Main(string[] args) { // args 就是命令行参数 } 那么如果不是控制台程序如何获取命令行参数呢?\n在 WPF 中有两种方法获取命令行参数\n第一种方法是重写应用的OnStartup方法,通过StartupEventArgs来获取命令行参数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // App.xaml.cs /// \u0026lt;summary\u0026gt; /// App.xaml 的交互逻辑 /// \u0026lt;/summary\u0026gt; public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 获取命令行参数 string[] args = e.Args; // do something } } 如果没有命令行参数,那么args就为null。\n第二种方法则比较灵活,可以在任意地方获取到命令行参数。\n1 string[] args = System.Environment.GetCommandLineArgs(); 直接使用Environment的静态方法来获取命令行参数,需要注意的是,第二种方法获取到的参数和前面一种方法结果不同。\n第二种方法获取的结果不会为null,通过Environment获取到的命令行参数第一个是当前程序的路径,从第2项开始才是命令行参数(如果有)。\n","permalink":"https://kira-96.github.io/posts/wpf-%E8%8E%B7%E5%8F%96%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0/","summary":"虽然平时很少会用到命令行参数,但有时候可以使用命令行参数来使程序执行不同的行为。 在写控制台程序的时候,我们可以直接得到程序命令行参数。 1 2 3","title":"WPF 获取命令行参数"},{"content":"前言 由于我平时是将树莓派(Respberry Pi)当成一个Linux电脑来使用,平时都是通过ssh连接到树莓派来进行操作的,所以一直都是通过终端进行操作的。而树莓派系统的终端又中规中距,不怎么好看。刚好这两天接触到了一个十分漂亮的Power Shell主题oh-my-posh,所以就想着能不能弄到树莓派上。折腾了半天,终于成功了,过程还算顺利。\n目标 我的目标是美化树莓派的终端,由于oh-my-posh是power shell的主题,所以首先需要安装power shell,然后再通过power shell安装oh-my-posh。\n安装 Power Shell 安装 oh-my-posh 安装 Power Shell Core 刚好前几天Power Shell Core 7发布了,所以我这里就安装了最新的版本。\nPower Shell 官网是这样的说明的。\n当前仅 Raspbian Stretch 支持 PowerShell。 CoreCLR 和 PowerShell Core 仅适用于 Pi 2 和 Pi 3 设备,因为其他设备(如 Pi 0)有不受支持的处理器。\n我是用的是树莓派 3B+,测试是可以的。\n具体的操作按照Power Shell官网的安装说明\n首先安装Power Shell的依赖:\n1 2 3 4 5 6 7 8 # Prerequisites # Update package lists sudo apt-get update # Install libunwind8 and libssl1.0 # Regex is used to ensure that we do not install libssl1.0-dev, as it is a variant that is not required sudo apt-get install \u0026#39;^libssl1.0.[0-9]$\u0026#39; libunwind8 -y 然后到这里下载最新的Power Shell二进制包。\n这里需要下载arm32位的二进制包。\npowershell-7.0.0-linux-arm32.tar.gz\n下载完成后解压到任意目录即可运行。\n或者使用官网的方式:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # Download and extract PowerShell # Grab the latest tar.gz wget https://github.com/PowerShell/PowerShell/releases/download/v7.0.0/powershell-7.0.0-linux-arm32.tar.gz # Make folder to put powershell mkdir ~/powershell # Unpack the tar.gz file tar -xvf ./powershell-7.0.0-linux-arm32.tar.gz -C ~/powershell # Start PowerShell ~/powershell/pwsh 最后,如果想要在任意位置都能启动Power Shell,需要创建启动Power Shell的软链接。\n1 sudo ln -s ~/path/to/powershell/pwsh /usr/bin/pwsh 或者参考官网的方式:\n1 2 3 4 5 6 7 # Start PowerShell from bash with sudo to create a symbolic link sudo ~/powershell/pwsh -c New-Item -ItemType SymbolicLink -Path \u0026#34;/usr/bin/pwsh\u0026#34; -Target \u0026#34;\\$PSHOME/pwsh\u0026#34; -Force # alternatively you can run following to create a symbolic link # sudo ln -s ~/powershell/pwsh /usr/bin/pwsh # Now to start PowerShell you can just run \u0026#34;pwsh\u0026#34; 现在只要在终端输入pwsh就可以进入Power Shell了。\n为 Power Shell 安装 oh-my-posh 安装之前需要先安装powerline字体,否则,oh-my-posh安装完成后会由于缺少字体而显示不正常。\n1 sudo apt-get install fonts-powerline 然后就可以为Power Shell安装oh-my-posh主题了。\n首先要进入Power Shell,在终端输入pwsh即可。\n在Power Shell下依次执行下面两个命令:\n1 2 Install-Module posh-git -Scope CurrentUser Install-Module oh-my-posh -Scope CurrentUser 安装过程中全部选 是(Y) 就可以了。\n安装完成后就可以使用oh-my-posh了:\n1 2 3 4 # Start the default settings Set-Prompt # Alternatively set the desired theme: Set-Theme Agnoster 最后需要保存Power Shell的配置,这样每次进入Power Shell就是我们设定的主题了。\n1 2 3 4 5 # 在 Power Shell下执行下面命令,如果不存在配置文件就创建一个 if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force } # 使用树莓派的编辑器修改配置文件 vi $PROFILE 在配置文件中输入下面内容:\n1 2 3 Import-Module posh-git Import-Module oh-my-posh Set-Theme Paradox 然后再重新进入Power Shell就可以看到主题已经成功应用了。\noh-my-posh提供了多种主题效果,可以看这里,如果需要更换主题,可以直接在Power Shell中执行:\n1 Set-Theme mytheme 效果 虽然 Power Shell 启动有一点慢,但显示效果还是很不错的。\n最后放上实际运行的效果:\n在树莓派中显示效果:\n在其它终端中的显示效果(Termius):\n参考\n在树莓派上安装PowerShellCore 安装powerline-fonts 安装oh-my-posh ","permalink":"https://kira-96.github.io/posts/%E5%9C%A8%E6%A0%91%E8%8E%93%E6%B4%BE%E4%B8%8A%E5%AE%89%E8%A3%85-power-shell-%E5%B9%B6%E7%94%A8-oh-my-posh-%E7%BE%8E%E5%8C%96/","summary":"前言 由于我平时是将树莓派(Respberry Pi)当成一个Linux电脑来使用,平时都是通过ssh连接到树莓派来进行操作的,所以一直都是通过","title":"在树莓派上安装 Power Shell 并用 oh-my-posh 美化"},{"content":"简介 JSON是一种常用的轻量级数据交换格式。与XML相比,JSON无论是体积还是可读性都更好,所以在网络数据传输和应用程序中被广泛的应用。\n那么,.NET平台使用最广泛的JSON库是什么呢?自然要数Newtonsoft.NET了,打开nuget包管理器第一个就是,在所有包下载量排行中排名第一。使用简单,性能可靠,文档也很齐全。\n使用 使用JSON最常用的就是对象的序列化和反序列化。\n先来看最基本的使用\n1 2 3 4 5 6 7 // 先定义一个类 public class TestJsonDeseClass { public Guid MessageGuid { get; set; } public string Message { get; set; } } 1 2 3 4 5 6 7 8 TestJsonDeseClass test = new TestJsonDeseClass() { MessageGuid = Guid.NewGuid(), Message = \u0026#34;Test Message\u0026#34; }; string json = JsonConvert.SerializeObject(test); TestJsonDeseClass des = JsonConvert.DeserializeObject\u0026lt;TestJsonDeseClass\u0026gt;(json); 只需要将类的成员属性设置为get和set就可以了,反序列化的时候,Json.NET会自动根据成员的名字为对象的成员赋值。\n那么如果不想序列化/反序列化某个成员变量呢?\n1 2 3 4 5 6 7 8 9 using Newtonsoft.Json; public class TestJsonDeseClass { [JsonIgnore] public Guid MessageGuid { get; set; } public string Message { get; set; } } 只需要在成员变量的定义前加上[JsonIgnore]的属性(Attribute)即可,序列化/反序列化的时候Json.NET会自动忽略该成员。\n如果JSON字符串中的属性名字和定义的类中的成员名字不一样怎么办呢?怎样才能正确的给成员变量赋值呢?\n1 2 3 4 5 6 7 8 9 using Newtonsoft.Json; public class TestJsonDeseClass { [JsonProperty(\u0026#34;Guid\u0026#34;)] public Guid MessageGuid { get; set; } public string Message { get; set; } } 只需要在成员变量的定义前加上[JsonProperty()]的属性(Attribute)即可,序列化/反序列化的时候Json.NET会将Json字符串中的\u0026quot;Guid\u0026quot;属性赋值给MessageGuid。\n那么,如果想让类的属性值只读的get,不想让外部能修改成员变量呢,如何设置呢?\n当然这样也是可以的,不过需要我们给类添加构造方法,在构造方法中对成员赋值,不能再使用默认的构造方法,因为默认的构造方法不会对成员赋值,而外部也无法对成员赋值。在添加了构造方法后,Json.NET会自动调用类的构造方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 using Newtonsoft.Json; public class TestJsonDeseClass { public Guid MessageGuid { get; } public string Message { get; } public TestJsonDeseClass(Guid messageGuid, string message) { MessageGuid = messageGuid; Message = message; } } 不过需要注意的是,构造方法的参数名称必须和成员变量(或者说是序列化/反序列化时的属性)名字一致,但可以不用区分大小写,才能正确对属性赋值。\n如果把上面的构造方法改成下面这个样子\n1 2 3 4 5 public TestJsonDeseClass(Guid guid, string message) { MessageGuid = guid; Message = message; } 就会导致反序列化的对象MessageGuid属性不能正确赋值,因为Json.NET无法从json字符串中找到名为guid的属性,你也没告诉它要拿名为MessageGuid的属性,自然就会出错了。\n那么,最后一个问题,如果我的类有多个构造方法,我怎样告诉Json.NET应该用哪一个呢?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using Newtonsoft.Json; public class TestJsonDeseClass { public Guid MessageGuid { get; } public string Message { get; } public TestJsonDeseClass() { } [JsonConstructor] public TestJsonDeseClass(Guid messageGuid, string message) { MessageGuid = messageGuid; Message = message; } } 只需要在对应的构造方法前面加上[JsonConstructor]的属性(Attribute)即可,反序列化的时候Json.NET会就会调用相应的构造方法来生成对象了。\n以上就是一些基本的用法,基本上能满足正常的使用了。当然还有一些更加高级和灵活的用法,这里就不多记录了,需要的时候再去看文档就可以了。\n参考\n文档\nSamples\n","permalink":"https://kira-96.github.io/posts/newtonsoft.net-%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/","summary":"简介 JSON是一种常用的轻量级数据交换格式。与XML相比,JSON无论是体积还是可读性都更好,所以在网络数据传输和应用程序中被广泛的应用。 那","title":"Newtonsoft.NET 基本使用"},{"content":"简介 Stylet是一个轻量且功能强大的MVVM框架。支持 .NET 4.5+ 和 .NET Core 3.0+。\nStylet的作者也是受到Caliburn.Micro的启发,并且在CM的基础上做了许多改进。所以Stylet使用起来感觉和Caliburn.Micro差别不是很大,但又有着一些不同。\n项目结构 这里选择创建一个 .NET Core 的 WPF 项目。\n这里项目结构风格和Caliburn.Micro类似,示例源代码\n虽然Stylet官方给出的例子里面View和ViewModel是放在一起的,但经过实际使用后发现采用CM的风格也是可以的。依照习惯,将Views和ViewModels分别放在两个文件夹中。\n使用 Bootstrapper.cs\n1 2 public class Bootstrapper : Bootstrapper\u0026lt;ShellViewModel\u0026gt; {} 这样就相当于执行了DisplayRootViewFor\u0026lt;ShellViewModel\u0026gt;()。\n然后再修改App.xaml如下就可以让程序启动了。\nApp.xaml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;Application x:Class=\u0026#34;WpfSample.App\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:s=\u0026#34;https://github.com/canton7/Stylet\u0026#34; xmlns:app=\u0026#34;clr-namespace:WpfSample\u0026#34;\u0026gt; \u0026lt;Application.Resources\u0026gt; \u0026lt;s:ApplicationLoader\u0026gt; \u0026lt;s:ApplicationLoader.Bootstrapper\u0026gt; \u0026lt;app:Bootstrapper /\u0026gt; \u0026lt;/s:ApplicationLoader.Bootstrapper\u0026gt; \u0026lt;/s:ApplicationLoader\u0026gt; \u0026lt;/Application.Resources\u0026gt; \u0026lt;/Application\u0026gt; 绑定 Stylet似乎不再支持Caliburn.Micro的通过x:Name来绑定的机制,必须通过Binding显式指定绑定属性的方式。\n1 \u0026lt;TextBox Text=\u0026#34;{Binding YourName, UpdateSourceTrigger=PropertyChanged}\u0026#34; /\u0026gt; Action的写法则有了更明显的差异。\n1 2 3 \u0026lt;Button Content=\u0026#34;Say Hello\u0026#34; xmlns:stylet=\u0026#34;https://github.com/canton7/Stylet\u0026#34; Command=\u0026#34;{stylet:Action SayHello}\u0026#34; /\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // ShellViewModel.cs private string _name; public string YourName { get =\u0026gt; _name; set { SetAndNotify(ref _name, value); NotifyOfPropertyChange(() =\u0026gt; CanSayHello); } } public bool CanSayHello =\u0026gt; !string.IsNullOrEmpty(YourName); public void SayHello() { _logger.Info(\u0026#34;Say Hello {0}\u0026#34;, _name); _windowManager.ShowMessageBox($\u0026#34;Hello {_name}\u0026#34;); } 不同于Caliburn.Micro的写法cal:Message.Attach=\u0026quot;[Click]=[OnClick()]\u0026quot;\nStylet可以直接指定相应的事件,可读性自然是提升了不少,并且依旧不需要在ViewModel中写ICommand,直接写函数即可。当然也可以通过CommandParameter传递参数。\n上面是通过Command的方式绑定,你也可以绑定到Click或者其它的事件。\n以下是官方的示例:\n1 \u0026lt;Button Click=\u0026#34;{s:Action DoSomething}\u0026#34;\u0026gt;Click me\u0026lt;/Button\u0026gt; 1 2 3 4 5 6 7 public void HasNoArguments() { } // This can accept EventArgs, or a subclass of EventArgs public void HasOneSingleArgument(EventArgs e) { } // Again, a subclass of EventArgs is OK public void HasTwoArguments(object sender, EventArgs e) { } 可以根据需求在ViewModel对应的函数中定义参数或者不定义。\nActionTarget\n1 2 3 4 5 6 7 8 9 10 11 12 class InnerViewModel { public void DoSomething() { } } class ViewModel { public InnerViewModel InnerViewModel { get; private set; } public ViewModel() { this.InnerViewModel = new InnerViewModel(); } } 1 \u0026lt;Button s:View.ActionTarget=\u0026#34;{Binding InnerViewModel}\u0026#34; Command=\u0026#34;{s:Action DoSomething}\u0026#34;\u0026gt;Click me\u0026lt;/Button\u0026gt; 通过指定Action.Target来绑定到其它的ViewModel。\n依赖注入 Stylet提供了两个类供ViewModel继承,Screen和Conductor,这一点和Caliburn.Micro类似。所有的ViewModel都会自动绑定到IoC,不需要在Bootstrapper中进行设置。\n必要时也可以重写Bootstrapper中的ConfigureIoC来注册一些其它的服务。\n这里注册了一个Logging service,Stylet中提供有ILogger的接口,但不建议使用,可以自己实现。\n1 2 3 4 5 6 protected override void ConfigureIoC(IStyletIoCBuilder builder) { base.ConfigureIoC(builder); builder.Bind\u0026lt;ILogger\u0026gt;().To\u0026lt;Logger\u0026gt;().InSingletonScope().AsWeakBinding(); } Stylet提供了多种注入的方式。\n通过构造函数注入\n1 2 3 4 5 6 7 8 9 public NavViewModel( IEventAggregator eventAggregator, FirstTabViewModel tab1, SecondTabViewModel tab2) { this._eventAggregator = eventAggregator; this.Items.Add(tab1); this.Items.Add(tab2); } 通过[Inject]自动注入\n使用[Inject]方式注入时也可以指定相应的Key\n1 2 3 4 // Logger.cs [Inject(Key = \u0026#34;filelogger\u0026#34;)] public class Logger : ILogger {} 1 2 [Inject(Key = \u0026#34;filelogger\u0026#34;)] private ILogger _logger; 抽象工厂\nStylet提供了一种抽象工厂的模式来获取相应的服务。\n这里我定义了一个IViewModelFactory的接口\n1 2 3 4 5 6 7 public interface IViewModelFactory { ShellViewModel GetShellViewModel(); NavViewModel GetNavViewModel(); FirstTabViewModel GetFirstTabViewModel(); SecondTabViewModel GetSecondTabViewModel(); } 然后在Bootstrapper的ConfigureIoC中添加如下代码\n1 2 3 4 5 6 7 // Bootstrapper.cs protected override void ConfigureIoC(IStyletIoCBuilder builder) { ... builder.Bind\u0026lt;IViewModelFactory\u0026gt;().ToAbstractFactory(); ... } 这样就可以通过注入的方式来获取到IViewModelFactory的实例了。\n注意这个过程中我并没有手动去实现IViewModelFactory的接口。\n1 2 3 4 5 6 // ShellViewModel.cs [Inject] private IViewModelFactory _viewModelFactory; // 这时就可以通过Factory来获取相应的ViewModel var vm = _viewModelFactory.GetNavViewModel(); IoC 虽然可以通过注入的方式来获取服务,但有时也需要通过IoC Container来获取相应的服务。Stylet依然有多种方式来获取。\n注入IoC Container\n1 2 3 4 5 6 // 注入IoC Conatiner [Inject] private IContainer _container; // 通过Container获取ViewModel var vm = _container.Get\u0026lt;NavViewModel\u0026gt;(); Static Service Locator\n用过Caliburn.Micro的可能都知道,CM提供了一种非常好用的获取服务的方式。\n1 var vm = IoC.Get\u0026lt;MyDialogViewModel\u0026gt;(); 而Stylet并没有提供这种方式。Stylet作者给出的原因是:\nI don\u0026rsquo;t want to encourage people to write such horrible code.\n但我就是喜欢简单粗暴的,通过IoC.Get的方式比较合我的胃口。Stylet的作者同样也给出了相应的方式链接\n但作者给出的代码中GetAllInstance是不能正确使用的。可以参考我的修改版SimpleIoC\n最后再Bootstrapper中添加下面代码即可。\n1 2 3 4 5 6 7 8 protected override void Configure() { base.Configure(); SimpleIoC.GetInstance = this.Container.Get; SimpleIoC.GetAllInstances = this.Container.GetAll; SimpleIoC.BuildUp = this.Container.BuildUp; } 使用:\n1 var vm = SimpleIoC.Get\u0026lt;NavViewModel\u0026gt;(); WindowManager 1 2 3 4 5 6 public interface IWindowManager { bool? ShowDialog(object viewModel); MessageBoxResult ShowMessageBox(string messageBoxText, string caption = \u0026#34;\u0026#34;, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.None, MessageBoxResult cancelResult = MessageBoxResult.None, IDictionary\u0026lt;MessageBoxResult, string\u0026gt; buttonLabels = null, FlowDirection? flowDirection = null, TextAlignment? textAlignment = null); void ShowWindow(object viewModel); } Stylet的IWindowManager提供了3个接口函数,一个MessageBox,其它两个用于显示窗口,和Caliburn.Micro用法相同,在使用时把ViewModel传如即可。IWindowManager可以直接通过注入的方式获得。\n1 2 3 4 [Inject] private IWindowManager _windowManager; _windowManager.ShowWindow(_viewModelFactory.GetNavViewModel()); EventAggregator EventAggregator和Caliburn.Micro中的用法相同。结合IHandle\u0026lt;T\u0026gt;用于在ViewModel中传递消息。\n订阅消息:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // ShellViewModel.cs public class ShellViewModel : Screen, IHandle\u0026lt;TabChangedEvent\u0026gt; { private readonly IEventAggregator _eventAggregator; ... public ShellViewModel(IEventAggregator eventAggregator) { DisplayName = \u0026#34;Hello Stylet!\u0026#34;; _eventAggregator = eventAggregator; _eventAggregator.Subscribe(this); } protected override void OnClose() { _eventAggregator.Unsubscribe(this); base.OnClose(); } public void Handle(TabChangedEvent message) { // TODO } ... } 发布消息:\n1 2 3 4 5 6 7 8 9 10 11 12 13 // AnotherViewModel.cs private readonly IEventAggregator _eventAggregator; public NavViewModel(IEventAggregator eventAggregator) { this._eventAggregator = eventAggregator; } // 发布消息 private Publish() { _eventAggregator.Publish(new TabChangedEvent()); } Stylet中的EventAggregator同时提供了channels,可以在不同的管道之中订阅/发布消息。\n总结 对于使用过Caliburn.Micro的朋友来说,Stylet非常容易上手,大部分的用法基本上都一样,同时Stylet又提供了一些新的内容。这里有些东西并没有讲到,如ViewManager等,但Stylet已经足够让我兴奋了,还有就是它的体积真的很小,只有100多KB,并且功能也足够强大,用起来也很方便,后面有机会可以在一些小项目中使用。\n","permalink":"https://kira-96.github.io/posts/stylet-%E6%A1%86%E6%9E%B6%E4%BD%93%E9%AA%8C/","summary":"简介 Stylet是一个轻量且功能强大的MVVM框架。支持 .NET 4.5+ 和 .NET Core 3.0+。 Stylet的作者也是受到Caliburn.Micro的启发,并","title":"Stylet 框架体验"},{"content":"这段时间公司的一个项目打算使用Named Pipe进行进程间的通讯,刚好花了点时间了解了一下,这里做一下笔记。\nNamed Pipe(命名管道),顾名思义,是通过在两个进程间搭建一个管道来进行通讯,这种方式的好处在于两者可以进行全双工的通讯,服务端也可以通过管道向客户端发送消息,对于两个进程之间的通讯来说再合适不过了,使用起来也相对比较灵活。\n服务端(Server) Named Pipe 命名空间\n1 using System.IO.Pipes; 创建管道\n1 2 3 4 5 6 7 8 9 10 11 12 13 PipeSecurity security = new PipeSecurity(); // 管道权限 // 设置规则,只有用户admin可以对管道进行读写,其它用户无权访问 security.AddAccessRule(new PipeAccessRule(\u0026#34;admin\u0026#34;, PipeAccessRights.ReadWrite, AccessControlType.Allow)); NamedPipeServerStream server = new NamedPipeServerStream( \u0026#34;SimpleServer\u0026#34;, // pipe name PipeDirection.InOut, // 数据传输方向,这里使用双工通讯 1, // MaxNumberOfServerInstance PipeTransmissionMode.Byte, // 字节流传输 PipeOptions.Asynchronous | PipeOptions.WriteThrough, 4096, // 输入缓冲大小 4096, // 输出缓冲大小 security); // 管道访问权限,这里只做笔记,通常不需要设置 管道创建好之后还不能立刻发送数据,因为管道的另一端(客户端)还没有连接,所以服务端需要等待连接。\n1 server.WaitForConnection(); // 阻塞方式 这里使用非阻塞的方式等待连接,当然也可以用阻塞的方式等待连接,不过需要放到一个新的线程中,避免将主线程阻塞。\n1 server.BeginWaitForConnection(new AsyncCallback(WaitConnectionCallback), server); // 非阻塞方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 private void WaitConnectionCallback(IAsyncResult asyncResult) { NamedPipeServerStream server = asyncResult.AsyncState as NamedPipeServerStream; server.EndWaitForConnection(asyncResult); StartListen(server); } private void StartListen(NamedPipeServerStream server) { Task.Run(async () =\u0026gt; { int bytesToRead; byte[] buffer; // server.WaitForConnection(); while (true) { try { bytesToRead = 256 * server.ReadByte(); bytesToRead += server.ReadByte(); // 演示 // 将收到的数据立马发送出去 server.WriteByte((byte)(bytesToRead / 256)); server.WriteByte((byte)(bytesToRead % 256)); buffer = new byte[bytesToRead]; server.Read(buffer, 0, bytesToRead); // 读取消息 // 演示 // 将收到的数据立马发送出去 await server.WriteAsync(buffer, 0, bytesToRead); } catch (System.IO.IOException) { // break if another pipe end closed break; } catch (Exception) { break; } // 处理数据 string content = Encoding.UTF8.GetString(buffer); Console.WriteLine(content); } server.Disconnect(); // 断开连接 // 重新等待连接 server.BeginWaitForConnection(new AsyncCallback(WaitConnectionCallback), server); }); } 在客户端连接之后,立马启动一个新的线程循环读取来自客户端的消息,这里的消息前两个字节指定了消息的长度。同时将收到的消息马上返回到管道的另一端(这里用于测试是否真的是全双工工作)。\n最后将消息的读取放到try{ ... } catch(...){ ... }中,因为并没有消息或者事件通知服务端客户端已经断开连接。但当客户端断开之后,服务端在读取时会抛出IOException,可以通过抓取这个错误来判断管道是否已经断开。\n当客户端断开之后,中断读取循环,服务端也断开连接,并再次等待客户端连接。\n客户端(Client) 创建客户端\n1 2 3 4 5 NamedPipeClientStream client = new NamedPipeClientStream( \u0026#34;.\u0026#34;, // The name of the remote computer, \u0026#34;.\u0026#34; 指本机 \u0026#34;SimpleServer\u0026#34;, // pipe name PipeDirection.InOut, // 数据传输方向 PipeOptions.Asynchronous | PipeOptions.WriteThrough); 连接到服务端\n1 client.Connect(); // 阻塞方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Task.Run(() =\u0026gt; { int bytesToRead; byte[] buffer; client.Connect(); // 连接管道 while (true) { try { // 读取消息长度 bytesToRead = 256 * client.ReadByte(); bytesToRead += client.ReadByte(); buffer = new byte[bytesToRead]; client.Read(buffer, 0, bytesToRead); // 读取消息 } catch (System.IO.IOException) { break; } catch (Exception) { break; } // 处理消息 string content = Encoding.UTF8.GetString(buffer); Console.WriteLine(content); } client.Close(); }); 这里使用一个新的线程去连接管道服务端,连接成功后循环读取来自服务端的消息。\n客户端发送\n1 2 3 4 5 6 7 8 9 10 11 12 13 string text = \u0026#34;Hello\u0026#34;; byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text); client.WriteByte((byte)(bytes.Length / 256)); // 发送消息头 client.WriteByte((byte)(bytes.Length % 256)); client.Write(bytes, 0, bytes.Length); // 发送消息 text = \u0026#34;World\u0026#34;; bytes = System.Text.Encoding.UTF8.GetBytes(text); client.WriteByte((byte)(bytes.Length / 256)); // 发送消息头 client.WriteByte((byte)(bytes.Length % 256)); client.Write(bytes, 0, bytes.Length); // 发送消息 总结 总的来说,Named Pipe使用还是比较简单的,结合序列化就可以直接在两个进程中传递消息对象了。需要注意的是一个服务端只能有一个客户端连接,而且在客户端断开连接之后,服务端也需要断开连接,并重新等待客户端连接,不然再有客户端尝试连接管道也无法建立。\n参考\nHow to: Use Named Pipes for Network Interprocess Communication\n","permalink":"https://kira-96.github.io/posts/%E4%BD%BF%E7%94%A8-named-pipe-%E8%BF%9B%E8%A1%8C%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E8%AE%AF/","summary":"这段时间公司的一个项目打算使用Named Pipe进行进程间的通讯,刚好花了点时间了解了一下,这里做一下笔记。 Named Pipe(命名管道),顾名思义","title":"使用 Named Pipe 进行进程间通讯"},{"content":"简介 Hprose(High Performance Remote Object Service Engine)是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。\n也是一个跨语言的RPC框架,但由于库的质量参差不齐,一些语言的库并不完善。这里以C#为例来实现一个简单的服务端和客户端程序。\n创建项目 新建解决方案,包含两个项目\nserver\n控制台程序,服务端\nclient\n控制台程序,客户端\n然后,通过Nuget分别为两个项目安装Hprose.RPC库。\n这里并不需要再创建额外的服务接口项目,只需要手动定义一个接口即可。\n创建服务接口 在server和client项目下都新建一个接口,作为服务接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // IHello.cs public class ServiceVersion { public string Name { get; set; } public string Version { get; set; } public ServiceVersion() { } public ServiceVersion(string name, string ver) { Name = name; Version = ver; } } // 服务接口 public interface IHello { ServiceVersion GetVersion(); List\u0026lt;string\u0026gt; SayHello(string name); } IHello里面的两个接口函数就是服务接口了。\n服务端程序 依旧是服务端实现接口,客户端来调用,在server项目下新建类Hello.cs\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Hello.cs namespace server { using System.Collections.Generic; public class Hello : IHello { public ServiceVersion GetVersion() { return new ServiceVersion(\u0026#34;Hello Service\u0026#34;, \u0026#34;0.0.1.21\u0026#34;); } public List\u0026lt;string\u0026gt; SayHello(string name) { return new List\u0026lt;string\u0026gt;() { $\u0026#34;你好 {name}\u0026#34;, $\u0026#34;Hello {name}\u0026#34;, $\u0026#34;Hola {name}\u0026#34;, $\u0026#34;Bonjour {name}\u0026#34;, $\u0026#34;こんにちは {name}\u0026#34;, $\u0026#34;hallo {name}\u0026#34; }; } } } 编写服务启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace server { using Hprose.RPC; using System.Net; class Program { static void Main() { HttpListener server = new HttpListener(); server.Prefixes.Add(\u0026#34;http://localhost:10240/\u0026#34;); server.Start(); Service service = new Service().Bind(server).AddInstanceMethods(new Hello()); System.Console.WriteLine(\u0026#34;Server listening at http://localhost:10240/ \\n Press any key exit ...\u0026#34;); System.Console.ReadKey(); server.Stop(); } } } 客户端程序 由于client项目刚刚也定义了IHello接口,这里就可以直接调用接口函数了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 namespace client { using Hprose.RPC; class Program { static void Main() { Client cli = new Client(\u0026#34;http://localhost:10240/\u0026#34;); IHello hello = cli.UseService\u0026lt;IHello\u0026gt;(); ServiceVersion ver = hello.GetVersion(); System.Console.WriteLine(\u0026#34;Remote Service Version: {0} - v{1}\u0026#34;, ver.Name, ver.Version); var hellos = hello.SayHello(\u0026#34;Hprose\u0026#34;); foreach (string item in hellos) { System.Console.WriteLine(item); } System.Console.ReadKey(); } } } 运行测试 先启动服务端程序,再启动客户端程序,可以看到客户端输出\nRemote Service Version: Hello Service - v0.0.1.21 你好 Hprose Hello Hprose Hola Hprose Bonjour Hprose こんにちは Hprose hallo Hprose 与Thrift和gRPC相比,Hprose实现起来要简单很多,暂时还没有尝试跨语言调用,不知道是不是同样简单。\n","permalink":"https://kira-96.github.io/posts/hprose-c_-%E5%88%9D%E6%8E%A2/","summary":"简介 Hprose(High Performance Remote Object Service Engine)是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,","title":"Hprose C# 初探"},{"content":"简介 Thrift是由Facebook为“大规模跨语言服务开发”而开发的一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。目前被作为一个RPC框架使用。\n下载 使用之前需要先下载Thrift的源代码和Thrift编译器。\n下载源代码后解压,进入到thrift-0.12.0\\lib\\csharp\\src目录下,打开Thrift.sln,根据需要编译相应的库,这里选择Thrift.45,即.NET 4.5可以使用的库,编译生成Thrift45.dll。\n创建项目 新建解决方案,包含3个项目\nThriftSample\n类库,Thrift生成的服务接口\nserver\n控制台程序,服务端\nclient\n控制台程序,客户端\n这3个项目都需要引用刚刚编译的Thrift45.dll,为什么不用Nuget来安装Thrift呢?因为我注意到Nuget上的Thrift已经好几年没更新了,还是手动编译最新的要好。\n然后,server和client同时引用项目ThriftSample。\n定义服务接口 在解决方案目录下新建一个文件Sample.thrift来定义服务接口,Thrift语法可以在网上找到\n1 2 3 4 5 6 7 8 9 10 11 namespace csharp kira.Interface service SampleService { ServiceVersion GetVersion() list\u0026lt;string\u0026gt; SayHello(1: string name) } struct ServiceVersion { 1: required string name; 2: required string version; } 这里指定了生成类的命名空间,以及定义了一个结构体作为返回值\n生成服务接口 这里就需要用到之前下载的Thrift编译器,可以直接在Thrift官网找到。将下载的thrift-0.12.0.exe也拷贝到解决方案目录下(和Sample.thrift相同目录),打开命令窗口,执行以下命令\n1 $ thrift-0.12.0.exe -gen csharp Sample.thrift 不得不说,Thrift的命令行语法真的比gRPC简洁多了。\n执行完没有错误的话,就可以看到目录下又多出了一个gen-csharp的文件夹,里面有对应命名空间的文件夹,最后找到生成的.cs文件。将文件全部拷贝到ThriftSample项目目录下,并将它们添加到项目,编译生成库。\n服务端程序 在server项目下新建类MySampleService并实现接口SampleService.Iface,重写其中的两个服务接口函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace server { using System.Collections.Generic; using kira.Interface; public class MySampleService : SampleService.Iface { public ServiceVersion GetVersion() { return new ServiceVersion() { Name = \u0026#34;My Sample Service\u0026#34;, Version = \u0026#34;0.0.1.20\u0026#34; }; } public List\u0026lt;string\u0026gt; SayHello(string name) { return new List\u0026lt;string\u0026gt;() { $\u0026#34;你好 {name}\u0026#34;, $\u0026#34;Hello {name}\u0026#34;, $\u0026#34;Hola {name}\u0026#34;, $\u0026#34;Bonjour {name}\u0026#34;, $\u0026#34;こんにちは {name}\u0026#34;, $\u0026#34;hallo {name}\u0026#34; }; } } } 编写服务启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace server { using Thrift.Server; using Thrift.Transport; using kira.Interface; class Program { static void Main(string[] args) { MySampleService service = new MySampleService(); SampleService.Processor processor = new SampleService.Processor(service); TServerTransport serverTransport = new TServerSocket(10240); TServer server = new TThreadPoolServer(processor, serverTransport); System.Console.WriteLine(\u0026#34;server listening at tcp://localhost:10240/\u0026#34;); server.Serve(); } } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 namespace client { using Thrift.Protocol; using Thrift.Transport; using kira.Interface; class Program { static void Main(string[] args) { TTransport transport = new TSocket(\u0026#34;localhost\u0026#34;, 10240); transport.Open(); TProtocol protocol = new TBinaryProtocol(transport); SampleService.Client cli = new SampleService.Client(protocol); ServiceVersion ver = cli.GetVersion(); System.Console.WriteLine(\u0026#34;Remote Service Version: {0} - v{1}\u0026#34;, ver.Name, ver.Version); var hellos = cli.SayHello(\u0026#34;Thrift\u0026#34;); foreach (string item in hellos) { System.Console.WriteLine(item); } System.Console.ReadKey(); transport.Close(); } } } 运行测试 老样子,先启动服务端程序,然后运行客户端程序,可以看到客户端输出\nRemote Service Version: My Sample Service - v0.0.1.20 你好 Thrift Hello Thrift Hola Thrift Bonjour Thrift こんにちは Thrift hallo Thrift 总结 对比Thrift和gRPC两个主流的RPC框架,个人感觉Thrift使用起来要更加灵活一些,当然这只是初步接触,较深层次的内容还没有去研究,实际项目的应用感觉两个都可以,有对比说Thrift框架的性能要优于gRPC,但对于小的项目来说已经完全够用了。\n","permalink":"https://kira-96.github.io/posts/thrift-c_-%E5%88%9D%E6%8E%A2/","summary":"简介 Thrift是由Facebook为“大规模跨语言服务开发”而开发的一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。目前","title":"Thrift C# 初探"},{"content":"简介 gRPC是Google开源的一个现代化、高性能的RPC框架,基于HTTP/2标准设计,同时提供多个语言版本,并支持跨语言调用,可以在任何环境中运行。\n创建项目 新建解决方案,包含3个项目\ngRpcSample\n类库,gRPC生成的接口,Server接口、Client接口等\nserver\n控制台程序,服务端\nclient\n控制台程序,客户端\n分别给3个项目安装Nuget程序包Grpc并安装所需依赖,然后为gRpcSample项目安装Grpc.Tools和Google.ProtoBuf程序包。\n同时,使项目server和client引用项目gRpcSample。\n定义服务接口 在gRpcSample项目的文件夹下新建Sample.proto文件,以文本方式打开,修改其中接口定义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 syntax = \u0026#34;proto3\u0026#34;; option csharp_namespace = \u0026#34;kira.Interface\u0026#34;; package sampleservice; service SampleService { rpc ServerVersion(VersionRequest) returns (VersionResponse) {} rpc SayHello(HelloRequest) returns (stream HelloResponse) {} } message VersionRequest {} message VersionResponse { string name = 1; string version = 2; } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } 这里指定了生成的类的命名空间,同时定义了两个服务函数,似乎每个函数都必须有参数(请求)和返回(响应),这一点不太清楚。具体的语法有条件的可以参考proto3 language guide。\n生成服务接口 在进行下面操作前,建议先将Grpc.Tools拷贝到解决方案目录下,不然的话下面的命令会很长很长\u0026hellip;\n具体操作是将解决方案下packages\\Grpc.Tools.2.23.0-pre1\\tools\\windows_x64\\里面的protoc.exe和grpc_csharp_plugin.exe拷贝到解决方案目录下,完成后就可以进行下一步。\n在解决方案目录下打开命令窗口,并执行下面命令\nTip: 直接进入到相应文件夹下,按住Shift键,在空白出单击鼠标右键,就可以看到菜单中多出了一项在此处打开命令窗口(W)\n1 $ protoc -IgRpcSample --csharp_out gRpcSample gRpcSample\\Sample.proto --grpc_out gRpcSample --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe 执行完没有错误的话,就可以看到gRpcSample项目下多出了两个文件Sample.cs和SampleGrpc.cs,将这两个文件添加到项目gRpcSample。\n编译gRpcSample通过。\n进行到这里,基本的工作就都完成了,剩下的就是编写服务端和客户端程序了。\n服务端程序 service项目新建类MySampleService,并继承SampleService.SampleServiceBase,重写刚刚定义的两个服务接口函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 namespace server { using System.Threading.Tasks; using Grpc.Core; using kira.Interface; public class MySampleService : SampleService.SampleServiceBase { public override Task\u0026lt;VersionResponse\u0026gt; ServerVersion(VersionRequest request, ServerCallContext context) { return Task.FromResult\u0026lt;VersionResponse\u0026gt;( new VersionResponse() { Name = \u0026#34;My Sample Service\u0026#34;, Version = \u0026#34;0.0.1.19\u0026#34; }); } public override async Task SayHello(HelloRequest request, IServerStreamWriter\u0026lt;HelloResponse\u0026gt; responseStream, ServerCallContext context) { string[] hellos = { \u0026#34;你好\u0026#34;, \u0026#34;Hello\u0026#34;, \u0026#34;Hola\u0026#34;, \u0026#34;Bonjour\u0026#34;, \u0026#34;こんにちは\u0026#34;, \u0026#34;hallo\u0026#34; }; foreach (string item in hellos) { await responseStream.WriteAsync(new HelloResponse() { Message = $\u0026#34;{item} {request.Name}\u0026#34; }); } } } } 编写服务启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace server { using Grpc.Core; using kira.Interface; class Program { static void Main(string[] args) { Server myServer = new Server() { Services = { SampleService.BindService(new MySampleService()) }, Ports = { new ServerPort(\u0026#34;localhost\u0026#34;, 10240, ServerCredentials.Insecure) } }; myServer.Start(); System.Console.WriteLine(\u0026#34;Sample Server listening on localhost:10240 \\nPress any key exit...\u0026#34;); System.Console.ReadKey(); myServer.ShutdownAsync().Wait(); } } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 namespace client { using System.Threading.Tasks; using Grpc.Core; using kira.Interface; class Program { static void Main(string[] args) { Program program = new Program(); program.TestService(); System.Console.ReadKey(); } async void TestService() { Channel channel = new Channel(\u0026#34;localhost:10240\u0026#34;, ChannelCredentials.Insecure); SampleService.SampleServiceClient cli = new SampleService.SampleServiceClient(channel); VersionResponse ver = cli.ServerVersion(new VersionRequest()); System.Console.WriteLine(\u0026#34;Remote Service Version: {0} - v{1}\u0026#34;, ver.Name, ver.Version); AsyncServerStreamingCall\u0026lt;HelloResponse\u0026gt; greetings = cli.SayHello(new HelloRequest() { Name = \u0026#34;gRPC\u0026#34; }); IAsyncStreamReader\u0026lt;HelloResponse\u0026gt; stream = greetings.ResponseStream; while (await stream.MoveNext()) { System.Console.WriteLine(stream.Current.Message); } } } } 运行测试 首先启动服务端程序,然后运行客户端程序,可以看到客户端输出\nRemote Service Version: My Sample Service - v0.0.1.19 你好 gRPC Hello gRPC Hola gRPC Bonjour gRPC こんにちは gRPC hallo gRPC OK,大功告成!\n","permalink":"https://kira-96.github.io/posts/grpc-c_-%E5%88%9D%E6%8E%A2/","summary":"简介 gRPC是Google开源的一个现代化、高性能的RPC框架,基于HTTP/2标准设计,同时提供多个语言版本,并支持跨语言调用,可以在任何","title":"gRPC C# 初探"},{"content":"进程间传递数据的方法 在进程间传递数据也就意味着两个不同的应用程序之间的通讯,大家可能会想到使用消息队列(Message Queue)来作为解决方案,当然这可能是最优解,然而这里我要讲的是另外一种方法,通过Windows的消息机制来传递数据,内容比较硬核。\n依旧用到了两个Windows的API,FindWindow和SendMessage,以及WPF如何和MFC窗口通讯,可以参考上一篇文章。\n传递数据的方式 这里需要先知道一个Window消息WM_COPYDATA 它在WinUser.h中的定义如下\n1 #define WM_COPYDATA 0x004A 这个消息就是这次要讲的内容,通过这个消息就可以在不同的窗口间传递数据了,它有两个参数,WPARAM是发送消息窗口的句柄,LPARAM是一个结构体的指针,这个结构体在WinUser.h里定义如下:\n1 2 3 4 5 6 7 8 /* * lParam of WM_COPYDATA message points to... */ typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData; DWORD cbData; _Field_size_bytes_(cbData) PVOID lpData; } COPYDATASTRUCT, *PCOPYDATASTRUCT; 嗯\u0026hellip;看起来也不是很复杂,为了能够正确处理这个指针,需要在C#中也定义一个相同的结构体\n1 2 3 4 5 6 7 8 9 10 11 12 /// \u0026lt;summary\u0026gt; /// COPYDATASTRUCT /// 对应 C++ 里的 COPYDATASTRUCT /// 不能更改 /// \u0026lt;/summary\u0026gt; [StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT { public IntPtr dwData; //可以是任意值 public int cbData; //指定lpData内存区域的字节数 public IntPtr lpData; //发送给目录窗口所在进程的数据 } 这样就可以了,其中最重要的就是lpData,它可以是任意对象的指针,只要C++和C#两边定义了一个相同的结构体,我们就可以用它来直接传递结构体对象\u0026#x1f62e;,这种方式比通过消息队列传递数据要快得多。\n那么先来定义一个简单的结构体吧:\n1 2 3 4 5 6 7 8 9 10 11 12 // C++ struct StructUser { char UserName[32]; char Password[32]; StructUser() { ZeroMemory(UserName, 32); ZeroMemory(Password, 32); } }; 1 2 3 4 5 6 7 8 9 // C# [StructLayout(LayoutKind.Sequential)] public struct StructUser { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string UserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string Password; } 这里定义了一个User的结构体,UserName和Password都是长度为32的字符串,注意C#中结构体的定义方式,这里指定了字符串的长度,就是为了和C++的结构体保持一致。\n先来看看C++中如何发送和处理WM_COPYDATA消息的:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 // 发送 StructUser user; char userName[32]; char password[32]; // GetDlgItem(IDC_EDIT_USERNAME)-\u0026gt;GetWindowTextA(userName, 32); // GetDlgItem(IDC_EDIT_PASSWORD)-\u0026gt;GetWindowTextA(password, 32); memcpy(user.UserName, userName, 32); memcpy(user.Password, password, 32); COPYDATASTRUCT copyData; copyData.dwData = 0; copyData.lpData = \u0026amp;user; copyData.cbData = sizeof(StructUser); HWND hWnd = nullptr; if (m_pTargerWnd == nullptr) { hWnd = ::FindWindow(nullptr, \u0026#34;WpfWindow\u0026#34;); } else { hWnd = m_pTargerWnd-\u0026gt;GetSafeHwnd(); } ::SendMessage(hWnd, WM_COPYDATA, (WPARAM)GetSafeHwnd(), (LPARAM)\u0026amp; copyData); // 接收 /* C++ 中相关代码 * 处理 WM_COPYDATA 消息 * Header File(.h) --------------------------------------------------------------------- ... afx_msg BOOL OnCopyData(CWnd *pWnd, COPYDATASTRUCT *pCopyDataStruct); ... DECLARE_MESSAGE_MAP() --------------------------------------------------------------------- * Source File(.cpp) BEGIN_MESSAGE_MAP(CxxxDlg, CDialogEx) ... ON_WM_COPYDATA() ... END_MESSAGE_MAP() ... */ BOOL CxxDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) { if (pWnd != nullptr) { m_pTargerWnd = pWnd; } if (pCopyDataStruct != nullptr) { StructUser* pUser = (StructUser*)(pCopyDataStruct-\u0026gt;lpData); // DWORD dwLen = pCopyDataStruct-\u0026gt;cbData; // GetDlgItem(IDC_EDIT_USERNAME)-\u0026gt;SetWindowTextA(pUser-\u0026gt;UserName); // GetDlgItem(IDC_EDIT_PASSWORD)-\u0026gt;SetWindowTextA(pUser-\u0026gt;Password); } return CDialogEx::OnCopyData(pWnd, pCopyDataStruct); } C#会比较复杂一些\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // 消息处理函数 private IntPtr WndProcFunc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { case WM_COPYDATA: // public const int WM_COPYDATA = 0x004A; IntPrt hWnd_Target = wParam; COPYDATASTRUCT param = Marshal.PtrToStructure\u0026lt;COPYDATASTRUCT\u0026gt;(lParam); StructUser user = Marshal.PtrToStructure\u0026lt;StructUser\u0026gt;(param.lpData); // UserName.Text = user.UserName; // Password.Text = user.Password; break; default: break; } return IntPtr.Zero; } // 发送消息 StructUser sctUser = new StructUser() { UserName = UserName.Text, Password = Password.Text }; IntPtr userPtr = Marshal.AllocHGlobal(Marshal.SizeOf\u0026lt;StructUser\u0026gt;()); Marshal.StructureToPtr\u0026lt;StructUser\u0026gt;(sctUser, userPtr, true); COPYDATASTRUCT copyData = new COPYDATASTRUCT() { dwData = IntPtr.Zero, cbData = Marshal.SizeOf\u0026lt;StructUser\u0026gt;(), lpData = userPtr, }; IntPtr copyDataPtr = Marshal.AllocHGlobal(Marshal.SizeOf\u0026lt;COPYDATASTRUCT\u0026gt;()); Marshal.StructureToPtr\u0026lt;COPYDATASTRUCT\u0026gt;(copyData, copyDataPtr, true); if (hWnd_Target == IntPtr.Zero) hWnd_Target = Win32Api.FindWindow(null, \u0026#34;MfcWindow\u0026#34;); hWnd_MainWnd = new WindowInteropHelper(this).Handle; if (hWnd_Target != IntPtr.Zero) Win32Api.SendMessage(hWnd_Target, WM_COPYDATA, hWnd_MainWnd, copyDataPtr); Marshal.FreeHGlobal(userPtr); // Marshal.FreeHGlobal(copyDataPtr); // 最后一定要释放掉非托管内存 处理消息的地方和上一篇一样,发送的地方比较复杂,总体来讲就是用Marshal开辟了两块非托管的内存来存放两个结构体的数据,等到消息被处理之后再将非托管内存释放掉,避免内存泄漏。\n那么,这次的内容就到这里了,主要内容都是代码,但其实也不复杂,只是需要慢慢消化。\n","permalink":"https://kira-96.github.io/posts/wpf%E5%92%8Cmfc%E8%BF%9B%E7%A8%8B%E9%97%B4%E4%BC%A0%E9%80%92%E6%95%B0%E6%8D%AE/","summary":"进程间传递数据的方法 在进程间传递数据也就意味着两个不同的应用程序之间的通讯,大家可能会想到使用消息队列(Message Queue)来作为解决","title":"WPF和MFC进程间传递数据"},{"content":"发送消息到指定窗口 发送消息相对来说比较简单,这里先讲,这里需要用到两个Windows的API\n1 2 3 4 5 6 7 8 9 10 11 12 // 查找指定窗口 [DllImport(\u0026#34;User32.dll\u0026#34;, EntryPoint = \u0026#34;FindWindow\u0026#34;, CharSet = CharSet.Auto)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); //消息发送API [DllImport(\u0026#34;User32.dll\u0026#34;, EntryPoint = \u0026#34;SendMessage\u0026#34;, CharSet = CharSet.Auto)] public static extern int SendMessage( IntPtr hWnd, // 信息发往的窗口的句柄 int Msg, // 消息ID IntPtr wParam, // 参数1 IntPtr lParam // 参数2 ); FindWindow就是根据窗口的名字去找到相应的窗口句柄,SendMessage就是发送消息到指定窗口了,写过MFC的应该不陌生\n1 2 3 4 IntPtr hWnd = FindWindow(null, windowName); if (hWnd == IntPtr.Zero) return; SendMessage(hWnd, WM_USER + 2, IntPtr.Zero, \u0026#34;msg\u0026#34;); 发送消息就讲这么多,剩下的需要自行摸索,下面是用WPF窗口接收消息\n获取WPF窗口句柄 Windows消息是通过窗口句柄来传递给指定窗口的,所以想要处理WPF窗口的收到消息,首先就需要获取自身的窗口句柄。\n1 HwndSource hWnd = PresentationSource.FromVisual(this) as HwndSource; 或者\n1 2 IntPtr hwnd = new WindowInteropHelper(this).Handle; HwndSource source = HwndSource.FromHwnd(hwnd); 这里的this就是WPF的窗口,当然也可以通过这种方式获取窗口任意控件的句柄\n添加钩子(AddHook) 得到窗口的句柄之后就可以为该窗口添加钩子来处理窗口收到的消息了\n1 2 3 4 5 6 7 8 9 10 11 12 protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); // Add Hook // HwndSource source = PresentationSource.FromVisual(this) as HwndSource; // source.AddHook(WndProcFunc); // 或者 HwndSource.FromHwnd(new WindowInteropHelper(this).Handle).AddHook(new HwndSourceHook(WndProcFunc)); } 或者\n1 2 3 4 5 6 7 public MainWindow() { InitializeComponent(); this.SourceInitialized += (s, e) =\u0026gt; { HwndSource.FromHwnd(new WindowInteropHelper(this).Handle).AddHook(new HwndSourceHook(WndProcFunc)); }; } 这个WndProcFunc就是我们的消息处理函数了,窗口在收到消息之后就会走到这个函数里面,它看起来像是这样\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { // Handle Msg Here switch (msg) { case WM_USER + 1: // 你的消息值 { // 处理 wParam, lParam break; } // ... 其它消息 default: break; } return IntPtr.Zero; } 这样就完了吗?是的,但是!目前还不能处理传递的参数,如果WPF程序和C++程序处于一个进程还好说,如果是两个进程,那么他们之间的内存是不共用的,所以即使WPF窗口拿到了指针也读不出指针里的内容。\n那么要怎样才能在WPF程序和C++程序之间传递值呢,有空在讲\u0026hellip;\n","permalink":"https://kira-96.github.io/posts/wpf%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86windows%E6%B6%88%E6%81%AF/","summary":"发送消息到指定窗口 发送消息相对来说比较简单,这里先讲,这里需要用到两个Windows的API 1 2 3 4 5 6 7 8 9 10 11 12 // 查找指定窗口 [DllImport(\u0026#34;User32.dll\u0026#34;, EntryPoint = \u0026#34;FindWindow\u0026#34;, CharSet","title":"WPF如何处理Windows消息"},{"content":"标题 1 2 3 4 5 6 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题 也可以使用闭合方式的标题,结尾的#可以不必和开头一致\n1 2 3 # 一级标题 # ## 二级标题 ## ... 另一种方式\n1 2 3 4 5 一级标题 ======= 二级标题 ------- 当然也可以用HTML的方式\n1 2 3 4 5 6 \u0026lt;h1\u0026gt;一级标题\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;二级标题\u0026lt;/h2\u0026gt; \u0026lt;h3\u0026gt;三级标题\u0026lt;/h3\u0026gt; \u0026lt;h4\u0026gt;四级标题\u0026lt;/h4\u0026gt; \u0026lt;h5\u0026gt;五级标题\u0026lt;/h5\u0026gt; \u0026lt;h6\u0026gt;六级标题\u0026lt;/h6\u0026gt; HTML的好处在于可以方便的使标题居中\n1 \u0026lt;h1 align=\u0026#34;center\u0026#34;\u0026gt;居中标题\u0026lt;/h1\u0026gt; 目录 可以使用[TOC]标记来自动生成目录,但兼容性貌似不怎么好\n1 [TOC] 分隔线 可以使用3个以上的*、-作为分隔线,中间也可以插入空格\n1 2 3 4 *** * * * --- - - - 字体 粗体\n在需要以粗体显示的文字前后各加两个*或_可以使文字加粗显示\n1 2 **粗体** __粗体__ 斜体\n在需要以斜体显示的文字前后各加一个*或_可以使文字已斜体显示\n1 2 *斜体* _斜体_ 删除线\n在文字前后各加两个~可以在文字上添加删除线\n1 ~~删除线~~ 当然也可以进行组合使用\n1 2 ***斜体加粗*** __~~粗体删除线~~__ 颜色 在写作过程中可能会遇到不少情况需要将文字用不同颜色标注,可以使用HTML的方式来实现,同时也可以设置字体和大小\n1 \u0026lt;font face=\u0026#34;微软雅黑\u0026#34; color=red size=12\u0026gt;落霞与孤鹜齐飞,秋水共长天一色。\u0026lt;/font\u0026gt; 段落 Markdown的换行有些奇特,直接Enter换行它好像不认,需要在段落结尾加两个空格+换行才可以,或者在上一段落和下一段落之间再加一行空行,即两次换行也可以。\n落霞与孤鹜齐飞,秋水共长天一色。 渔舟唱晚,响穷彭蠡之滨;\n雁阵惊寒,声断衡阳之浦。\n1 2 3 4 落霞与孤鹜齐飞,秋水共长天一色。 渔舟唱晚,响穷彭蠡之滨; 雁阵惊寒,声断衡阳之浦。 引用 写在\u0026gt;后的文字即可显示为引用,引用可以嵌套使用\n1 2 3 \u0026gt; 引用的文字 \u0026gt;\u0026gt; 嵌套引用的文字 \u0026gt;\u0026gt;\u0026gt; 更多嵌套 表格 表头 表头 表头 表头 内容 居左 居中 居右 1 2 3 |表头|表头|表头|表头| |---|:--|:--:|---:| |内容|居左|居中|居右| 第一行是表头,第二行代表对齐方式,默认是居左,在-左边加:即可居左对齐,在-右边加:可居右对齐,两边都加:表示居中对齐\n列表 有序列表\n1 2 3 1. 列表1 2. 列表2 3. 列表3 无序列表\n可以使用*、+或者-作为标记\n1 2 3 * 列表1 + 列表2 - 列表3 任务列表\n@mentions, #refs, links, formatting, and tags supported list syntax required (any unordered or ordered list supported) this is a complete item this is an incomplete item 1 2 - [x] 已完成的任务 - [ ] 未完成的任务 链接 可以直接输入网址,如:https://github.com/\n或者使用格式:[Text](url)\n点击这里返回主页\n1 点击[这里](https://kira-96.github.io/)返回主页 也可以使用HTML的方式\n1 点击\u0026lt;a href=\u0026#34;https://kira-96.github.io/\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;这里\u0026lt;/a\u0026gt;返回主页 还有一种就是使用索引的方式 例:谷歌、百度\n1 2 3 4 例:[谷歌][1]、[百度][2] [1]: https://www.google.com.hk/ \u0026#34;google\u0026#34; [2]: https://www.baidu.com/ \u0026#34;百度\u0026#34; 锚 主要用于在页面内跳转\n点击这里查看链接的用法\n1 点击[这里](#链接)查看链接的用法 图片 图片和链接的格式很像,url可以使用相对位置和绝对位置,当然网络位置也可以\n![Alt Text](url)\n1 ![图片](https://image-url.jpg) 也可以使用HTML的方式\n1 \u0026lt;img src=\u0026#34;https://image-url.jpg\u0026#34; width=\u0026#34;50%\u0026#34; height=\u0026#34;50%\u0026#34;\u0026gt; 设置对齐方式\n1 2 3 \u0026lt;div align=center\u0026gt; \u0026lt;img src=\u0026#34;https://image-url.jpg\u0026#34; width=\u0026#34;50%\u0026#34; height=\u0026#34;50%\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; 标注 这个用的并不多,看起来像是课本上文言文里面那种注释的感觉\n例:\n滕王阁序的作者是王勃1。\n1 2 3 滕王阁序的作者是王勃[^1]。 [^1]: 王勃(约650——676年),唐代诗人。汉族,字子安。绛州龙门(今山西河津)人。王勃与杨炯、卢照邻、骆宾王齐名,世称“初唐四杰”,其中王勃是“初唐四杰”之首。 行内代码 可以直接使用两个`(反引号)包裹行内代码\n例:我们学习的第一行代码通常都是printf(\u0026quot;Hello World!\u0026quot;)。\n1 我们学习的第一行代码通常都是`printf(\u0026#34;Hello World!\u0026#34;)`。 语法高亮 1 2 3 4 5 int main(void) { printf(\u0026#34;Hello World!\\n\u0026#34;); return 0; } 1 2 3 4 5 6 7 ``` cpp int main(void) { printf(\u0026#34;Hello World!\\n\u0026#34;); return 0; } ``` 公式 公式对于写论文的同学来说是非常有用的,Markdown的公式也比word的公式编辑方便多了。\n行内公式,使用$ $包括在内。\n如:$e=mc^2$\n1 $e=mc^2$ 单行公式,公式会单独占用一行,使用$$ $$包括在内。\n$$Fe+CuSO_4=FeSO_4+Cu$$\n1 $$Fe+CuSO_4=FeSO_4+Cu$$ 其中具体的符号和字母之类的需要的时候可以到网上去找,如Markdown 数学公式。\n转义字符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \\\\ 反斜杠 \\` 反引号 \\* 星号 \\_ 下划线 \\{\\} 大括号 \\[\\] 中括号 \\(\\) 小括号 \\# 井号 \\+ 加号 \\- 减号 \\. 英文句号 \\! 感叹号 注释 可以使用HTML的注释方式,会在生成的HTML中以注释的形式存在,不显示出来。\n1 \u0026lt;!-- 我是注释内容 --\u0026gt; 或者使用\n1 2 3 \u0026lt;div style=\u0026#39;display: none\u0026#39;\u0026gt; 我是注释内容 \u0026lt;/div\u0026gt; \u0026#x1f603; Emoji \u0026#x1f389; Markdown甚至支持Emoji\n\u0026#x1f60d;\u0026#x1f61c;\u0026#x1f620;\u0026#x1f4a2;\u0026#x1f637;\u0026#x1f47f;\u0026#x1f608;\u0026#x1f495;\nEmoji Cheat Sheet\n写在最后 自从接触了Markdown之后,我就很少使用Word这类工具了。日常工作和生活中用它来写文档和笔记真的是相当舒服,语法简单好记,完全可以满足需求,使用起来方便快捷,还可以借助HTML来实现一些比较复杂的功能。\n不过我们公司内部似乎没什么人使用,可能是由于我们公司并不是互联网企业,所以没有那么潮流,感觉可以借机会安利一波,对于提高整体的工作效率也有不小的帮助。\n王勃(约650——676年),唐代诗人。汉族,字子安。绛州龙门(今山西河津)人。王勃与杨炯、卢照邻、骆宾王齐名,世称“初唐四杰”,其中王勃是“初唐四杰”之首。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://kira-96.github.io/posts/markdown-%E5%B8%B8%E7%94%A8%E8%AF%AD%E6%B3%95%E5%A4%87%E5%BF%98/","summary":"标题 1 2 3 4 5 6 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题 也可以使用闭合方式的标题,结尾的#可以不必和开头一致 1 2 3 # 一级","title":"Markdown 常用语法备忘"},{"content":"桌面端 Typora 收费,支持 Mac, Windows, Linux\nMarkText 支持 Mac, Windows, Linux\niA Writer 收费, 支持 Mac, Windows\n移动端 iA Writer iOS - 收费 Android - 免费\n熊掌记 支持 Mac, iOS, 可免费使用,部分功能需要订购\n笔记软件 Joplin 支持 Windows, Mac, Linux, Android, iOS 真正的全平台,支持Markdown语法,可以配合OneDrive或者WebDav同步笔记 这里推荐使用坚果云做同步盘。\n","permalink":"https://kira-96.github.io/posts/%E5%A5%BD%E7%94%A8%E7%9A%84-markdown-%E7%BC%96%E8%BE%91%E5%99%A8/","summary":"桌面端 Typora 收费,支持 Mac, Windows, Linux MarkText 支持 Mac, Windows, Linux iA Writer 收费, 支持 Mac, Windows 移动端 iA Writer iOS - 收费 Android - 免费 熊掌记 支持 Mac, iOS, 可免费使用,部分功能需要订购 笔记软件 Joplin 支持 Windows, Mac,","title":"好用的 Markdown 编辑器 - 全平台"}]
\ No newline at end of file
+[{"content":"音乐 Listen1 音乐播放器 [Windows/Linux/MacOS/Android]\nYesPlayMusic 高颜值的第三方网易云播放器 [Windows/Linux/MacOS]\nAIMP [Windows/Android]\nDopamine [Windows]\n视频 PotPlayer [Windows]\nMPC-BE [Windows]\nQQ影音 [Window/MacOS/Android/iOS]\nMX Player [Android]\n美图下载 Pixiviz\npixivFANBOX\nSauceNAO\nPixEz [Android/iOS]\n下载 Flud 种子下载器 [Android]\nInternet Download Manager [Windows]\nFree Download Manager [Windows/MacOS/Linux/Android]\nMotrix [Windows/MacOS/Linux]\n截图、录屏 PixPin [Windows/MacOS]\nSnipaste [Windows/MacOS]\nGifCam [Windows]\nVeryCapture [Windows]\nScreenToGif [Windows]\nOBS Studio [Windows/Linux/MacOS]\npaint.net [Windows]\n写作 Visual Studio Code [Windows/MacOS/Linux]\nTypora [Windows/MacOS/Linux] (付费使用)\nMarkText [Window/MacOS/Linux]\nTypedown [Windows]\nJoplin [Windows/MacOS/Linux/Android/iOS]\n其它工具 Firefox [Windows/MacOS/Linux/Android/iOS]\nPowerToys [Windows]\nDevToys [Windows]\nWatt Toolkit [Windows/MacOS/Linux/Android/iOS]\nCrystal Disk Mark [Windows]\nGeek Uninstaller [Windows]\n7-Zip [Windows]\nZArchiver [Android]\nDism++ [Windows]\nLive2DViewerEX [Windows/Android]\nKeePass [Windows/MacOS/Linux/Android/iOS/\u0026hellip;]\nWinSCP [Windows]\nLocalSend [Windows/MacOS/Linux/Android/iOS/\u0026hellip;]\nWireShark [Windows/MacOS/Linux]\nDICOM Dump [Windows]\nInno Setup [Windows]\nOh My Posh [Windows/MacOS/Linux]\nTermius [Windows/MacOS/Linux/Android/iOS]\ntermux [Android]\nAurora Store [Android]\nVentoy [Windows/Linux]\nRufus [Windows]\nputty [Windows/Linux/MacOS]\n模拟器 PPSSPP - A PSP emulator [Windows/MacOS/Linux/Android/iOS]\nYuzu – 任天堂 Switch 模拟器 [Windows | Android | Linux | MacOS | iOS]\nRyujinx - Nintendo Switch Emulator [Windows/Linux/MacOS]\nCemu - Wii U Emulator [Windows]\nDolphin - 任天堂 GameCube 和 Wii 模拟器 [Windows/MacOS/Linux]\nRPCS3 - The PlayStation 3 Emulator [Windows/Linux/MacOS/FreeBSD]\nXenia - Xbox 360 Research Emulator [Windows]\nPS Remote Play [Windows/MacOS/Android/iOS]\nChiaki - Free and Open Source PlayStation Remote Play Client [Windows/Linux/MacOS/Android/Switch]\n开源字体 Fira Code\nCascadia Code\nNerd Fonts\n思源黑体\n思源宋体\nHarmonyOS Sans\nMiSans\nOPPO Sans\n","permalink":"https://kira-96.github.io/posts/%E4%B8%AA%E4%BA%BA%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%88%86%E4%BA%AB/","summary":"音乐 Listen1 音乐播放器 [Windows/Linux/MacOS/Android] YesPlayMusic 高颜值的第三方网易云播放器 [Windows/Linux/MacOS] AIMP [Windows/Android] Dopamine [Windows] 视频 PotPlayer [Windows] MPC-BE [Windows] QQ影音 [Window/MacOS/Android/iOS] MX Player [Android] 美图下载 Pixiviz pixivFANBOX SauceNAO PixEz [Android/iOS] 下载 Flud 种子下载器 [Android] Internet Download Manager [Windows] Free Download Manager [Windows/MacOS/Linux/Android] Motrix [Windows/MacOS/Linux]","title":"个人常用软件分享"},{"content":"原文:IOC Access Security\n功能 访问安全功能用于保护IOC数据库,限制来自未经授权的CA或pvAccess客户端访问。访问安全性基于以下几点:\nWho 客户端的用户ID(Channel Access/pvAccess)。\nWhere 用户登录的主机 ID。客户端运行的主机,但不会分辨用户是本地用户或远程登录到主机的用户。\nWhat 记录的各个字段都受到保护。每条记录都有一个字段包含记录的访问安全组(ASG)。每个字段都有一个访问安全级别(ASL0或ASL1)。安全级别在记录定义文件(.dbd)中定义。\nWhen 访问规则可以包含类似于CALC Record的输入计算。\n定义 ASL 访问安全级别\nASG 访问安全组\nUAG 用户访问组\nHAG 主机访问组\n快速上手 为了启用特定 IOC 的访问安全性,需要完成以下操作:\n创建访问安全文件(.acf) 可能需要修改IOC数据库 记录实例可能需要设置访问安全组ASG字段。如果ASG为空,记录将会使用“DEFAULT”访问安全组。\n访问安全文件可以在iocInit之后通过asSubInit和asSubProcess作为关联的子程序重新加载。将值1写入此记录将导致重新加载。\n必须启动脚本在的iocInit之前包含以下命令:\n1 2 3 4 asSetFilename(\u0026#34;/full/path/to/accessSecurityFile\u0026#34;) /* 下面是一个可选命令 */ /* 使用宏替换 */ asSetSubstitutions(\u0026#34;var1=sub1,var2=sub2,...\u0026#34;) 如果在iocInit之前未执行asSetFilename,就不会启用访问安全限制。\n如果给定asSetFilename,但在首次初始化访问安全性时发生错误,则对该IOC的所有访问都会被拒绝。\n成功启动访问安全性后,尝试重新启动时出现错误,将会保持上次的访问安全配置。\n启动IOC并启用访问安全后,可以通过asSetFilename、asSetSubstitutions和asInit来更改访问安全规则。也可以使用函数asInitialize、asInitFile和asInitFP。\n在启动IOC之后重新初始化访问安全配置操作是“非常昂贵”的操作,尽量不要这样做。\n访问安全配置文件 本节介绍包含用户访问组(UAG)、主机访问组(HAG)和访问安全组(ASG)。IOC会读取访问配置文件(建议使用扩展名.acf)然后创建访问配置数据库。首先给出一个简单的例子,然后是完整的语法描述。\n简单示例\n1 2 3 4 5 6 7 8 9 UAG(uag) {user1,user2} HAG(hag) {host1,host2} ASG(DEFAULT) { RULE(1,READ) RULE(1,WRITE) { UAG(uag) HAG(hag) } } 上面的规则提供了无限制的读权限(READ),而位于主机host1和host2上的用户user1和user2则拥有写权限(WRITE)。\n语法定义\n在以下描述中:\n[] 可选项\n| 备选项\n... 任意数量的定义\n元素\u0026lt;name\u0026gt;、\u0026lt;user\u0026gt;、\u0026lt;host\u0026gt;、\u0026lt;pvname\u0026gt;和\u0026lt;calculation\u0026gt;可以是带引号或不带引号的字符串。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 UAG(\u0026lt;name\u0026gt;) [{ \u0026lt;user\u0026gt; [, \u0026lt;user\u0026gt; ...] }] ... HAG(\u0026lt;name\u0026gt;) [{ \u0026lt;host\u0026gt; [, \u0026lt;host\u0026gt; ...] }] ... ASG(\u0026lt;name\u0026gt;) [{ [INP\u0026lt;index\u0026gt;(\u0026lt;pvname\u0026gt;) ...] RULE(\u0026lt;level\u0026gt;,NONE | READ | WRITE [, NOTRAPWRITE | TRAPWRITE]) { [UAG(\u0026lt;name\u0026gt; [,\u0026lt;name\u0026gt; ...])] [HAG(\u0026lt;name\u0026gt; [,\u0026lt;name\u0026gt; ...])] CALC(\u0026lt;calculation\u0026gt;) } ... }] ... UAG:用户访问组。这是用户名列表,列表可以空。一个用户名可以出现在多个UAG中。用户名必须和运行CA客户端的主机上的用户名相同。对于vxWorks客户端,用户名通常取自引导参数的用户字段。\nHAG:主机访问组。这是主机名列表,列表可以空。同一主机名可以出现在多个HAG中。主机名必须和运行CA客户端的主机主机名相同。对于vxWorks客户端,主机名通常取自引导参数的目标名称。\nASG:访问安全组。DEFAULT是默认的访问安全组。\nINP\u0026lt;index\u0026gt;:index必须是A到L中的一个值。类似于CALC record的INP字段。如果在ASG的规则中定义了CALC字段,则需要INP字段。\nRULE:定义访问权限\u0026lt;level\u0026gt;必须为0或1。级别1字段的权限继承了级别0字段的权限。权限为NONE、READ和WRITE,WRITE也继承了READ权限。标准EPICS记录类型的所有字段除VAL、CMD(命令)和RES(重置)外都设置为1级。可选参数指定是否应捕获写入,如果未给定,则默认为NOTRAPWRITE。\nUAG指定可以访问的用户访问组列表。如果未定义UAG,则允许所有用户访问。\nHAG指定具有访问权限的主机访问组列表。如果未定义HAG,则允许所有主机访问。\nCALC与计算记录的CALC字段类似,但结果必须计算为TRUE或FALSE。只有当计算结果为TRUE才适用该规则(RULE),其中实际测试对于(0.99 \u0026lt; result \u0026lt; 1.01)为TRUE。任何其他结果都被认为FALSE,并将导致该规则被忽略。\n可以为ASG定义多条RULE,相同的RULE级别和访问权限也可以有多个。用于客户端的TRAPWRITE设置由通过规则检查的第一个WRITE规则确定。\n每个记录类型的字段都有一个关联的访问安全级别ASL0或ASL1(默认值)。操作员通常更改的字段被分配为ASL0,其他字段被分配给ASL1。例如,模拟输出记录的VAL字段被分配为ASL0,其他字段分配为ASL1。这是因为在正常操作过程中只应修改VAL字段。\n创建或修改访问配置文件后,可以使用ascheck命令查找语法错误:\n1 ascheck -S \u0026#34;xxx=yyy,...\u0026#34; \u0026lt; \u0026#34;filename\u0026#34; -S表示使用宏替换。此命令会显示语法错误的位置,正确则不会有任何输出。\n实验 首先新建一个示例IOC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ mkdir example $ cd example/ $ makeBaseApp.pl -t example test $ makeBaseApp.pl -i -t example test The following target architectures are available in base: linux-loong64 linux-x86_64 What architecture do you want to use? linux-x86_64 The following applications are available: test What application should the IOC(s) boot? The default uses the IOC\u0026#39;s name, even if not listed above. Application name? test $ make 然后创建访问安全配置文件accessSecurity.acf。\n1 2 cd iocBoot/ioctest/ touch accessSecurity.acf 修改配置文件内容,示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 UAG(read) {deepin} UAG(write) {deepin} HAG(hosts) {LAPTOP-CTDCXXXX} ASG(DEFAULT) { RULE(1,READ) RULE(1,WRITE) { HAG(hosts) } } ASG(deepin) { RULE(1,READ) { UAG(read,write) HAG(hosts) } RULE(1,WRITE,TRAPWRITE) { UAG(write) HAG(hosts) } } 稍微解释一下:\n创建了两个用户访问组(UAG),名称为read和write,两个用户访问组都只包含用户deepin。\n创建了一个主机访问组(HAG),名称为hosts,包含主机名LAPTOP-CTDCXXXX。\n创建了默认(DEFAULT)访问安全组(ASG),不限制读取(READ)权限,只有hosts主机访问组的用户拥有写入(WRITE)权限。\n创建了访问安全组(ASG),名称为deepin,hosts主机访问组所包含主机上的deepin用户才拥有读取(READ)和写入(WRITE)权限。\n可以使用ascheck工具检查一下语法是否正确。\n1 ascheck accessSecurity.acf 然后还可以修改一下db文件,例:\n1 2 cd example/db/ vi dbExample2.db 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 record(ai, \u0026#34;$(user):aiExample$(no)\u0026#34;) { field(DESC, \u0026#34;Analog input No. $(no)\u0026#34;) field(INP, \u0026#34;$(user):calcExample$(no).VAL NPP NMS\u0026#34;) field(EGUF, \u0026#34;10\u0026#34;) field(EGU, \u0026#34;Counts\u0026#34;) field(HOPR, \u0026#34;10\u0026#34;) field(LOPR, \u0026#34;0\u0026#34;) field(HIHI, \u0026#34;8\u0026#34;) field(HIGH, \u0026#34;6\u0026#34;) field(LOW, \u0026#34;4\u0026#34;) field(LOLO, \u0026#34;2\u0026#34;) field(HHSV, \u0026#34;MAJOR\u0026#34;) field(HSV, \u0026#34;MINOR\u0026#34;) field(LSV, \u0026#34;MINOR\u0026#34;) field(LLSV, \u0026#34;MAJOR\u0026#34;) +\tfield(ASG, \u0026#34;deepin\u0026#34;) } alias(\u0026#34;$(user):aiExample$(no)\u0026#34;,\u0026#34;$(user):ai$(no)\u0026#34;) 这里指定$(user):aiExample$(no) record使用 deepin 访问安全组。\n最后,修改st.cmd来启用访问安全配置功能。\n1 2 cd example/iocBoot/ioctest/ vi st.cmd 1 2 3 4 5 6 7 #- Run this to trace the stages of iocInit #-traceIocInit + asSetFilename(\u0026#34;${TOP}/iocBoot/${IOC}/accessSecurity.acf\u0026#34;) cd \u0026#34;${TOP}/iocBoot/${IOC}\u0026#34; iocInit 启动IOC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 cd example/iocBoot/ioctest/ ./st.cmd #!../../bin/linux-x86_64/test \u0026lt; envPaths epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;ioctest\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;/home/deepin/example\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/usr/local/epics/base-7.0.8\u0026#34;) epicsEnvSet(\u0026#34;EPICS_HOST_ARCH\u0026#34;, \u0026#34;linux-x86_64\u0026#34;) cd \u0026#34;/home/deepin/example\u0026#34; ## Register all support components dbLoadDatabase \u0026#34;dbd/test.dbd\u0026#34; test_registerRecordDeviceDriver pdbbase ## Load record instances dbLoadTemplate \u0026#34;db/user.substitutions\u0026#34; dbLoadRecords \u0026#34;db/testVersion.db\u0026#34;, \u0026#34;user=deepin\u0026#34; dbLoadRecords \u0026#34;db/dbSubExample.db\u0026#34;, \u0026#34;user=deepin\u0026#34; asSetFilename(\u0026#34;/home/deepin/example/iocBoot/ioctest/accessSecurity.acf\u0026#34;) cd \u0026#34;/home/deepin/example/iocBoot/ioctest\u0026#34; iocInit Starting iocInit ############################################################################ ## EPICS R7.0.8 ## Rev. 2024-03-01T16:27+0800 ## Rev. Date build date/time: ############################################################################ iocRun: All initialization complete ## Start any sequence programs #seq sncExample, \u0026#34;user=deepin\u0026#34; epics\u0026gt; 然后打开一个新的终端窗口进行测试。\n注意,我这里的主机名是LAPTOP-CTDCXXXX,主机有两个用户deepin和root。\n分别使用两个用户访问使用默认(DEFAULT)和名为deepin访问安全组的变量。\n1 2 3 4 deepin@LAPTOP-CTDCXXXX:~$ caget deepin:circle:angle deepin:circle:angle 186 deepin@LAPTOP-CTDCXXXX:~$ caget deepin:aiExample1 deepin:aiExample1 6 用户deepin对使用不同访问安全组的变量都可以访问。\n1 2 3 4 5 6 7 8 # 使用root用户 deepin@LAPTOP-CTDCXXXX:~$ sudo su root@LAPTOP-CTDCXXXX:/home/deepin# caget deepin:circle:angle deepin:circle:angle 55 root@LAPTOP-CTDCXXXX:/home/deepin# caget deepin:aiExample1 Read operation timed out: some PV data was not read. deepin:aiExample1 *** no read access 用户root可以访问使用默认(DEFAULT)访问安全组的变量,而不可访问使用名为deepin访问安全组的变量。\n测试结果与编写的访问安全配置规则相符合,说明访问安全配置成功。\n不过,此次实验只测试了本机上的不同用户,对于更为复杂的控制系统的数据安全访问权限,则需要做更完善的安全配置。\n","permalink":"https://kira-96.github.io/posts/epics-ioc-access-security/","summary":"原文:IOC Access Security\n功能 访问安全功能用于保护IOC数据库,限制来自未经授权的CA或pvAccess客户端访问。访问安全性基于以下几点:\nWho 客户端的用户ID(Channel Access/pvAccess)。\nWhere 用户登录的主机 ID。客户端运行的主机,但不会分辨用户是本地用户或远程登录到主机的用户。\nWhat 记录的各个字段都受到保护。每条记录都有一个字段包含记录的访问安全组(ASG)。每个字段都有一个访问安全级别(ASL0或ASL1)。安全级别在记录定义文件(.dbd)中定义。\nWhen 访问规则可以包含类似于CALC Record的输入计算。\n定义 ASL 访问安全级别\nASG 访问安全组\nUAG 用户访问组\nHAG 主机访问组\n快速上手 为了启用特定 IOC 的访问安全性,需要完成以下操作:\n创建访问安全文件(.acf) 可能需要修改IOC数据库 记录实例可能需要设置访问安全组ASG字段。如果ASG为空,记录将会使用“DEFAULT”访问安全组。\n访问安全文件可以在iocInit之后通过asSubInit和asSubProcess作为关联的子程序重新加载。将值1写入此记录将导致重新加载。\n必须启动脚本在的iocInit之前包含以下命令:\n1 2 3 4 asSetFilename(\u0026#34;/full/path/to/accessSecurityFile\u0026#34;) /* 下面是一个可选命令 */ /* 使用宏替换 */ asSetSubstitutions(\u0026#34;var1=sub1,var2=sub2,...\u0026#34;) 如果在iocInit之前未执行asSetFilename,就不会启用访问安全限制。\n如果给定asSetFilename,但在首次初始化访问安全性时发生错误,则对该IOC的所有访问都会被拒绝。\n成功启动访问安全性后,尝试重新启动时出现错误,将会保持上次的访问安全配置。\n启动IOC并启用访问安全后,可以通过asSetFilename、asSetSubstitutions和asInit来更改访问安全规则。也可以使用函数asInitialize、asInitFile和asInitFP。\n在启动IOC之后重新初始化访问安全配置操作是“非常昂贵”的操作,尽量不要这样做。\n访问安全配置文件 本节介绍包含用户访问组(UAG)、主机访问组(HAG)和访问安全组(ASG)。IOC会读取访问配置文件(建议使用扩展名.acf)然后创建访问配置数据库。首先给出一个简单的例子,然后是完整的语法描述。\n简单示例\n1 2 3 4 5 6 7 8 9 UAG(uag) {user1,user2} HAG(hag) {host1,host2} ASG(DEFAULT) { RULE(1,READ) RULE(1,WRITE) { UAG(uag) HAG(hag) } } 上面的规则提供了无限制的读权限(READ),而位于主机host1和host2上的用户user1和user2则拥有写权限(WRITE)。","title":"EPICS IOC 访问安全"},{"content":"前言 IgH EtherCAT Master 是一个开源的EtherCAT主站驱动程序,用于管理EtherCAT从站设备,支持Linux操作系统,工控上使用的比较多。\ndls ethercat是由英国钻石光源开发的用于 EPICS 控制系统 EtherCAT 设备的支持程序,基于 IgH Master 主站程序开发,实现对 EtherCAT 总线设备的读写。\n交叉编译环境:Ubuntu\n运行开发板:龙芯2K0500金龙开发板\n内核版本:Linux LS-GD 5.10.0-rt17.lsgd #1 PREEMPT_RT\n相关软件包下载地址 epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov)\nepics-modules/asyn: EPICS module for driver and device support\nepics-modules/busy: APS BCDA synApps module: busy\nepics-modules/autosave: APS BCDA synApps module: autosave\ndls-controls/ethercat: EPICS support to read/write to ethercat based hardware\nIgH EtherCAT Master for Linux\nPREEMPT RT patch\n配置交叉编译环境 关于这一节,之前的文章已经详细讲过,参考配置交叉编译环境。\n如果你使用的是其他开发套件,请按照开发手册安装配置好环境。\n编译 IgH EtherCAT Master 源码一定要下载 stable-1.5 分支的,其他版本我也没有测试!\n打补丁 1 2 3 4 5 6 7 # 先将补丁复制到ethercat驱动源码下 cp ethercat-master/etc/makeDocumentation/configurable-error-suppression.patch ethercat-stable-1.5/ cd ethercat-stable-1.5/ # 打补丁 patch -p1 \u0026lt; configurable-error-suppression.patch # 这里需要注意一下patch的输出结果 由于补丁的时间比较早,跟现有的源码不太匹配,有些地方补丁会失败(FAILED)。比如我这里的报错,可能需要再手动修改一下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 patching file lib/Makefile.am Hunk #1 succeeded at 32 (offset -2 lines). patching file lib/common.c patching file lib/domain.c patching file lib/liberror-documentation.txt patching file lib/liberror.c patching file lib/liberror.h patching file lib/master.c Hunk #1 succeeded at 37 (offset -2 lines). ... Hunk #12 FAILED at 419. Hunk #13 FAILED at 448. ... 2 out of 31 hunks FAILED -- saving rejects to file lib/master.c.rej patching file lib/reg_request.c Hunk #1 succeeded at 39 (offset -2 lines). ... patching file lib/sdo_request.c Hunk #1 succeeded at 39 (offset -2 lines). ... patching file lib/slave_config.c Hunk #1 succeeded at 39 (offset -1 lines). ... patching file lib/voe_handler.c Hunk #1 succeeded at 40 (offset -2 lines). ... 我们参考lib/master.c.rej,手动修改一下。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # lib/master.c.rej --- lib/master.c\tTue Feb 12 17:31:08 2013 +0100 +++ lib/master.c\tMon Mar 23 15:52:53 2015 +0000 @@ -419,7 +431,8 @@ if (EC_IOCTL_ERRNO(ret) == EIO \u0026amp;\u0026amp; abort_code) { *abort_code = download.abort_code; } - fprintf(stderr, \u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, + ecrt_errcode = ECRT_ERRMASTERSDODOWNLOAD; + ERRPRINTF(\u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, strerror(EC_IOCTL_ERRNO(ret))); return -EC_IOCTL_ERRNO(ret); } @@ -448,7 +461,8 @@ if (EC_IOCTL_ERRNO(ret) == EIO \u0026amp;\u0026amp; abort_code) { *abort_code = download.abort_code; } - fprintf(stderr, \u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, + ecrt_errcode = ECRT_ERRMASTERSDODOWNLOADCOMPLETE; + ERRPRINTF(\u0026#34;Failed to execute SDO download: %s\\n\u0026#34;, strerror(EC_IOCTL_ERRNO(ret))); return -EC_IOCTL_ERRNO(ret); } 然后这个补丁还有点问题,我们需要手动修改一下lib/liberror.c。这里源文件和头文件变量定义不一致,编译会报错,以头文件为准。\n1 2 3 4 5 6 7 #include \u0026#34;liberror.h\u0026#34; int ecrt_err_to_stderr = 1; // char *ecrt_errstring[ERRSTRING_LEN]; char ecrt_errstring[ERRSTRING_LEN]; int ecrt_errcode; 准备内核源码 由于需要将源码编译成相应的系统驱动,所以这里需要使用内核的源码。\n如果是标准系统,可以直接安装linux-headers。\n1 2 # Ubuntu/Debian sudo apt install linux-headers-$(uname -r) 树莓派系统\n1 sudo apt install raspberrypi-kernel-headers 而对于开发板系统,我们可以使用随开发板提供的内核源码。\n内核的编译步骤请根据开发板的用户手册完成。\n最好再给内核打上 PREEMPT_RT 补丁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 解压内核源码 tar -xvf linux-5.10-2k500-src-f45937d-build.20230721100738.tar.gz # linux-5.10-2k500-cbd-src cd linux-5.10-2k500-cbd-src/ # 为内核打上实时补丁(可选) patch -p1 \u0026lt; patch-5.10-rt17.patch # 配置交叉编译器 ./set_env.sh # 编译内核 make loongson_2k500_defconfig # make menuconfig make uImage # 编译模块 make modules 编译 EtherCAT Master 驱动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cd ethercat-stable-1.5/ # to create the configure script, if downloaded from the repo ./bootstrap # 这里需要注意是否出现报错,需要安装 autoconf、pkg-config 等工具 # 执行configure # --host 指定程序运行的主机架构 # --with-linux-dir 指定源码目录 # 如果是安装的linux-headers,通常在 /usr/src/linux-headers-xxx # 如果直接使用内核源码,则必须通过上述编译步骤! # --prefix 指定安装目录 ./configure --host=loongarch64-linux-gnu CC=loongarch64-linux-gnu-gcc --enable-generic=yes --enable-8139too=no --with-linux-dir=/path/to/linux-5.10-2k500-cbd-src --prefix=/path/to/__install_dir # 编译 make all modules # 或者 # make # make ARCH=loongarch CORSS_COMPILE=loongarch64-linux-gnu- modules # 安装生成的文件 # make install 如果完全按照上述步骤,应该可以编译成功。\n下面整理一下生成的文件。\n1 2 3 4 5 # 复制生成的主程序 cp tool/ethercat /path/to/__install_dir/bin/ # 复制生成的驱动程序 cp device/ec_generic.ko /path/to/__install_dir/modules/ cp master/ec_master.ko /path/to/__install_dir/modules/ 这是我整理的文件,后面需要将这些文件下载到开发板。\n__install_dir ├─ bin │ └─ ethercat (主程序) ├─ etc (配置) ├─ include (头文件) ├─ lib (libethercat.so) ├─ modules (驱动目录) │ ├─ ec_master.ko │ └─ ec_generic.ko ├─ sbin │ └─ ethercatctl └─ share └─ bash-completion/ (bash自动补全) 编译 EPICS ethercat 模块 以下步骤需要先安装好EPICS Base!\n编译 asyn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cd asyn touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # SSCAN模块路径 # SSCAN=$(SUPPORT)/sscan # CALC模块路径 # CALC=$(SUPPORT)/calc # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 autosave 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cd autosave touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 busy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cd busy touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # ASYN模块路径 ASYN=$(SUPPORT)/asyn # AUTOSAVE模块路径 AUTOSAVE=$(SUPPORT)/autosave # BUSY模块路径 BUSY=$(SUPPORT)/busy # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 ethercat 这一步可以说是最麻烦,问题最多的。编译出什么问题都需要去找到相应的Makefile修改。\n由于该软件包已经长时间无人维护,建议使用我修改过的版本。\n首先需要安装所需的包libxml2-dev。但我们实际上并不需要用这个软件包,我们只需要它的头文件。\n软件依赖的动态库(.so)文件,我们则需要从开发板系统中拷贝出来,无法直接用编译电脑的动态库。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 cd ethercat-master/ # 创建 3rd 目录,用于放置所需的头文件和动态库 mkdir 3rd mkdir 3rd/include mkdir 3rd/lib # 安装 libxml2-dev sudo apt install libxml2-dev # 从系统目录中复制出libxml2的头文件 # 因为还需要做一些修改,不能直接使用 cp -r /usr/include/libxml2/ ./3rd/include/ # 复制刚刚编译生成的 libethercat cp /path/to/__install_dir/lib/libethercat.so* ./3rd/lib/ # 从开发板系统复制所需要的动态库 # libxml2 依赖 libz 和 liblzma scp root@192.168.1.10:/usr/lib/libxml2.so.2.9.12 ./3rd/lib/ scp root@192.168.1.10:/usr/lib/libz.so.1.2.11 ./3rd/lib/ scp root@192.168.1.10:/usr/lib/liblzma.so.5.2.5 ./3rd/lib/ # 手动创建一下链接 cd 3rd/lib/ ln -s libxml2.so.2.9.12 libxml2.so.2 ln -s libxml2.so.2 libxml2.so ln -s liblzma.so.5.2.5 liblzma.so.5 ln -s liblzma.so.5 liblzma.so ln -s libz.so.1.2.11 libz.so.1 ln -s libz.so.1 libz.so 然后需要修改一下libxml2的头文件,不然编译的时候会报错。\n报错信息:\n1 2 3 4 5 6 7 8 9 10 In file included from ../../../libxml2/libxml/parser.h:812, from ../../../libxml2/libxml/globals.h:18, from ../../../libxml2/libxml/threads.h:35, from ../../../libxml2/libxml/xmlmemory.h:218, from ../../../libxml2/libxml/tree.h:1307, from ../parser.c:10: ../../../libxml2/libxml/encoding.h:31:10: fatal error: unicode/ucnv.h: No such file or directory #include \u0026lt;unicode/ucnv.h\u0026gt; ^~~~~~~~~~~~~~~~ compilation terminated. 解决方法:\n1 2 3 4 5 6 # 修改 xmlversion.h vi 3rd/include/libxml2/libxml/xmlversion.h # 找到下面行,禁用 LIBXML_ICU_ENABLED #define LIBXML_ICU_ENABLED # 将 #if 1 改为 #if 0 1 2 3 4 5 6 7 8 9 /** * LIBXML_ICU_ENABLED: * * Whether icu support is available */ - #if 0 + #if 1 #define LIBXML_ICU_ENABLED #endif 做好上面的准备工作,还需要修改源码中的路径配置,然后才能正常编译。\n下面都是我踩坑留下的记录。\n修改 configure/RELEASE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd ethercat-master/ # 首先修改 configure/RELEASE vi configure/RELEASE # 需要修改的有4项 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # ASYN模块路径 ASYN=$(SUPPORT)/asyn # BUSY模块路径 BUSY=$(SUPPORT)/busy # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 修改 ethercatApp/scannerSrc/Makefile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 cd ethercat-master/ # 修改 ethercatApp/scannerSrc/Makefile vi ethercatApp/scannerSrc/Makefile # 需要修改 EtherCAT Master 源码相关路径、 # libxml2头文件路径、动态库(.so)路径 # 修改 ETHERLAB 源码路径 ETHERLAB=/path/to/ethercat-stable-1.5 ETHERLABPREFIX=$(ETHERLAB) USR_INCLUDES += -I$(ETHERLABPREFIX)/include # 修改动态库路径 USR_LDFLAGS += -L$(TOP)/3rd/lib -Wl,-rpath=$(TOP)/3rd/lib # 修改 libxml2 头文件路径 USR_INCLUDES += -I$(TOP)/3rd/include/libxml2 USR_SYS_LIBS += ethercat xml2 # 下面类似 scanner_INCLUDES += -I$(ETHERLAB)/lib serialtool_INCLUDES += -I$(ETHERLAB)/master get-slave-revisions_INCLUDES += -I$(ETHERLAB)/master 修改 ethercatApp/src/Makefile,与上面类似。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 cd ethercat-master/ # 修改 ethercatApp/src/Makefile vi ethercatApp/src/Makefile # 需要修改 EtherCAT Master 源码相关路径、 # libxml2头文件路径、动态库(.so)路径 # 修改 ETHERLAB 源码路径 ETHERLAB=/path/to/ethercat-stable-1.5 # ecAsyn_INCLUDES += -I$(ETHERLAB)/src/ethercat-$(subst -,.,$(VERSION))/include # gadc_INCLUDES += -I$(ETHERLAB)/src/ethercat-$(subst -,.,$(VERSION))/include ecAsyn_INCLUDES += -I$(ETHERLAB)/include gadc_INCLUDES += -I$(ETHERLAB)/include # 修改 libxml2 头文件路径 USR_INCLUDES += -I$(TOP)/3rd/include/libxml2 # 添加动态库路径 USR_LDFLAGS += -L$(TOP)/3rd/lib -Wl,-rpath=$(TOP)/3rd/lib USR_SYS_LIBS += xml2 修改源码 由于ethercat-master的源码原本是为x86_64架构编写的,编译到LoongArch架构的设备上运行可能会出现一些奇怪的错误。\n例如,在运行slaveinfo时会出现Segmentation fault错误,而这通常是空指针导致内存访问出错。\n原因分析:\nslaveinfo在运行时会根据程序自己的路径寻找slave-types.txt文件,问题就出在这里。来看源码 ethercatApp/scannerSrc/slave-list-path.c。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // ethercatApp/scannerSrc/slave-list-path.c int get_root_dir_index(const char *program_name) { // Search for the binary path char binary_dir[] = \u0026#34;bin/linux-x86_64/\u0026#34;; // Find the binary path in the program path and return the pointer char *found = strstr(program_name, binary_dir); // Handle the case where it is not found if (found == NULL) { return -1; } // Calculate the difference in the pointers to get the index return found - program_name; } 这个get_root_dir_index函数是用于计算当前程序(slaveinfo)所在的根目录。这里向上查找的路径为bin/linux-x86_64,当然不能找到LoongArch的目录bin/linux-loong64了。所以,此函数只在路径为bin/linux-x86_64/slaveinfo时才能正常运行,其它情况都返回-1。(第一次见到这样写的,真无语了……)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // ethercatApp/scannerSrc/slave-list-path.c char *get_slave_list_filename(const char *program_path) { char relative_path[] = \u0026#34;etc/scripts/slave-types.txt\u0026#34;; char *slave_list_filename = NULL; // Get absolute path of application char *real_path = calloc(PATH_MAX, sizeof(char)); get_app_path(program_path, real_path); // Get root directory int root_dir_index = get_root_dir_index(real_path); if (root_dir_index != -1) { slave_list_filename = calloc(root_dir_index + strlen(relative_path) + 1, sizeof(char)); strncpy(slave_list_filename, real_path, root_dir_index); } // Append relative path strcat(slave_list_filename, relative_path); // Check file struct stat fstat; int result = stat(slave_list_filename, \u0026amp;fstat); if (result) { printf(\u0026#34;Could not find slave list file at %s\\n\u0026#34;, slave_list_filename); } // Cleanup free(real_path); return slave_list_filename; } 而在get_slave_list_filename函数中,只处理了get_root_dir_index正常返回的情况。当get_root_dir_index返回-1时,slave_list_filename始终为NULL,这就导致后续操作出错。\n其实这个问题直接把程序放到bin/linux-x86_64目录下运行就可以了,不过既然找到了问题,索性就改一改。\n现在已经知道了出错的地方,该如何修改呢?这里我本着尽量少改动源码的原则,在get_root_dir_index查找根目录出错时,直接使用当前目录。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 char *get_slave_list_filename(const char *program_path) { char relative_path[] = \u0026#34;etc/scripts/slave-types.txt\u0026#34;; char *slave_list_filename = NULL; // Get absolute path of application char *real_path = calloc(PATH_MAX, sizeof(char)); get_app_path(program_path, real_path); // Get root directory int root_dir_index = get_root_dir_index(real_path); if (root_dir_index != -1) { slave_list_filename = calloc(root_dir_index + strlen(relative_path) + 1, sizeof(char)); strncpy(slave_list_filename, real_path, root_dir_index); } + else + { + slave_list_filename = calloc(PATH_MAX, sizeof(char)); + if (NULL != getcwd(slave_list_filename, PATH_MAX)) + { + strcat(slave_list_filename, \u0026#34;/\u0026#34;); + } + } // Append relative path strcat(slave_list_filename, relative_path); // Check file struct stat fstat; int result = stat(slave_list_filename, \u0026amp;fstat); if (result) { printf(\u0026#34;Could not find slave list file at %s\\n\u0026#34;, slave_list_filename); } // Cleanup free(real_path); return slave_list_filename; } 目前只发现了这个问题,希望后面没有坑了。\n编译 最后终于可以开始编译了。\n1 2 3 cd ethercat-master/ # 执行交叉编译 make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 到这里,编译过程还可能会出错,不过已经可以编译出我们所需要的东西了。\n最终编译得到scanner、slaveinfo程序就可以了。\n以下是我编译时的输出:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... Installing library ../../../lib/linux-loong64/libscannerlib.a ... Installing created executable ../../../bin/linux-loong64/serialtool Installing created executable ../../../bin/linux-loong64/get-slave-revisions Installing created executable ../../../bin/linux-loong64/scanner Installing created executable ../../../bin/linux-loong64/slaveinfo Installing created executable ../../../bin/linux-loong64/parsertest ... Installing shared library ../../../lib/linux-loong64/libecAsyn.so Installing library ../../../lib/linux-loong64/libecAsyn.a Installing created executable ../../../bin/linux-loong64/parsertest ... Installing template file ../../../db/EK1100.template ... make -C ./protocol install make[2]: 进入目录“/home/deepin/ethercat-master/ethercatApp/protocol” make[2]: *** 没有规则可制作目标“install”。 停止。 make[2]: 离开目录“/home/deepin/ethercat-master/ethercatApp/protocol” make[1]: *** [/usr/local/epics/base-7.0.8/configure/RULES_DIRS:85:protocol.install] 错误 2 make[1]: 离开目录“/home/deepin/ethercat-master/ethercatApp” make: *** [/usr/local/epics/base-7.0.8/configure/RULES_DIRS:85:ethercatApp.install] 错误 2 最后的错误,我直接忽略了,因为已经得到了需要的可执行程序。\n整理一下编译生成的文件:\nethercat-master ├─ bin │ └─ linux-loong64 ├─ db ├─ dbd └─ lib └─ linux-loong64 测试运行 将整理好的文件下载到开发板后,我们测试运行一下。\n测试安装 EtherCAT 主站驱动程序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@LS-GD modules]# modinfo ec_generic.ko filename: /root/__install/modules/ec_generic.ko version: 1.5.2 unknown license: GPL description: EtherCAT master generic Ethernet device module author: Florian Pose \u0026lt;fp@igh-essen.com\u0026gt; srcversion: 848BB80F1C588A2FDA42EDB depends: ec_master name: ec_generic vermagic: 5.10.0-rt17.lsgd preempt_rt mod_unload modversions LOONGARCH 64BIT [root@LS-GD modules]# insmod ec_master.ko [root@LS-GD modules]# insmod ec_generic.ko [root@LS-GD modules]# lsmod Module Size Used by ec_generic 6427 0 ec_master 464125 1 ec_generic 测试ethercat主程序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 [root@LS-GD tool]# ./ethercat Please specify a command! Usage: ethercat \u0026lt;COMMAND\u0026gt; [OPTIONS] [ARGUMENTS] Commands (can be abbreviated): alias Write alias addresses. config Show slave configurations. crc CRC error register diagnosis. cstruct Generate slave PDO information in C language. data Output binary domain process data. debug Set the master\u0026#39;s debug level. domains Show configured domains. download Write an SDO entry to a slave. eoe Display Ethernet over EtherCAT statictics. foe_read Read a file from a slave via FoE. foe_write Store a file on a slave via FoE. graph Output the bus topology as a graph. master Show master and Ethernet device information. pdos List Sync managers, PDO assignment and mapping. reg_read Output a slave\u0026#39;s register contents. reg_write Write data to a slave\u0026#39;s registers. rescan Rescan the bus. sdos List SDO dictionaries. sii_read Output a slave\u0026#39;s SII contents. sii_write Write SII contents to a slave. slaves Display slaves on the bus. soe_read Read an SoE IDN from a slave. soe_write Write an SoE IDN to a slave. states Request application-layer states. upload Read an SDO entry from a slave. version Show version information. xml Generate slave information XML. Global options: --master -m \u0026lt;master\u0026gt; Comma separated list of masters to select, ranges are allowed. Examples: \u0026#39;1,3\u0026#39;, \u0026#39;5-7,9\u0026#39;, \u0026#39;-3\u0026#39;. Default: \u0026#39;-\u0026#39; (all). --force -f Force a command. --quiet -q Output less information. --verbose -v Output more information. --help -h Show this help. Numerical values can be specified either with decimal (no prefix), octal (prefix \u0026#39;0\u0026#39;) or hexadecimal (prefix \u0026#39;0x\u0026#39;) base. Call \u0026#39;ethercat \u0026lt;COMMAND\u0026gt; --help\u0026#39; for command-specific help. Send bug reports to fp@igh.de. 测试scanner主程序。\n1 2 3 [root@LS-GD linux-loong64]# chmod +x scanner [root@LS-GD linux-loong64]# ./scanner usage: scanner [-m master_index] [-s] [-q] scanner.xml socket_path 可以看到,驱动程序和主程序都能在开发板上运行,说明已经编译完成了。\n安装 EtherCAT 主站到开发板系统 首先将编译好的EtherCAT Master下载到开发板系统,然后将各个目录下的文件放到相应的系统目录下。(这里我还以__install_dir的目录结构为例。)\n原文件/目录 系统文件/目录 bin/ethercat /usr/bin/ethercat etc/init.d/ethercat /etc/init.d/ethercat etc/sysconfig/ethercat /etc/sysconfig/ethercat etc/ethercat.conf /etc/ethercat.conf include/ - lib/libethercat.so* /usr/lib/libethercat.so* modules/ /lib/modules/5.10.0-rt17.lsgd/ sbin/ethercatctl /sbin/ethercatctl share/bash-completion/ /usr/share/bash-completion/ 注意:如果系统目录存在/lib/modules/{内核版本}目录,则可以将modules目录下的ec_master.ko和ec_generic.ko复制到该目录下,然后在终端执行depmod命令。否则,可以按照下面的步骤做相应修改。\n例如,将modules下的驱动文件放到开发板文件系统的/root/modules/目录下。\n修改/etc/init.d/ethercat和/sbin/ethercatctl脚本文件。\n例:/etc/init.d/ethercat\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 LSMOD=/sbin/lsmod MODPROBE=/sbin/modprobe + INSMOD=/sbin/insmod RMMOD=/sbin/rmmod MODINFO=/sbin/modinfo - ETHERCAT=/home/loongson/__install_dir/bin/ethercat + ETHERCAT=/usr/bin/ethercat MASTER_ARGS= + MODULE_DIR=/root/modules start) echo -n \u0026#34;Starting EtherCAT master 1.5.2 \u0026#34; ... # load master module - if ! ${MODPROBE} ${MODPROBE_FLAGS} ec_master \u0026#34;${MASTER_ARGS}\u0026#34; \\ + if ! ${INSMOD} ${MODULE_DIR}/ec_master.ko \u0026#34;${MASTER_ARGS}\u0026#34; \\ main_devices=\u0026#34;${DEVICES}\u0026#34; backup_devices=\u0026#34;${BACKUPS}\u0026#34;; then exit_fail fi # check for modules to replace for MODULE in ${DEVICE_MODULES}; do ECMODULE=ec_${MODULE} - if ! ${MODINFO} \u0026#34;${ECMODULE}\u0026#34; \u0026gt; /dev/null; then - continue # ec_* module not found - fi if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ]; then if ${LSMOD} | grep \u0026#34;^${MODULE} \u0026#34; \u0026gt; /dev/null; then if ! ${RMMOD} \u0026#34;${MODULE}\u0026#34;; then exit_fail fi fi fi - if ! ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${ECMODULE}\u0026#34;; then + if ! ${INSMOD} \u0026#34;${MODULE_DIR}/${ECMODULE}.ko\u0026#34;; then if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ]; then ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${MODULE}\u0026#34; # try to restore fi exit_fail fi done exit_success ;; 例:/sbin/ethercatctl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 LSMOD=/sbin/lsmod MODPROBE=/sbin/modprobe + INSMOD=/sbin/insmod RMMOD=/sbin/rmmod MODINFO=/sbin/modinfo IP=/bin/ip - ETHERCAT=/home/loongson/__install_dir/bin/ethercat + ETHERCAT=/usr/bin/ethercat + MODULE_DIR=/root/modules #------------------------------------------------------------------------------ - ETHERCAT_CONFIG=/home/loongson/__install_dir/etc/ethercat.conf + ETHERCAT_CONFIG=/etc/ethercat.conf start) ... # load master module - if ! ${MODPROBE} ${MODPROBE_FLAGS} ec_master \\ + if ! ${INSMOD} ${MODULE_DIR}/ec_master.ko \\ main_devices=\u0026#34;${DEVICES}\u0026#34; backup_devices=\u0026#34;${BACKUPS}\u0026#34;; then exit 1 fi LOADED_MODULES=ec_master # check for modules to replace for MODULE in ${DEVICE_MODULES}; do ECMODULE=ec_${MODULE} - if ! ${MODINFO} \u0026#34;${ECMODULE}\u0026#34; \u0026gt; /dev/null; then - continue # ec_* module not found - fi if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;ccat\u0026#34; ]; then # unload standard module and check if unloading was successful ${RMMOD} \u0026#34;${MODULE}\u0026#34; 2\u0026gt; /dev/null || true if ${LSMOD} | grep \u0026#34;^${MODULE} \u0026#34; \u0026gt; /dev/null; then # could not unload module ${RMMOD} ${LOADED_MODULES} exit 1 fi fi - if ! ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${ECMODULE}\u0026#34;; then + if ! ${INSMOD} \u0026#34;${MODULE_DIR}/${ECMODULE}.ko\u0026#34;; then if [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;generic\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;${MODULE}\u0026#34; != \u0026#34;ccat\u0026#34; ]; then ${MODPROBE} ${MODPROBE_FLAGS} \u0026#34;${MODULE}\u0026#34; # try to restore fi ${RMMOD} ${LOADED_MODULES} exit 1 fi LOADED_MODULES=\u0026#34;${ECMODULE} ${LOADED_MODULES}\u0026#34; done exit 0 ;; 修改 EtherCAT Master 配置文件。\n例:/etc/sysconfig/ethercat和/etc/ethercat.conf\n1 2 3 4 5 6 MASTER0_DEVICE=\u0026#34;00:11:22:33:44:55\u0026#34; #MASTER1_DEVICE=\u0026#34;\u0026#34; #MASTER0_BACKUP=\u0026#34;\u0026#34; DEVICE_MODULES=\u0026#34;generic\u0026#34; MASTER\u0026lt;X\u0026gt;_DEVICE配置网卡的物理地址(MAC),可通过ifconfig命令查看。\nDEVICE_MODULES配置使用的模块名称,这里仅使用通用网卡驱动generic。\n运行 EtherCAT Master 启动EtherCAT主站程序。\n1 2 3 /etc/init.d/ethercat start # 或者 /sbin/ethercatctl start 如果一切正常,可以看到/dev目录下有EtherCAT0设备文件。\n查看主站信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [root@LS-GD ~]# ethercat master Master0 Phase: Idle Active: no Slaves: 1 Ethernet devices: Main: 00:11:22:33:44:55 (attached) Link: UP Tx frames: 370862 Tx bytes: 22319896 Rx frames: 370861 Rx bytes: 22319836 Tx errors: 0 Tx frame rate [1/s]: 125 125 125 Tx rate [KByte/s]: 7.3 7.3 7.3 Rx frame rate [1/s]: 125 125 125 Rx rate [KByte/s]: 7.3 7.3 7.3 Common: Tx frames: 1108336 Tx bytes: 66704720 Rx frames: 1108307 Rx bytes: 66702980 Lost frames: 29 Tx frame rate [1/s]: 125 125 125 Tx rate [KByte/s]: 7.3 7.3 7.3 Rx frame rate [1/s]: 125 125 125 Rx rate [KByte/s]: 7.3 7.3 7.3 Loss rate [1/s]: 0 0 0 Frame loss [%]: 0.0 0.0 0.0 Distributed clocks: Reference clock: Slave 0 DC reference time: 0 Application time: 0 2000-01-01 00:00:00.000000000 查看从站设备\n1 2 [root@LS-GD ~]# ethercat slaves 0 0:0 PREOP + XB6-EC0002(Modules/Slots and MDP) 生成从站信息XML文件\n1 [root@LS-GD ~]# ethercat xml \u0026gt; scanner.xml 其他ethercat命令\n使用ethercat -h命令查看其他命令的使用方法。\n参考 IgH EtherCAT Master Building and installing IgH EtherCAT Master IgH EtherCAT Master for Linux EtherCAT DRIVER AND TOOLS FOR EPICS AND LINUX AT PSI INTEGRATION OF EtherCAT HARDWARE INTO THE EPICS BASED DISTRIBUTED CONTROL SYSTEM AT iThemba LABS 在“福珑2.0”主机上编译EPICS Ehtercat驱动软件的体验 ","permalink":"https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/","summary":"前言 IgH EtherCAT Master 是一个开源的EtherCAT主站驱动程序,用于管理EtherCAT从站设备,支持Linux操作系统,工控上使用的比较多。\ndls ethercat是由英国钻石光源开发的用于 EPICS 控制系统 EtherCAT 设备的支持程序,基于 IgH Master 主站程序开发,实现对 EtherCAT 总线设备的读写。\n交叉编译环境:Ubuntu\n运行开发板:龙芯2K0500金龙开发板\n内核版本:Linux LS-GD 5.10.0-rt17.lsgd #1 PREEMPT_RT\n相关软件包下载地址 epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov)\nepics-modules/asyn: EPICS module for driver and device support\nepics-modules/busy: APS BCDA synApps module: busy\nepics-modules/autosave: APS BCDA synApps module: autosave\ndls-controls/ethercat: EPICS support to read/write to ethercat based hardware\nIgH EtherCAT Master for Linux\nPREEMPT RT patch","title":"龙芯开发板移植 IgH EtherCAT Master"},{"content":"Hugo原生支持GoAT,可以直接使用goat代码块。\n1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · a b a b a b a b \u0026amp; A M S i o i q f b B x u j o e a ( - x d r a \u0026gt; e f R \u0026gt; c o C n u o b ( n r ) ) d n e e J d r o s i n N o R D t o i u a a n g d d l o i t n e D i a g o n a l s C V u e r r v t e i d c a l n o t A N C : o u l d r r i A a / I v n s i n e e h s t d - e - t r l B i h i i s i o n ' s r e q n . u * o o b t t o e l a s d ' * l i n e D o n S e e ? a r c 3 h 本博客还支持Mermaid.js作图。\nsequenceDiagram participant Alice participant Bob Alice-\u003e\u003eJohn: Hello John, how are you? loop Healthcheck John-\u003e\u003eJohn: Fight against hypochondria end Note right of John: Rational thoughts prevail! John--\u003e\u003eAlice: Great! John-\u003e\u003eBob: How about you? Bob--\u003e\u003eJohn: Jolly good! gantt dateFormat YYYY-MM-DD title Adding GANTT diagram to mermaid excludes weekdays 2014-01-10 section A section Completed task :done, des1, 2014-01-06,2014-01-08 Active task :active, des2, 2014-01-09, 3d Future task : des3, after des2, 5d Future task2 : des4, after des3, 5d gitGraph commit commit branch develop commit commit commit checkout main commit commit xychart-beta title \"Sales Revenue\" x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec] y-axis \"Revenue (in $)\" 4000 --\u003e 11000 bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000] 参考\nDiagrams | Hugo Hugo博客添加mermaid作图 Render ASCII art as SVG diagrams Mermaid 入门文档 ","permalink":"https://kira-96.github.io/notes/hugo-diagrams/","summary":"Hugo原生支持GoAT,可以直接使用goat代码块。\n1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · a b a b a b a b \u0026amp; A M S i o i q f b B x u j o e a ( - x d r a \u0026gt; e f R \u0026gt; c o C n u o b ( n r ) ) d n e e J d r o s i n N o R D t o i u a a n g d d l o i t n e D i a g o n a l s C V u e r r v t e i d c a l n o t A N C : o u l d r r i A a / I v n s i n e e h s t d - e - t r l B i h i i s i o n ' s r e q n .","title":"Hugo Diagrams"},{"content":"前言 MODBUS是一种应用层消息传递协议,通常用于 I/O 系统通信和可编程逻辑控制器(PLC)通信。\n链接类型 描述 MODBUS TCP TCP/IP 使用502端口 MODBUS RTU RTU通常通过串行通信链路运行,即RS-232、 RS-422 或 RS-485。RTU 使用额外的 CRC 进行数据包检查。协议直接将每个字节作为 8 个数据位传输,因此使用“二进制” 而不是 ASCII 编码。使用串行链路开始和结束时,消息帧是按时间而不是按特定字符检测的。 MODBUS ASCII 串行协议,通常在串行通信链路上运行,即 RS-232、RS-422 或 RS-485。串行 ASCII 使用额外的 LRC 数据包检查。该协议将每个字节编码为 2 个 ASCII 字符。消息帧的开始和结束由特定字符检测 (“:” 开始消息,CR/LF 结束消息)。该协议效率低于 Modbus RTU,但在某些环境中可能更可靠。 Modbus 提供对以下 4 种类型的数据的访问:\n主表 对象类型 访问 说明 离散输入 1bit 只读 这种类型的数据可以由 I/O 系统提供。 线圈 1bit 读写 此类数据可由应用程序更改。 输入寄存器 16位字(2字节) 只读 这种类型的数据可以由 I/O 系统提供。 保持寄存器 16位字(2字节) 读写 此类数据可由应用程序更改。 Modbus 通信由从 Modbus 客户端发送到 Modbus 服务器的请求消息组成。服务器使用响应消息进行回复。Modbus 请求消息包含:\n描述数据传输类型的 Modbus 功能码(1字节)。 Modbus 地址(2字节),用于描述从服务器中读取或写入数据的地址。 对于写入操作,则需要传输写入的数据。 Modbus模块 支持以下 9 个 Modbus 功能码:\n访问 功能说明 功能码 1bit 读取线圈 1 1bit 读取离散输入 2 1bit 写入单线圈 5 1bit 写入多个线圈 15 16位字访问(2字节) 读取输入寄存器 4 16位字访问(2字节) 读取保持寄存器 3 16位字访问(2字节) 写入单个寄存器 6 16位字访问(2字节) 写入多个寄存器 16 16位字访问(2字节) 读/写多个寄存器 23 Modbus读取操作仅限于传输125个16位字或2000 bit。Modbus写入操作仅限于传输123个16位字或1968 bit。\n编译MODBUS模块 使用到的模块下载地址 epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov)\nepics-modules/asyn: EPICS module for driver and device support\nepics-modules/modbus: EPICS support for communication with Programmable Logic Controllers (PLCs) and other devices via the Modbus protocol over TCP, serial RTU, and serial ASCII links epics-modules/sscan: APS BCDA synApps module: sscan\nepics-modules/calc: APS BCDA synApps module: calc\nepics-modules/ipac: IPAC Carrier and Communication Module Drivers\nsequencer / Download and Installation — EPICS Sequencer Version 2.2 (bessy.de)\n以下步骤需要先安装好EPICS Base.\n编译 SSCAN(可选) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cd sscan touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 CALC(可选) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd calc touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # SSCAN模块路径 SSCAN=$(SUPPORT)/sscan # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 asyn(必需) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cd asyn touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # SSCAN模块路径 SSCAN=$(SUPPORT)/sscan # CALC模块路径 CALC=$(SUPPORT)/calc # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译 modbus 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cd modbus touch configure/RELEASE.local vi configure/RELEASE.local # 修改成和EPICS Base一样的架构 EPICS_HOST_ARCH=linux-loong64 # EPICS Base路径(示例) EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 # 放置EPICS模块的路径(示例) SUPPORT=/home/ubuntu/loongson/modules # ASYN模块路径 ASYN=$(SUPPORT)/asyn # 直接编译 # make # 交叉编译(示例) make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ 编译完成后,可以看到bin\\\u0026lt;EPICS_HOST_ARCH\u0026gt;路径下生成了可执行程序modbusApp,它就是与Modbus设备通信的主程序了。\n使用 MODBUS 程序 在Modbus模块的iocBoot\\iocTest目录下,可以看到很多示例程序。这里总结一下,我们使用时主要需要编写两部分内容。\n用于配置设备连接和通信的.cmd文件 用于使用模板解析数据的.substitutions文件 这里给出示例并做简要说明。\nenvPaths文件:用于配置程序运行时的环境变量路径。\n这里需要配置好base、asyn、modbus模块的路径。\n1 2 3 4 5 6 7 8 9 # envPaths epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;app\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;..\u0026#34;) epicsEnvSet(\u0026#34;SUPPORT\u0026#34;,\u0026#34;/root/modules\u0026#34;) epicsEnvSet(\u0026#34;ASYN\u0026#34;,\u0026#34;/root/modules/asyn\u0026#34;) epicsEnvSet(\u0026#34;MODBUS\u0026#34;,\u0026#34;/root/modules/modbus\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/root/base\u0026#34;) # epicsEnvSet(\u0026#34;EPICS_CAS_SERVER_PORT\u0026#34;, 9001) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 # AMSAMOTION.cmd \u0026lt; envPaths dbLoadDatabase(\u0026#34;$(MODBUS)/dbd/modbusApp.dbd\u0026#34;) modbusApp_registerRecordDeviceDriver(pdbbase) # MODBUS TCP 配置 # Use the following commands for TCP/IP #drvAsynIPPortConfigure(const char *portName, # const char *hostInfo, # unsigned int priority, # int noAutoConnect, # int noProcessEos); drvAsynIPPortConfigure(\u0026#34;AMSAMOTION\u0026#34;,\u0026#34;192.168.xxx.xxx:502\u0026#34;,0,0,1) #asynSetOption(\u0026#34;AMSAMOTION\u0026#34;,0, \u0026#34;disconnectOnReadTimeout\u0026#34;, \u0026#34;Y\u0026#34;) # MODBUS RTU配置 #drvAsynSerialPortConfigure(const char *portName, # const char *ttyName, # unsigned int priority, # int noAutoConnect, # int noProcessEos); # drvAsynSerialPortConfigure(\u0026#34;Koyo1\u0026#34;, \u0026#34;/dev/ttyS1\u0026#34;, 0, 0, 0) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;baud\u0026#34;,\u0026#34;38400\u0026#34;) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;parity\u0026#34;,\u0026#34;none\u0026#34;) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;bits\u0026#34;,\u0026#34;8\u0026#34;) # asynSetOption(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;stop\u0026#34;,\u0026#34;1\u0026#34;) # Modbus ASCII 还需配置其他项 # asynOctetSetOutputEos(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;\\r\\n\u0026#34;) # asynOctetSetInputEos(\u0026#34;Koyo1\u0026#34;,0,\u0026#34;\\r\\n\u0026#34;) # 超时设置 #modbusInterposeConfig(const char *portName, # modbusLinkType linkType, # int timeoutMsec, # int writeDelayMsec) # Modbus Link Type: 0 = TCP/IP,1 = RTU,2 = ASCII modbusInterposeConfig(\u0026#34;AMSAMOTION\u0026#34;,0,5000,0) # 读取/写入配置 #drvModbusAsynConfigure(portName, # tcpPortName, # slaveAddress, # modbusFunction, # modbusStartAddress, # modbusLength, # dataType, # pollMsec, # plcType); drvModbusAsynConfigure(\u0026#34;AMSA:AI\u0026#34;, \u0026#34;AMSAMOTION\u0026#34;, 1, 4, 0, 6, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:AO1\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 6, 0, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:AO2\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 6, 1, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:AOSta\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;,1, 3, 0, 2, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DI\u0026#34;, \u0026#34;AMSAMOTION\u0026#34;, 1, 2, 0, 8, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO1\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 0, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO2\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 1, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO3\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 2, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO4\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 3, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO5\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 4, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO6\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 5, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO7\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 6, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DO8\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;, 1, 5, 7, 1, 0, 100, \u0026#34;AMSA\u0026#34;) drvModbusAsynConfigure(\u0026#34;AMSA:DOSta\u0026#34;,\u0026#34;AMSAMOTION\u0026#34;,1, 1, 0, 8, 0, 100, \u0026#34;AMSA\u0026#34;) # Enable ASYN_TRACEIO_HEX on modbus server asynSetTraceIOMask(\u0026#34;AMSAMOTION\u0026#34;,0,4) # Dump up to 512 bytes in asynTrace asynSetTraceIOTruncateSize(\u0026#34;AMSAMOTION\u0026#34;,0,512) dbLoadTemplate(\u0026#34;AMSAMOTION.substitutions\u0026#34;) iocInit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 # AMSAMOTION.substitutions # asyn record for the underlying asyn octet port file \u0026#34;$(ASYN)/db/asynRecord.db\u0026#34; { pattern {P, R, PORT, ADDR, IMAX, OMAX} {AMSAMOTION: OctetAsyn, AMSAMOTION, 0, 80, 80} } file \u0026#34;$(TOP)/db/ai.template\u0026#34; { pattern {P, R, PORT, OFFSET, BITS, EGUL, EGUF, PREC, SCAN} {AMSAMOTION:, AI1, AMSA:AI, 0, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI2, AMSA:AI, 1, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI3, AMSA:AI, 2, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI4, AMSA:AI, 3, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI5, AMSA:AI, 4, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION:, AI6, AMSA:AI, 5, 0xFFFF, 0, 65535, 0, \u0026#34;I/O Intr\u0026#34;} } file \u0026#34;$(TOP)/db/ao.template\u0026#34; { pattern {P, R, PORT, OFFSET, BITS, EGUL, EGUF, PREC} {AMSAMOTION: AO1, AMSA:AO1, 0, 0xFFFF, 0, 65535, 0} {AMSAMOTION: AO2, AMSA:AO2, 0, 0xFFFF, 0, 65535, 0} } file \u0026#34;$(TOP)/db/ai.template\u0026#34; { pattern {P, R, PORT, OFFSET, BITS, EGUL, EGUF, PREC, SCAN} {AMSAMOTION:, AO1:STATE, AMSA:AOSta, 0, 0xFFFF, 0, 65535, 0, \u0026#34;1 second\u0026#34;} {AMSAMOTION:, AO2:STATE, AMSA:AOSta, 1, 0xFFFF, 0, 65535, 0, \u0026#34;1 second\u0026#34;} } file \u0026#34;$(TOP)/db/bi_bit.template\u0026#34; { pattern {P, R, PORT, OFFSET, ZNAM, ONAM, ZSV, OSV, SCAN} {AMSAMOTION: DI1, AMSA:DI, 0, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI2, AMSA:DI, 1, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI3, AMSA:DI, 2, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI4, AMSA:DI, 3, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI5, AMSA:DI, 4, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI6, AMSA:DI, 5, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI7, AMSA:DI, 6, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DI8, AMSA:DI, 7, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} } file \u0026#34;$(TOP)/db/bi_bit.template\u0026#34; { pattern {P, R, PORT, OFFSET, ZNAM, ONAM, ZSV, OSV, SCAN} {AMSAMOTION: DO1:STATE, AMSA:DOSta, 0, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO2:STATE, AMSA:DOSta, 1, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO3:STATE, AMSA:DOSta, 2, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO4:STATE, AMSA:DOSta, 3, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO5:STATE, AMSA:DOSta, 4, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO6:STATE, AMSA:DOSta, 5, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO7:STATE, AMSA:DOSta, 6, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} {AMSAMOTION: DO8:STATE, AMSA:DOSta, 7, OFF, ON, NO_ALARM, MAJOR, \u0026#34;I/O Intr\u0026#34;} } file \u0026#34;$(TOP)/db/bo_bit.template\u0026#34; { pattern {P, R, PORT, OFFSET, ZNAM, ONAM} {AMSAMOTION: DO1, AMSA:DO1, 0, OFF, ON} {AMSAMOTION: DO2, AMSA:DO2, 0, OFF, ON} {AMSAMOTION: DO3, AMSA:DO3, 0, OFF, ON} {AMSAMOTION: DO4, AMSA:DO4, 0, OFF, ON} {AMSAMOTION: DO5, AMSA:DO5, 0, OFF, ON} {AMSAMOTION: DO6, AMSA:DO6, 0, OFF, ON} {AMSAMOTION: DO7, AMSA:DO7, 0, OFF, ON} {AMSAMOTION: DO8, AMSA:DO8, 0, OFF, ON} } 最后运行程序,在终端执行:\n1 /path/to/modbus/bin/\u0026lt;EPICS_HOST_ARCH\u0026gt;/modbusApp AMSAMOTION.cmd 或者在.cmd文件第一行添加下面一行:\n1 #!../bin/\u0026lt;EPICS_HOST_ARCH\u0026gt;/modbusApp 然后直接执行.cmd脚本。\n1 2 chmod +x AMSAMOTION.cmd ./AMSAMOTION.cmd 参考\nOverview of Modbus Creating a modbus port driver EPICS Process Database Concepts ","permalink":"https://kira-96.github.io/posts/build-epics-module-modbus/","summary":"前言 MODBUS是一种应用层消息传递协议,通常用于 I/O 系统通信和可编程逻辑控制器(PLC)通信。\n链接类型 描述 MODBUS TCP TCP/IP 使用502端口 MODBUS RTU RTU通常通过串行通信链路运行,即RS-232、 RS-422 或 RS-485。RTU 使用额外的 CRC 进行数据包检查。协议直接将每个字节作为 8 个数据位传输,因此使用“二进制” 而不是 ASCII 编码。使用串行链路开始和结束时,消息帧是按时间而不是按特定字符检测的。 MODBUS ASCII 串行协议,通常在串行通信链路上运行,即 RS-232、RS-422 或 RS-485。串行 ASCII 使用额外的 LRC 数据包检查。该协议将每个字节编码为 2 个 ASCII 字符。消息帧的开始和结束由特定字符检测 (“:” 开始消息,CR/LF 结束消息)。该协议效率低于 Modbus RTU,但在某些环境中可能更可靠。 Modbus 提供对以下 4 种类型的数据的访问:\n主表 对象类型 访问 说明 离散输入 1bit 只读 这种类型的数据可以由 I/O 系统提供。 线圈 1bit 读写 此类数据可由应用程序更改。 输入寄存器 16位字(2字节) 只读 这种类型的数据可以由 I/O 系统提供。 保持寄存器 16位字(2字节) 读写 此类数据可由应用程序更改。 Modbus 通信由从 Modbus 客户端发送到 Modbus 服务器的请求消息组成。服务器使用响应消息进行回复。Modbus 请求消息包含:","title":"EPICS的MODBUS模块的编译和使用"},{"content":"关于 ACAI ACAI 是一个C++封装的Channel Access协议应用开发接口(API),提供异步通道访问接口。\nACAI Channel Access Interface\nEPICS Qt依赖ACAI提供的Channel Access接口。\n前置步骤 这篇笔记是交叉编译EPICS和IOC内容的补充。\n在进行下面步骤前,请完成配置交叉编译环境和编译 EPICS Base。\n这里依旧以龙芯架构为例。\nEPICS base 编译完成后,可以看到bin目录下有linux-loong64、linux-x86_64两个目录,linux-x86_64目录下比linux-loong64目录多出了许多perl脚本,我们需要把这些脚本复制到龙架构的目录下,下面编译需要用到。\n1 $ cp ./bin/linux-x86_64/*.pl ./bin/linux-loong64/ 编译 在EPICS-Qt安装中已经介绍过编译ACAI。这次是使用交叉编译方式,步骤略有不同。\n1 2 3 4 5 6 7 8 9 10 11 12 cd ~/loongson/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改交叉编译的目标架构,和EPICS base中保持一致 EPICS_HOST_ARCH=linux-loong64 # 修改EPICS_BASE路径,例: EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ # 等待编译完成 编译完成后可以在lib/linux-loong64/目录下找到libacai.so。\n","permalink":"https://kira-96.github.io/notes/cross-compiling-acai/","summary":"关于 ACAI ACAI 是一个C++封装的Channel Access协议应用开发接口(API),提供异步通道访问接口。\nACAI Channel Access Interface\nEPICS Qt依赖ACAI提供的Channel Access接口。\n前置步骤 这篇笔记是交叉编译EPICS和IOC内容的补充。\n在进行下面步骤前,请完成配置交叉编译环境和编译 EPICS Base。\n这里依旧以龙芯架构为例。\nEPICS base 编译完成后,可以看到bin目录下有linux-loong64、linux-x86_64两个目录,linux-x86_64目录下比linux-loong64目录多出了许多perl脚本,我们需要把这些脚本复制到龙架构的目录下,下面编译需要用到。\n1 $ cp ./bin/linux-x86_64/*.pl ./bin/linux-loong64/ 编译 在EPICS-Qt安装中已经介绍过编译ACAI。这次是使用交叉编译方式,步骤略有不同。\n1 2 3 4 5 6 7 8 9 10 11 12 cd ~/loongson/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改交叉编译的目标架构,和EPICS base中保持一致 EPICS_HOST_ARCH=linux-loong64 # 修改EPICS_BASE路径,例: EPICS_BASE=/home/ubuntu/loongson/base-7.0.8 make LD=loongarch64-linux-gnu-ld CC=loongarch64-linux-gnu-gcc CCC=loongarch64-linux-gnu-g++ # 等待编译完成 编译完成后可以在lib/linux-loong64/目录下找到libacai.so。","title":"交叉编译 ACAI"},{"content":"前言 之前已经讲过在龙芯3A5000(loongarch64)上编译运行EPICS,不过这种情况只适用于有完整开发环境的情况下进行编译。一些时候,我们只有编译器,而缺少make,perl等工具,比如一些开发板厂商提供的开发套件。这种情况下,就需要通过交叉编译(cross-compiling)的方式来编译EPICS。\n这里以龙芯金龙2K500先锋开发板为例,我们使用Ubuntu-20.04作为构建系统,详细讲解如何构建出可以在开发板上运行的EPICS工具包,并部署在开发板上。\n由于开发板上没有开发环境,即使编译出目标平台的EPICS Base,我们依然不能直接在开发板上创建和编译IOC。所以,我们还是使用Ubuntu-20.04作为构建系统,创建并编译IOC,最后在开发板上运行。\n配置交叉编译环境 关于这一节,之前的文章已经详细讲过,参考配置交叉编译环境。\n如果你使用的是其他开发套件,请按照开发手册安装配置好环境。\n编译 EPICS Base 首先,下载、解压Base,参考以前的文章。\n在龙芯3A5000(loongarch64)上编译运行EPICS中我已经详细讲解了如何在龙架构上编译EPICS,这次,需要在原来对源码修改的基础上,再增加对交叉编译的支持。\n添加configure/os/CONFIG.linux-x86_64.linux-loong64\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # CONFIG.linux-x86_64.linux-loong64 # # Definitions for linux-x86_64 host - linux-loong64 target builds # Sites may override these in CONFIG_SITE.linux-x86_64.linux-loong64 #------------------------------------------------------- VALID_BUILDS = Ioc Command GNU_TARGET = loongarch64-linux-gnu # prefix of compiler tools CMPLR_SUFFIX = CMPLR_PREFIX = $(addsuffix -,$(GNU_TARGET)) # Provide a link-time path for readline if needed OP_SYS_INCLUDES += $(READLINE_DIR:%=-I%/include) READLINE_LDFLAGS = $(READLINE_DIR:%=-L%/lib) RUNTIME_LDFLAGS_READLINE_YES_NO = $(READLINE_DIR:%=-Wl,-rpath,%/lib) RUNTIME_LDFLAGS += \\ $(RUNTIME_LDFLAGS_READLINE_$(LINKER_USE_RPATH)_$(STATIC_BUILD)) SHRLIBDIR_LDFLAGS += $(READLINE_LDFLAGS) PRODDIR_LDFLAGS += $(READLINE_LDFLAGS) # Library flags STATIC_LDFLAGS_YES= -Wl,-Bstatic STATIC_LDFLAGS_NO= STATIC_LDLIBS_YES= -Wl,-Bdynamic STATIC_LDLIBS_NO= 添加configure/os/CONFIG_SITE.linux-x86_64.linux-loong64\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # CONFIG_SITE.linux-x86_64.linux-loong64 # # Site specific definitions for linux-x86_64 host - linux-loong64 target builds #------------------------------------------------------- # Set GNU crosscompiler target name GNU_TARGET = loongarch64-linux-gnu # Set GNU tools install path # Examples is the installation at the APS: GNU_DIR = /opt/loongson-gnu-toolchain-8.3-x86_64-loongarch64-linux-gnu-rc1.2 # If cross-building shared libraries and the paths on the target machine are # different than on the build host, you should uncomment the lines below to # disable embedding compile-time library paths into the generated files. # You will need to provide another way for programs to find their shared # libraries at runtime, such as by setting LD_LIBRARY_PATH or (better) using # mechanisms related to /etc/ld.so.conf #SHRLIBDIR_RPATH_LDFLAGS_YES_NO = #PRODDIR_RPATH_LDFLAGS_YES_NO = # However it is usually simpler to set STATIC_BUILD=YES here and not # try to use shared libraries at all when cross-building, like this: #STATIC_BUILD=YES #SHARED_LIBRARIES=NO # To use libreadline, point this to its install prefix #READLINE_DIR = $(GNU_DIR) #READLINE_DIR = /tools/cross/linux-x86.linux-loong64/readline # See CONFIG_SITE.Common.linux-loong64 for other COMMANDLINE_LIBRARY values #COMMANDLINE_LIBRARY = READLINE GNU_DIR需要改为安装交叉编译工具链的路径。\nSTATIC_BUILD和SHARED_LIBRARIES可以设置是否为非静态编译,即是否生成动态库.so,两个设置项必须一起修改,这里的设置会覆盖掉configure/CONFIG_SITE中的设置。由于动态库在编译一些其他工具时还会用到,所以这里我选择生成动态库。\n1 2 # STATIC_BUILD=YES # SHARED_LIBRARIES=NO 然后,需要设置交叉编译的目标架构。\n新增configure/CONFIG_SITE.local,或者直接修改configure/CONFIG_SITE(不推荐)。\n1 CROSS_COMPILER_TARGET_ARCHS=linux-loong64 最后,进行编译即可。(确保构建系统上有make和perl,应该都有吧。)\n1 2 3 # 到源码目录下 $ cd ~/loongson/base-7.0.8 $ make -j8 等待编译完成即可。\n编译完成后,可以看到bin和lib目录下,都有linux-loong64、linux-x86_64两个目录,其中linux-loong64目录下就是我们要在开发板上运行的EPICS工具包了。linux-x86_64目录下的则是编译生成的本机的EPICS工具包,待会儿我们还会用到。\n由于开发板的存储空间很小,只有几百兆,所以,我们只能单独将龙架构的内容下载到板子上。\n目录如下:\nbase ├─ bin │ └─ linux-loong64 ├─ db ├─ dbd ├─ include (可选) └─ lib └─ linux-loong64 将目录中的内容全部打包下载到开发板即可。\n1 2 3 4 5 # 在开发板上运行 $ cd base/bin/linux-loong64 # 运行软IOC测试 $ ./softIoc epics\u0026gt; ※ 如果使用非静态编译(生成动态库)的方式编译,运行时可能会提示找不到动态库。需要将动态库添加到系统的动态库路径。\n如下(根据实际情况修改路径):\n1 $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/base/lib/linux-loong64 ※ include 目录不是EPICS运行必需的,但如果要基于EPICS进行开发,可能需要用到EPICS的头文件。\n例如:交叉编译 ACAI\n编译 IOC 前面已经讲了,我们需要在构建主机上交叉编译IOC,具体步骤和直接创建编译IOC基本一样。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ cd ~ # 创建目录 $ mkdir test $ cd test/ # 替换成自己编译EPICS的目录,注意是使用linux-x86_64目录下的脚本 $ ~/loongson/base-7.0.8/bin/linux-x86_64/makeBaseApp.pl -t example test $ ~/loongson/base-7.0.8/bin/linux-x86_64/makeBaseApp.pl -i -t example test The following target architectures are available in base: linux-loong64 linux-x86_64 What architecture do you want to use? linux-loong64 The following applications are available: test What application should the IOC(s) boot? The default uses the IOC\u0026#39;s name, even if not listed above. Application name? test 这里唯一多的步骤就是选择目标架构,输入linux-loong64即可。\n然后修改编译设置,这里就不用生成动态库了,直接使用静态编译。\n添加configure/CONFIG_SITE.local,启用静态编译。\n1 2 3 4 5 6 7 8 9 10 11 # Build shared libraries (DLLs on Windows). # Must be either YES or NO. Definitions in the target-specific # os/CONFIG.Common.\u0026lt;target\u0026gt; and os/CONFIG_SITE.Common.\u0026lt;target\u0026gt; files may # override this setting. On Windows only these combinations are valid: # SHARED_LIBRARIES = YES and STATIC_BUILD = NO # SHARED_LIBRARIES = NO and STATIC_BUILD = YES SHARED_LIBRARIES=NO # Build client objects statically. # Must be either YES or NO. STATIC_BUILD=YES 最后编译。\n1 2 $ cd ~/test $ make 等待编译完成即可。然后将生成的可执行文件下载到开发板,这里依旧是只下载运行IOC所必需的内容。\n目录如下:\ntest ├─ bin │ └─ linux-loong64 ├─ db ├─ dbd └─ iocBoot └─ ioctest 在把IOC复制到开发板上之后,还需要根据实际情况修改IOC运行的环境变量。\n修改iocBoot/ioctest/envPaths\n1 2 3 4 epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;ioctest\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;/root/test\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/root/base\u0026#34;) epicsEnvSet(\u0026#34;EPICS_HOST_ARCH\u0026#34;,\u0026#34;linux-loong64\u0026#34;) 最后在开发板上运行IOC。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # 在开发板上运行 $ cd ~/test/iocBoot/ioctest # 添加可执行权限 $ chmod +x st.cmd $ ./st.cmd #!../../bin/linux-loong64/test \u0026lt; envPaths epicsEnvSet(\u0026#34;IOC\u0026#34;,\u0026#34;ioctest\u0026#34;) epicsEnvSet(\u0026#34;TOP\u0026#34;,\u0026#34;/root/test\u0026#34;) epicsEnvSet(\u0026#34;EPICS_BASE\u0026#34;,\u0026#34;/root/base\u0026#34;) epicsEnvSet(\u0026#34;EPICS_HOST_ARCH\u0026#34;,\u0026#34;linux-loong64\u0026#34;) cd \u0026#34;/root/test\u0026#34; ## Register all support components dbLoadDatabase \u0026#34;dbd/test.dbd\u0026#34; test_registerRecordDeviceDriver pdbbase Warning: IOC is booting with TOP = \u0026#34;/root/test\u0026#34; but was built with TOP = \u0026#34;/home/ubuntu/test\u0026#34; ## Load record instances dbLoadTemplate \u0026#34;db/user.substitutions\u0026#34; dbLoadRecords \u0026#34;db/testVersion.db\u0026#34;, \u0026#34;user=lsgd\u0026#34; dbLoadRecords \u0026#34;db/dbSubExample.db\u0026#34;, \u0026#34;user=lsgd\u0026#34; cd \u0026#34;/root/test/iocBoot/ioctest\u0026#34; iocInit Starting iocInit ############################################################################ ## EPICS R7.0.8 ## Rev. 2023-12-30T14:06+0800 ## Rev. Date build date/time: ############################################################################ iocRun: All initialization complete ## Start any sequence programs #seq sncExample, \u0026#34;user=lsgd\u0026#34; epics\u0026gt; 这里可以看到启动时会有一个Warning,警告IOC运行时和编译时的TOP路径不一致,但实际上并不影响IOC运行,忽略即可。\n此时可以使用dbl、dbpr等命令查看变量。或者在其他终端运行camonitor程序。\n","permalink":"https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/","summary":"前言 之前已经讲过在龙芯3A5000(loongarch64)上编译运行EPICS,不过这种情况只适用于有完整开发环境的情况下进行编译。一些时候,我们只有编译器,而缺少make,perl等工具,比如一些开发板厂商提供的开发套件。这种情况下,就需要通过交叉编译(cross-compiling)的方式来编译EPICS。\n这里以龙芯金龙2K500先锋开发板为例,我们使用Ubuntu-20.04作为构建系统,详细讲解如何构建出可以在开发板上运行的EPICS工具包,并部署在开发板上。\n由于开发板上没有开发环境,即使编译出目标平台的EPICS Base,我们依然不能直接在开发板上创建和编译IOC。所以,我们还是使用Ubuntu-20.04作为构建系统,创建并编译IOC,最后在开发板上运行。\n配置交叉编译环境 关于这一节,之前的文章已经详细讲过,参考配置交叉编译环境。\n如果你使用的是其他开发套件,请按照开发手册安装配置好环境。\n编译 EPICS Base 首先,下载、解压Base,参考以前的文章。\n在龙芯3A5000(loongarch64)上编译运行EPICS中我已经详细讲解了如何在龙架构上编译EPICS,这次,需要在原来对源码修改的基础上,再增加对交叉编译的支持。\n添加configure/os/CONFIG.linux-x86_64.linux-loong64\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # CONFIG.linux-x86_64.linux-loong64 # # Definitions for linux-x86_64 host - linux-loong64 target builds # Sites may override these in CONFIG_SITE.linux-x86_64.linux-loong64 #------------------------------------------------------- VALID_BUILDS = Ioc Command GNU_TARGET = loongarch64-linux-gnu # prefix of compiler tools CMPLR_SUFFIX = CMPLR_PREFIX = $(addsuffix -,$(GNU_TARGET)) # Provide a link-time path for readline if needed OP_SYS_INCLUDES += $(READLINE_DIR:%=-I%/include) READLINE_LDFLAGS = $(READLINE_DIR:%=-L%/lib) RUNTIME_LDFLAGS_READLINE_YES_NO = $(READLINE_DIR:%=-Wl,-rpath,%/lib) RUNTIME_LDFLAGS += \\ $(RUNTIME_LDFLAGS_READLINE_$(LINKER_USE_RPATH)_$(STATIC_BUILD)) SHRLIBDIR_LDFLAGS += $(READLINE_LDFLAGS) PRODDIR_LDFLAGS += $(READLINE_LDFLAGS) # Library flags STATIC_LDFLAGS_YES= -Wl,-Bstatic STATIC_LDFLAGS_NO= STATIC_LDLIBS_YES= -Wl,-Bdynamic STATIC_LDLIBS_NO= 添加configure/os/CONFIG_SITE.","title":"交叉编译EPICS和IOC"},{"content":"需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。\n这里使用MinGW环境编译EPICS,不使用MSVC编译器。\n安装 Strawberry Perl 这里选择 Strawberry Perl 5.38.2.1。\n直接安装即可,需要注意的是,安装路径不能有空格和中文,最好放在盘符的根目录下。\n例:D:\\Strawberry\n安装完成后检查系统环境变量,查看系统Path环境变量是否有Strawberry Perl的路径。没有则手动添加,以安装在D盘为例。\n1 2 3 D:\\Strawberry\\c\\bin D:\\Strawberry\\perl\\site\\bin D:\\Strawberry\\perl\\bin 其中D:\\Strawberry\\c\\bin就是MinGW环境的路径。\n查看Perl版本,检查一下是不是装好了。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026gt; perl -v This is perl 5, version 38, subversion 2 (v5.38.2) built for MSWin32-x64-multi-thread Copyright 1987-2023, Larry Wall Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5 source kit. Complete documentation for Perl, including FAQ lists, should be found on this system using \u0026#34;man perl\u0026#34; or \u0026#34;perldoc perl\u0026#34;. If you have access to the Internet, point your browser at https://www.perl.org/, the Perl Home Page. 注意:perl 5.32.1.1 之后的版本可能会提示\nLocale \u0026lsquo;Chinese (Simplified)_China.936\u0026rsquo; is unsupported, and may crash the interpreter.\n需要设置环境变量LC_ALL=C。\n编译安装EPICS Base 修改base源码目录下的startup/windows.bat文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 rem The location of Strawberry Perl (pathname). If empty, Strawberry Perl rem is assumed to already be in PATH and will not be added. If nonempty, rem Strawberry Perl will be added to PATH. rem 设置Strawberry Perl安装路径 set _strawberry_perl_home=D:\\Strawberry rem The EPICS host architecture specification for EPICS_HOST_ARCH rem (\u0026lt;os\u0026gt;-\u0026lt;arch\u0026gt;[-\u0026lt;toolset\u0026gt;] as defined in configure/CONFIG_SITE). rem 设置编译主机架构,这里使用mingw set _epics_host_arch=windows-x64-mingw rem The install location of EPICS Base (pathname). If nonempty and rem _auto_path_append is yes, it will be used to add the host architecture rem bin directory to PATH. set _epics_base=C:\\EPICS\\base-7.0.8 rem Set the environment for Microsoft Visual Studio rem 使用 rem 注释掉下面一行 rem call \u0026#34;%_visual_studio_home%\\VC\\Auxiliary\\Build\\vcvarsall.bat\u0026#34; x64 注意,编译需要使用命令行工具cmd,不能用powershell。\n注意,这里使用的是 perl 自带的编译工具,如果要使用其他版本的MinGW,则需要自行配置环境变量。\n比如这里使用 MinGW 12.2.0,则需要设置环境变量。\n1 set Path=C:\\WINDOWS;C:\\WINDOWS\\System32;C:\\WINDOWS\\System32\\Wbem;D:\\Strawberry\\perl\\bin;D:\\Tools\\mingw1220_64\\bin;D:\\Tools\\mingw1220_64\\libexec\\gcc\\x86_64-w64-mingw32\\12.2.0 1 2 3 4 5 6 7 cd base-7.0.8 .\\startup\\windows.bat # 根据使用的编译工具执行命令 # perl 环境的make命名为gmake gmake -j16 # mingw32-make -j16 等待编译完成。\n编译完成后的工具在bin\\windows-x64-mingw目录下。\n测试使用:\n1 2 3 cd bin\\windows-x64-mingw softIoc.exe epics\u0026gt; ","permalink":"https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/","summary":"需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。\n这里使用MinGW环境编译EPICS,不使用MSVC编译器。\n安装 Strawberry Perl 这里选择 Strawberry Perl 5.38.2.1。\n直接安装即可,需要注意的是,安装路径不能有空格和中文,最好放在盘符的根目录下。\n例:D:\\Strawberry\n安装完成后检查系统环境变量,查看系统Path环境变量是否有Strawberry Perl的路径。没有则手动添加,以安装在D盘为例。\n1 2 3 D:\\Strawberry\\c\\bin D:\\Strawberry\\perl\\site\\bin D:\\Strawberry\\perl\\bin 其中D:\\Strawberry\\c\\bin就是MinGW环境的路径。\n查看Perl版本,检查一下是不是装好了。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026gt; perl -v This is perl 5, version 38, subversion 2 (v5.38.2) built for MSWin32-x64-multi-thread Copyright 1987-2023, Larry Wall Perl may be copied only under the terms of either the Artistic License or the GNU General Public License, which may be found in the Perl 5 source kit.","title":"Windows上使用MinGW编译安装EPICS"},{"content":"前言 Linux 内核提供了丰富的设备驱动接口,其中GPIO和LED属于是最基本的一类了。之前就已经讲过用户空间下的GPIO读写操作,LED设备的操作也基本相同。其实完全可以使用GPIO驱动去控制LED,但LED的驱动针对LED提供了更多的功能,一起来看一下吧。\n配置设备树 设备树中的LED节点配置,例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* kernel/arch/arm/boot/dts/imx6ul-14x14-evk-c-emmc.dts */ leds { compatible = \u0026#34;gpio-leds\u0026#34;; pinctrl-names = \u0026#34;default\u0026#34;; status = \u0026#34;okay\u0026#34;; led1{ label = \u0026#34;led1\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led2{ label = \u0026#34;led2\u0026#34;; gpios = \u0026lt;\u0026amp;gpio1 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led3{ label = \u0026#34;heartbeat\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 5 GPIO_ACTIVE_LOW\u0026gt;; linux,default-trigger = \u0026#34;heartbeat\u0026#34;; }; }; 节点属性说明:\nlabel:LED设备的名字,名字必须是唯一的。如果没有设置,则会使用节点的名字。\ngpios:GPIO的编号,以及高低电平设置,GPIO_ACTIVE_LOW低电平点亮,GPIO_ACTIVE_HIGH高电平点亮。\ndefault-state:默认状态,on/off。\nlinux,default-trigger:设置LED的触发器。backlight-背光灯,heartbeat-心跳灯,timer-定时,default-on-默认开状态,disk-activity-硬盘状态,gpio,none。\n用户空间下的LED操作 注意:以下操作都需要root权限!\n用户空间下的GPIO文件系统接口在/sys/class/leds/{label}目录下。\nLED节点有以下属性可以配置:\ntrigger\n设置LED的触发器。\n1 echo heartbeat \u0026gt; /sys/class/leds/led1/trigger brightness\n设置LED的开关或者亮度。\n1 2 3 4 # 关闭LED echo 0 \u0026gt; /sys/class/leds/led1/brightness # 打开LED echo 1 \u0026gt; /sys/class/leds/led1/brightness 对于写入的任何非0值,都会是打开LED的操作,可写入值范围为0~255。\n而对于使用PWM控制的LED灯,brightness才可以控制灯的亮度。参考呼吸灯的实现。\n程序控制LED灯 示例读取:\n1 2 3 4 5 6 7 8 9 10 11 quint8 readLed(const QString \u0026amp;led) { QFile file(QString(\u0026#34;/sys/class/leds/%1/brightness\u0026#34;).arg(led)); if (!file.open(QIODevice::ReadOnly)) return 0; QByteArray ba = file.readAll(); file.close(); return (quint8)QString(ba).toInt(); } 示例写入:\n1 2 3 4 5 6 7 8 9 bool writeLed(const QString \u0026amp;led, quint8 value) { QFile file(QString(\u0026#34;/sys/class/leds/%1/brightness\u0026#34;).arg(led)); if (!file.open(QIODevice::WriteOnly)) return false; QByteArray ba = QString::number(value).toLatin1(); return file.write(ba) == ba.size(); } 参考\nDocumentation/devicetree/bindings/leds/common.txt (v4.13) LED子系统详解 ","permalink":"https://kira-96.github.io/posts/linux-led%E5%AD%90%E7%B3%BB%E7%BB%9F/","summary":"前言 Linux 内核提供了丰富的设备驱动接口,其中GPIO和LED属于是最基本的一类了。之前就已经讲过用户空间下的GPIO读写操作,LED设备的操作也基本相同。其实完全可以使用GPIO驱动去控制LED,但LED的驱动针对LED提供了更多的功能,一起来看一下吧。\n配置设备树 设备树中的LED节点配置,例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* kernel/arch/arm/boot/dts/imx6ul-14x14-evk-c-emmc.dts */ leds { compatible = \u0026#34;gpio-leds\u0026#34;; pinctrl-names = \u0026#34;default\u0026#34;; status = \u0026#34;okay\u0026#34;; led1{ label = \u0026#34;led1\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led2{ label = \u0026#34;led2\u0026#34;; gpios = \u0026lt;\u0026amp;gpio1 9 GPIO_ACTIVE_LOW\u0026gt;; default-state = \u0026#34;off\u0026#34;; }; led3{ label = \u0026#34;heartbeat\u0026#34;; gpios = \u0026lt;\u0026amp;gpio5 5 GPIO_ACTIVE_LOW\u0026gt;; linux,default-trigger = \u0026#34;heartbeat\u0026#34;; }; }; 节点属性说明:","title":"Linux LED子系统"},{"content":"前言 本文主要记录了EPICS Qt在Linux上的安装步骤。这里以loongnix操作系统为例,Ubuntu系统上编译安装步骤类似。\nEPICS Qt是一个基于Qt的分层框架,使用Channel Access (CA) and PV Access(PVA)访问EPICS数据。它是为快速开发控制系统图形界面而设计的,最初是在澳大利亚同步加速器开发的。\n安装EPICS 这里不再写具体步骤了,总之就是非常简单,下载、解压、编译即可。具体步骤可以参考以前的文章。\n安装Qt 直接使用终端安装Qt\n1 2 3 4 5 sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator sudo apt install qtdeclarative5-dev qttools5-dev # 安装Qt Svg库,编译QWT时需要用到 sudo apt install libqt5svg5-dev 安装QWT Qt EPICS推荐使用Qwt 6.1.4,如果在Ubuntu 20.04上直接通过终端安装也是这个版本。我使用Qwt 6.2.0编译,也是没有问题的,这里以Qwt 6.2.0为例。\n最新测试:Qwt 6.3.0也可以用 ~\n先下载Qwt的源码 下载Qwt-6.2.0。 下载完成后解压\n1 2 3 4 # 解压tar.bz2 tar -jxvf qwt-6.2.0.tar.bz2 # 解压zip unzip qwt-6.2.0.zip 解压完成后编译Qwt,使用QtCreator或者在终端使用qmake都可以。\n然后手动将编译生成的文件复制到以下位置,例:\n1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin.so /usr/lib/loongarch64-linux-gnu/qt5/plugins/designer/ # 复制qwt头文件 sudo mkdir /usr/include/qwt sudo cp qwt-6.2.0/src/*.h /usr/include/qwt 安装ACAI ACAI Channel Access Interface\nEPICS Qt依赖ACAI提供的Channel Access接口。\n1 2 3 4 5 6 7 8 cd /usr/local/epics/modules/ git clone https://github.com/andrewstarritt/acai.git cd acai vi configure/RELEASE.local # 修改EPICS_BASE路径,例: # EPICS_BASE=/usr/local/epics/base-7.0.8 make -j8 # 等待编译完成 安装google protobuf 如果需要EPICS Qt支持EPICS Archiver Appliance,需要安装google protobuf。\n1 sudo apt install protobuf-compiler libprotobuf-dev EPICS Qt 首先克隆EPICS Qt的两个代码仓库。\n1 2 3 4 # framework and support libraries git clone https://github.com/qtepics/qeframework.git # QEGui display manager git clone https://github.com/qtepics/qegui.git 这里我将代码都放在~/QtEpics目录。\n在开始编译前,需要先配置一些环境变量(根据自己的实际情况设置)。具体可以参考 EPICS Qt Environment Variables\n1 2 3 4 5 6 7 8 9 10 11 12 export EPICS_HOST_ARCH=linux-loong64 export EPICS_BASE=/usr/local/epics/base-7.0.8 export ACAI=/usr/local/epics/modules/acai export QWT_INCLUDE_PATH=/usr/include/qwt export QWT_ROOT=/usr/lib/loongarch64-linux-gnu export QE_FRAMEWORK=\u0026#34;$HOME/QtEpics/qeframework\u0026#34; # 支持PV Access export QE_PVACCESS_SUPPORT=YES # 支持Archiver Appliance export QE_ARCHAPPL_SUPPORT=YES export PROTOBUF_INCLUDE_PATH=/usr/include/google/protobuf export PROTOBUF_LIB_DIR=/usr/lib/loongarch64-linux-gnu 如果环境变量设置了支持Archiver Appliance,需要先编译archapplDataSup。\n1 2 cd ~/QtEpics/qeframework/archapplDataSup/ make 编译完成后,可以看到~/QtEpics/qeframework/lib/linux-loong64目录下有libarchapplData.a、libarchapplData.so两个文件。\n然后依次编译 qeframework qeplugin qegui。EPICS Qt文档说明需要修改configure/RELEASE文件,但我这里修改后似乎没有生效,可能是使用了Qt Creator的原因,只能通过上面的环境变量设置。\n注意:这里设置完环境变量,需要直接通过终端打开Qt Creator。\n编译qeframework $HOME/QtEpics/qeframework/qeframeworkSup/project/framework.pro 编译qeplugin $HOME/QtEpics/qeframework/qepluginApp/project/qeplugin.pro 编译qegui $HOME/QtEpics/qegui/qeguiApp/project/QEGuiApp.pro 最后将编译生成的文件复制到系统目录,示例:\n1 2 3 4 sudo cp /usr/local/epics/modules/acai/lib/linux-loong64/libacai.so* /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loong64/libarchapplData.so /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loong64/libQEFramework.so /usr/lib/loongarch64-linux-gnu/ sudo cp ~/QtEpics/qeframework/lib/linux-loong64/designer/libQEPlugin.so /usr/lib/loongarch64-linux-gnu/qt5/plugins/designer/ 运行QEGuiApp\n1 2 cd ~/epics/qtepics/qegui/bin/linux-loong64 ./qegui 运行测试 运行时环境变量设置,例:\n1 2 3 export QE_ARCHIVE_TYPE=ARCHAPPL export QE_ARCHIVE_LIST=\u0026#34;http://192.168.1.2:17665/mgmt/bpl\u0026#34; export EPICS_CA_ADDR_LIST=\u0026#34;192.168.1.2:5732 192.168.1.3:6666\u0026#34; 问题汇总 编译过程中可能会遇到一些问题\n找不到Qwt的头文件 解决办法: 修改qeframework/qeframeworkSup/project/common/common.pri\n1 2 INCLUDEPATH += $$PWD +INCLUDEPATH += $$(QWT_INCLUDE_PATH) 找不到QEFramework的头文件 解决办法: 修改对应项目的项目文件\n1 +INCLUDEPATH += $$(QE_FRAMEWORK)/include Windows上编译踩坑\n我这里使用的是MinGW编译器,仅供参考。Windows编译安装EPICS,可以参考我的笔记。\nACAI编译报错,这个是由于平台函数的差异导致的报错。\n我已经提交了修复补丁,参考 fix build error on windows。\n编译 google protobuf\n网上有很多关于windows编译google protobuf的文章。请自行搜索解决。\n编译 QEFramework\n这里需要修改configure/RELEASE.local,示例:\n1 2 3 4 5 6 7 8 9 10 EPICS_HOST_ARCH=windows-x64-mingw EPICS_BASE=C:\\Users\\YourName\\.epics\\base-7.0.8 ACAI=D:\\source\\acai QE_FRAMEWORK=D:\\source\\qeframework QE_PVACCESS_SUPPORT=YES QE_ARCHAPPL_SUPPORT=YES PROTOBUF_INCLUDE_PATH=D:\\source\\protobuf-3.21.12\\src PROTOBUF_LIB_DIR=D:\\source\\protobuf-3.21.12\\build QWT_INCLUDE_PATH=D:\\source\\qwt-6.3.0\\src QWT_ROOT=D:\\source\\qwt-6.3.0\\build\\Desktop_Qt_5_15_14_MinGW_64_bit-Release Windows上运行程序\n将编译过程中生成的DLL全部复制到qegui程序目录下,大致汇总一下有:\nqwt.dll,libprotobuf.dll,acai.dll,QEFramework.dll,archapplData.dll(这个是在Windows上才会生成,在qeframework\\archapplDataSup\\src\\O.windows-x64-mingw\\目录下可以找到)\ndesigner/QEPlugin.dll,这个也很重要,不然qegui加载.ui文件会显示空白。\nEPICS相关的DLL:\nca.dll,Com.dll,nt.dll,pvAccess.dll,pvData.dll\n以及Qt相关的DLL。\n参考链接 EPICS Qt at GitHub EPICS Qt Getting Started Archiver Appliance Support for EPICS Qt ","permalink":"https://kira-96.github.io/posts/epics-qt%E5%AE%89%E8%A3%85/","summary":"前言 本文主要记录了EPICS Qt在Linux上的安装步骤。这里以loongnix操作系统为例,Ubuntu系统上编译安装步骤类似。\nEPICS Qt是一个基于Qt的分层框架,使用Channel Access (CA) and PV Access(PVA)访问EPICS数据。它是为快速开发控制系统图形界面而设计的,最初是在澳大利亚同步加速器开发的。\n安装EPICS 这里不再写具体步骤了,总之就是非常简单,下载、解压、编译即可。具体步骤可以参考以前的文章。\n安装Qt 直接使用终端安装Qt\n1 2 3 4 5 sudo apt update sudo apt install qtbase5-dev qt5-qmake qtcreator sudo apt install qtdeclarative5-dev qttools5-dev # 安装Qt Svg库,编译QWT时需要用到 sudo apt install libqt5svg5-dev 安装QWT Qt EPICS推荐使用Qwt 6.1.4,如果在Ubuntu 20.04上直接通过终端安装也是这个版本。我使用Qwt 6.2.0编译,也是没有问题的,这里以Qwt 6.2.0为例。\n最新测试:Qwt 6.3.0也可以用 ~\n先下载Qwt的源码 下载Qwt-6.2.0。 下载完成后解压\n1 2 3 4 # 解压tar.bz2 tar -jxvf qwt-6.2.0.tar.bz2 # 解压zip unzip qwt-6.2.0.zip 解压完成后编译Qwt,使用QtCreator或者在终端使用qmake都可以。\n然后手动将编译生成的文件复制到以下位置,例:\n1 2 3 4 5 6 7 # 复制编译生成的qwt sudo cp -r build-qwt-unknown-Release/lib/* /usr/lib/loongarch64-linux-gnu/ # 复制编译生成的designer插件 sudo cp build-qwt-unknown-Release/designer/plugins/designer/libqwt_designer_plugin.","title":"EPICS Qt安装"},{"content":"前言 本来这篇文章应该在上周就写完的,不过突然被安排出差,一直忙到了现在,终于可以静下心来做些其它事情。\n之前和龙芯3A5000主机一起送过来的还有一块龙芯2K500的迷你开发板,整个板子不到巴掌大小。之前只是简单做了上电启动,这次拿到了比较完整的开发资料,可以尝试为开发板编写一些程序了。\n由于暂时没有屏幕,只能先试着做一些其它的事情,如通信和IO控制,其中最简单,最基础的就是LED灯的控制。然后我就发现,这个板子居然有一颗可以调节亮度的LED灯!没错,之前做的LED控制都只能进行开关操作,而可以调节亮度,意味着可以做出更多的显示效果,这次我就做了一个呼吸灯的效果。\n然后,我也简单了解了一下这种亮度调节的原理,实际上就是通过调节PWM输出的占空比,改变一个周期内输出的高低电平所占的比例,实现控制LED灯亮度的效果。由于引脚输出的电压是固定的,所以不能通过改变电平来控制亮度,而改变高低电平的占空比则是另一种思路,嵌入式设备的屏幕背光亮度调节也是基于同样的原理。\n开发板上电启动、连接串口终端 由于暂时没有屏幕,想要和开发板进行交互就只能通过终端的方式,通常开发板都会有调试串口,我们先通过串口终端登录设备,配置好网口IP地址后,再通过网络连接登录设备。\n串口的连接方式如下图:\n将绿、白、黑三色线以图中方式接好(红线不用接),USB端插入到电脑,应该不需要装驱动,电脑可以直接识别出串口设备。\n打开串口终端工具,比如Windows MobaXterm,linux minicom等,我比较喜欢用putty。\n配置好端口,设置\n波特率:115200\n数据位:8位\n停止位:1位\n校验:无\n硬件流控:无\n然后给开发板接通电源,就可以看到调试输出信息了。\n查看系统信息,可以看到运行的是安装了PREEMPT_RT补丁的实时操作系统。\n1 2 3 [root@LS-GD ~]# uname -a Linux LS-GD 5.10.0.lsgd-g434b00a6badf #1 PREEMPT Wed Sep 14 12:57:58 CST 2022 loongarch64 GNU/Linux [root@LS-GD ~]# 配置交叉编译环境 2K500开发板是loongarch64架构的嵌入式板卡。下载好对应的交叉编译工具链后,解压到系统/opt/目录下。按手册来就好~\n1 $ sudo tar -xf toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18.tar.xz -C /opt/ 然后我们需要将交叉编译器添加到系统路径,方便我们接下来使用。\n这里我直接将配置写成脚本,方便下次使用。\n1 2 3 4 5 6 7 8 $ cd /opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 # 创建脚本 $ sudo touch environment-setup-loongarch64-linux-gnu # 添加可执行权限 $ sudo chmod +x environment-setup-loongarch64-linux-gnu # 修改脚本内容 $ sudo vi environment-setup-loongarch64-linux-gnu # 内容如下~ 1 2 3 4 5 6 7 8 # environment-setup-loongarch64-linux-gnu CC_PREFIX=/opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 export PATH=$CC_PREFIX/bin:$PATH export LD_LIBRARY_PATH=$CC_PREFIX/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=$CC_PREFIX/loongarch64-linux-gnu/lib64:$LD_LIBRARY_PATH export ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu- 接下来测试一下交叉编译环境\n1 2 3 4 5 6 7 8 9 10 $ cd ~ $ . /opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18/environment-setup-loongarch64-linux-gnu $ loongarch64-linux-gnu-gcc -v Using built-in specs. COLLECT_GCC=loongarch64-linux-gnu-gcc COLLECT_LTO_WRAPPER=/opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18/bin/../libexec/gcc/loongarch64-linux-gnu/8.3.0/lto-wrapper Target: loongarch64-linux-gnu Configured with: /dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/src/gcc/configure --build=x86_64-redhat-linux --host=x86_64-redhat-linux --target=loongarch64-linux-gnu --program-prefix=loongarch64-linux-gnu- --prefix=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross --libdir=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross/lib --with-gxx-include-dir=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross/sysroot/usr/include/c++ --with-sysroot=/dev/shm/build_loongarch64_gcc8-host-x86_64_2022-07-18/cross/sysroot --with-native-system-header-dir=/usr/include --with-arch=loongarch64 --with-abi=lp64 --with-multilib-list=lp64d,lp64s --with-pkgversion=\u0026#39;LoongArch\\ GNU\\ toolchain\\ vec.32-rc2\u0026#39; --disable-linker-build-id --with-newlib --without-headers --disable-shared --enable-threads=posix --enable-tls --enable-languages=c,c++,fortran --enable-__cxa_atexit --enable-libquadmath-support --disable-gcov --disable-libcc1 --enable-initfini-array --disable-nls --disable-bootstrap --with-glibc-version=2.28 Thread model: posix gcc version 8.3.0 (LoongArch GNU toolchain vec.32-rc2) 可以看到loongarch交叉编译器的版本信息,配置交叉编译环境完成。\n编写程序 控制LED灯的亮度,Linux系统已经有驱动实现了,我们要做的操作就是向相应的文件中写入数值即可,剩下的都是系统的事情。\n对于一般的LED灯,只有开关两个选项,写入0为关闭,写入非0值打开。\n而对于PWM控制的LED灯,需要写入具体数值来控制灯的亮度,同样,0为关闭,写入数值越大LED灯就越亮,当然,这是有上限的。这里经过测试,写入255后,LED灯达到最亮。\n而这次写的呼吸灯程序,则是逐渐改变LED灯的亮度,实现LED灯缓慢闪烁的效果。\n这里我将亮度分为10个级别,从0到255(2^8 - 1),每100ms改变一下LED灯的亮度,一个周期刚好为2秒(从灭到最亮,然后从最亮到灭)。\n至于为什么是分为这10个级别,而不是从0~255变化,大家可以自己试试,看一下效果。\n以下是完整程序:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * file: led-pwm.c */ #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; /** * @brief 呼吸灯效果 * @param fd 设备文件 */ void breath(int fd); int main(int argc, char *argv[]) { if (argc \u0026lt; 2) { printf(\u0026#34;Please input LED device.\\n\u0026#34;); return -1; } int fd; char file[64]; sprintf(file, \u0026#34;%s/brightness\u0026#34;, argv[1]); /* 打开设备文件 */ fd = open(file, O_WRONLY); if (fd \u0026lt; 0) { printf(\u0026#34;Error open file: %s\\n\u0026#34;, file); return fd; } breath(fd); /* 关闭设备文件 */ close(fd); return 0; } void breath(int fd) { const int values[15] = { 0, 1, 2, 4, 8, 16, 32, 64, 96, 128, 160, 192, 224, 255, 255 }; char buf[5]; while (1) { for (int i = 0; i \u0026lt; 30; i++) { int j = i \u0026lt; 15 ? i : (29 - i); sprintf(buf, \u0026#34;%d\u0026#34;, values[j]); write(fd, buf, sizeof(buf)); usleep(100000); // 休眠100ms } } } 然后编译,得到在开发板上运行可执行程序。\n1 $ loongarch64-linux-gnu-gcc led-pwm.c -o led-pwm 下载程序到开发板 运行前的最后一步,需要将编译好可执行程序复制到开发板上,我通常是使用scp命令将文件复制到开发板。\n需要先在串口终端通过ifconfig设置开发板的网口IP,第一次使用scp前,需要先用ssh登录到开发板。\n系统默认账户为root,默认密码为123\n由于之前不知道默认密码是什么,所以先用passwd命令改了密码😅。\n1 2 3 4 5 6 7 8 # 例:将可执行程序复制到开发板 $ scp ./led-pwm root@192.168.0.10:~/ # 使用 ssh 登录开发板 $ ssh root@192.168.0.10 # 为程序添加可执行权限(在开发板操作) ~ $ chmod +x ./led-pwm # 运行程序 ~ $ ./led-pwm /sys/class/leds/led1-pwm 实际运行效果 由于开发板的LED2默认是心跳模式[heartbeat],所以会一直一闪一闪的,在运行呼吸灯程序前,可以先把LED2改为[none]模式。\n1 ~ $ echo none \u0026gt; /sys/class/leds/led2/trigger 执行编写的呼吸灯程序\n1 ~ $ ./led-pwm /sys/class/leds/led1-pwm 实际运行效果如下:\n\u003c!DOCTYPE HTML\u003e ","permalink":"https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF2k500%E5%BC%80%E5%8F%91%E6%9D%BF%E4%B8%8A%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%91%BC%E5%90%B8%E7%81%AF%E6%95%88%E6%9E%9C/","summary":"前言 本来这篇文章应该在上周就写完的,不过突然被安排出差,一直忙到了现在,终于可以静下心来做些其它事情。\n之前和龙芯3A5000主机一起送过来的还有一块龙芯2K500的迷你开发板,整个板子不到巴掌大小。之前只是简单做了上电启动,这次拿到了比较完整的开发资料,可以尝试为开发板编写一些程序了。\n由于暂时没有屏幕,只能先试着做一些其它的事情,如通信和IO控制,其中最简单,最基础的就是LED灯的控制。然后我就发现,这个板子居然有一颗可以调节亮度的LED灯!没错,之前做的LED控制都只能进行开关操作,而可以调节亮度,意味着可以做出更多的显示效果,这次我就做了一个呼吸灯的效果。\n然后,我也简单了解了一下这种亮度调节的原理,实际上就是通过调节PWM输出的占空比,改变一个周期内输出的高低电平所占的比例,实现控制LED灯亮度的效果。由于引脚输出的电压是固定的,所以不能通过改变电平来控制亮度,而改变高低电平的占空比则是另一种思路,嵌入式设备的屏幕背光亮度调节也是基于同样的原理。\n开发板上电启动、连接串口终端 由于暂时没有屏幕,想要和开发板进行交互就只能通过终端的方式,通常开发板都会有调试串口,我们先通过串口终端登录设备,配置好网口IP地址后,再通过网络连接登录设备。\n串口的连接方式如下图:\n将绿、白、黑三色线以图中方式接好(红线不用接),USB端插入到电脑,应该不需要装驱动,电脑可以直接识别出串口设备。\n打开串口终端工具,比如Windows MobaXterm,linux minicom等,我比较喜欢用putty。\n配置好端口,设置\n波特率:115200\n数据位:8位\n停止位:1位\n校验:无\n硬件流控:无\n然后给开发板接通电源,就可以看到调试输出信息了。\n查看系统信息,可以看到运行的是安装了PREEMPT_RT补丁的实时操作系统。\n1 2 3 [root@LS-GD ~]# uname -a Linux LS-GD 5.10.0.lsgd-g434b00a6badf #1 PREEMPT Wed Sep 14 12:57:58 CST 2022 loongarch64 GNU/Linux [root@LS-GD ~]# 配置交叉编译环境 2K500开发板是loongarch64架构的嵌入式板卡。下载好对应的交叉编译工具链后,解压到系统/opt/目录下。按手册来就好~\n1 $ sudo tar -xf toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18.tar.xz -C /opt/ 然后我们需要将交叉编译器添加到系统路径,方便我们接下来使用。\n这里我直接将配置写成脚本,方便下次使用。\n1 2 3 4 5 6 7 8 $ cd /opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 # 创建脚本 $ sudo touch environment-setup-loongarch64-linux-gnu # 添加可执行权限 $ sudo chmod +x environment-setup-loongarch64-linux-gnu # 修改脚本内容 $ sudo vi environment-setup-loongarch64-linux-gnu # 内容如下~ 1 2 3 4 5 6 7 8 # environment-setup-loongarch64-linux-gnu CC_PREFIX=/opt/toolchain-loongarch64-linux-gnu-gcc8-host-x86_64-2022-07-18 export PATH=$CC_PREFIX/bin:$PATH export LD_LIBRARY_PATH=$CC_PREFIX/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=$CC_PREFIX/loongarch64-linux-gnu/lib64:$LD_LIBRARY_PATH export ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu- 接下来测试一下交叉编译环境","title":"龙芯2K500开发板上实现的呼吸灯效果"},{"content":"使用weston-screenshooter\n但必须启用weston桌面--debug选项,否则会出现以下错误:\n1 2 3 [root@RK356X:/]# weston-screenshooter [02:41:05.145] libwayland: error in client communication (pid 776) weston_screenshooter@5: error 0: screenshooter failed: permission denied. Debug protocol must be enabled 以RK3568开发板,buildroot系统为例,修改/etc/init.d/S50launcher,找到weston所在行,添加--debug选项。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ...... # Uncomment to disable mirror mode # unset WESTON_DRM_MIRROR export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/var/run} export QT_QPA_PLATFORM=${QT_QPA_PLATFORM:-wayland} weston --tty=2 --debug --idle-time=0\u0026amp; { # Wait for weston ready while [ ! -e ${XDG_RUNTIME_DIR}/wayland-0 ]; do sleep .1 done /usr/bin/QLauncher \u0026amp; }\u0026amp; ...... forlinx开发板使用的yocto系统也类似,修改/lib/systemd/system/weston.service,在weston后添加--debug选项。\n1 2 3 $ vi /lib/systemd/system/weston.service # 修改如下 # ExecStart=/usr/bin/weston --debug --log=${XDG_RUNTIME_DIR}/weston.log $OPTARGS 然后重启系统,之后就可以使用weston-screenshooter截取屏幕了。\n链接\nwayland-project/weston weston.ini配置文件 ","permalink":"https://kira-96.github.io/notes/weston-screenshot/","summary":"使用weston-screenshooter\n但必须启用weston桌面--debug选项,否则会出现以下错误:\n1 2 3 [root@RK356X:/]# weston-screenshooter [02:41:05.145] libwayland: error in client communication (pid 776) weston_screenshooter@5: error 0: screenshooter failed: permission denied. Debug protocol must be enabled 以RK3568开发板,buildroot系统为例,修改/etc/init.d/S50launcher,找到weston所在行,添加--debug选项。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ...... # Uncomment to disable mirror mode # unset WESTON_DRM_MIRROR export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/var/run} export QT_QPA_PLATFORM=${QT_QPA_PLATFORM:-wayland} weston --tty=2 --debug --idle-time=0\u0026amp; { # Wait for weston ready while [ !","title":"weston桌面系统截屏方法"},{"content":"前言 之前尝试过在龙芯3A4000上编译运行EPICS,由于3A4000还是mips64指令集,而3A5000则是龙芯的自主指令集loongarch64,适配起来步骤也会有所不同。\n这次使用的是龙博特龙芯3A5000电脑主机。\n虽然EPICS官方并没有适配loongarch和mips64,无法做到开箱即用,但只要有gcc、g++、make、perl这些工具,理论上就能编译运行EPICS,在开始编译前,确保你的设备上已经装好了这些工具。\n关于如何称呼「龙架构」,龙芯社区也有一些讨论。最初我直接使用loongarch64,后来也使用过la64作为简写,直到我看到如何称呼龙架构?,我觉得有必要和社区保持一致,后续统一使用 loong64 作为架构标识。\n下载 base 这里我们就以目前最新版本7.0.7为例,其它版本的Base也类似。\n1 2 3 $ cd ~/下载/ $ wget https://epics.anl.gov/download/base/base-7.0.7.tar.gz $ tar -xzvf base-7.0.7.tar.gz 你可以在你觉得合适的位置编译安装Base,这里按我们的习惯,放在/usr/local/epics目录下。\n1 2 $ mkdir /usr/local/epics $ mv base-7.0.7 /usr/local/epics/ 编译 按照一般步骤,现在就可以开始编译了,我们可以先尝试一下,看看是什么结果。\n1 2 3 $ cd /usr/local/epics/base-7.0.7/ # 执行 `make` 命令 $ make 不出所料,果然失败了,输出的错误和在3A4000上编译时的错误也有一些不同。\n下面是在3A4000上编译时输出的错误:\n下面一行报错是差不多的,在loongarch64上编译却多了上面一行报错,意思就是没有识别出loongarch64架构。\n但是先不要慌,这里同时也给出了报错的位置,让我们看看EpicsHostArch.pl里写了些什么。\n1 $ vi ./src/tools/EpicsHostArch.pl 它其实就是一个perl脚本,用来判断当前的系统和cpu架构,而loongarch64显然没有做适配,所以就出现了上面错误。\n\u0026ldquo;Architecture \u0026rsquo;loongarch64-linux-gnu-thread-multi\u0026rsquo; not recognized\u0026rdquo;\n既然识别不了loongarch64,那我们就手动添加一行,让它可以识别就行了,即使看不太懂上面的脚本也没关系,看个半懂就行了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 sub HostArch { my $arch = $Config{archname}; for ($arch) { return \u0026#39;linux-x86_64\u0026#39; if m/^x86_64-linux/; return \u0026#39;linux-x86\u0026#39; if m/^i[3-6]86-linux/; return \u0026#39;linux-arm\u0026#39; if m/^arm-linux/; return \u0026#39;linux-aarch64\u0026#39; if m/^aarch64-linux/; return \u0026#39;linux-ppc64\u0026#39; if m/^powerpc64-linux/; return \u0026#39;linux-loong64\u0026#39; if m/^loongarch64-linux/; return \u0026#39;windows-x64\u0026#39; if m/^MSWin32-x64/; return \u0026#39;win32-x86\u0026#39; if m/^MSWin32-x86/; return \u0026#34;cygwin-x86_64\u0026#34; if m/^x86_64-cygwin/; return \u0026#34;cygwin-x86\u0026#34; if m/^i[3-6]86-cygwin/; return \u0026#39;solaris-sparc\u0026#39; if m/^sun4-solaris/; return \u0026#39;solaris-x86\u0026#39; if m/^i86pc-solaris/; my ($kernel, $hostname, $release, $version, $cpu) = uname; if (m/^darwin/) { for ($cpu) { return \u0026#39;darwin-x86\u0026#39; if m/^x86_64/; return \u0026#39;darwin-aarch64\u0026#39; if m/^arm64/; } die \u0026#34;$0: macOS CPU type \u0026#39;$cpu\u0026#39; not recognized\\n\u0026#34;; } die \u0026#34;$0: Architecture \u0026#39;$arch\u0026#39; not recognized\\n\u0026#34;; } } 我们在上面位置添加一行内容,来让它可以识别loongarch64架构。\n1 2 3 4 return \u0026#39;linux-aarch64\u0026#39; if m/^aarch64-linux/; return \u0026#39;linux-ppc64\u0026#39; if m/^powerpc64-linux/; + return \u0026#39;linux-loong64\u0026#39; if m/^loongarch64-linux/; return \u0026#39;windows-x64\u0026#39; if m/^MSWin32-x64/; 此时我们再执行一下make命令。\n可以看到,现在已经可以识别出loongarch64-linux了,报错和在3A4000上编译时也基本一样了。\n以下步骤同样适用于在3A4000(mips64)上编译EPICS,只需要将loong64全部替换为mips64\n剩下的报错就是,没有找到对应的编译配置项,我们同样可以仿照已经做了适配的架构来改写,直接按照下面步骤来就可以了。\n添加 CONFIG.Common.linux-loong64 1 2 3 4 $ cd configure/os/ # 添加 CONFIG.Common.linux-loong64 $ cp CONFIG.Common.linux-aarch64 CONFIG.Common.linux-loong64 $ vi CONFIG.Common.linux-loong64 修改成如下内容:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # CONFIG.Common.linux-loong64 # # Definitions for linux-loong64 target builds # Override these settings in CONFIG_SITE.Common.linux-loong64 #------------------------------------------------------- # Include definitions common to all Linux targets include $(CONFIG)/os/CONFIG.Common.linuxCommon ARCH_CLASS = loongarch ARCH_DEP_CFLAGS = $(GNU_ARCH_CFLAGS) $(GNU_TUNE_CFLAGS) ARCH_DEP_CFLAGS += $(GNU_DEP_CFLAGS) 添加 CONFIG.linux-loong64.Common 1 2 3 # 添加 CONFIG.linux-loong64.Common $ cp CONFIG.linux-aarch64.Common CONFIG.linux-loong64.Common $ vi CONFIG.linux-loong64.Common 修改成如下内容(内容没有变化,可以不修改):\n1 2 3 4 5 6 7 8 # CONFIG.linux-loong64.Common # # Definitions for linux-loong64 host builds # Sites may override these definitions in CONFIG_SITE.linux-loong64.Common #------------------------------------------------------- # Include definitions common to unix hosts include $(CONFIG)/os/CONFIG.UnixCommon.Common 添加 CONFIG.linux-loong64.linux-loong64 1 2 3 # 添加 CONFIG.linux-loong64.linux-loong64 $ cp CONFIG.linux-aarch64.linux-aarch64 CONFIG.linux-loong64.linux-loong64 $ vi CONFIG.linux-loong64.linux-loong64 修改成如下内容(内容没有变化,可以不修改):\n1 2 3 4 5 6 7 8 # CONFIG.linux-loong64.linux-loong64 # # Definitions for native linux-loong64 builds # Override these definitions in CONFIG_SITE.linux-loong64.linux-loong64 #------------------------------------------------------- # Include common gnu compiler definitions include $(CONFIG)/CONFIG.gnuCommon 添加 CONFIG_SITE.Common.linux-loong64 1 2 3 # 添加 CONFIG_SITE.Common.linux-loong64 $ cp CONFIG_SITE.Common.linux-aarch64 CONFIG_SITE.Common.linux-loong64 $ vi CONFIG_SITE.Common.linux-loong64 修改成如下内容。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # CONFIG_SITE.Common.linux-loong64 # # Site Specific definitions for all linux-loong64 targets #------------------------------------------------------- # NOTE for SHARED_LIBRARIES: In most cases if this is set to YES the # shared libraries will be found automatically. However if the .so # files are installed at a different path to their compile-time path # then in order to be found at runtime do one of these: # a) LD_LIBRARY_PATH must include the full absolute pathname to # $(INSTALL_LOCATION)/lib/$(EPICS_HOST_ARCH) when invoking base # executables. # b) Add the runtime path to SHRLIB_DEPLIB_DIRS and PROD_DEPLIB_DIRS, which # will add the named directory to the list contained in the executables. # c) Add the runtime path to /etc/ld.so.conf and run ldconfig # to inform the system of the shared library location. # Depending on your version of Linux you\u0026#39;ll want one of the following # lines to enable command-line editing and history in iocsh. If you\u0026#39;re # not sure which, start with the top one and work downwards until the # build doesn\u0026#39;t fail to link the readline library. If none of them work, # comment them all out to build without readline support. # No other libraries needed (recent Fedora, Ubuntu etc.): #COMMANDLINE_LIBRARY = READLINE # Needs -lncurses (RHEL 5 etc.): #COMMANDLINE_LIBRARY = READLINE_NCURSES # Needs -lcurses (older versions) #COMMANDLINE_LIBRARY = READLINE_CURSES # Readline is broken or you don\u0026#39;t want use it: #COMMANDLINE_LIBRARY = EPICS # WARNING: Variables that are set in $(CONFIG)/CONFIG.gnuCommon cannot be # overridden in this file for native builds, e.g. variables such as # OPT_CFLAGS_YES, WARN_CFLAGS, SHRLIB_LDFLAGS # They must be set in CONFIG_SITE.linux-loong64.linux-loong64 or for # cross-builds in CONFIG_SITE.\u0026lt;host-arch\u0026gt;.linux-loong64 instead. # Tune GNU compiler output for a specific cpu-type # (e.g. loongarch64, la264, la364, la464 etc.) GNU_ARCH_CFLAGS = -march=loongarch64 GNU_TUNE_CFLAGS = -mtune=loongarch64 # Enable soft-float feature (e.g. none, 32, 64) GNU_DEP_CFLAGS = -mfpu=none 添加 CONFIG_SITE.linux-loong64.linux-loong64 1 2 3 # 添加 CONFIG_SITE.linux-loong64.linux-loong64 $ cp CONFIG_SITE.linux-aarch64.linux-aarch64 CONFIG_SITE.linux-loong64.linux-loong64 $ vi CONFIG_SITE.linux-loong64.linux-loong64 修改成如下内容:\n1 2 3 4 5 6 7 8 9 10 11 # CONFIG_SITE.linux-loong64.linux-loong64 # # Site specific definitions for native linux-loong64 builds #------------------------------------------------------- # It makes sense to include debugging symbols even in optimized builds # in case you want to attach gdb to the process or examine a core-dump. # This does cost disk space, but not memory as debug symbols are not # loaded into RAM when the binary is loaded. #OPT_CFLAGS_YES += -g #OPT_CXXFLAGS_YES += -g 这里是对编译器的优化选项,暂时不知道怎么改,我就直接注释掉了。\n重新编译 到这里就全部改好了,其实最主要修改的就是第一个文件。下面就可以尝试编译了,这次应该没问题了。\n1 2 3 $ cd /usr/local/epics/base-7.0.7/ # 执行 `make` 命令 $ make -j8 接下来就静静等待编译完成。\n编译完后查看编译输出目录bin/linux-loong64/(截图比较旧)。\n添加到PATH 为了方便以后使用,我们将编译输出的可执行文件目录添加到PATH。\n1 2 3 4 5 $ cd ~ $ mkdir .epics $ cd .epics/ $ touch env $ vi env 编辑 env 如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/sh # EPICS base shell setup export EPICS_BASE=\u0026#34;/usr/local/epics/base-7.0.7\u0026#34; export EPICS_HOST_ARCH=linux-loong64 # affix colons on either side of $PATH to simplify matching case \u0026#34;:${PATH}:\u0026#34; in *:\u0026#34;${EPICS_BASE}/bin/${EPICS_HOST_ARCH}\u0026#34;:*) ;; *) # Prepending path in case a system-installed epics needs to be overridden export PATH=\u0026#34;${EPICS_BASE}/bin/${EPICS_HOST_ARCH}:$PATH\u0026#34; ;; esac 然后修改 .bashrc\n1 2 $ cd ~ $ vi .bashrc 在文件最后添加一行\n1 . \u0026#34;$HOME/.epics/env\u0026#34; 执行下面命令,使添加PATH生效\n1 $ . .bashrc 运行EPICS IOC 做完上面的步骤,EPICS base的配置就完成了,我们来尝试运行一下。\n在终端执行softIoc。\n运行正常,大功告成!\n只要base可以成功运行,其它的一些模块应该也没有问题,后续我会继续尝试在龙芯上安装EPICS其它的模块。\n链接 EPICS - Experimental Physics and Industrial Control System (anl.gov) epics-base - (launchpad.net) / epics-base/epics-base / EPICS Base (anl.gov) ","permalink":"https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF3a5000loongarch64%E4%B8%8A%E7%BC%96%E8%AF%91%E8%BF%90%E8%A1%8Cepics/","summary":"前言 之前尝试过在龙芯3A4000上编译运行EPICS,由于3A4000还是mips64指令集,而3A5000则是龙芯的自主指令集loongarch64,适配起来步骤也会有所不同。\n这次使用的是龙博特龙芯3A5000电脑主机。\n虽然EPICS官方并没有适配loongarch和mips64,无法做到开箱即用,但只要有gcc、g++、make、perl这些工具,理论上就能编译运行EPICS,在开始编译前,确保你的设备上已经装好了这些工具。\n关于如何称呼「龙架构」,龙芯社区也有一些讨论。最初我直接使用loongarch64,后来也使用过la64作为简写,直到我看到如何称呼龙架构?,我觉得有必要和社区保持一致,后续统一使用 loong64 作为架构标识。\n下载 base 这里我们就以目前最新版本7.0.7为例,其它版本的Base也类似。\n1 2 3 $ cd ~/下载/ $ wget https://epics.anl.gov/download/base/base-7.0.7.tar.gz $ tar -xzvf base-7.0.7.tar.gz 你可以在你觉得合适的位置编译安装Base,这里按我们的习惯,放在/usr/local/epics目录下。\n1 2 $ mkdir /usr/local/epics $ mv base-7.0.7 /usr/local/epics/ 编译 按照一般步骤,现在就可以开始编译了,我们可以先尝试一下,看看是什么结果。\n1 2 3 $ cd /usr/local/epics/base-7.0.7/ # 执行 `make` 命令 $ make 不出所料,果然失败了,输出的错误和在3A4000上编译时的错误也有一些不同。\n下面是在3A4000上编译时输出的错误:\n下面一行报错是差不多的,在loongarch64上编译却多了上面一行报错,意思就是没有识别出loongarch64架构。\n但是先不要慌,这里同时也给出了报错的位置,让我们看看EpicsHostArch.pl里写了些什么。\n1 $ vi ./src/tools/EpicsHostArch.pl 它其实就是一个perl脚本,用来判断当前的系统和cpu架构,而loongarch64显然没有做适配,所以就出现了上面错误。\n\u0026ldquo;Architecture \u0026rsquo;loongarch64-linux-gnu-thread-multi\u0026rsquo; not recognized\u0026rdquo;\n既然识别不了loongarch64,那我们就手动添加一行,让它可以识别就行了,即使看不太懂上面的脚本也没关系,看个半懂就行了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 sub HostArch { my $arch = $Config{archname}; for ($arch) { return \u0026#39;linux-x86_64\u0026#39; if m/^x86_64-linux/; return \u0026#39;linux-x86\u0026#39; if m/^i[3-6]86-linux/; return \u0026#39;linux-arm\u0026#39; if m/^arm-linux/; return \u0026#39;linux-aarch64\u0026#39; if m/^aarch64-linux/; return \u0026#39;linux-ppc64\u0026#39; if m/^powerpc64-linux/; return \u0026#39;linux-loong64\u0026#39; if m/^loongarch64-linux/; return \u0026#39;windows-x64\u0026#39; if m/^MSWin32-x64/; return \u0026#39;win32-x86\u0026#39; if m/^MSWin32-x86/; return \u0026#34;cygwin-x86_64\u0026#34; if m/^x86_64-cygwin/; return \u0026#34;cygwin-x86\u0026#34; if m/^i[3-6]86-cygwin/; return \u0026#39;solaris-sparc\u0026#39; if m/^sun4-solaris/; return \u0026#39;solaris-x86\u0026#39; if m/^i86pc-solaris/; my ($kernel, $hostname, $release, $version, $cpu) = uname; if (m/^darwin/) { for ($cpu) { return \u0026#39;darwin-x86\u0026#39; if m/^x86_64/; return \u0026#39;darwin-aarch64\u0026#39; if m/^arm64/; } die \u0026#34;$0: macOS CPU type \u0026#39;$cpu\u0026#39; not recognized\\n\u0026#34;; } die \u0026#34;$0: Architecture \u0026#39;$arch\u0026#39; not recognized\\n\u0026#34;; } } 我们在上面位置添加一行内容,来让它可以识别loongarch64架构。","title":"龙芯3A5000(LoongArch64)上编译运行EPICS"},{"content":"前言 由于换了新的工作,我的工作方向也有了很大的变化,之前基本上是单纯的写代码,现在则经常需要和硬件设备交互,开发平台也转到了Linux+Qt。硬件设备的控制,其中最基本的就是LED灯以及一些开关继电器的操作,其本质就是GPIO的操作。考虑到系统的精简和成本控制,最好是可以直接通过Linux系统去控制,当然也有其它替代方案,比如使用支持Modbus协议的IO模块。关于Modbus的使用,后面有空再讲,这里就记录一下最简单的Linux系统下的GPIO控制,用户空间下的GPIO文件系统接口。\n在此之前,有必要再了解一下GPIO的概念。\n“通用输入/输出”(GPIO)是一种灵活的软件控制数字信号。它们由多种芯片提供,对于使用嵌入式和定制硬件的Linux开发人员来说很熟悉。每个GPIO代表一个连接到特定引脚的位,即球栅阵列(BGA)封装上的“球”。电路板示意图显示了哪些外部硬件连接到哪些GPIO。驱动程序可以通用地编写,以便板设置代码将这样的引脚配置数据传递给驱动程序。\nA “General Purpose Input/Output” (GPIO) is a flexible software-controlled digital signal. They are provided from many kinds of chip, and are familiar to Linux developers working with embedded and custom hardware. Each GPIO represents a bit connected to a particular pin, or “ball” on Ball Grid Array (BGA) packages. Board schematics show which external hardware connects to which GPIOs. Drivers can be written generically, so that board setup code passes such pin configuration data to drivers.\n在单片机上,我们可以很方便的控制GPIO,但在嵌入式Linux上则不一样,通常GPIO对于用户来说是不可见的,不过Linux系统也提供了相应的接口供用户控制GPIO,每个非专用的引脚都可以用作GPIO。\n配置IO多路复用器(IOMUXC) 将需要复用的IO添加到pinctrl_hog节点,例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* kernel/arch/arm64/boot/dts/freescale/OK8MP-C.dts */ \u0026amp;iomuxc { pinctrl-names = \u0026#34;default\u0026#34;; pinctrl-0 = \u0026lt;\u0026amp;pinctrl_hog\u0026gt;; pinctrl_hog: hoggrp { fsl,pins = \u0026lt; MX8MP_IOMUXC_HDMI_DDC_SCL__HDMIMIX_HDMI_SCL\t0x400001c3 MX8MP_IOMUXC_HDMI_DDC_SDA__HDMIMIX_HDMI_SDA\t0x400001c3 MX8MP_IOMUXC_HDMI_HPD__HDMIMIX_HDMI_HPD\t0x40000019 MX8MP_IOMUXC_HDMI_CEC__HDMIMIX_HDMI_CEC\t0x40000019 /* GPIO */ MX8MP_IOMUXC_GPIO1_IO07__GPIO1_IO07\t0x159 MX8MP_IOMUXC_GPIO1_IO09__GPIO1_IO09\t0x159 MX8MP_IOMUXC_GPIO1_IO12__GPIO1_IO12\t0x159 MX8MP_IOMUXC_ECSPI2_MOSI__GPIO5_IO11\t0x159 MX8MP_IOMUXC_ECSPI2_MISO__GPIO5_IO12\t0x159 MX8MP_IOMUXC_ECSPI2_SS0__GPIO5_IO13\t0x159 \u0026gt;; }; } 具体的GPIO名字要参照xxxx-pinfunc.h里面的定义,配置为GPIO时,一定要使用IOMUXC_xxxx_xxxx__GPIOn_IOxx的宏定义。\n然后是后面的上下拉配置,具体的计算方法和参数意义如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PAD_CTL_HYS (1 \u0026lt;\u0026lt; 16) /* Hysteresis 滞后使能*/ PAD_CTL_PUS_100K_DOWN (0 \u0026lt;\u0026lt; 14) /* 100KOhm Pull Down */ PAD_CTL_PUS_47K_UP (1 \u0026lt;\u0026lt; 14) /* 47KOhm Pull Up */ PAD_CTL_PUS_100K_UP (2 \u0026lt;\u0026lt; 14) /* 100KOhm Pull Up */ PAD_CTL_PUS_22K_UP (3 \u0026lt;\u0026lt; 14) /* 22KOhm Pull Up */ PAD_CTL_PUE (1 \u0026lt;\u0026lt; 13) /* Pull / Keep Enable */ PAD_CTL_PKE (1 \u0026lt;\u0026lt; 12) /* Pull / Keep Select 0: Keeper 1: Pull */ PAD_CTL_ODE (1 \u0026lt;\u0026lt; 11) /* Open Drain Enable 漏极开路 */ PAD_CTL_SPEED_LOW (1 \u0026lt;\u0026lt; 6) /* 带宽配置 */ PAD_CTL_SPEED_MED (2 \u0026lt;\u0026lt; 6) PAD_CTL_SPEED_HIGH (3 \u0026lt;\u0026lt; 6) PAD_CTL_DSE_DISABLE (0 \u0026lt;\u0026lt; 3) /* Drive Strength Field 驱动能力 */ PAD_CTL_DSE_240ohm (1 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_120ohm (2 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_80ohm (3 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_60ohm (4 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_48ohm (5 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_40ohm (6 \u0026lt;\u0026lt; 3) PAD_CTL_DSE_34ohm (7 \u0026lt;\u0026lt; 3) PAD_CTL_SRE_FAST (1 \u0026lt;\u0026lt; 0) /* Slew Rate Field 压摆率 */ PAD_CTL_SRE_SLOW (0 \u0026lt;\u0026lt; 0) 注意:不要直接设置为0,没有任何作用。可以使用0x80000000,它表示“我不知道,保持默认值”。\n用户空间下的GPIO读写操作 用户空间下的GPIO文件系统接口在/sys/class/gpio/目录下。\n注意:以下操作都需要root权限!\n使能GPIO 在此之前,需要先知道GPIO对应的编号数值,计算方法如下:\nGPIOn_IOx = (n - 1) × 32 + x\n例:GPIO5_IO13 = (5 - 1) × 32 + 13 = 141\n执行命令\n1 2 3 # echo N \u0026gt; /sys/class/gpio/export # N为GPIO对应的编号,例: echo 141 \u0026gt; /sys/class/gpio/export 如果需要取消使能GPIO,则执行命令\n1 2 3 # echo N \u0026gt; /sys/class/gpio/unexport # N为GPIO对应的编号,例: echo 141 \u0026gt; /sys/class/gpio/unexport GPIO配置 使能GPIO之后,/sys/class/gpio/目录下就多出来了相应的GPIO节点目录。例:gpio141\nGPIO节点有以下属性可以配置:\n/sys/class/gpio/gpioN/\ndirection\n读取为in或者out。通常可以写入此值。写入out默认输出为低。为了确保操作无误,可以写入值“low”和“high”将GPIO配置为具有该初始值的输出。\n请注意,如果内核不支持更改GPIO的方向,或者该属性是由内核代码导出的,而内核代码没有明确允许用户空间重新配置该GPIO方向,则该属性将不存在。\nvalue\n读取为0(low)或1(high)。如果GPIO被配置为输出,则可以写入该值;任何非零值都被视为高。\n如果引脚可以配置为中断生成,并且它已经配置为生成中断(请参阅“edge”的描述),那么您可以对该文件进行轮询(poll),每当触发中断时,轮询(poll)将返回。如果使用poll,请设置事件为POLLPRI和POLLERR。如果使用select,则将文件描述符设置为exceptfds。轮询返回后,要么lseek到sysfs文件的开头并读取新值,要么关闭文件并重新打开以读取值。\nedge\n读取为none、rising、falling或者both。编写这些字符串以选择将对“value”文件返回进行轮询的信号边缘。\n仅当引脚可以配置为中断生成输入引脚时,此属性才存在。\nactive_low\n读取为0(假)或1(真)。写入任何非零值以反转读取和写入的值属性。现有和后续轮询支持通过边缘属性配置“rising”和“falling”边缘将遵循此设置。\n例:\n将GPIO配置为输出\n1 echo out \u0026gt; /sys/class/gpio/gpio{N}/direction 将GPIO配置为输入,上升沿触发中断\n1 2 echo in \u0026gt; /sys/class/gpio/gpio{N}/direction echo rising \u0026gt; /sys/class/gpio/gpio{N}/edge GPIO读写 GPIO配置为输入(in)时,只能读取输入值,不能写入\nGPIO配置为输出(out)时,可以读取当前值和写入新值\n1 2 3 4 5 # 读取时 cat /sys/class/gpio/gpio{N}/value # 写入时 echo 0 \u0026gt; /sys/class/gpio/gpio{N}/value echo 1 \u0026gt; /sys/class/gpio/gpio{N}/value 程序控制GPIO 示例读取:\n1 2 3 4 5 6 7 8 9 10 11 int readGpio(unsigned short io) { QFile file(QString(\u0026#34;/sys/class/gpio/gpio%1/value\u0026#34;).arg(io)); if (!file.open(QIODevice::ReadOnly)) return 0; QByteArray ba = file.readAll(); file.close(); return QString(ba).toInt(); } 示例写入:\n1 2 3 4 5 6 bool writeGpio(unsigned short io, unsigned char value) { char buf[128]; sprintf(buf, \u0026#34;echo %d \u0026gt; /sys/class/gpio/gpio%d/value\u0026#34;, value, io); return ::system(buf) == 0; } 参考\nLegacy GPIO Interfaces GPIO Sysfs Interface for Userspace Definitive GPIO guide ","permalink":"https://kira-96.github.io/posts/linux-gpio%E6%93%8D%E4%BD%9C%E5%85%B6%E4%B8%80/","summary":"前言 由于换了新的工作,我的工作方向也有了很大的变化,之前基本上是单纯的写代码,现在则经常需要和硬件设备交互,开发平台也转到了Linux+Qt。硬件设备的控制,其中最基本的就是LED灯以及一些开关继电器的操作,其本质就是GPIO的操作。考虑到系统的精简和成本控制,最好是可以直接通过Linux系统去控制,当然也有其它替代方案,比如使用支持Modbus协议的IO模块。关于Modbus的使用,后面有空再讲,这里就记录一下最简单的Linux系统下的GPIO控制,用户空间下的GPIO文件系统接口。\n在此之前,有必要再了解一下GPIO的概念。\n“通用输入/输出”(GPIO)是一种灵活的软件控制数字信号。它们由多种芯片提供,对于使用嵌入式和定制硬件的Linux开发人员来说很熟悉。每个GPIO代表一个连接到特定引脚的位,即球栅阵列(BGA)封装上的“球”。电路板示意图显示了哪些外部硬件连接到哪些GPIO。驱动程序可以通用地编写,以便板设置代码将这样的引脚配置数据传递给驱动程序。\nA “General Purpose Input/Output” (GPIO) is a flexible software-controlled digital signal. They are provided from many kinds of chip, and are familiar to Linux developers working with embedded and custom hardware. Each GPIO represents a bit connected to a particular pin, or “ball” on Ball Grid Array (BGA) packages. Board schematics show which external hardware connects to which GPIOs. Drivers can be written generically, so that board setup code passes such pin configuration data to drivers.","title":"Linux GPIO 操作其一"},{"content":"问题描述 在使用 WSL 更新软件包的时候经常会遇到这样一个报错\n1 /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link 意思是说 /usr/lib/wsl/lib/libcuda.so.1 不是一个符号链接。\n问题分析 通过名字可以判断这应该是nVidia显卡驱动相关的库,进入 /usr/lib/wsl/lib/ 目录,可以看到有 libcuda.so、libcuda.so.1、libcuda.so.1.1 三个文件,都是文件形式,而通过报错我们知道 libcuda.so、libcuda.so.1 应该是符号链接文件。\n它们关系应该是:\nlibcuda.so -\u0026gt; libcuda.so.1 -\u0026gt; libcuda.so.1.1\n知道原因就好解决了,把 libcuda.so、libcuda.so.1 删掉,再重新创建符号链接就可以了。\n1 2 ubuntu@dell:/usr/lib/wsl/lib$ sudo rm libcuda.so rm: 无法删除 \u0026#39;libcuda.so\u0026#39;: 只读文件系统 很遗憾,这样是不行的。最后经过多方查找,终于找到了解决方案。\n解决方法 解决方法就是上面的方法,但不是在 WSL 中操作。\n使用管理员权限执行 cmd 命令:\n1 2 3 4 5 C:\u0026gt;cd C:\\Windows\\System32\\lxss\\lib C:\\Windows\\System32\\lxss\\lib\u0026gt;del /s /q \u0026#34;libcuda.so\u0026#34; C:\\Windows\\System32\\lxss\\lib\u0026gt;del /s /q \u0026#34;libcuda.so.1\u0026#34; C:\\Windows\\System32\\lxss\\lib\u0026gt;mklink libcuda.so.1 libcuda.so.1.1 C:\\Windows\\System32\\lxss\\lib\u0026gt;mklink libcuda.so libcuda.so.1 或者在Powershell中执行:\n1 2 3 4 5 6 cd C:\\Windows\\System32\\lxss\\lib rm libcuda.so rm libcuda.so.1 wsl -e /bin/bash ln -s libcuda.so.1.1 libcuda.so.1 ln -s libcuda.so.1.1 libcuda.so 然后在 wsl 中执行:\n1 $ sudo ldconfig 参考\nldconfig: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link libcuda.so.1 is not a symbolic link #5548 ","permalink":"https://kira-96.github.io/posts/wsl-libcuda.so.1-is-not-a-symbolic-link-%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/","summary":"问题描述 在使用 WSL 更新软件包的时候经常会遇到这样一个报错 1 /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link 意思是说 /usr/lib/wsl/lib/libcuda.so.1 不是一个符号链接。 问题分析 通过名字可以判断这应该是nVidi","title":"WSL libcuda.so.1 is not a symbolic link 的解决方法"},{"content":"前言 由于这半年来一直在做嵌入式Linux系统软件开发工作,所以经常和嵌入式设备打交道,最早接触的嵌入式Linux应该就是树莓派了,而我的树莓派一般也不接屏幕,基本上都使用VNC远程连接,所以就想着能不能把VNC也移植到嵌入式设备上,最后找到了x11vnc。\nVNC(虚拟网络计算)是一种非常有用的网络图形协议(应用程序在一台计算机上运行,但在另一台计算机上显示其窗口),但与X不同,查看端非常简单,不保持任何状态。它是一种远程帧缓冲区(RFB)协议。\nx11vnc允许用户通过任何VNC viewer远程查看并与real X显示器(即与物理监视器、键盘和鼠标相对应的显示器)交互。\n准备工作 需要先用git克隆下面两个仓库,libvncserver和x11vnc。 x11vnc是基于libvncserver的服务端程序。\nGitHub - LibVNC/libvncserver: LibVNCServer/LibVNCClient are cross-platform C libraries that allow you to easily implement VNC server or client functionality in your program. GitHub - LibVNC/x11vnc: a VNC server for real X displays 编译 libvncserver 编译x11vnc需要libvncserver,libvncserver按照 README 编译即可\n1 2 3 4 mkdir build cd build cmake .. cmake --build . 编译完成后将build文件夹下生成的.so文件复制到 sysroot\n例:\n1 2 3 4 5 6 7 # 复制 .so 文件 $ sudo cp ./libvncclient.so* /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/lib/ # 复制 pkgconfig 文件 $ sudo cp ./libvnc*.pc /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/lib/pkgconfig/ # 复制头文件 $ sudo cp -r ../rfb /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/include/ $ sudo cp ./rfb/rfbconfig.h /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/include/rfb/ 编译 x11vnc x11vnc 需要使用 autoconf 和 automake 生成 configure 和 makefile\n1 2 3 4 5 6 7 8 9 10 11 12 13 $ cd x11vnc-master # 使用aclocal工具生成aclocal.m4 $ aclocal # 使用autoconf工具生成configure文件 $ autoconf # 使用autoheader工具生成config.h.in文件 $ autoheader # 使用automake生成Makefile.in文件 $ automake --add-missing # configure 配置交叉编译,如果libvncserver没有正确编译安装,这里会提示找不到libvncserver $ ./configure CC=arm-poky-linux-gnueabi-gcc AR=arm-poky-linux-gnueabi-ar AS=arm-poky-linux-gnueabi-as LD=arm-poky-linux-gnueabi-ld --host=arm-poky-linux --prefix=/home/ubuntu/ CFLAGS=\u0026#34;-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7 --sysroot=/opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi\u0026#34; # 编译安装 $ make install 安装完成后在 /home/ubuntu/ (configure 步骤设置的 \u0026ndash;prefix)目录下出现了bin和 share两个目录,bin目录下的就是 x11vnc 的可执行文件。\n安装运行 将 libvncserver 编译得到的 *.so 文件和 x11vnc 可执行文件复制到开发板即可。\n1 2 3 4 # 复制可执行文件 $ scp -r /home/ubuntu/bin root@192.168.10.7:~/ # 复制 libvncserver $ scp /opt/fsl-imx-x11/4.1.15-2.0.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/lib/libvnc*.so.0.9.13 root@192.168.10.7:/usr/lib/ ssh登录开发板\n1 2 3 4 5 6 7 8 9 10 11 12 # 创建符号链接 root@imx6ulevk:~# ln -s /usr/lib/libvncclient.so.0.9.13 /usr/lib/libvncclient.so.1 root@imx6ulevk:~# ln -s /usr/lib/libvncclient.so.1 /usr/lib/libvncclient.so root@imx6ulevk:~# ln -s /usr/lib/libvncserver.so.0.9.13 /usr/lib/libvncserver.so.1 root@imx6ulevk:~# ln -s /usr/lib/libvncserver.so.1 /usr/lib/libvncserver.so # 重命名文件夹 root@imx6ulevk:~# mv ~/bin ~/x11vnc root@imx6ulevk:~# cd ~/x11vnc root@imx6ulevk:~# chmod +x ./x11vnc # 运行 x11vnc # x11vnc -display :0 root@imx6ulevk:~# ./x11vnc 然后在电脑上启动 VNC Viewer, 输入开发板的ip地址就可以通过远程桌面访问设备了。\n实际运行效果如图\n参考\nautoconf / automake工具使用介绍 给imx6嵌入式平台移植x11vnc搭建远程控制环境 ","permalink":"https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E9%80%82%E7%94%A8%E4%BA%8E-imx6ul-%E7%9A%84-x11vnc/","summary":"前言 由于这半年来一直在做嵌入式Linux系统软件开发工作,所以经常和嵌入式设备打交道,最早接触的嵌入式Linux应该就是树莓派了,而我的树莓","title":"交叉编译适用于 iMX6UL 的 x11vnc"},{"content":"开始一份新工作 好久没有更新博客了,最近几个月都在忙新工作的事情,虽然只是短短3个月,但回想起来已经是很久之前了。从4月份面试,然后体检,提交各种材料证明,到5月份工作交接,6月份正式离职进入新单位,搬家等等,这几个月真是忙的有点喘不过气。即使到了新的单位,也没有闲下来的时间,来了之后就要上手做项目,到这周为止,项目总算初步完成,这才有时间来自我整理一下。即使5月份买了PS5游戏机也没怎么有时间玩,没什么游戏是一方面,忙也是一方面。\n为什么要换工作 其实这个问题我也一直在思考,包括在到了新单位之后,看到到手少得可怜的工资,我也在问自己,为什么要换工作?\n总的来说,上一家公司给我的印象还是挺不错的,大学毕业后,当时没有找到一份好的工作,是它接纳了我,这三年来,我也在公司学到了很多,和同事相处也还都不错,所以决定从那里离开,我也是下了很大的决心。\n最主要的原因还是我渴望做一些更有创造性的内容,而不是整天的修修补补,虽然修bug也是程序员的一项重要工作,但每天都面对着相同的内容,难免会让人心生厌倦。很多时候我都会想,自己是不是会一直这样待下去,在同一家公司一直工作,直到退休,这可能是个不错的选择,但我不想做一个整天闲着的人,性格使然,我这一生可能注定是忙碌的,我渴望接触和学习新的知识,而不是一直待在安乐窝里,所以我选择走出去,见识一下更广阔的天地。\n关于新工作 新工作在上海,一个研究所里,除了工资,其它都还可以,但出来打工,工资不才是最重要的吗?目前工作了刚好一个月,工资到手4k多,据说还是两个月的,这让我真的难以接受,要不是自己还有点存款,日子真是没法混了。\n以上是吐槽,6月份从上一家公司离职,休息了一天就到新单位报到,第二个星期就直接上手做项目,可以说是赶得很紧了。而关于工作的内容,目前是在做嵌入式Linux方向的软件,之前没有做过Linux平台的软件开发,一直都是Windows平台,但由于有了一些工作经验,所以上手其实很快,总之就是一边学习一边做项目,结合自己以前的开发经验,其实动手做起来并不难。中间也有遇到一些问题,因为是做嵌入式开发,所以难免会涉及到如何控制芯片管脚输出,通过管脚输出来控制外围设备,这些涉及到硬件领域的内容我其实并不在行,虽然大学时候学过单片机相关的课程,但在Linux系统中控制芯片和直接编程控制芯片引脚还是有很大的不同的,所以自己也在周末抽时间学习相关的内容。\n总的来说,这一个月来学到的新东西还是很多的,每天都忙碌着学习和工作,还是很充实的,同时新的工作环境,也让我接触到了更多领域的人,这又何尝不是一种进步呢?\n但是,新工作是真的累,这一个月的项目做下来,感觉自己有点吃不消,虽然有安排组员一起工作,但我对他们的工作成果并不满意,以后有必要进行一些指导,总是一个人做会很累很累,而且现在项目也比较多,人手也不够。\n个人感觉在新单位还是有很大的发展空间的,因为这里使用的技术都相对要落后,自己可以发挥以前学到的一些项目管理的经验,积攒更多的经验,对于技术人员来说才是成长。\n最后 由于工作的方向换了,所以以后可能会写一些嵌入式开发相关的内容,还有Qt之类的一些学习经验,总之,随学随记,不定期更新。\n","permalink":"https://kira-96.github.io/posts/%E5%BC%80%E5%A7%8B%E4%B8%80%E4%BB%BD%E6%96%B0%E5%B7%A5%E4%BD%9C/","summary":"开始一份新工作 好久没有更新博客了,最近几个月都在忙新工作的事情,虽然只是短短3个月,但回想起来已经是很久之前了。从4月份面试,然后体检,提交","title":"开始一份新工作"},{"content":"计算切片间距的方法:\n对于CT扫描出的断层图像,没有存储Spacing Between Slices信息,但可以利用位置信息计算得到。\n需要先读取相邻两层切片的位置信息,假设为pos1和pos2,然后计算两个位置的距离即为切片间距。\n1 2 3 4 5 // double pos1[3], pos2[3]; double spacing = sqrt( (pos1[0] - pos2[0]) * (pos1[0] - pos2[0]) + (pos1[1] - pos2[1]) * (pos1[1] - pos2[1]) + (pos1[2] - pos2[2]) * (pos1[2] - pos2[2])); ","permalink":"https://kira-96.github.io/notes/calculate-spacing-between-slices/","summary":"计算切片间距的方法: 对于CT扫描出的断层图像,没有存储Spacing Between Slices信息,但可以利用位置信息计算得到。 需要先读取相邻两层切片的","title":"Calculate Spacing Between Slices"},{"content":"前言 The Medical Imaging Interaction Toolkit (MITK)是一个免费的开源软件,用于开发交互式医学影像处理软件。最近突然安排我做相关的一些工作,首先就要从编译开始,当然官网也有编译好的版本,可以直接下载使用。本来在Windows上编译这种开源的软件就很麻烦,在加上github上的东西下载巨慢,常常出错,折腾了好久才编译完成,这里就记录一下踩过的那些坑。\n准备工作 Visual Studio 2017 CMake (\u0026gt;=3.19) Qt 5.12.10 (\u0026gt;=5.12.9) Python3 Git OpenSSL 安装包 Doxygen MITK MITK-Diffusion 我这里使用的是Visual Studio 2017版本,2019应该也可以。\nQt需要安装5.12.9以上的版本,官方编译似乎用的5.12.10,所以我这里也选择5.12.10版本,安装过程中尽量把所有组件都选上,因为编译时会使用到很多组件。\nCMake,Python,OpenSSL都选择64位版本安装。\n最后就是用git克隆下MITK和MITK-Diffusion的仓库。\n由于MITK在编译的过程中会下载一些第三方的软件包,所以要能克隆github上的仓库才行,最好有梯子,我就是这里卡了很久。\nCMake 相关配置 在MITK仓库目录下新建build文件夹(名字可以随意),然后打开cmake GUI工具,填写source code和build文件夹路径 点击Configure按钮,第一次会弹出对话框选择编译器,根据需要配置即可,这里选择msvc-2017,x64 此时会出现错误提示,找不到Qt,需要手动设置一下Qt5的路径,找到Qt5_DIR项,在Value中填写路径,例:D:/Qt/Qt5.12.10/msvc2017_64/lib/cmake/Qt5,再次点击Configure按钮 检查是否所有的安装路径都正确被检测到(Qt,OpenSSL),确保没有选项是红色 如果只需要编译MITK,那么就可以直接跳到第9步 找到MITK_EXTENSION_DIRS选项,在Value中填写MITK-Diffusion仓库的路径,再次点击Configure按钮 找到MITK_BUILD_CONFIGURATION选项,Value设置为DiffusionRelease,再次点击Configure按钮 此时下方输出会报错,提示找不到NumPy,需要先安装python库NumPy,打开终端,执行pip3 install --user NumPy,完成后再次点击Configure按钮,确保没有错误,没有选项是红色 点击Generate按钮,下方输出Generating done之后,点击Open Project按钮 附上我的配置:\n编译 直接编译ALL_BUILD项目即可,但此时编译可能会有一堆莫名奇妙的错误,可以先到MITK仓库目录下找到CMakeExternals目录,然后将里面所有的文件换行符改为Windows下的换行符(CRLF),可以使用VS Code或者Notepad++等工具,过程会有点枯燥。\n确保网络可用,最好有国外朋友帮忙,因为编译过程中会下载很多github上的仓库,没有国外朋友帮忙很容易出错。编译ALL_BUILD项目,第一次编译会非常慢,通常需要几个小时,建议先去忙点其它事情。\n编译过程中经常会遇到警告被视为错误,没有生成object,导致编译出错,双击错误,打开错误文件,然后再找到错误文件的位置,用记事本打开,选择文件→另存为,选择保存编码为Unicode,再次编译。\n在编译MITK-Diffusion的过程中,可能会遇到一些类型转换的报错,如无法将itk::Point转换为mitk::PointSet::PointType等,可能一些编译器能通过,但msvc会报错,只需要稍微修改一下,将出错的参数强制转换成对应类型即可,如:mitk::PointSet::PointType(itkPoint)。\n重复3、4若干次,直到编译成功。\n编译后的可执行文件在build/MITK-build/bin目录下。\n","permalink":"https://kira-96.github.io/posts/build-mitk-on-windows/","summary":"前言 The Medical Imaging Interaction Toolkit (MITK)是一个免费的开源软件,用于开发交互式医学影像处理软件。最近突然安排我做相关的一些工作,首先就要从编译开始,当然官网","title":"在 Windows 上编译 MITK"},{"content":"前言 今天偶然看到一个新的开源的git服务软件Gitea,一看到界面,瞬间就爱了,因为之前我自己用的是gitblit,界面比较简单,主要是用来管理公司的一些小项目。今天看到Gitea之后,就决定迁移到过去,简单折腾了一下,配置起来比gitblit要简单一些,但界面却更加漂亮了,总体上看起来比较像github,并且还支持主题系统,很合我的胃口。\n下载二进制包 首先去下载对应系统的二进制包,可以去github或者官网下载最新的发布版本。\n我是在windows下配置的,所以选择下载windows版的可执行程序。\n开启服务 下载后不需要安装,直接就能运行,但直接运行的话会有一个控制台显示在桌面,所以可以考虑将程序作为一个系统服务在后台运行。\n由于不需要安装,可以直接将下载的可执行程序放在自己想要安装的目录,eg: D:\\gitea\\gitea.exe\n以管理员方式打开cmd或者powershell,执行命令:\n1 sc create gitea start= auto binPath= \u0026#34;\\\u0026#34;D:\\gitea\\gitea.exe\\\u0026#34; web --config \\\u0026#34;D:\\gitea\\custom\\conf\\app.ini\\\u0026#34;\u0026#34; 打开系统的服务管理界面,找到gitea,此时是已停止状态,按下鼠标右键,在弹出的菜单选择开始,然后可以看到状态变为正在启动。\n打开浏览器访问 http://localhost:3000,应该就能看到Gitea的界面了,点击页面上的探索(explore),还需要进行一些配置才能正常运行,数据库可以直接使用sqlite,这样就不需要再安装其它的数据库了,然后就是一些目录配置,根据需要选择目录就行。\n最后可以创建管理员账号,也可以不用设置,完成安装后第一个注册的账号会自动成为管理员。\n到这里安装和配置就完成了,此时再看服务中的gitea的状态,已经变为正在运行。\n迁移仓库到 Gitea Gitea 自带迁移外部仓库的功能,但我一直导入失败,提示没有导入本地仓库的权限。后来只能将本地的仓库推送到Gitea,不过效果是一样的。\n首先在Gitea中新建一个同名的仓库,复制仓库的git链接。\n修改本地仓库的origin\n1 $ git remote origin set-url http://localhost:3000/user/myrepo.git 或者直接修改.git/config文件中的origin url。\n推送本地仓库到Gitea\n1 $ git push origin main 静静等待推送完成,然后刷新gitea的仓库页面,就可以看到所有的提交记录了。\n启用 SSH 编辑 D:\\gitea\\custom\\conf\\app.ini,在[server]部分新增一项配置\n1 2 [server] START_SSH_SERVER = true 保存后,在服务中重启 gitea 即可。\n在gitea的账号管理中新增SSH密钥,然后就可以使用ssh的方式管理账户所拥有的仓库了。\n最后 总的来看,Gitea的配置十分简单,基本上下载后就可以使用,没有其它的依赖,gitblit则需要安装java运行环境,界面十分漂亮,功能也比较完善,自用完全没有问题。\n","permalink":"https://kira-96.github.io/posts/install-gitea/","summary":"前言 今天偶然看到一个新的开源的git服务软件Gitea,一看到界面,瞬间就爱了,因为之前我自己用的是gitblit,界面比较简单,主要是用来","title":"Gitea 安装使用"},{"content":"首先需要安装Docker\n安装完成后,拉取以下3个仓库\n1 2 3 $ docker pull dcm4che/slapd-dcm4chee:2.4.56-23.1 $ docker pull dcm4che/postgres-dcm4chee:13.1-23 $ docker pull dcm4che/dcm4chee-arc-psql:5.23.1 拉取完成后,就可以启动服务了,这里有两种方式,一种是使用Docker命令行依次启动上面3个服务,不过比较麻烦,也容易出错,另一种是直接使用Docker Copmose,相对来说要简单很多。这里就直接使用 Docker Compose的方式。\n创建以下两个文件:\ndocker-compose.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 version: \u0026#34;3\u0026#34; services: ldap: image: dcm4che/slapd-dcm4chee:2.4.56-23.1 logging: driver: json-file options: max-size: \u0026#34;10m\u0026#34; ports: - \u0026#34;389:389\u0026#34; env_file: docker-compose.env volumes: - /var/local/dcm4chee-arc/ldap:/var/lib/openldap/openldap-data - /var/local/dcm4chee-arc/slapd.d:/etc/openldap/slapd.d db: image: dcm4che/postgres-dcm4chee:13.1-23 logging: driver: json-file options: max-size: \u0026#34;10m\u0026#34; ports: - \u0026#34;5432:5432\u0026#34; env_file: docker-compose.env volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/local/dcm4chee-arc/db:/var/lib/postgresql/data arc: image: dcm4che/dcm4chee-arc-psql:5.23.1 logging: driver: json-file options: max-size: \u0026#34;10m\u0026#34; ports: - \u0026#34;8080:8080\u0026#34; - \u0026#34;8443:8443\u0026#34; - \u0026#34;9990:9990\u0026#34; - \u0026#34;9993:9993\u0026#34; - \u0026#34;11112:11112\u0026#34; - \u0026#34;2762:2762\u0026#34; - \u0026#34;2575:2575\u0026#34; - \u0026#34;12575:12575\u0026#34; env_file: docker-compose.env environment: WILDFLY_CHOWN: /opt/wildfly/standalone /storage WILDFLY_WAIT_FOR: ldap:389 db:5432 depends_on: - ldap - db volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - /var/local/dcm4chee-arc/wildfly:/opt/wildfly/standalone - /var/local/dcm4chee-arc/storage:/storage docker-compose.env\n1 2 3 4 5 TZ=Asia/Shanghai STORAGE_DIR=/storage/fs1 POSTGRES_DB=pacsdb POSTGRES_USER=pacs POSTGRES_PASSWORD=pacs 其中TZ是用来设置时区的。\n启动\n1 $ docker-compose -p dcm4chee up -d 停止\n1 $ docker-compose -p dcm4chee stop 重新启动\n1 $ docker-compose -p dcm4chee start 删除\n1 $ docker-compose -p dcm4chee down 参考\nRun minimum set of archive services on a single host\n","permalink":"https://kira-96.github.io/notes/running-dcm4chee-arc-light-on-docker/","summary":"首先需要安装Docker 安装完成后,拉取以下3个仓库 1 2 3 $ docker pull dcm4che/slapd-dcm4chee:2.4.56-23.1 $ docker pull dcm4che/postgres-dcm4chee:13.1-23 $ docker pull dcm4che/dcm4chee-arc-psql:5.23.1 拉取完成后,就可以启动服务了,这里有两种方式,一种是使用D","title":"Running Dcm4chee Arc Light on Docker"},{"content":"GDI+绘制椭圆时只支持输入一个矩形范围,无法绘制倾斜的椭圆。\n绘制椭圆的 API:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 摘要: 绘制边界 System.Drawing.RectangleF 定义的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // rect: System.Drawing.RectangleF 结构,它定义椭圆的边界。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, RectangleF rect); // // 摘要: 绘制一个由边框(该边框由一对坐标、高度和宽度指定)定义的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // x: 定义椭圆的边框的左上角的 X 坐标。 // y: 定义椭圆的边框的左上角的 Y 坐标。 // width: 定义椭圆的边框的宽度。 // height: 定义椭圆的边框的高度。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, float x, float y, float width, float height); // // 摘要: 绘制边界 System.Drawing.Rectangle 结构指定的椭圆。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // rect: System.Drawing.Rectangle 结构,它定义椭圆的边界。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, Rectangle rect); // // 摘要: 绘制一个由边框定义的椭圆,该边框由矩形的左上角坐标、高度和宽度指定。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // x: 定义椭圆的边框的左上角的 X 坐标。 // y: 定义椭圆的边框的左上角的 Y 坐标。 // width: 定义椭圆的边框的宽度。 // height: 定义椭圆的边框的高度。 // 异常: // T:System.ArgumentNullException: pen 为 null。 public void DrawEllipse(Pen pen, int x, int y, int width, int height); 可以看到,绘制椭圆的API,都只支持输入一个矩形区域,长和宽都只能是水平或者竖直的,因此,绘制出椭圆的长轴和短轴也是水平或竖直的,而无法绘制一个旋转过的椭圆,如下图:\n给定椭圆的4个顶点,绘制出椭圆。虽然不能直接调用API绘制椭圆,但是可以通过绘制4条连续的贝塞尔曲线来闭合成一个椭圆。\n1 2 3 4 5 6 7 8 9 10 // // 摘要: 用 System.Drawing.PointF 结构数组绘制一系列贝塞尔样条。 // 参数: // pen: System.Drawing.Pen,它确定曲线的颜色、宽度和样式。 // points: System.Drawing.PointF 结构的数组,这些结构表示确定曲线的点。 // 此数组中的点数应为 3 的倍数加 1,如 4、7 或 10。 // // 异常: // T:System.ArgumentNullException: pen 为 null。- 或 -points 为 null。 public void DrawBeziers(Pen pen, PointF[] points); 使用这种方法实际上只需要知道椭圆的中心点,两个轴的长度以及旋转角度即可,而这些都可以通过4个顶点计算得到。\n根据以上的信息,依次计算出连续贝塞尔曲线的13个控制点,然后调用DrawBeziers绘制椭圆。\n各个点的位置如图:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /** * C# */ // MAGICAL CONSTANT to map ellipse to beziers // 2/3*(sqrt(2)-1) const double Ellipse2Beziers = 0.2761423749154; // GDI Bitmap using var bitmap = new System.Drawing.Bitmap(width, height); using var graphics = System.Drawing.Graphics.FromImage(bitmap); // 椭圆的4个顶点 PointF[] ellipse = new PointF[4] { point1, point2, point3, point4 }; // 两个轴的长度 double r1 = ellipse[2].DistanceTo(ellipse[3]); double r2 = ellipse[0].DistanceTo(ellipse[1]); // 旋转角度 double angle = -SysMath.Atan2(ellipse[2].Y - ellipse[3].Y, ellipse[2].X - ellipse[3].X); double sin = SysMath.Sin(angle); double cos = SysMath.Cos(angle); // 贝塞尔曲线控制点相对于中心点的偏移长度 SizeF offset = new SizeF((float)(r1 * Ellipse2Beziers), (float)(r2 * Ellipse2Beziers)); // 椭圆中心点 PointF center = new PointF((ellipse[0].X + ellipse[1].X) / 2f, (ellipse[0].Y + ellipse[1].Y) / 2f); // 贝塞尔曲线的控制点 PointF[] beziers = new PointF[13] { new PointF((float)(center.X - r1 / 2.0), center.Y), new PointF((float)(center.X - r1 / 2.0), center.Y - offset.Height), new PointF(center.X - offset.Width, (float)(center.Y - r2 / 2.0)), new PointF(center.X, (float)(center.Y - r2 / 2.0)), new PointF(center.X + offset.Width, (float)(center.Y - r2 / 2.0)), new PointF((float)(center.X + r1 / 2.0), center.Y - offset.Height), new PointF((float)(center.X + r1 / 2.0), center.Y), new PointF((float)(center.X + r1 / 2.0), center.Y + offset.Height), new PointF(center.X + offset.Width, (float)(center.Y + r2 / 2.0)), new PointF(center.X, (float)(center.Y + r2 / 2.0)), new PointF(center.X - offset.Width, (float)(center.Y + r2 / 2.0)), new PointF((float)(center.X - r1 / 2.0), center.Y + offset.Height), new PointF((float)(center.X - r1 / 2.0), center.Y) }; // 旋转变换 double offsetX = center.X - center.X * cos - center.Y * sin; double offsetY = center.Y + center.X * sin - center.Y * cos; for (int j = 0; j \u0026lt; beziers.Length; j++) { beziers[j] = new PointF( (float)(beziers[j].X * cos + beziers[j].Y * sin + offsetX), (float)(beziers[j].Y * cos - beziers[j].X * sin + offsetY)); } // 绘制曲线 graphics.DrawBeziers(new Pen(Brushes.White, 1f)/* Pen */, beziers); 通过上面代码就可以完美的绘制出一个旋转任意角度的椭圆了。\n参考\nMFC上如何绘制一个可以旋转的椭圆\nDrawing Rotated and Skewed Ellipses\n","permalink":"https://kira-96.github.io/notes/draw-rotate-ellipse-with-gdi/","summary":"GDI+绘制椭圆时只支持输入一个矩形范围,无法绘制倾斜的椭圆。 绘制椭圆的 API: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30","title":"GDI+绘制旋转的椭圆"},{"content":"规则\n私有 Tag (gggg, xxxx)\nGroup number (gggg) 必须为奇数 (odd),并且 (0001, xxxx),(0003, xxxx),(0005, xxxx),(0007, xxxx),(FFFF, xxxx) 不允许使用。\n(gggg, 0000) were Group Length Elements, which have been retired.(已弃用)\n(gggg, 0001-000F),(gggg, 0100-0FFF) 不允许使用。\n(gggg, 0010-00FF) 供私有tag创建者(Private Creator)使用,用于在该group中插入一个未使用的标识码(identification code),私有标识码的VR应该为LO (Long String),VM应该为1。\n(gggg, 1000-FFFF) 为 Data Element。\nPrivate Creator 和 Data Element 的对应关系为:\n例:\nData Element (0029, 1000-10FF) 的 Private Creator 是 (0029, 0010)\nData Element (0029, 1100-11FF) 的 Private Creator 是 (0029, 0011)\nData Element (0029, 1200-12FF) 的 Private Creator 是 (0029, 0012)\n……\nData Element (0029, FF00-FFFF) 的 Private Creator 是 (0029, 00FF)\n标准\nhttp://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.8.html\n","permalink":"https://kira-96.github.io/notes/private-dicom-element/","summary":"规则 私有 Tag (gggg, xxxx) Group number (gggg) 必须为奇数 (odd),并且 (0001, xxxx),(0003, xxxx),(0005, xxxx),(0007, xxxx),(FFF","title":"Private Dicom Element"},{"content":"说明 这里是个人工作时常用的一些git命令,现在越来越多了,小本本都快记不下了,这里稍微做一下整理。\n工具下载 首先是git的下载地址:\n官网:https://git-scm.com/ taobao镜像:https://npm.taobao.org/mirrors/git-for-windows/ 由于官网的下载速度很慢,推荐使用taobao镜像的下载地址。\n设置用户名和邮箱 全局配置\n1 2 $ git config --global user.name [name] $ git config --global user.email [email] 配置当前仓库\n1 2 $ git config user.name [name] $ git config user.email [email] 查看用户名和邮箱\n1 2 $ git config user.name $ git config user.email 生成SSH密钥 1 $ ssh-keygen -t rsa -C \u0026#34;[email]\u0026#34; 执行完毕和用户目录下就会生成一个**.ssh**文件夹。\n拉取远程代码库 可以直接clone远程代码仓库(推荐)\n1 $ git clone https://github.com/libgit2/libgit2 mylibgit 先初始化仓库,再拉取代码\n1 2 3 4 5 $ cd ./mylibgit $ git init $ git remote add origin git@github.com:libgit2/libgit2.git $ git pull origin master $ git push -u origin master 提交代码变更 查看工作区变更\n1 $ git status 添加文件到暂存区\n1 $ git add README.md 添加所有变更文件到暂存区\n1 $ git add . 提交到本地仓库\n1 $ git commit -m \u0026#39;Update README\u0026#39; 推送到远程代码仓库(main是分支)\n1 $ git push origin main 版本切换 查看版本/提交记录\n1 $ git log 查看版本/提交简介\n1 $ git log --pertty=oneline 撤销提交,保留代码变更 撤销上次提交\n1 $ git reset --soft HEAD^ 撤销前n次提交\n1 $ git reset --soft HEAD~n 回退到某次提交记录\n1 $ git reset --soft commit-id 版本回退,不保留代码变更 回退到当前最新提交\n1 $ git reset --hard HEAD 回退到上一版本\n1 $ git reset --hard HEAD^ 回退到之前的第n个版本\n1 $ git reset --hard HEAD~n 回退到某个版本/重新切换回未来版本\n1 $ git reset --hard commit-id 强制推送:在已经推送到远程的记录又被修改的情况下\n1 $ git push origin main --force 1 $ git push -f origin main 分支管理 查看当前仓库分支\n1 $ git branch 查看当前仓库以及远程仓库所有分支\n1 $ git branch -a 在当前分支的基础上创建新分支\n1 $ git checkout -b 分支名 删除已合并的本地分支\n1 $ git branch -d 分支名 删除未合并的本地分支\n1 $ git branch -D 分支名 删除远程仓库分支\n1 $ git push origin -d 分支名 或\n1 $ git push origin :分支名 删除远程已经删除的分支\n1 $ git remote prune origin 标签-tag tag和分支操作类似\n查看tag\n1 $ git tag 给当前版本添加tag\n1 $ git tag 标签名 给某一版本添加tag\n1 $ git tag 标签名 commit-id 删除标签\n1 $ git tag -d 标签名 删除远程标签\n1 $ git push origin -d 标签名 推送标签到远程仓库\n1 $ git push origin 标签名 子模块(submodule) 当前仓库添加子模块\n1 $ git submodule add \u0026lt;url\u0026gt; \u0026lt;path\u0026gt; 拉取仓库后拉取子模块\n1 2 $ git submodule init $ git submodule update 1 $ git submodule update --init 或\n1 $ git clone --recurse-submodules 更新子模块\n1 $ git submodule update --remote \u0026lt;submodule\u0026gt; 删除子模块\n1 2 $ git submodule deinit \u0026lt;submodule\u0026gt; $ git rm --cached \u0026lt;submodule\u0026gt; 保持fork之后的仓库和上游同步 遇到一些好的代码仓库,有时候会fork一份到自己的账号,但一旦原来的代码仓库有了新的提交,如何保持自己的仓库和源仓库代码同步呢?\n一种方式是通过远程仓库向fork之后的仓库提交一个PR,但这样会导致提交记录不一致,非常EP,在网上搜罗了好久,终于找到一个完美的方法。\n首先需要将fork之后的仓库clone到本地。\n然后设置本地仓库的上游仓库地址为源仓库\n1 $ git remote add upstream git@github.com:kira-96/myblog.git 同步上游仓库变更\n1 2 3 $ git fetch upstream $ git checkout main $ git merge upstream/main 推送到远程仓库\n1 $ git push origin main 参考\ngit思维导图 保持fork之后的项目和上游同步 ","permalink":"https://kira-96.github.io/posts/%E4%B8%80%E4%BA%9B%E5%B8%B8%E7%94%A8%E7%9A%84git%E5%91%BD%E4%BB%A4/","summary":"说明 这里是个人工作时常用的一些git命令,现在越来越多了,小本本都快记不下了,这里稍微做一下整理。 工具下载 首先是git的下载地址: 官网:ht","title":"一些常用的git命令"},{"content":" 食用方法 Step 1\n将ChineseSimplified.isl放到Inno Setup安装目录下的\u0026quot;Languages\u0026quot;文件夹里面\nStep 2\n如果你是通过新建脚本的方式创建脚本,在Languages选项勾选Chinese Simplified即可:\n如果你需要在现有脚本中添加简体中文支持 直接在你的脚本的[Languages]部分添加下面一行即可\n1 Name: \u0026#34;chinesesimplified\u0026#34;; MessagesFile: \u0026#34;compiler:Languages\\ChineseSimplified.isl\u0026#34; 示例:\n1 2 3 [Languages] Name: \u0026#34;english\u0026#34;; MessagesFile: \u0026#34;compiler:Default.isl\u0026#34; Name: \u0026#34;chinesesimplified\u0026#34;; MessagesFile: \u0026#34;compiler:Languages\\ChineseSimplified.isl\u0026#34; 注意:此翻译版本支持 Inno Setup 6.1.0+ 的软件,Inno Setup 5 的翻译文件在这里\n查看6.1.0+和6.0.0+的区别\n查看6.0.3+和6.0.0+的区别\n链接 Inno Setup issrc ","permalink":"https://kira-96.github.io/Inno-Setup-Chinese-Simplified-Translation/","summary":"食用方法 Step 1 将ChineseSimplified.isl放到Inno Setup安装目录下的\u0026quot;Languages\u0026quot;文件夹里","title":"Inno Setup 简体中文语言包"},{"content":"简介 Prism是一个用于WPF、Xamarin Forms、WinUI等的MVVM框架,刚刚学习,这里只是个人总结的一些知识点笔记。\nIoC IContainerProvider\n1 2 3 4 protected override Window CreateShell() { return Container.Resolve\u0026lt;MainWindow\u0026gt;(); } 1 2 3 4 5 6 public void OnInitialized(IContainerProvider containerProvider) { var regionManager = containerProvider.Resolve\u0026lt;IRegionManager\u0026gt;(); var viewA = containerProvider.Resolve\u0026lt;ViewA\u0026gt;(); ... } IContainerRegistry\n1 2 3 4 5 6 7 8 9 // App.xaml.cs protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.Register\u0026lt;IApplicationCommands, ApplicationCommands\u0026gt;(); containerRegistry.RegisterDialog\u0026lt;NotificationDialog, NotificationDialogViewModel\u0026gt;(); containerRegistry.RegisterForNavigation\u0026lt;Page1\u0026gt;(); containerRegistry.RegisterForNavigation\u0026lt;Page2\u0026gt;(); ... } Module IModule\n1 2 3 4 5 6 7 8 9 10 public class SimpleModule : IModule { public void OnInitialized(IContainerProvider containerProvider) { } public void RegisterTypes(IContainerRegistry containerRegistry) { } } 使用App.config加载模块\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!-- App.config --\u0026gt; \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;configSections\u0026gt; \u0026lt;section name=\u0026#34;modules\u0026#34; type=\u0026#34;Prism.Modularity.ModulesConfigurationSection, Prism.Wpf\u0026#34; /\u0026gt; \u0026lt;/configSections\u0026gt; \u0026lt;startup\u0026gt; \u0026lt;/startup\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module assemblyFile=\u0026#34;Simple.dll\u0026#34; moduleType=\u0026#34;Simple.SimpleModule, Simple, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\u0026#34; moduleName=\u0026#34;SimpleModule\u0026#34; startupLoaded=\u0026#34;True\u0026#34; /\u0026gt; \u0026lt;/modules\u0026gt; \u0026lt;/configuration\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override IModuleCatalog CreateModuleCatalog() { return new ConfigurationModuleCatalog(); } ... } 直接引用加载模块\n1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule\u0026lt;SimpleModule\u0026gt;(); } ... } 指定模块文件夹\n1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = @\u0026#34;.\\Modules\u0026#34; }; } ... } 使用ModuleCatalog加载模块\n1 2 3 4 5 6 7 8 \u0026lt;m:ModuleCatalog xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:m=\u0026#34;clr-namespace:Prism.Modularity;assembly=Prism.Wpf\u0026#34;\u0026gt; \u0026lt;m:ModuleInfo ModuleName=\u0026#34;Simple\u0026#34; ModuleType=\u0026#34;Simple.SimpleModule, Simple, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null\u0026#34; /\u0026gt; \u0026lt;/m:ModuleCatalog\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 // App.xaml.cs public partial class App : PrismApplication { ... protected override IModuleCatalog CreateModuleCatalog() { return ModuleCatalog.CreateFromXaml(new Uri(\u0026#34;/Modules;component/ModuleCatalog.xaml\u0026#34;, UriKind.Relative)); } ... } Command DelegateCommand\n1 2 3 4 5 6 7 8 9 10 11 12 public DelegateCommand ExecuteDelegateCommand { get; } public DelegateCommand\u0026lt;string\u0026gt; ExecuteGenericDelegateCommand { get; } public DelegateCommand DelegateCommandObservesProperty { get; } public DelegateCommand DelegateCommandObservesCanExecute { get; } ExecuteDelegateCommand = new DelegateCommand(Execute, CanExecute); DelegateCommandObservesProperty = new DelegateCommand(Execute, CanExecute).ObservesProperty(() =\u0026gt; IsEnabled); DelegateCommandObservesCanExecute = new DelegateCommand(Execute).ObservesCanExecute(() =\u0026gt; IsEnabled); ExecuteGenericDelegateCommand = new DelegateCommand\u0026lt;string\u0026gt;(ExecuteGeneric).ObservesCanExecute(() =\u0026gt; IsEnabled); CompositeCommand\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public CompositeCommand SampleCommand { get; } = new CompositeCommand(true); ... DelegateCommand UpdateCommand = new DelegateCommand(Update).ObservesCanExecute(() =\u0026gt; CanUpdate); SampleCommand.RegisterCommand(UpdateCommand); ... private void OnIsActiveChanged() { UpdateCommand.IsActive = IsActive; IsActiveChanged?.Invoke(this, new EventArgs()); } Event To Command\n1 2 3 4 5 6 \u0026lt;i:Interaction.Triggers\u0026gt; \u0026lt;i:EventTrigger EventName=\u0026#34;SelectionChanged\u0026#34;\u0026gt; \u0026lt;prism:InvokeCommandAction Command=\u0026#34;{Binding PersonSelectedCommand}\u0026#34; CommandParameter=\u0026#34;{Binding ElementName=ListOfPerson, Path=SelectedItem}\u0026#34; /\u0026gt; \u0026lt;/i:EventTrigger\u0026gt; \u0026lt;/i:Interaction.Triggers\u0026gt; BindableBase 1 2 3 4 public class ViewAViewModel : BindableBase, IActiveAware { ... } ViewModelLocator AutoWireViewModel\n1 2 3 4 \u0026lt;Window x:Class=\u0026#34;Demo.Views.MainWindow\u0026#34; ... xmlns:prism=\u0026#34;http://prismlibrary.com/\u0026#34; prism:ViewModelLocator.AutoWireViewModel=\u0026#34;True\u0026#34;\u0026gt; 更改命名约定\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // App.xaml.cs public partial class App : PrismApplication { ... protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =\u0026gt; { var viewName = viewType.FullName.Replace(\u0026#34;.ViewModels.\u0026#34;, \u0026#34;.CustomNamespace.\u0026#34;); var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName; var viewModelName = $\u0026#34;{viewName}ViewModel, {viewAssemblyName}\u0026#34;; return Type.GetType(viewModelName); }); } ... } 自定义ViewModel注册\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // App.xaml.cs public partial class App : PrismApplication { ... protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); // type / type ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel)); // type / factory ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () =\u0026gt; Container.Resolve\u0026lt;CustomViewModel\u0026gt;()); // generic factory ViewModelLocationProvider.Register\u0026lt;MainWindow\u0026gt;(() =\u0026gt; Container.Resolve\u0026lt;CustomViewModel\u0026gt;()); // generic type ViewModelLocationProvider.Register\u0026lt;MainWindow, CustomViewModel\u0026gt;(); } ... } EventAggregator IEventAggragator\n1 2 3 4 public interface IEventAggregator { TEventType GetEvent\u0026lt;TEventType\u0026gt;() where TEventType : EventBase; } 创建消息事件类\n1 2 3 public class SimpleMessageEvent : PubSubEvent\u0026lt;string\u0026gt; { } 订阅事件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 private readonly IEventAggregator eventAggregator; public MainPageViewModel(IEventAggregator ea) { eventAggregator = ea; ea.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Subscribe(ShowMessage); // Subscribing on the UI Thread // ea.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Subscribe(ShowMessage, ThreadOption.UIThread); } public void ShowMessage(string payload) { // TODO } 发布消息\n1 eventAggregator.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Publish(\u0026#34;Hello!\u0026#34;); 筛选订阅\n1 ea.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Subscribe(ShowMessage, ThreadOption.UIThread, keepSubscriberReferenceAlive, x =\u0026gt; x.Contains(\u0026#34; \u0026#34;)); 取消订阅\n1 eventAggregator.GetEvent\u0026lt;SimpleMessageEvent\u0026gt;().Unsubscribe(ShowMessage); RegionManager 1 2 3 4 5 6 7 8 9 \u0026lt;Window x:Class=\u0026#34;Regions.Views.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:prism=\u0026#34;http://prismlibrary.com/\u0026#34; Title=\u0026#34;Shell\u0026#34;\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;ContentControl prism:RegionManager.RegionName=\u0026#34;ContentRegion\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; 1 2 3 // IContainerProvider containerProvider var regionManager = containerProvider.Resolve\u0026lt;IRegionManager\u0026gt;(); regionManager.RegisterViewWithRegion(\u0026#34;ContentRegion\u0026#34;, typeof(ViewA)); 1 2 3 4 5 6 7 // IContainerProvider containerProvider var regionManager = containerProvider.Resolve\u0026lt;IRegionManager\u0026gt;(); var region = regionManager.Regions[\u0026#34;ContentRegion\u0026#34;]; region.Add(containerProvider.Resolve\u0026lt;ViewA\u0026gt;()); region.Add(containerProvider.Resolve\u0026lt;ViewB\u0026gt;()); region.Add(containerProvider.Resolve\u0026lt;ViewC\u0026gt;()); RegionNavigation 1 2 // IRegionManager regionManager regionManager.RequestNavigate(regionName: \u0026#34;NavigateRegion\u0026#34;, source: \u0026#34;Page1\u0026#34;); Navigation Callback\n1 2 3 4 5 6 7 // IRegionManager regionManager regionManager.RequestNavigate(regionName: \u0026#34;NavigateRegion\u0026#34;, source: \u0026#34;Page1\u0026#34;, navigationCallback: NavigationComplete); private void NavigationComplete(NavigationResult result) { dialogService.ShowDialog(\u0026#34;NotificationDialog\u0026#34;, new DialogParameters($\u0026#34;message=Navigate to {result.Context.Uri} complete.\u0026#34;), null); } Navigation Parameters\n1 2 3 4 5 6 var parameters = new NavigationParameters { { \u0026#34;content\u0026#34;, \u0026#34;Hello!\u0026#34; } }; // IRegionManager regionManager regionManager.RequestNavigate(regionName: \u0026#34;NavigateRegion\u0026#34;, source: \u0026#34;Page1\u0026#34;, navigationParameters: parameters); INavigationAware\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Page1ViewModel : BindableBase, INavigationAware { ... public bool IsNavigationTarget(NavigationContext navigationContext) { return true; } public void OnNavigatedFrom(NavigationContext navigationContext) { } public void OnNavigatedTo(NavigationContext navigationContext) { if (navigationContext.Parameters[\u0026#34;content\u0026#34;] is string content) { // TODO } } ... } IConfirmNavigationRequest\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Page1ViewModel : BindableBase, IConfirmNavigationRequest { ... public void ConfirmNavigationRequest(NavigationContext navigationContext, Action\u0026lt;bool\u0026gt; continuationCallback) { bool result = true; ButtonResult buttonResult = ButtonResult.None; dialogService.ShowDialog(\u0026#34;NotificationDialog\u0026#34;, new DialogParameters($\u0026#34;message=Do you to navigate?\u0026#34;), res =\u0026gt; { buttonResult = res.Result; }); if (buttonResult != ButtonResult.OK) result = false; continuationCallback(result); } ... } IRegionMemberLifetime\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Page1ViewModel : BindableBase, INavigationAware, IRegionMemberLifetime { public bool KeepAlive { get { return false; } } public bool IsNavigationTarget(NavigationContext navigationContext) { return false; } public void OnNavigatedFrom(NavigationContext navigationContext) { } public void OnNavigatedTo(NavigationContext navigationContext) { } } Navigation Journal\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Page1ViewModel : BindableBase, INavigationAware { private IRegionNavigationJournal journal; public DelegateCommand GoForwardCommand { get; } public DelegateCommand GoBackCommand { get; } public Page1ViewModel() { GoForwardCommand = new DelegateCommand(GoForward, CanGoForward); GoBackCommand = new DelegateCommand(GoBack); } ... public bool IsNavigationTarget(NavigationContext navigationContext) { return true; } public void OnNavigatedFrom(NavigationContext navigationContext) { } public void OnNavigatedTo(NavigationContext navigationContext) { journal = navigationContext.NavigationService.Journal; GoForwardCommand.RaiseCanExecuteChanged(); } ... private bool CanGoForward() { return journal != null \u0026amp;\u0026amp; journal.CanGoForward; } private void GoForward() { journal?.GoForward(); } private void GoBack() { journal?.GoBack(); } } DialogService See DOC. Dialog Service\n1 2 3 4 5 6 // DialogServiceModule.cs public void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterDialog\u0026lt;NotificationDialog, NotificationDialogViewModel\u0026gt;(); // containerRegistry.RegisterDialogWindow\u0026lt;MyRibbonWindow\u0026gt;(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 // viewmodel private readonly IDialogService dialogService; public MainViewModel(IDialogService dialogService) { this.dialogService = dialogService; } private void NavigationComplete(NavigationResult result) { // Show Dialog with parameters. dialogService.ShowDialog(\u0026#34;NotificationDialog\u0026#34;, new DialogParameters($\u0026#34;message=Navigate to {result.Context.Uri} complete.\u0026#34;), null); } 参考 Documentation Prism-Samples-Wpf ","permalink":"https://kira-96.github.io/posts/prism-note/","summary":"简介 Prism是一个用于WPF、Xamarin Forms、WinUI等的MVVM框架,刚刚学习,这里只是个人总结的一些知识点笔记。 IoC IContainerProvider 1 2 3","title":"Prism note"},{"content":"简介 Inno Setup是一个免费的安装包生成软件,完全开源免费,使用起来也非常方便,文档也十分全面。与其它同类软件相比十分的小巧便携,功能也十分全面。\n近期Inno Setup的6.1.0版本也即将发布,也带来了更多的新功能。由于正式版本还没有发布,这里就使用的先行版本。\n下载页面 Inno Setup 6.1版本新增了安装过程中的下载页面,在所有选项准备完毕,正式开始安装之前可以下载需要的文件。官方也给出了下载示例的代码 CodeDownloadFiles.iss。\n[Code] var DownloadPage: TDownloadWizardPage; function OnDownloadProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean; begin if Progress = ProgressMax then Log(Format(\u0026#39;Successfully downloaded file to {tmp}: %s\u0026#39;, [FileName])); Result := True; end; procedure InitializeWizard; begin DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadProgress); end; function NextButtonClick(CurPageID: Integer): Boolean; begin if CurPageID = wpReady then begin DownloadPage.Clear; DownloadPage.Add(\u0026#39;https://files.jrsoftware.org/is/6/innosetup-6.1.0-dev.exe\u0026#39;, \u0026#39;innosetup-6.1.0-dev.exe\u0026#39;, \u0026#39;\u0026#39;); DownloadPage.Add(\u0026#39;https://jrsoftware.org/download.php/iscrypt.dll\u0026#39;, \u0026#39;ISCrypt.dll\u0026#39;, \u0026#39;2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc\u0026#39;); DownloadPage.Show; try try DownloadPage.Download; Result := True; except SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK); Result := False; end; finally DownloadPage.Hide; end; end else Result := True; end; 上面代码在初始化时创建了一个下载页面,并在wpReady之后显示。\nCreateDownloadPage的原型:\nfunction CreateDownloadPage(const ACaption, ADescription: String; const OnDownloadProgress: TOnDownloadProgress): TDownloadWizardPage; 创建一个下载页面用于下载文件并显示进度。\n前两个参数指定页面的标题和页面描述,第3个参数是在下载进度更新后的回调函数TOnDownloadProgress,可以指定为nil(空)。\nTOnDownloadProgress = function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean; CreateDownloadPage返回TDownloadWizardPage类型:\nTDownloadWizardPage = class(TOutputProgressWizardPage) property AbortButton: TNewButton; read; procedure Add(const Url, BaseName, RequiredSHA256OfFile: String); procedure Clear; function Download: Int64; end; 可以看到,TDownloadWizardPage有一个Add的方法,用于新增一个下载任务,它有3个参数:\nUrl:下载链接,BaseName:下载后的文件名称\nRequiredSHA256OfFile:文件的哈希值,用于校验下载文件,值为空时,则忽略校验\nClear方法清空下载任务列表,Download方法开始下载任务。\n具体的使用可以看上面的NextButtonClick函数里的写法。\n另一个方法是使用DownloadTemporaryFile函数:\nfunction DownloadTemporaryFile(const Url, FileName, RequiredSHA256OfFile: String; const OnDownloadProgress: TOnDownloadProgress): Int64; [Code] function OnDownloadProgress(const Url, Filename: string; const Progress, ProgressMax: Int64): Boolean; begin if ProgressMax \u0026lt;\u0026gt; 0 then Log(Format(\u0026#39; %d of %d bytes done.\u0026#39;, [Progress, ProgressMax])) else Log(Format(\u0026#39; %d bytes done.\u0026#39;, [Progress])); Result := True; end; function InitializeSetup: Boolean; begin try DownloadTemporaryFile(\u0026#39;https://jrsoftware.org/download.php/is.exe\u0026#39;, \u0026#39;innosetup-latest.exe\u0026#39;, \u0026#39;\u0026#39;, @OnDownloadProgress); DownloadTemporaryFile(\u0026#39;https://jrsoftware.org/download.php/iscrypt.dll\u0026#39;, \u0026#39;ISCrypt.dll\u0026#39;, \u0026#39;2f6294f9aa09f59a574b5dcd33be54e16b39377984f3d5658cda44950fa0f8fc\u0026#39;, @OnDownloadProgress); Result := True; except Log(GetExceptionMessage); Result := False; end; end; 使用起来和前一种方法有所不同,但大致都是类似的,这里不再赘述。\n消息框设计器 软件的工具菜单(Tools)中新增了消息框设计器(MessageBox Designer)工具。\n工具提供了两种消息框,Message Box和Task Dialog Message Box,工具可以设置对话框的图标,按钮和默认选项等。\n将鼠标指针放在需要插入对话框的代码位置,打开MessageBox Designer,完成选项后点击OK即可,然后就可以看到先前鼠标所在的位置插入了一段MessageBox代码。\n1 2 3 [CustomMessages] DownloadComplete=下载完成 DownloadCompleteMessage=下载已完成。 // Display a message box SuppressibleTaskDialogMsgBox(CustomMessage(\u0026#39;DownloadComplete\u0026#39;), CustomMessage(\u0026#39;DownloadCompleteMessage\u0026#39;), mbInformation, MB_OK, [\u0026#39;OK\u0026#39;], 0, IDOK); 链接 Inno Setup 6 Revision History Inno Setup 简体中文翻译 ","permalink":"https://kira-96.github.io/posts/inno-setup-6.1.0-%E6%96%B0%E5%A2%9E%E7%9A%84%E5%8A%9F%E8%83%BD%E4%BD%93%E9%AA%8C/","summary":"简介 Inno Setup是一个免费的安装包生成软件,完全开源免费,使用起来也非常方便,文档也十分全面。与其它同类软件相比十分的小巧便携,功能也十分全","title":"Inno Setup 6.1.0 新增的功能体验"},{"content":"简介 在编码的时候难免会遇到不同编程语言之间的接口调用,其中最通用的就是C的动态链接库,几乎所有语言都可以调用C的接口函数。那么这种关系能否反过来呢?用C调用其它的函数接口,当然也是可以的,只需要将函数指针作为参数传递进去就可以了。\nC#中使用Delegate来表示函数指针。\n准备工作 首先新建一个C++的动态库,随便定义一个函数指针类型,以及一个导出函数。大致内容如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include \u0026#34;pch.h\u0026#34; #include \u0026lt;iostream\u0026gt; using namespace std; #ifdef DYNLIB_EXPORTS #define DLL_API extern \u0026#34;C\u0026#34; __declspec(dllexport) #else #define DLL_API extern \u0026#34;C\u0026#34; __declspec(dllimport) #endif typedef struct { int Left; int Top; int Right; int Bottom; } MyRect, * MyRectPtr; // 函数指针 typedef VOID(CALLBACK* PRINTCALLBACK)(MyRectPtr); BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } // 导出函数,使用上面定义的函数指针类型作为参数 DLL_API VOID Print(MyRectPtr pRect, PRINTCALLBACK callback) { if (callback == NULL) { cout \u0026lt;\u0026lt; \u0026#39;\\t\u0026#39; \u0026lt;\u0026lt; pRect-\u0026gt;Top \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; pRect-\u0026gt;Left \u0026lt;\u0026lt; \u0026#34;\\t\\t\u0026#34; \u0026lt;\u0026lt; pRect-\u0026gt;Right \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#39;\\t\u0026#39; \u0026lt;\u0026lt; pRect-\u0026gt;Bottom \u0026lt;\u0026lt; endl; } else { callback(pRect); } } 准备好了C++的部分,再来写C#部分的代码:\n首先定义一个和C++部分相同的MyRect结构体和函数指针类型delegate\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 对应C++部分的MyRect [StructLayout(LayoutKind.Sequential)] struct MyRect { public int Left; public int Top; public int Right; public int Bottom; } // 对应C++部分的 PRINTCALLBACK [UnmanagedFunctionPointer(CallingConvention.StdCall)] delegate void PrintRect(ref MyRect myRect); 这样准备工作就做完了,一定要保证C++部分和C#部分定义的数据类型和接口一致,不然调用时会出问题的。\n调用 导入C++动态库的函数入口\n1 2 [DllImport(\u0026#34;DynLib.dll\u0026#34;, EntryPoint = \u0026#34;#1\u0026#34;, CallingConvention = CallingConvention.Cdecl)] public static extern void PrintInCpp(ref MyRect pRect, PrintRect callback); 然后定义好C#这边的delegate实例,就完成了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void Print(ref MyRect myRect) { Console.WriteLine(\u0026#34;C# Print func called.\u0026#34;); Console.WriteLine(\u0026#34;[({0},{1}),({2},{3})]\u0026#34;, myRect.Left, myRect.Top, myRect.Right, myRect.Bottom); } static void Main() { var rect = new MyRect() { Left = 100, Top = 100, Right = 220, Bottom = 200 }; PrintInCpp(ref rect, null); // callback为null PrintInCpp(ref rect, new PrintRect(Print)); Console.WriteLine(\u0026#34;Press any key exit...\u0026#34;); Console.ReadKey(true); } 这里调用了两次接口函数,为了方便看出区别,在callback参数为NULL时,会由c++打印结果,否则c++会调用外部的函数接口。\n输出结果:\n1 2 3 4 5 6 7 ./delegatefunc 100 100 220 200 C# Print func called. [(100,100),(220,200)] Press any key exit... ","permalink":"https://kira-96.github.io/posts/%E4%BC%A0%E9%80%92%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E5%88%B0c__/","summary":"简介 在编码的时候难免会遇到不同编程语言之间的接口调用,其中最通用的就是C的动态链接库,几乎所有语言都可以调用C的接口函数。那么这种关系能否反","title":"C#传递函数指针到C++"},{"content":"常用图像像素相关的一些Tag Tag VR Keyword (0028,0002) US Samples Per Pixel (0028,0004) CS Photometric Interpretation (0028,0006) US Planar Configuration (0028,0010) US Rows (0028,0011) US Columns (0028,0100) US Bits Allocated (0028,0101) US Bits Stored (0028,0102) US High Bit (0028,0103) US Pixel Representation (7FE0,0010) OB/OW Pixel Data 相关Tag说明 Sample Per Pixel Samples per Pixel (0028,0002) is the number of separate planes in this image. One and three image planes are defined. Other numbers of image planes are allowed, but their meaning is not defined by this Standard.\nFor monochrome (gray scale) and palette color images, the number of planes is 1. For RGB and other three vector color models, the value of this Attribute is 3.\nSamples Per Pixel 指此图像中平面的个数。对于灰度图像,它的值为1,对于RGB等彩色图像,它的值为3。 听起来可能比较拗口,简单解释一下,对于灰度的图像,它只有一个灰度值,所以是1,而彩色的图像通常是由RGB三个通道混合而成,它的值为3。\nPhotometric Interpretation 指定解析图像像素的格式。个人比较习惯叫它图像类型,可以根据这个tag判断图像是灰度还是彩色图像。\nMONOCHROME1\n灰度图,最小值显示为白色,像素值越大就越黑。\nMONOCHROME2\n灰度图,最小值显示为黑色,像素值越大就越亮。这应该是最常用的格式了。\nPALETTE COLOR\n自带调色板的图像,显示出来是彩色的,所以属于彩图。当使用它时,Samples Per Pixel的值必须为1。并且必须要有RGB三种颜色的调色板查找表,它的像素值(Pixel Data)用于查找表。\nThe pixel value is used as an index into each of the Red, Blue, and Green Palette Color Lookup Tables (0028,1101-1103\u0026amp;1201-1203).\nRGB\n彩色图像,每个像素由RGB三种颜色组成,Samples Per Pixel值必须为3。\nYBR_FULL\n通过色度信号来表示颜色的格式,每个像素由一个亮度Y(luminance)和两个色度Cb(蓝色)、Cr(红色)组成。Samples Per Pixel值为3。 $$Y=+0.2990R+0.5870G+0.1140B$$ $$Cb=-0.1687R-0.3313G+0.5000B+128$$ $$Cr=+0.5000R-0.4187G-0.0813B+128$$\nYBR_FULL_422\n类似于YBR_FULL,通过色度信号来表示颜色的格式,每个像素点都有对应的亮度Y(luminance),每两个像素点采集一次色度信号,缺少的色度信息通过内插补点的方式运算得到。 Samples Per Pixel的值应该为3,Planar Configuration的值必须是0,像素存储的格式为:Y, Y, Cb, Cr, ...\nYBR_PARTIAL_422(Retired)\n类似于YBR_FULL_422,通过色度信号来表示颜色的格式,不过亮度和色度的计算方式和YBR_FULL的计算方式不同。 $$Y=+0.2568R+0.5041G+0.0979B+16$$ $$Cb=-0.1482R-0.2910G+0.4392B+128$$ $$Cr=+0.4392R-0.3678G-0.0714B+128$$\nYBR_PARTIAL_420\n类似于YBR_PARTIAL_422,通过色度信号来表示颜色的格式,不同的是,用4:2:2的采样方式时,行方向的色度信息会被丢掉一半,而4:2:0的采样方式,不仅会把行方向的色度信息丢掉一半,列方向的色度信息也会被丢掉一半。色度的采样(Cb,Cr)只有亮度Y(luminance)的 1/4。 Samples Per Pixel的值应该为3,Planar Configuration的值必须是0。\nYBR_ICT\nIrreversible Color Transformation.(不可逆颜色变换)\nYCbCr的计算方式和YBR_FULL一样。Y为0时表示黑色,Cb,Cr都为0时表示没有颜色。 JPEG 2000有损压缩的彩色图像。Samples Per Pixel的值应该为3,Planar Configuration的值必须是0。\nYBR_RCT\nReversible Color Transformation.(可逆颜色变换)\nJPEG 2000无损压缩的彩色图像。Samples Per Pixel的值应该为3。 从RGB转换到YBR_RCT $$Y=floor(\\frac{R+2G+B}{4})$$ $$Cb=B-G$$ $$Cr=R-G$$ 从YBR_RCT转换到RGB $$R=Cr+G$$ $$G=Y-floor(\\frac{Cb+Cr}{4})$$ $$B=Cb+G$$\n不再使用的格式\nHSV、ARGB、CMYK\nPlanar Configuration 指定颜色是按照像素来排列的或是按平面(plane)来排列的。当Samples Per Pixel大于1时应设定此值。 当值为0时表示颜色按像素排列。对于RGB图像,像素的格式为:R1,G1,B1,R2,G2,B2,... 当值为1时表示颜色按平面排列。对于RGB图像,像素的格式为:R1,R2,R3,...Rn,G1,G2,G3,...Gn,B1,B2,B3,...Bn\nRows 图像的行数量,即图像的高(Height)。\nColumns 图像的列数量,即图像的宽(Width)。\nBits Allocated, Bits Stored, High Bit, Pixel Representation Bits Allocated指定每个像素分配多少位(bit)。值应当为1或者8的倍数。而对于图像像素,Bits Allocated的值通常为8或者16,实际上可以理解为每个像素分配多少字节,因为是8的倍数。\nBits Stored指定存储每个像素占用了多少位(bit),值不能大于Bits Allocated。\nHigh Bit则指定了像素的最高位,通常应该是Bits Stored - 1。\nPixel Representation指定了像素数据的类型。值只能为0或者1,对于彩色图像,值只能为0。 值为0时,表示像素为无符号整型(unsigned integer)。 值为1时,表示像素为2的补码,其实就是有符号整型,即允许存在负数。 这里一定要注意,如果不能正确处理负数,全部按照无符号整型来计算的话,就会遇到符号位的问题,即一个负数会变成一个很大的正数,图像上原本是黑色的区域会变得很亮。\nPixel Data 图像像素。一堆二进制数字,通常会存放在Dicom文件最后的位置。\n最后 其实,在写这篇文章之前,一些东西我都还是一知半解的,在写的过程中我也是不断的在查阅资料和源码。其中一些东西难免会掺杂了自己的理解,如果有错误的地方欢迎指正。\n参考\nDICOM Standard Browser 颜色空间 ","permalink":"https://kira-96.github.io/posts/dicom%E5%9B%BE%E5%83%8F%E5%83%8F%E7%B4%A0%E7%9B%B8%E5%85%B3tag%E8%AF%B4%E6%98%8E/","summary":"常用图像像素相关的一些Tag Tag VR Keyword (0028,0002) US Samples Per Pixel (0028,0004) CS Photometric Interpretation (0028,0006) US Planar Configuration (0028,0010) US Rows (0028,0011) US Columns (0028,0100) US Bits Allocated (0028,0101) US Bits Stored (0028,0102) US High Bit (0028,0103) US Pixel Representation (7FE0,0010) OB/OW Pixel Data 相关Tag说明 Sample Per Pixel Samples per Pixel (0028,0002)","title":"DICOM图像像素相关Tag说明"},{"content":"文档 The Rust Programming Language Second edition\nRust 程序设计语言(第二版)简体中文版\nRust by Example\n通过例子学 Rust\nAsync programming in Rust with async-std\nasync-std 中文文档\n其它 Rust Language Cheat Sheet\nRust Fundamentals\nRust语言中文社区\n","permalink":"https://kira-96.github.io/posts/rust-%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%BA%90%E6%94%B6%E9%9B%86/","summary":"文档 The Rust Programming Language Second edition Rust 程序设计语言(第二版)简体中文版 Rust by Example 通过例子学 Rust Async programming in Rust with async-std async-std 中文文档 其它 Rust Language Cheat Sheet Rust Fundamentals Rust语言中文社区","title":"Rust 学习资源收集"},{"content":"前言 由于我是从事医疗行业软件开发的,所以必不可少的会和图像打交道,最近刚刚好在做一个图像旋转相关的功能,借此又复习(预习)了一下线性代数,趁着没忘赶紧做一下笔记。\nDICOM 中与方位计算有关的 Tag 在开始之前,有必要先了解一下DICOM中与图像方位计算有关的几个Tag,主要有3个。\nTag Keyword (0020,0032) Image Position (Patient) (0020,0037) Image Orientation (Patient) 其中Image Position指的是图像左上角的像素在患者坐标系中的位置。 Image Orientation由6个数字组成,分别是图像的行(Row)方向和列(Column)方向的单位向量与x/y/z坐标轴夹角的余弦值(cosine)。\n有了上面两个Tag的值,就可以计算出图像在空间坐标系中的位置和方位了。\n计算法向量 现在我们已经有了图像平面上两个垂直的向量,行和列方向的向量,使用行列式就能计算出图像所在平面的法向量了。\n$$u × v = \\left[\\begin{matrix} i \u0026amp; j \u0026amp; k \\\\ u_1 \u0026amp; u_2 \u0026amp; u_3 \\\\ v_1 \u0026amp; v_2 \u0026amp; v_3 \\end{matrix}\\right]$$\n具体怎么算,可以看这里,讲得很详细。\n代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 double[] vector1 = new[] {orientation[0], orientation[1], orientation[2]}; double[] vector2 = new[] {orientation[3], orientation[4], orientation[5]}; double[] normal = new double[] { 0, 0, 0 }; normal[0] = vector1[1] * vector2[2] - vector1[2] * vector2[1]; normal[1] = vector1[2] * vector2[0] - vector1[0] * vector2[2]; normal[2] = vector1[0] * vector2[1] - vector1[1] * vector2[0]; var temp = Math.Sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); normal[0] /= temp; normal[1] /= temp; normal[2] /= temp; 其中orientation就是Image Orientation中的6个值。计算得到的normal就是图像所在平面的法向量的单位向量。\n图像方位矩阵 在3D图形变换中经常使用的是4维矩阵,把图像的位置信息也放到矩阵中,可以方便的进行位置变换的计算。\n这里分别用u,v,w表示图像的行/列/法线方向向量,S代表图像位置。\n$$u=(u_1,u_2,u_3)$$ $$v=(v_1,v_2,v_3)$$ $$w=(w_1,w_2,w_3)$$ $$S=(s_x,s_y,s_z)$$\n4维矩阵则表示为\n$$matrix=\\left[\\begin{matrix} u_1 \u0026amp; v_1 \u0026amp; w_1 \u0026amp; s_x \\\\ u_2 \u0026amp; v_2 \u0026amp; w_2 \u0026amp; s_y \\\\ u_3 \u0026amp; v_3 \u0026amp; w_3 \u0026amp; s_z \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n图像旋转 先来看特殊情况下的旋转,即矩阵绕坐标轴的旋转。T为变换矩阵。\n绕X轴旋转\n$$T=\\left[\\begin{matrix} 1 \u0026amp; 0 \u0026amp; 0 \u0026amp; 0 \\\\ 0 \u0026amp; cos\\theta \u0026amp; -sin\\theta \u0026amp; 0 \\\\ 0 \u0026amp; sin\\theta \u0026amp; cos\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n绕Y轴旋转\n$$T=\\left[\\begin{matrix} cos\\theta \u0026amp; 0 \u0026amp; sin\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 1 \u0026amp; 0 \u0026amp; 0 \\\\ -sin\\theta \u0026amp; 0 \u0026amp; cos\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n绕Z轴旋转\n$$T=\\left[\\begin{matrix} cos\\theta \u0026amp; -sin\\theta \u0026amp; 0 \u0026amp; 0 \\\\ sin\\theta \u0026amp; cos\\theta \u0026amp; 0 \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 1 \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n直接用$matrix \\times T$就可以得到旋转后的矩阵了。\n但是图像旋转并不一定是绕坐标轴旋转,这里说图像旋转指的是在图像所在平面上的旋转,即图像矩阵绕法线旋转一定角度,不存在其它情况,所以需要一种更加通用的计算方法。角度的正负按右手定则决定。\n经测试,下面的方法并不通用,下面的旋转矩阵适用于点位置的变换,不适用于DICOM中的方位变换\n这里直接给出结果,图像矩阵绕向量$(u,v,w)$旋转$\\theta$的变换矩阵T。\n$$T=\\left[\\begin{matrix} u^2+(1-u^2)cos\\theta \u0026amp; u v(1-cos\\theta)-w sin\\theta \u0026amp; u w(1-cos\\theta)+v sin\\theta \u0026amp; 0 \\\\ u v(1-cos\\theta)+w sin\\theta \u0026amp; v^2+(1-v^2)cos\\theta \u0026amp; v w(1-cos\\theta)-u sin\\theta \u0026amp; 0 \\\\ u w(1-cos\\theta)-v sin\\theta \u0026amp; v w(1-cos\\theta)+u sin\\theta \u0026amp; w^2+(1-w^2)cos\\theta \u0026amp; 0 \\\\ 0 \u0026amp; 0 \u0026amp; 0 \u0026amp; 1 \\end{matrix}\\right]$$\n代码示例:\n这里用到了MatrixD,主要是用于矩阵的运算。\n这里其实是我想的复杂了,DICOM图像旋转的本质就是两个方向向量的旋转,只需要将两个方向向量绕法向量旋转即可。而这一切fo-dicom都已经为我们做好了。\n1 2 3 4 5 6 7 8 9 10 Vector3D forward = new Vector3D(new[] { orientation[0], orientation[1], orientation[2] }); Vector3D down = new Vector3D(new[] { orientation[3], orientation[4], orientation[5] }); Orientation3D orientation3D = new Orientation3D(forward, down); // 旋转,顺时针为正,逆时针为负 orientation3D.Pitch(angle * Math.PI / 180.0); // orientation3D.Forward // orientation3D.Down 实现起来很简单,浏览Pitch的源码就会发现,其实就是将两个向量绕Right向量(即法向量)旋转,得到新的Forward和Down就是旋转后图像的方位信息。\n这里借一张图来说明问题:\n在创建Orientation3D时的参数Forward和Down就是图像的行方向和列方向的方向向量,Pitch方法将Forward和Down向量旋转一个角度,得到的就是旋转后图像的行和列的方向向量。\n图像翻转 翻转后的方位计算则比较简单,如果是水平翻转,只需要将水平(Row)方向的向量反向即可,竖直翻转将竖直(Column)方向的向量反向即可。\n向量反向只需要将 (u,v,w) 3个值前面添加负号即可。\n1 2 3 4 5 6 7 8 9 10 11 12 // 水平翻转 var newOrientation = new double[6] { -orientation[0], -orientation[1], -orientation[2], orientation[3], orientation[4], orientation[5] }; // 竖直翻转 var newOrientation = new double[6] { orientation[0], orientation[1], orientation[2], -orientation[3], -orientation[4], -orientation[5] }; 不过,像这么奇葩的功能应该不会有人去用吧。\n注意\n对图像矩阵进行旋转或者翻转操作之后,由于图像左上角的像素已经发生变化,所以原有的位置信息也已经改变,需要重新计算才能保证图像在空间中处于正确的位置。对于图像翻转来说或许能够轻易计算出来,不过旋转之后的图像却比较难计算了。\n最后,附上一段计算图像方位的代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // [R] Right - 沿着X方向递减 // [L] Left - 沿着X方向递增 // [A] Anterior - 沿着Y方向递减 // [P] Posterior - 沿着Y方向递增 // [F] Feet - 沿着Z方向递减 // [H] Head - 沿着Z方向递增 static string ComputeOrientation(Vector3D vector) { char x = vector.X \u0026lt; 0 ? \u0026#39;R\u0026#39; : \u0026#39;L\u0026#39;; char y = vector.Y \u0026lt; 0 ? \u0026#39;A\u0026#39; : \u0026#39;P\u0026#39;; char z = vector.Z \u0026lt; 0 ? \u0026#39;F\u0026#39; : \u0026#39;H\u0026#39;; double x1 = Math.Abs(vector.X); double y1 = Math.Abs(vector.Y); double z1 = Math.Abs(vector.Z); string result = \u0026#34;\u0026#34;; for (int i = 0; i \u0026lt; 3; i++) { if (x1 \u0026gt; 0.0001 \u0026amp;\u0026amp; x1 \u0026gt; y1 \u0026amp;\u0026amp; x1 \u0026gt; z1) { result += x; x1 = 0; } else if (y1 \u0026gt; 0.0001 \u0026amp;\u0026amp; y1 \u0026gt; x1 \u0026amp;\u0026amp; y1 \u0026gt; z1) { result += y; y1 = 0; } else if (z1 \u0026gt; 0.0001 \u0026amp;\u0026amp; z1 \u0026gt; x1 \u0026amp;\u0026amp; z1 \u0026gt; y1) { result += z; z1 = 0; } else { break; } } return result; } 这里用到了Vector3D,表示一个3维向量,可以使用数组代替。\n参考\n知乎:如何理解线性代数?\n行列式,快速求出法向量\nMRI的DICOM图像方位算法的研究\n三维空间几何变换矩阵\n图形学 位移,旋转,缩放矩阵变换\nDICOM中几个判断图像方向的tag\nDICOM Standard Browser\n","permalink":"https://kira-96.github.io/posts/%E4%B8%89%E7%BB%B4%E5%9B%BE%E5%BD%A2%E7%9F%A9%E9%98%B5%E5%8F%98%E6%8D%A2/","summary":"前言 由于我是从事医疗行业软件开发的,所以必不可少的会和图像打交道,最近刚刚好在做一个图像旋转相关的功能,借此又复习(预习)了一下线性代数,趁","title":"三维图形矩阵变换"},{"content":"前言 由于我很少用MFC,只有工作上需要的时候才会用到,所以我也是个新手,遇到问题需要到网上找很久资料。这里只是记录一些特殊情景下会用到的技巧,方便以后查找。\n隐藏窗口任务栏图标 这个问题我在网上找了很久,大致有两种方案:\n修改窗口的扩展样式\n1 ModifyStyleEx(WS_EX_APPWINDOW,WS_EX_TOOLWINDOW); 但是这样做会导致整个窗口的样式会变得很难看。\n将一个隐藏窗口设置成主窗口的父窗口\n这样做比较麻烦,而且任务视图下也不能再看到窗口,显然不是我想要的效果。\n最后终于找到了一个比较完美的解决方案,通过COM的方式移除任务栏列表中的图标。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 显示/隐藏任务栏图标(COM方式) bool ShowInTaskbar(HWND hWnd, bool isShow) { CoInitialize(nullptr); ITaskbarList* pTaskbarList; HRESULT hr = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_ITaskbarList, (void**)\u0026amp;pTaskbarList); if (SUCCEEDED(hr)) { pTaskbarList-\u0026gt;HrInit(); if (isShow) { pTaskbarList-\u0026gt;AddTab(hWnd); } else { pTaskbarList-\u0026gt;DeleteTab(hWnd); } CoUninitialize(); return true; } CoUninitialize(); return false; } 程序启动时默认隐藏窗口 一种比较简单的方法是在程序启动时将窗口移动到屏幕外的不可见区域。 如果直接在OnInitDialog中设置ShowWindow(SW_HIDE)是无效的,因为此时窗口还没有显示出来,自然也无法隐藏。\n这里的思路是在程序启动的时候先将窗口移动到屏幕外,然后通过另一个线程将窗口隐藏起来,这样做虽然程序启动后窗口还是会一闪即逝,但由于是在屏幕之外,实际上并不能看到,然后在窗口需要显示的时候调用ShowWindow(SW_SHOW)即可。\n1 2 3 4 5 6 7 8 9 10 11 12 // CxxxDlg.h 头文件 #include \u0026lt;future\u0026gt; class CxxxDlg : public CDialogEx { ... ... ... private: std::future\u0026lt;int\u0026gt; hideTask; // 后台隐藏窗口线程 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 BOOL CxxxDlg::OnInitDialog() { ... CRect rcClient; GetWindowRect(\u0026amp;rcClient); // 将窗口移动到屏幕外 MoveWindow(-rcClient.Width(), rcClient.top, rcClient.Width(), rcClient.Height()); // 新建线程,延时1s后隐藏窗口 hideTask = std::async( std::launch::async, [\u0026amp;] { std::this_thread::sleep_for(std::chrono::seconds(1)); // ShowWindow(SW_HIDE); ShowWindowAsync(m_hWnd, SW_HIDE); std::this_thread::sleep_for(std::chrono::seconds(1)); // 延时1s,待窗口完全隐藏后再将窗口居中 CenterWindow(); return 0; }); ... return TRUE; // 除非将焦点设置到控件,否则返回 TRUE } 点击关闭时隐藏窗口 有时候我们希望在点击主窗口的关闭按钮之后将窗口隐藏或者最小化,而不是退出程序。这时只需要拦截掉窗口的关闭消息即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // CxxxDlg.h 头文件 class CxxxDlg : public CDialogEx { ... protected: afx_msg void OnSysCommand(UINT nID, LPARAM lParam); ... } void CxxxDlg::OnSysCommand(UINT nID, LPARAM lParam) { ///////////////////////////////////// // 这里捕获窗口的关闭消息 // 不直接关闭窗口,而是隐藏起来 if (nID == SC_CLOSE) { ShowWindowAsync(m_hWnd, SW_HIDE); } ///////////////////////////////////// else { CDialogEx::OnSysCommand(nID, lParam); } } 这样就可以拦截掉窗口的关闭消息了,这样不仅仅是点击关闭按钮时会隐藏窗口,通过窗口菜单关闭窗口或是使用Alt+F4都不能真正关闭窗口。那么我怎样才能退出程序呢,总不能用任务管理器吧。\n其实很简单,在需要退出程序的时候向窗口发送WM_CLOSE消息即可。\n1 SendMessage(WM_CLOSE); CFileDialog导致CDialogEx“失去焦点”的解决方法 继承自CDialogEx的窗口在使用CFileDialog之后会导致窗口标题栏变成灰色,很像是窗口失去了焦点,此时窗口仍然能够正常操作,但即使鼠标点击在窗口上窗口的标题栏依旧是灰色,无法恢复到窗口激活状态的颜色,即使将窗口属性设置为WS_EX_TOPMOST(置顶)依旧是这样,必须点击窗口外的其它区域或者使用Tab切换一下才能恢复正常。\n而继承自CDialog的窗口则没有这个问题,可以将窗口的基类改成CDialog来避免这个问题。对于我这样的强迫症来说是无法忍受的,所以用尽千方百计终于找到了一个可行的解决方案。\n经过尝试,单纯让窗口获取焦点或者将窗口放到前台的方法都是无效的。\n1 2 3 4 5 6 7 8 9 10 11 12 CFileDialog dlg( TRUE, _T(\u0026#34;ini\u0026#34;), nullptr, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T(\u0026#34;ini(*.ini)|*.ini|TEXT(*.txt)|*.txt|\u0026#34;), this); auto dlgResult = dlg.DoModal(); // 无效的方法 this-\u0026gt;SetFocus(); this-\u0026gt;SetForegroundWindow(); ... 所以只能曲线救国,既然通过手动切换的方式可以恢复到正常状态,不妨先切换到其它窗口再切换回来。\n1 2 3 4 5 6 7 8 9 10 11 12 CFileDialog dlg( TRUE, _T(\u0026#34;ini\u0026#34;), nullptr, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T(\u0026#34;ini(*.ini)|*.ini|TEXT(*.txt)|*.txt|\u0026#34;), this); auto dlgResult = dlg.DoModal(); // 先将焦点放到桌面,再切换回本窗口 ::SetForegroundWindow(::GetDesktopWindow()); this-\u0026gt;SetForegroundWindow(); ... 完美解决了问题。\n窗口启用视觉样式 启用视觉样式之后,可以让程序看起来更加现代化一些,只支持Window XP以后的系统。 可以通过为程序添加清单文件来实现,不过比较麻烦。在VC++ 2005之后,直接添加编译器指令到代码中就可以了。\n在预编译头文件中添加下面代码即可。\n1 2 3 4 5 6 7 8 9 #if defined _M_IX86 #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;x86\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #elif defined _M_IA64 #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;ia64\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #elif defined _M_X64 #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;amd64\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #else #pragma comment(linker,\u0026#34;/manifestdependency:\\\u0026#34;type=\u0026#39;win32\u0026#39; name=\u0026#39;Microsoft.Windows.Common-Controls\u0026#39; version=\u0026#39;6.0.0.0\u0026#39; processorArchitecture=\u0026#39;*\u0026#39; publicKeyToken=\u0026#39;6595b64144ccf1df\u0026#39; language=\u0026#39;*\u0026#39;\\\u0026#34;\u0026#34;) #endif 任务栏显示进度 为窗口的任务栏图标添加进度显示,也可以为任务栏添加按钮。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ITaskbarList3* pTaskbar; // 初始化COM组件 CoInitialize(NULL); CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(\u0026amp;pTaskbar)); // TBPF_NOPROGRESS\t= 0, // 正常状态,不显示进度 // TBPF_INDETERMINATE\t= 0x1, // 忙碌状态,不显示进度 // TBPF_NORMAL\t= 0x2, // 正常状态,显示进度(绿色) // TBPF_ERROR\t= 0x4, // 错误状态,显示进度(红色) // TBPF_PAUSED\t= 0x8 // 停止状态,显示进度(黄色) pTaskbar-\u0026gt;SetProgressState(GetSafeHwnd(), TBPF_NORMAL); pTaskbar-\u0026gt;SetProgressValue(GetSafeHwnd(), 60, 100); // 设置提示信息 pTaskbar-\u0026gt;SetThumbnailTooltip(GetSafeHwnd(), TEXT(\u0026#34;Tooltip\u0026#34;)); // 设置覆盖图标 HICON hIcon = AfxGetApp()-\u0026gt;LoadIcon(IDI_ICON_ERR); pTaskbar-\u0026gt;SetOverlayIcon(GetSafeHwnd(), hIcon, _T(\u0026#34;Error\u0026#34;)); // 添加任务栏按钮 THUMBBUTTONMASK dwMask = THB_ICON | THB_TOOLTIP; THUMBBUTTON buttons[3]; buttons[0].iId = 0; buttons[0].dwMask = dwMask; buttons[0].hIcon = hIcon; memcpy(buttons[0].szTip, TEXT(\u0026#34;Tooltip\u0026#34;), sizeof(buttons[0].szTip)); // ... pTaskbar-\u0026gt;ThumbBarAddButtons(GetSafeHwnd(), 3, buttons); // 最后释放COM组件 CoUninitialize(); 未完待续,持续更新中\u0026hellip; 参考\nMFC简单的启动时隐藏界面方式(仅启动时隐藏)\nEnabling Visual Styles\n","permalink":"https://kira-96.github.io/posts/%E4%B8%80%E4%BA%9B%E5%9F%BA%E6%9C%AC%E6%B2%A1%E4%BB%80%E4%B9%88%E7%94%A8%E7%9A%84mfc%E6%8A%80%E5%B7%A7/","summary":"前言 由于我很少用MFC,只有工作上需要的时候才会用到,所以我也是个新手,遇到问题需要到网上找很久资料。这里只是记录一些特殊情景下会用到的技巧","title":"一些基本没什么用的MFC技巧"},{"content":"前言 最近公司的系统也开始陆续向 Windows 10 迁移了,我的办公电脑也终于换上了新系统。为了适应新的开发环境,有时候需要获取一些系统相关的信息,这里就稍微总结一下。\n这篇文章并不只是写如何获取Win10系统主题色,也会包含一些其它的内容,主要是用C++和C#语言,可能会持续更新。\n检测是否为 Windows10 系统 C++ 方式\n在Win10上已经不能直接通过GetVersion或GetVersionEx的方式获取系统信息,单纯使用这两个函数编译时会报错。 这里提供另外一种方式:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool IsWindows10() { typedef void(__stdcall*NTPROC)(DWORD*, DWORD*, DWORD*); HMODULE inst = LoadLibrary(\u0026#34;ntdll.dll\u0026#34;); NTPROC ntProc = reinterpret_cast\u0026lt;NTPROC\u0026gt;(GetProcAddress(inst, \u0026#34;RtlGetNtVersionNumbers\u0026#34;)); DWORD dwMajor, dwMinor, dwBuildNumber; ntProc(\u0026amp;dwMajor, \u0026amp;dwMinor, \u0026amp;dwBuildNumber); FreeLibrary(inst); return dwMajor == 10; } 当然上面的方法也适用于C#,但这里使用C#的方法来检测系统版本。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /// \u0026lt;summary\u0026gt; /// Gets if the Operating System is Windows 10 /// \u0026lt;/summary\u0026gt; /// \u0026lt;returns\u0026gt;True if Windows 10\u0026lt;/returns\u0026gt; public static bool IsWindows10 { get { // IMPORTANT: Windows 8.1. and Windows 10 will ONLY admit their real version if your program\u0026#39;s manifest // claims to be compatible. Otherwise they claim to be Windows 8. See the first comment on: // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724833%28v=vs.85%29.aspx // Get Operating system information OperatingSystem os = Environment.OSVersion; // Get the Operating system version information Version vi = os.Version; // Pre-NT versions of Windows are PlatformID.Win32Windows. We\u0026#39;re not interested in those. if (os.Platform == PlatformID.Win32NT) { if (vi.Major == 10) { return true; } } return false; } } 使用上面的方法时需要注意,首先要为你的程序添加清单文件(app.manifest),并且取消对Windows 10系统兼容的注释。 如:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ... \u0026lt;compatibility xmlns=\u0026#34;urn:schemas-microsoft-com:compatibility.v1\u0026#34;\u0026gt; \u0026lt;application\u0026gt; \u0026lt;!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的 Windows 版本的列表。取消评论适当的元素, Windows 将自动选择最兼容的环境。 --\u0026gt; \u0026lt;!-- Windows Vista --\u0026gt; \u0026lt;!--\u0026lt;supportedOS Id=\u0026#34;{e2011457-1546-43c5-a5fe-008deee3d3f0}\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!-- Windows 7 --\u0026gt; \u0026lt;supportedOS Id=\u0026#34;{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\u0026#34; /\u0026gt; \u0026lt;!-- Windows 8 --\u0026gt; \u0026lt;!--\u0026lt;supportedOS Id=\u0026#34;{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!-- Windows 8.1 --\u0026gt; \u0026lt;!--\u0026lt;supportedOS Id=\u0026#34;{1f676c76-80e1-4239-95bb-83d0f6d0da78}\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!-- Windows 10 --\u0026gt; \u0026lt;supportedOS Id=\u0026#34;{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\u0026#34; /\u0026gt; \u0026lt;/application\u0026gt; \u0026lt;/compatibility\u0026gt; ... 是否为 Win7 以下版本\n1 2 3 4 5 6 7 8 9 10 11 12 public static bool IsWindows7OrLower { get { Version v = Environment.OSVersion.Version; int versionMajor = v.Major; int versionMinor = v.Minor; double version = versionMajor + (double)versionMinor / 10; return version \u0026lt;= 6.1; } } 获取 Window 10 主题色(Accent Color) 在UWP中可以轻易的获取SystemAccentColor。但随着Win10移动端的失利,UWP基本已被微软宣告死亡。 这里就讲一下如何通过其它方式读取到系统的主题色。\n在Win10上,当系统的主题色发生变化时,系统会给所有窗口都发送主题色变更的消息\n1 #define WM_DWMCOLORIZATIONCOLORCHANGED 0x0320 1 2 3 4 5 6 7 8 9 10 11 12 13 LRESULT CxxxDlg::OnColorizationColorChanged(WPARAM wParam, LPARAM lParam) { DWORD color = 0; BOOL opaque = FALSE; HRESULT hr = DwmGetColorizationColor(\u0026amp;color, \u0026amp;opaque); if (SUCCEEDED(hr)) { // Update the application to use the new color. } return 0; } 但我使用之后发现,并不能正确的获取到系统的主题颜色,而是DWM颜色。 所以这里使用了另一种方式,通过读取注册表的方式获取系统主题色。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\DWM\\AccentColor public static Color GetSystemAccentColor() { using (RegistryKey dwm = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\DWM\u0026#34;, false)) { if (dwm.GetValueNames().Contains(\u0026#34;AccentColor\u0026#34;)) { // 这里不要尝试转换成uint,因为有可能符号位为 1(负数),会导致强制转换错误 // 直接进行下面的位操作即可 int accentColor = (int)dwm.GetValue(\u0026#34;AccentColor\u0026#34;); // 注意:读取到的颜色为 AABBGGRR return Color.FromArgb( (byte)((accentColor \u0026gt;\u0026gt; 24) \u0026amp; 0xFF), (byte)(accentColor \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 8) \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 16) \u0026amp; 0xFF)); } } return SystemParameters.WindowGlassColor; // 近似的系统主题色 } 以上的方式是通过读取注册表的方式,所以理论上任何语言都是通用的,再结合WM_DWMCOLORIZATIONCOLORCHANGED消息,就可以完美做到程序跟随系统主题色。\n同样的,也可以通过读取注册表的方式获取到DWM颜色。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\DWM\\ColorizationColor public static Color GetColorizationColor() { using (RegistryKey dwm = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\DWM\u0026#34;, false)) { if (dwm.GetValueNames().Contains(\u0026#34;ColorizationColor\u0026#34;)) { int accentColor = (int)dwm.GetValue(\u0026#34;ColorizationColor\u0026#34;); // 注意:读取到的颜色为 AARRGGBB return Color.FromArgb( (byte)((accentColor \u0026gt;\u0026gt; 24) \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 16) \u0026amp; 0xFF), (byte)((accentColor \u0026gt;\u0026gt; 8) \u0026amp; 0xFF), (byte)(accentColor \u0026amp; 0xFF)); } } return SystemParameters.WindowGlassColor; } 亮色主题还是暗色主题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\\AppsUseLightTheme // 应用是亮色还是暗色 public static bool AppsUseLightTheme() { using (RegistryKey personalize = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\u0026#34;, false)) { if (personalize.GetValueNames().Contains(\u0026#34;AppsUseLightTheme\u0026#34;)) { return (int)personalize.GetValue(\u0026#34;AppsUseLightTheme\u0026#34;) == 1; } } return true; } 对于较高版本的Win10,系统也可以设置亮色/暗色模式,我使用的版本暂且不支持。\n需要读取注册表的路径为\\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\\SystemUsesLightTheme。 读取方式应该和上面差别不大。\n主题色是否应用到窗口标题栏和边框 如果没有在系统个性化设置中设置将主题色应用到窗口标题栏和边框时,Win10窗口的标题栏和边框会始终为白色(对应亮色模式)或黑色(对应暗色模式)。 设置了将主题色应用到窗口标题栏和边框后,窗口的标题栏会跟随系统主题色的变化而变化。 那么我们怎样知道用户是怎样设置的呢?依旧是通过读取注册表的方式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // \\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\DWM\\ColorPrevalence public static bool IsWindowPrevalenceAccentColor() { using (RegistryKey dwm = Registry.CurrentUser.OpenSubKey(@\u0026#34;Software\\Microsoft\\Windows\\DWM\u0026#34;, false)) { if (dwm.GetValueNames().Contains(\u0026#34;ColorPrevalence\u0026#34;)) { int colorPrevalence = (int)dwm.GetValue(\u0026#34;ColorPrevalence\u0026#34;); return colorPrevalence == 1; } } return false; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // c++ 读取注册表 bool IsWindowPrevalenceAccentColor() { LPCTSTR dwm = _T(\u0026#34;Software\\\\Microsoft\\\\Windows\\\\DWM\u0026#34;); HKEY hDwmKey; if (ERROR_SUCCESS != RegOpenKeyEx(HKEY_CURRENT_USER, dwm, 0, KEY_READ, \u0026amp;hDwmKey)) { return false; } DWORD type = REG_DWORD; DWORD value = 0; DWORD cbData = 4; if (ERROR_SUCCESS != RegQueryValueEx(hDwmKey, _T(\u0026#34;ColorPrevalence\u0026#34;), nullptr, \u0026amp;type, (LPBYTE)\u0026amp;value, \u0026amp;cbData)) { RegCloseKey(hDwmKey); return false; } RegCloseKey(hDwmKey); return value == 1; } 根据背景色计算前景色 还有一个遗留的问题就是,窗口的标题栏颜色跟随系统主题色变化时,标题文字的颜色也会动态变换成白色或者黑色,至于什么情况下是白色,什么时候是黑色,暂时还没研究出来。\n最后在网上找到了一些算法,通过背景的颜色计算出前景色,效果还是很理想的。\n方法1:\n1 2 3 4 5 6 7 8 9 10 11 /// \u0026lt;summary\u0026gt; /// 根据背景色计算前景色(白/黑) /// https://github.com/loilo/windows-titlebar-color/blob/master/WindowsAccentColors.js#L53 /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;background\u0026#34;\u0026gt;背景颜色\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;前景颜色(白/黑)\u0026lt;/returns\u0026gt; public static Color GetForegroundColor(Color background) { return (background.R * 2 + background.G * 5 + background.B) \u0026lt;= 1024 /* 8*128 */ ? Colors.White : Colors.Black; } 方法2:\n1 2 3 4 5 6 7 8 9 10 11 12 /// \u0026lt;summary\u0026gt; /// 计算能在任何背景色上清晰显示的前景色 /// https://www.cnblogs.com/walterlv/p/10236517.html /// \u0026lt;/summary\u0026gt; /// \u0026lt;param name=\u0026#34;background\u0026#34;\u0026gt;背景颜色\u0026lt;/param\u0026gt; /// \u0026lt;returns\u0026gt;前景颜色(黑/白)\u0026lt;/returns\u0026gt; public static Color GetReverseForegroundColor(Color background) { double grayLevel = (0.299 * background.R + 0.587 * background.G + 0.114 * background.B) / 255; return grayLevel \u0026gt; 0.5 ? Colors.Black : Colors.White; } 持续更新中\u0026hellip;\n参考\nC++ 获取并判断操作系统版本,解决Win10、 Windows Server 2012 R2 读取失败的方案\nwpf/winform获取windows10系统颜色和主题色\nWM_DWMCOLORIZATIONCOLORCHANGED message\nDwmGetColorizationColor function\n分享一个算法,计算能在任何背景色上清晰显示的前景色\n","permalink":"https://kira-96.github.io/posts/%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96windows10%E4%B8%BB%E9%A2%98%E9%A2%9C%E8%89%B2/","summary":"前言 最近公司的系统也开始陆续向 Windows 10 迁移了,我的办公电脑也终于换上了新系统。为了适应新的开发环境,有时候需要获取一些系统相关的信息,这里就稍微","title":"如何获取Windows10主题颜色"},{"content":"前言 经常看到其它程序在最小化或者窗口隐藏后依旧会在通知栏显示一个托盘图标,像微信、QQ之类的,即使主窗口不显示,程序并不会退出,依旧可以通过托盘图标进行操作。 那么怎样为自己的程序添加一个托盘图标呢?这次就来讲一讲。\n实现(C#) 首先,为项目添加引用System.Windows.Forms。\n这里以WPF为例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 // 主窗口的后台代码 // MainWindow.xaml.cs using System.Windows; using System.Windows.Forms; public partial class MainWindow : Window { private NotifyIcon notifyIcon; // 托盘图标 // 托盘菜单 private System.Windows.Controls.ContextMenu trayIconContextMenu; public MainWindow() { InitializeComponent(); InitializeTrayIcon(); } private void InitializeTrayIcon() { notifyIcon = new NotifyIcon() { Visible = true, Text = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, // 使用嵌入的资源 Icon = new System.Drawing.Icon( System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(\u0026#34;AppNamespace.icon.ico\u0026#34;), System.Windows.Forms.SystemInformation.SmallIconSize) }; notifyIcon.MouseClick += TrayIconMouseClick; // notifyIcon.MouseDoubleClick += TrayIconMouseDoubleClick; trayIconContextMenu = (System.Windows.Controls.ContextMenu)FindResource(\u0026#34;TrayIconContextMenu\u0026#34;); } private void Window_Deactivated(object s, System.EventArgs e) { trayIconContextMenu.IsOpen = false; } private void TrayIconMouseClick(object s, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { // Open the Notify icon context menu trayIconContextMenu.IsOpen = true; // Required to close the Tray icon when Deactivated is called // See: http://copycodetheory.blogspot.be/2012/07/notify-icon-in-wpf-applications.html Activate(); } } } 需要注意的是NotifyIcon的Icon属性必须设置,否则就不能显示托盘图标。而且图标必须是.ico格式,其它格式png,jpg等都是不行的。 我这里是使用的嵌入的资源,也可以使用文件路径的方式:\n1 Icon = new System.Drawing.Icon(\u0026#34;icon.ico\u0026#34;); 上面代码也没有使用NotifyIcon的ContextMenu属性,而是使用的WPF中的ContextMenu,直接在鼠标右键单击托盘图标的时候将trayIconContextMenu显示出来。\n下面是trayIconContextMenu的定义:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!-- MainWindow.xaml --\u0026gt; \u0026lt;Window.Resources\u0026gt; \u0026lt;ContextMenu x:Key=\u0026#34;TrayIconContextMenu\u0026#34; Placement=\u0026#34;MousePoint\u0026#34;\u0026gt; \u0026lt;MenuItem Header=\u0026#34;Show Window\u0026#34; ToolTip=\u0026#34;show main window\u0026#34; Click=\u0026#34;MenuItemShowClick\u0026#34;\u0026gt; \u0026lt;MenuItem.Icon\u0026gt; \u0026lt;Image Source=\u0026#34;pack://application:,,,/Icons/window.png\u0026#34; /\u0026gt; \u0026lt;/MenuItem.Icon\u0026gt; \u0026lt;/MenuItem\u0026gt; \u0026lt;Separator /\u0026gt; \u0026lt;MenuItem Header=\u0026#34;Exit\u0026#34; ToolTip=\u0026#34;exit\u0026#34; Click=\u0026#34;MenuItemExitClick\u0026#34;\u0026gt; \u0026lt;MenuItem.Icon\u0026gt; \u0026lt;Image Source=\u0026#34;pack://application:,,,/Icons/exit.png\u0026#34; /\u0026gt; \u0026lt;/MenuItem.Icon\u0026gt; \u0026lt;/MenuItem\u0026gt; \u0026lt;/ContextMenu\u0026gt; \u0026lt;/Window.Resources\u0026gt; 通过上面的代码就可以为应用程序添加一个系统托盘图标了。\n系统托盘图标还有一个功能就是可以在通知栏显示通知。\n1 notifyIcon.ShowBalloonTip(0, \u0026#34;消息\u0026#34;, \u0026#34;程序正在运行\u0026#34;, ToolTipIcon.Info); 第一个参数是显示超时,不过现在已经没用了。\ntimeout: 气球状提示应显示的时间段,以毫秒为单位。从 Windows Vista 开始,此参数已被否决。 通知显示时间现在基于系统的辅助功能设置。\nBalloonTip在Win7上显示为气球状提示,在Win10上显示为Toast通知。\n其它 经过本人测试,NotifyIcon在控制台程序上也是可以使用的,遗憾的是一部分功能不能正常使用。 BalloonTip可以正常显示。但菜单是无法使用的,因为不能触发鼠标事件。\n","permalink":"https://kira-96.github.io/posts/%E4%B8%BA%E7%A8%8B%E5%BA%8F%E6%B7%BB%E5%8A%A0%E9%80%9A%E7%9F%A5%E6%A0%8F%E5%9B%BE%E6%A0%87/","summary":"前言 经常看到其它程序在最小化或者窗口隐藏后依旧会在通知栏显示一个托盘图标,像微信、QQ之类的,即使主窗口不显示,程序并不会退出,依旧可以通过","title":"为程序添加通知栏图标"},{"content":"简介 通常来说,应用程序可以启动任意数目的实例,前提是你的电脑内存足够大,想启动多少都可以。但有时候我们只希望程序同时只有一个实例在运行,应用程序不会重复运行。这就是应用程序的单例模式。\n实现(WPF) 当然,实现的方法不是唯一的。\n可以在程序启动的时候查找应用程序的窗口名字,如果查找的结果不为空,就说明已经有一个实例正在运行,但如果你的程序碰巧和其它程序的窗口名字重复就不好说了。\n也可以在程序启动时获取应用程序进程的名字,然后再查找系统所有进程,看是否有重复的,原理和上一种类似。\n这里要讲的并不是上面两种,而是通过Mutex来实现,使用比前两种更加简单和有效。\nSystem.Threading.Mutex官方说明是可用于进程间同步的同步基元。。\n使用起来也很简单,重写应用程序的OnStartup方法:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // App.xaml.cs /// \u0026lt;summary\u0026gt; /// App.xaml 的交互逻辑 /// \u0026lt;/summary\u0026gt; public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { Mutex mutex = new Mutex(true, \u0026#34;MutexName\u0026#34;, out bool createNew); if (createNew) { base.OnStartup(e); } else { MessageBox.Show(\u0026#34;程序已在运行中。\u0026#34;, \u0026#34;提示\u0026#34;, MessageBoxButton.OK, MessageBoxImage.Information); Application.Current.Shutdown(); } } } 注意Mutex的第2个参数,可以是任意字符串,越复杂越好,避免和其它程序冲突。 如果createNew为false,就说明已经有一个实例正在运行了,直接退出当前程序。\n注意:必须保证Mutex在程序运行过程中不被垃圾回收,否则就失效了。\n上面的写法是可以的,但如果我们使用的是Caliburn.Micro或者Stylet等框架,程序启动时都是通过Bootstrapper的,在Bootstrapper中的写法会有一些不同。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 使用 Stylet public class Bootstrapper : Bootstrapper\u0026lt;ShellViewModel\u0026gt; { /// \u0026lt;summary\u0026gt; /// 必须定义在类内部,一旦被释放就无效了 /// \u0026lt;/summary\u0026gt; private Mutex mutex; protected override void OnStart() { mutex = new Mutex(true, MUTEX_NAME, out bool createNew); if (!createNew) { MessageBox.Show(\u0026#34;程序已在运行中。\u0026#34;, \u0026#34;提示\u0026#34;, MessageBoxButton.OK, MessageBoxImage.Information); // 退出当前应用程序 // 尽量不要使用 // Application.Shutdown(); // 因为在这里使用会触发主窗口的Closing事件 System.Environment.Exit(0); } base.OnStart(); } } 需要注意的是要将Mutex定义在类的内部,如果定义在OnStartup函数体内,那么在程序运行时它就失效了,就不能使程序以单例模式运行了。\n如果你想要写在其它地方,可以将Mutex定义为static,这样就可以保证它在应用程序运行过程中不会失效了。\n还有就是在这里尽量使用System.Environment.Exit(0)而不是Application.Current.Shutdown(),因为我发现这里会触发主窗口的Closing事件,如果你不希望触发它,那么就使用Environment.Exit(0)。\n补充 关于应用程序单例模式还可以通过WindowsFormsApplicationBase来实现,但我也没试过。使用Mutex已经足够满足要求了。\n最后再加一项功能,我想要在当前程序检测到已经存在正在运行的实例时,退出并激活已经存在的程序窗口。\n这一点可以通过Windows的API来实现,思路就是查找应用程序的主窗口,并将其激活。代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [DllImport(\u0026#34;user32.dll\u0026#34;, EntryPoint = \u0026#34;FindWindow\u0026#34;)] public static extern IntPtr FindWindow(string classname, string windowname); [DllImport(\u0026#34;user32.dll\u0026#34;, EntryPoint = \u0026#34;ShowWindow\u0026#34;)] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport(\u0026#34;user32.dll\u0026#34;, EntryPoint = \u0026#34;SetForegroundWindow\u0026#34;)] public static extern bool SetForegroundWindow(IntPtr hWnd); public const int SW_NORMAL = 1; public const int SW_RESTORE = 9; public static void FindWindowAndActive(string classname, string windowname) { IntPtr hWnd = FindWindow(classname, windowname); ShowWindow(hWnd, SW_NORMAL); SetForegroundWindow(hWnd); } 在应用程序退出之前调用FindWindowAndActive函数即可。\n1 2 3 4 ... FindWindowAndActive(null, \u0026#34;Main Window\u0026#34;); // 激活已经存在的实例窗口 System.Environment.Exit(0); // 退出当前应用程序 ... ","permalink":"https://kira-96.github.io/posts/%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","summary":"简介 通常来说,应用程序可以启动任意数目的实例,前提是你的电脑内存足够大,想启动多少都可以。但有时候我们只希望程序同时只有一个实例在运行,应用","title":"应用程序单例模式"},{"content":"虽然平时很少会用到命令行参数,但有时候可以使用命令行参数来使程序执行不同的行为。\n在写控制台程序的时候,我们可以直接得到程序命令行参数。\n1 2 3 4 static void Main(string[] args) { // args 就是命令行参数 } 那么如果不是控制台程序如何获取命令行参数呢?\n在 WPF 中有两种方法获取命令行参数\n第一种方法是重写应用的OnStartup方法,通过StartupEventArgs来获取命令行参数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // App.xaml.cs /// \u0026lt;summary\u0026gt; /// App.xaml 的交互逻辑 /// \u0026lt;/summary\u0026gt; public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // 获取命令行参数 string[] args = e.Args; // do something } } 如果没有命令行参数,那么args就为null。\n第二种方法则比较灵活,可以在任意地方获取到命令行参数。\n1 string[] args = System.Environment.GetCommandLineArgs(); 直接使用Environment的静态方法来获取命令行参数,需要注意的是,第二种方法获取到的参数和前面一种方法结果不同。\n第二种方法获取的结果不会为null,通过Environment获取到的命令行参数第一个是当前程序的路径,从第2项开始才是命令行参数(如果有)。\n","permalink":"https://kira-96.github.io/posts/wpf-%E8%8E%B7%E5%8F%96%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0/","summary":"虽然平时很少会用到命令行参数,但有时候可以使用命令行参数来使程序执行不同的行为。 在写控制台程序的时候,我们可以直接得到程序命令行参数。 1 2 3","title":"WPF 获取命令行参数"},{"content":"前言 由于我平时是将树莓派(Respberry Pi)当成一个Linux电脑来使用,平时都是通过ssh连接到树莓派来进行操作的,所以一直都是通过终端进行操作的。而树莓派系统的终端又中规中距,不怎么好看。刚好这两天接触到了一个十分漂亮的Power Shell主题oh-my-posh,所以就想着能不能弄到树莓派上。折腾了半天,终于成功了,过程还算顺利。\n目标 我的目标是美化树莓派的终端,由于oh-my-posh是power shell的主题,所以首先需要安装power shell,然后再通过power shell安装oh-my-posh。\n安装 Power Shell 安装 oh-my-posh 安装 Power Shell Core 刚好前几天Power Shell Core 7发布了,所以我这里就安装了最新的版本。\nPower Shell 官网是这样的说明的。\n当前仅 Raspbian Stretch 支持 PowerShell。 CoreCLR 和 PowerShell Core 仅适用于 Pi 2 和 Pi 3 设备,因为其他设备(如 Pi 0)有不受支持的处理器。\n我是用的是树莓派 3B+,测试是可以的。\n具体的操作按照Power Shell官网的安装说明\n首先安装Power Shell的依赖:\n1 2 3 4 5 6 7 8 # Prerequisites # Update package lists sudo apt-get update # Install libunwind8 and libssl1.0 # Regex is used to ensure that we do not install libssl1.0-dev, as it is a variant that is not required sudo apt-get install \u0026#39;^libssl1.0.[0-9]$\u0026#39; libunwind8 -y 然后到这里下载最新的Power Shell二进制包。\n这里需要下载arm32位的二进制包。\npowershell-7.0.0-linux-arm32.tar.gz\n下载完成后解压到任意目录即可运行。\n或者使用官网的方式:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # Download and extract PowerShell # Grab the latest tar.gz wget https://github.com/PowerShell/PowerShell/releases/download/v7.0.0/powershell-7.0.0-linux-arm32.tar.gz # Make folder to put powershell mkdir ~/powershell # Unpack the tar.gz file tar -xvf ./powershell-7.0.0-linux-arm32.tar.gz -C ~/powershell # Start PowerShell ~/powershell/pwsh 最后,如果想要在任意位置都能启动Power Shell,需要创建启动Power Shell的软链接。\n1 sudo ln -s ~/path/to/powershell/pwsh /usr/bin/pwsh 或者参考官网的方式:\n1 2 3 4 5 6 7 # Start PowerShell from bash with sudo to create a symbolic link sudo ~/powershell/pwsh -c New-Item -ItemType SymbolicLink -Path \u0026#34;/usr/bin/pwsh\u0026#34; -Target \u0026#34;\\$PSHOME/pwsh\u0026#34; -Force # alternatively you can run following to create a symbolic link # sudo ln -s ~/powershell/pwsh /usr/bin/pwsh # Now to start PowerShell you can just run \u0026#34;pwsh\u0026#34; 现在只要在终端输入pwsh就可以进入Power Shell了。\n为 Power Shell 安装 oh-my-posh 安装之前需要先安装powerline字体,否则,oh-my-posh安装完成后会由于缺少字体而显示不正常。\n1 sudo apt-get install fonts-powerline 然后就可以为Power Shell安装oh-my-posh主题了。\n首先要进入Power Shell,在终端输入pwsh即可。\n在Power Shell下依次执行下面两个命令:\n1 2 Install-Module posh-git -Scope CurrentUser Install-Module oh-my-posh -Scope CurrentUser 安装过程中全部选 是(Y) 就可以了。\n安装完成后就可以使用oh-my-posh了:\n1 2 3 4 # Start the default settings Set-Prompt # Alternatively set the desired theme: Set-Theme Agnoster 最后需要保存Power Shell的配置,这样每次进入Power Shell就是我们设定的主题了。\n1 2 3 4 5 # 在 Power Shell下执行下面命令,如果不存在配置文件就创建一个 if (!(Test-Path -Path $PROFILE )) { New-Item -Type File -Path $PROFILE -Force } # 使用树莓派的编辑器修改配置文件 vi $PROFILE 在配置文件中输入下面内容:\n1 2 3 Import-Module posh-git Import-Module oh-my-posh Set-Theme Paradox 然后再重新进入Power Shell就可以看到主题已经成功应用了。\noh-my-posh提供了多种主题效果,可以看这里,如果需要更换主题,可以直接在Power Shell中执行:\n1 Set-Theme mytheme 效果 虽然 Power Shell 启动有一点慢,但显示效果还是很不错的。\n最后放上实际运行的效果:\n在树莓派中显示效果:\n在其它终端中的显示效果(Termius):\n参考\n在树莓派上安装PowerShellCore 安装powerline-fonts 安装oh-my-posh ","permalink":"https://kira-96.github.io/posts/%E5%9C%A8%E6%A0%91%E8%8E%93%E6%B4%BE%E4%B8%8A%E5%AE%89%E8%A3%85-power-shell-%E5%B9%B6%E7%94%A8-oh-my-posh-%E7%BE%8E%E5%8C%96/","summary":"前言 由于我平时是将树莓派(Respberry Pi)当成一个Linux电脑来使用,平时都是通过ssh连接到树莓派来进行操作的,所以一直都是通过","title":"在树莓派上安装 Power Shell 并用 oh-my-posh 美化"},{"content":"简介 JSON是一种常用的轻量级数据交换格式。与XML相比,JSON无论是体积还是可读性都更好,所以在网络数据传输和应用程序中被广泛的应用。\n那么,.NET平台使用最广泛的JSON库是什么呢?自然要数Newtonsoft.NET了,打开nuget包管理器第一个就是,在所有包下载量排行中排名第一。使用简单,性能可靠,文档也很齐全。\n使用 使用JSON最常用的就是对象的序列化和反序列化。\n先来看最基本的使用\n1 2 3 4 5 6 7 // 先定义一个类 public class TestJsonDeseClass { public Guid MessageGuid { get; set; } public string Message { get; set; } } 1 2 3 4 5 6 7 8 TestJsonDeseClass test = new TestJsonDeseClass() { MessageGuid = Guid.NewGuid(), Message = \u0026#34;Test Message\u0026#34; }; string json = JsonConvert.SerializeObject(test); TestJsonDeseClass des = JsonConvert.DeserializeObject\u0026lt;TestJsonDeseClass\u0026gt;(json); 只需要将类的成员属性设置为get和set就可以了,反序列化的时候,Json.NET会自动根据成员的名字为对象的成员赋值。\n那么如果不想序列化/反序列化某个成员变量呢?\n1 2 3 4 5 6 7 8 9 using Newtonsoft.Json; public class TestJsonDeseClass { [JsonIgnore] public Guid MessageGuid { get; set; } public string Message { get; set; } } 只需要在成员变量的定义前加上[JsonIgnore]的属性(Attribute)即可,序列化/反序列化的时候Json.NET会自动忽略该成员。\n如果JSON字符串中的属性名字和定义的类中的成员名字不一样怎么办呢?怎样才能正确的给成员变量赋值呢?\n1 2 3 4 5 6 7 8 9 using Newtonsoft.Json; public class TestJsonDeseClass { [JsonProperty(\u0026#34;Guid\u0026#34;)] public Guid MessageGuid { get; set; } public string Message { get; set; } } 只需要在成员变量的定义前加上[JsonProperty()]的属性(Attribute)即可,序列化/反序列化的时候Json.NET会将Json字符串中的\u0026quot;Guid\u0026quot;属性赋值给MessageGuid。\n那么,如果想让类的属性值只读的get,不想让外部能修改成员变量呢,如何设置呢?\n当然这样也是可以的,不过需要我们给类添加构造方法,在构造方法中对成员赋值,不能再使用默认的构造方法,因为默认的构造方法不会对成员赋值,而外部也无法对成员赋值。在添加了构造方法后,Json.NET会自动调用类的构造方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 using Newtonsoft.Json; public class TestJsonDeseClass { public Guid MessageGuid { get; } public string Message { get; } public TestJsonDeseClass(Guid messageGuid, string message) { MessageGuid = messageGuid; Message = message; } } 不过需要注意的是,构造方法的参数名称必须和成员变量(或者说是序列化/反序列化时的属性)名字一致,但可以不用区分大小写,才能正确对属性赋值。\n如果把上面的构造方法改成下面这个样子\n1 2 3 4 5 public TestJsonDeseClass(Guid guid, string message) { MessageGuid = guid; Message = message; } 就会导致反序列化的对象MessageGuid属性不能正确赋值,因为Json.NET无法从json字符串中找到名为guid的属性,你也没告诉它要拿名为MessageGuid的属性,自然就会出错了。\n那么,最后一个问题,如果我的类有多个构造方法,我怎样告诉Json.NET应该用哪一个呢?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using Newtonsoft.Json; public class TestJsonDeseClass { public Guid MessageGuid { get; } public string Message { get; } public TestJsonDeseClass() { } [JsonConstructor] public TestJsonDeseClass(Guid messageGuid, string message) { MessageGuid = messageGuid; Message = message; } } 只需要在对应的构造方法前面加上[JsonConstructor]的属性(Attribute)即可,反序列化的时候Json.NET会就会调用相应的构造方法来生成对象了。\n以上就是一些基本的用法,基本上能满足正常的使用了。当然还有一些更加高级和灵活的用法,这里就不多记录了,需要的时候再去看文档就可以了。\n参考\n文档\nSamples\n","permalink":"https://kira-96.github.io/posts/newtonsoft.net-%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8/","summary":"简介 JSON是一种常用的轻量级数据交换格式。与XML相比,JSON无论是体积还是可读性都更好,所以在网络数据传输和应用程序中被广泛的应用。 那","title":"Newtonsoft.NET 基本使用"},{"content":"简介 Stylet是一个轻量且功能强大的MVVM框架。支持 .NET 4.5+ 和 .NET Core 3.0+。\nStylet的作者也是受到Caliburn.Micro的启发,并且在CM的基础上做了许多改进。所以Stylet使用起来感觉和Caliburn.Micro差别不是很大,但又有着一些不同。\n项目结构 这里选择创建一个 .NET Core 的 WPF 项目。\n这里项目结构风格和Caliburn.Micro类似,示例源代码\n虽然Stylet官方给出的例子里面View和ViewModel是放在一起的,但经过实际使用后发现采用CM的风格也是可以的。依照习惯,将Views和ViewModels分别放在两个文件夹中。\n使用 Bootstrapper.cs\n1 2 public class Bootstrapper : Bootstrapper\u0026lt;ShellViewModel\u0026gt; {} 这样就相当于执行了DisplayRootViewFor\u0026lt;ShellViewModel\u0026gt;()。\n然后再修改App.xaml如下就可以让程序启动了。\nApp.xaml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;Application x:Class=\u0026#34;WpfSample.App\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:s=\u0026#34;https://github.com/canton7/Stylet\u0026#34; xmlns:app=\u0026#34;clr-namespace:WpfSample\u0026#34;\u0026gt; \u0026lt;Application.Resources\u0026gt; \u0026lt;s:ApplicationLoader\u0026gt; \u0026lt;s:ApplicationLoader.Bootstrapper\u0026gt; \u0026lt;app:Bootstrapper /\u0026gt; \u0026lt;/s:ApplicationLoader.Bootstrapper\u0026gt; \u0026lt;/s:ApplicationLoader\u0026gt; \u0026lt;/Application.Resources\u0026gt; \u0026lt;/Application\u0026gt; 绑定 Stylet似乎不再支持Caliburn.Micro的通过x:Name来绑定的机制,必须通过Binding显式指定绑定属性的方式。\n1 \u0026lt;TextBox Text=\u0026#34;{Binding YourName, UpdateSourceTrigger=PropertyChanged}\u0026#34; /\u0026gt; Action的写法则有了更明显的差异。\n1 2 3 \u0026lt;Button Content=\u0026#34;Say Hello\u0026#34; xmlns:stylet=\u0026#34;https://github.com/canton7/Stylet\u0026#34; Command=\u0026#34;{stylet:Action SayHello}\u0026#34; /\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // ShellViewModel.cs private string _name; public string YourName { get =\u0026gt; _name; set { SetAndNotify(ref _name, value); NotifyOfPropertyChange(() =\u0026gt; CanSayHello); } } public bool CanSayHello =\u0026gt; !string.IsNullOrEmpty(YourName); public void SayHello() { _logger.Info(\u0026#34;Say Hello {0}\u0026#34;, _name); _windowManager.ShowMessageBox($\u0026#34;Hello {_name}\u0026#34;); } 不同于Caliburn.Micro的写法cal:Message.Attach=\u0026quot;[Click]=[OnClick()]\u0026quot;\nStylet可以直接指定相应的事件,可读性自然是提升了不少,并且依旧不需要在ViewModel中写ICommand,直接写函数即可。当然也可以通过CommandParameter传递参数。\n上面是通过Command的方式绑定,你也可以绑定到Click或者其它的事件。\n以下是官方的示例:\n1 \u0026lt;Button Click=\u0026#34;{s:Action DoSomething}\u0026#34;\u0026gt;Click me\u0026lt;/Button\u0026gt; 1 2 3 4 5 6 7 public void HasNoArguments() { } // This can accept EventArgs, or a subclass of EventArgs public void HasOneSingleArgument(EventArgs e) { } // Again, a subclass of EventArgs is OK public void HasTwoArguments(object sender, EventArgs e) { } 可以根据需求在ViewModel对应的函数中定义参数或者不定义。\nActionTarget\n1 2 3 4 5 6 7 8 9 10 11 12 class InnerViewModel { public void DoSomething() { } } class ViewModel { public InnerViewModel InnerViewModel { get; private set; } public ViewModel() { this.InnerViewModel = new InnerViewModel(); } } 1 \u0026lt;Button s:View.ActionTarget=\u0026#34;{Binding InnerViewModel}\u0026#34; Command=\u0026#34;{s:Action DoSomething}\u0026#34;\u0026gt;Click me\u0026lt;/Button\u0026gt; 通过指定Action.Target来绑定到其它的ViewModel。\n依赖注入 Stylet提供了两个类供ViewModel继承,Screen和Conductor,这一点和Caliburn.Micro类似。所有的ViewModel都会自动绑定到IoC,不需要在Bootstrapper中进行设置。\n必要时也可以重写Bootstrapper中的ConfigureIoC来注册一些其它的服务。\n这里注册了一个Logging service,Stylet中提供有ILogger的接口,但不建议使用,可以自己实现。\n1 2 3 4 5 6 protected override void ConfigureIoC(IStyletIoCBuilder builder) { base.ConfigureIoC(builder); builder.Bind\u0026lt;ILogger\u0026gt;().To\u0026lt;Logger\u0026gt;().InSingletonScope().AsWeakBinding(); } Stylet提供了多种注入的方式。\n通过构造函数注入\n1 2 3 4 5 6 7 8 9 public NavViewModel( IEventAggregator eventAggregator, FirstTabViewModel tab1, SecondTabViewModel tab2) { this._eventAggregator = eventAggregator; this.Items.Add(tab1); this.Items.Add(tab2); } 通过[Inject]自动注入\n使用[Inject]方式注入时也可以指定相应的Key\n1 2 3 4 // Logger.cs [Inject(Key = \u0026#34;filelogger\u0026#34;)] public class Logger : ILogger {} 1 2 [Inject(Key = \u0026#34;filelogger\u0026#34;)] private ILogger _logger; 抽象工厂\nStylet提供了一种抽象工厂的模式来获取相应的服务。\n这里我定义了一个IViewModelFactory的接口\n1 2 3 4 5 6 7 public interface IViewModelFactory { ShellViewModel GetShellViewModel(); NavViewModel GetNavViewModel(); FirstTabViewModel GetFirstTabViewModel(); SecondTabViewModel GetSecondTabViewModel(); } 然后在Bootstrapper的ConfigureIoC中添加如下代码\n1 2 3 4 5 6 7 // Bootstrapper.cs protected override void ConfigureIoC(IStyletIoCBuilder builder) { ... builder.Bind\u0026lt;IViewModelFactory\u0026gt;().ToAbstractFactory(); ... } 这样就可以通过注入的方式来获取到IViewModelFactory的实例了。\n注意这个过程中我并没有手动去实现IViewModelFactory的接口。\n1 2 3 4 5 6 // ShellViewModel.cs [Inject] private IViewModelFactory _viewModelFactory; // 这时就可以通过Factory来获取相应的ViewModel var vm = _viewModelFactory.GetNavViewModel(); IoC 虽然可以通过注入的方式来获取服务,但有时也需要通过IoC Container来获取相应的服务。Stylet依然有多种方式来获取。\n注入IoC Container\n1 2 3 4 5 6 // 注入IoC Conatiner [Inject] private IContainer _container; // 通过Container获取ViewModel var vm = _container.Get\u0026lt;NavViewModel\u0026gt;(); Static Service Locator\n用过Caliburn.Micro的可能都知道,CM提供了一种非常好用的获取服务的方式。\n1 var vm = IoC.Get\u0026lt;MyDialogViewModel\u0026gt;(); 而Stylet并没有提供这种方式。Stylet作者给出的原因是:\nI don\u0026rsquo;t want to encourage people to write such horrible code.\n但我就是喜欢简单粗暴的,通过IoC.Get的方式比较合我的胃口。Stylet的作者同样也给出了相应的方式链接\n但作者给出的代码中GetAllInstance是不能正确使用的。可以参考我的修改版SimpleIoC\n最后再Bootstrapper中添加下面代码即可。\n1 2 3 4 5 6 7 8 protected override void Configure() { base.Configure(); SimpleIoC.GetInstance = this.Container.Get; SimpleIoC.GetAllInstances = this.Container.GetAll; SimpleIoC.BuildUp = this.Container.BuildUp; } 使用:\n1 var vm = SimpleIoC.Get\u0026lt;NavViewModel\u0026gt;(); WindowManager 1 2 3 4 5 6 public interface IWindowManager { bool? ShowDialog(object viewModel); MessageBoxResult ShowMessageBox(string messageBoxText, string caption = \u0026#34;\u0026#34;, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.None, MessageBoxResult cancelResult = MessageBoxResult.None, IDictionary\u0026lt;MessageBoxResult, string\u0026gt; buttonLabels = null, FlowDirection? flowDirection = null, TextAlignment? textAlignment = null); void ShowWindow(object viewModel); } Stylet的IWindowManager提供了3个接口函数,一个MessageBox,其它两个用于显示窗口,和Caliburn.Micro用法相同,在使用时把ViewModel传如即可。IWindowManager可以直接通过注入的方式获得。\n1 2 3 4 [Inject] private IWindowManager _windowManager; _windowManager.ShowWindow(_viewModelFactory.GetNavViewModel()); EventAggregator EventAggregator和Caliburn.Micro中的用法相同。结合IHandle\u0026lt;T\u0026gt;用于在ViewModel中传递消息。\n订阅消息:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // ShellViewModel.cs public class ShellViewModel : Screen, IHandle\u0026lt;TabChangedEvent\u0026gt; { private readonly IEventAggregator _eventAggregator; ... public ShellViewModel(IEventAggregator eventAggregator) { DisplayName = \u0026#34;Hello Stylet!\u0026#34;; _eventAggregator = eventAggregator; _eventAggregator.Subscribe(this); } protected override void OnClose() { _eventAggregator.Unsubscribe(this); base.OnClose(); } public void Handle(TabChangedEvent message) { // TODO } ... } 发布消息:\n1 2 3 4 5 6 7 8 9 10 11 12 13 // AnotherViewModel.cs private readonly IEventAggregator _eventAggregator; public NavViewModel(IEventAggregator eventAggregator) { this._eventAggregator = eventAggregator; } // 发布消息 private Publish() { _eventAggregator.Publish(new TabChangedEvent()); } Stylet中的EventAggregator同时提供了channels,可以在不同的管道之中订阅/发布消息。\n总结 对于使用过Caliburn.Micro的朋友来说,Stylet非常容易上手,大部分的用法基本上都一样,同时Stylet又提供了一些新的内容。这里有些东西并没有讲到,如ViewManager等,但Stylet已经足够让我兴奋了,还有就是它的体积真的很小,只有100多KB,并且功能也足够强大,用起来也很方便,后面有机会可以在一些小项目中使用。\n","permalink":"https://kira-96.github.io/posts/stylet-%E6%A1%86%E6%9E%B6%E4%BD%93%E9%AA%8C/","summary":"简介 Stylet是一个轻量且功能强大的MVVM框架。支持 .NET 4.5+ 和 .NET Core 3.0+。 Stylet的作者也是受到Caliburn.Micro的启发,并","title":"Stylet 框架体验"},{"content":"这段时间公司的一个项目打算使用Named Pipe进行进程间的通讯,刚好花了点时间了解了一下,这里做一下笔记。\nNamed Pipe(命名管道),顾名思义,是通过在两个进程间搭建一个管道来进行通讯,这种方式的好处在于两者可以进行全双工的通讯,服务端也可以通过管道向客户端发送消息,对于两个进程之间的通讯来说再合适不过了,使用起来也相对比较灵活。\n服务端(Server) Named Pipe 命名空间\n1 using System.IO.Pipes; 创建管道\n1 2 3 4 5 6 7 8 9 10 11 12 13 PipeSecurity security = new PipeSecurity(); // 管道权限 // 设置规则,只有用户admin可以对管道进行读写,其它用户无权访问 security.AddAccessRule(new PipeAccessRule(\u0026#34;admin\u0026#34;, PipeAccessRights.ReadWrite, AccessControlType.Allow)); NamedPipeServerStream server = new NamedPipeServerStream( \u0026#34;SimpleServer\u0026#34;, // pipe name PipeDirection.InOut, // 数据传输方向,这里使用双工通讯 1, // MaxNumberOfServerInstance PipeTransmissionMode.Byte, // 字节流传输 PipeOptions.Asynchronous | PipeOptions.WriteThrough, 4096, // 输入缓冲大小 4096, // 输出缓冲大小 security); // 管道访问权限,这里只做笔记,通常不需要设置 管道创建好之后还不能立刻发送数据,因为管道的另一端(客户端)还没有连接,所以服务端需要等待连接。\n1 server.WaitForConnection(); // 阻塞方式 这里使用非阻塞的方式等待连接,当然也可以用阻塞的方式等待连接,不过需要放到一个新的线程中,避免将主线程阻塞。\n1 server.BeginWaitForConnection(new AsyncCallback(WaitConnectionCallback), server); // 非阻塞方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 private void WaitConnectionCallback(IAsyncResult asyncResult) { NamedPipeServerStream server = asyncResult.AsyncState as NamedPipeServerStream; server.EndWaitForConnection(asyncResult); StartListen(server); } private void StartListen(NamedPipeServerStream server) { Task.Run(async () =\u0026gt; { int bytesToRead; byte[] buffer; // server.WaitForConnection(); while (true) { try { bytesToRead = 256 * server.ReadByte(); bytesToRead += server.ReadByte(); // 演示 // 将收到的数据立马发送出去 server.WriteByte((byte)(bytesToRead / 256)); server.WriteByte((byte)(bytesToRead % 256)); buffer = new byte[bytesToRead]; server.Read(buffer, 0, bytesToRead); // 读取消息 // 演示 // 将收到的数据立马发送出去 await server.WriteAsync(buffer, 0, bytesToRead); } catch (System.IO.IOException) { // break if another pipe end closed break; } catch (Exception) { break; } // 处理数据 string content = Encoding.UTF8.GetString(buffer); Console.WriteLine(content); } server.Disconnect(); // 断开连接 // 重新等待连接 server.BeginWaitForConnection(new AsyncCallback(WaitConnectionCallback), server); }); } 在客户端连接之后,立马启动一个新的线程循环读取来自客户端的消息,这里的消息前两个字节指定了消息的长度。同时将收到的消息马上返回到管道的另一端(这里用于测试是否真的是全双工工作)。\n最后将消息的读取放到try{ ... } catch(...){ ... }中,因为并没有消息或者事件通知服务端客户端已经断开连接。但当客户端断开之后,服务端在读取时会抛出IOException,可以通过抓取这个错误来判断管道是否已经断开。\n当客户端断开之后,中断读取循环,服务端也断开连接,并再次等待客户端连接。\n客户端(Client) 创建客户端\n1 2 3 4 5 NamedPipeClientStream client = new NamedPipeClientStream( \u0026#34;.\u0026#34;, // The name of the remote computer, \u0026#34;.\u0026#34; 指本机 \u0026#34;SimpleServer\u0026#34;, // pipe name PipeDirection.InOut, // 数据传输方向 PipeOptions.Asynchronous | PipeOptions.WriteThrough); 连接到服务端\n1 client.Connect(); // 阻塞方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Task.Run(() =\u0026gt; { int bytesToRead; byte[] buffer; client.Connect(); // 连接管道 while (true) { try { // 读取消息长度 bytesToRead = 256 * client.ReadByte(); bytesToRead += client.ReadByte(); buffer = new byte[bytesToRead]; client.Read(buffer, 0, bytesToRead); // 读取消息 } catch (System.IO.IOException) { break; } catch (Exception) { break; } // 处理消息 string content = Encoding.UTF8.GetString(buffer); Console.WriteLine(content); } client.Close(); }); 这里使用一个新的线程去连接管道服务端,连接成功后循环读取来自服务端的消息。\n客户端发送\n1 2 3 4 5 6 7 8 9 10 11 12 13 string text = \u0026#34;Hello\u0026#34;; byte[] bytes = System.Text.Encoding.UTF8.GetBytes(text); client.WriteByte((byte)(bytes.Length / 256)); // 发送消息头 client.WriteByte((byte)(bytes.Length % 256)); client.Write(bytes, 0, bytes.Length); // 发送消息 text = \u0026#34;World\u0026#34;; bytes = System.Text.Encoding.UTF8.GetBytes(text); client.WriteByte((byte)(bytes.Length / 256)); // 发送消息头 client.WriteByte((byte)(bytes.Length % 256)); client.Write(bytes, 0, bytes.Length); // 发送消息 总结 总的来说,Named Pipe使用还是比较简单的,结合序列化就可以直接在两个进程中传递消息对象了。需要注意的是一个服务端只能有一个客户端连接,而且在客户端断开连接之后,服务端也需要断开连接,并重新等待客户端连接,不然再有客户端尝试连接管道也无法建立。\n参考\nHow to: Use Named Pipes for Network Interprocess Communication\n","permalink":"https://kira-96.github.io/posts/%E4%BD%BF%E7%94%A8-named-pipe-%E8%BF%9B%E8%A1%8C%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E8%AE%AF/","summary":"这段时间公司的一个项目打算使用Named Pipe进行进程间的通讯,刚好花了点时间了解了一下,这里做一下笔记。 Named Pipe(命名管道),顾名思义","title":"使用 Named Pipe 进行进程间通讯"},{"content":"简介 Hprose(High Performance Remote Object Service Engine)是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,而且功能强大。\n也是一个跨语言的RPC框架,但由于库的质量参差不齐,一些语言的库并不完善。这里以C#为例来实现一个简单的服务端和客户端程序。\n创建项目 新建解决方案,包含两个项目\nserver\n控制台程序,服务端\nclient\n控制台程序,客户端\n然后,通过Nuget分别为两个项目安装Hprose.RPC库。\n这里并不需要再创建额外的服务接口项目,只需要手动定义一个接口即可。\n创建服务接口 在server和client项目下都新建一个接口,作为服务接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // IHello.cs public class ServiceVersion { public string Name { get; set; } public string Version { get; set; } public ServiceVersion() { } public ServiceVersion(string name, string ver) { Name = name; Version = ver; } } // 服务接口 public interface IHello { ServiceVersion GetVersion(); List\u0026lt;string\u0026gt; SayHello(string name); } IHello里面的两个接口函数就是服务接口了。\n服务端程序 依旧是服务端实现接口,客户端来调用,在server项目下新建类Hello.cs\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Hello.cs namespace server { using System.Collections.Generic; public class Hello : IHello { public ServiceVersion GetVersion() { return new ServiceVersion(\u0026#34;Hello Service\u0026#34;, \u0026#34;0.0.1.21\u0026#34;); } public List\u0026lt;string\u0026gt; SayHello(string name) { return new List\u0026lt;string\u0026gt;() { $\u0026#34;你好 {name}\u0026#34;, $\u0026#34;Hello {name}\u0026#34;, $\u0026#34;Hola {name}\u0026#34;, $\u0026#34;Bonjour {name}\u0026#34;, $\u0026#34;こんにちは {name}\u0026#34;, $\u0026#34;hallo {name}\u0026#34; }; } } } 编写服务启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace server { using Hprose.RPC; using System.Net; class Program { static void Main() { HttpListener server = new HttpListener(); server.Prefixes.Add(\u0026#34;http://localhost:10240/\u0026#34;); server.Start(); Service service = new Service().Bind(server).AddInstanceMethods(new Hello()); System.Console.WriteLine(\u0026#34;Server listening at http://localhost:10240/ \\n Press any key exit ...\u0026#34;); System.Console.ReadKey(); server.Stop(); } } } 客户端程序 由于client项目刚刚也定义了IHello接口,这里就可以直接调用接口函数了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 namespace client { using Hprose.RPC; class Program { static void Main() { Client cli = new Client(\u0026#34;http://localhost:10240/\u0026#34;); IHello hello = cli.UseService\u0026lt;IHello\u0026gt;(); ServiceVersion ver = hello.GetVersion(); System.Console.WriteLine(\u0026#34;Remote Service Version: {0} - v{1}\u0026#34;, ver.Name, ver.Version); var hellos = hello.SayHello(\u0026#34;Hprose\u0026#34;); foreach (string item in hellos) { System.Console.WriteLine(item); } System.Console.ReadKey(); } } } 运行测试 先启动服务端程序,再启动客户端程序,可以看到客户端输出\nRemote Service Version: Hello Service - v0.0.1.21 你好 Hprose Hello Hprose Hola Hprose Bonjour Hprose こんにちは Hprose hallo Hprose 与Thrift和gRPC相比,Hprose实现起来要简单很多,暂时还没有尝试跨语言调用,不知道是不是同样简单。\n","permalink":"https://kira-96.github.io/posts/hprose-c_-%E5%88%9D%E6%8E%A2/","summary":"简介 Hprose(High Performance Remote Object Service Engine)是一款先进的轻量级、跨语言、跨平台、无侵入式、高性能动态远程对象调用引擎库。它不仅简单易用,","title":"Hprose C# 初探"},{"content":"简介 Thrift是由Facebook为“大规模跨语言服务开发”而开发的一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。目前被作为一个RPC框架使用。\n下载 使用之前需要先下载Thrift的源代码和Thrift编译器。\n下载源代码后解压,进入到thrift-0.12.0\\lib\\csharp\\src目录下,打开Thrift.sln,根据需要编译相应的库,这里选择Thrift.45,即.NET 4.5可以使用的库,编译生成Thrift45.dll。\n创建项目 新建解决方案,包含3个项目\nThriftSample\n类库,Thrift生成的服务接口\nserver\n控制台程序,服务端\nclient\n控制台程序,客户端\n这3个项目都需要引用刚刚编译的Thrift45.dll,为什么不用Nuget来安装Thrift呢?因为我注意到Nuget上的Thrift已经好几年没更新了,还是手动编译最新的要好。\n然后,server和client同时引用项目ThriftSample。\n定义服务接口 在解决方案目录下新建一个文件Sample.thrift来定义服务接口,Thrift语法可以在网上找到\n1 2 3 4 5 6 7 8 9 10 11 namespace csharp kira.Interface service SampleService { ServiceVersion GetVersion() list\u0026lt;string\u0026gt; SayHello(1: string name) } struct ServiceVersion { 1: required string name; 2: required string version; } 这里指定了生成类的命名空间,以及定义了一个结构体作为返回值\n生成服务接口 这里就需要用到之前下载的Thrift编译器,可以直接在Thrift官网找到。将下载的thrift-0.12.0.exe也拷贝到解决方案目录下(和Sample.thrift相同目录),打开命令窗口,执行以下命令\n1 $ thrift-0.12.0.exe -gen csharp Sample.thrift 不得不说,Thrift的命令行语法真的比gRPC简洁多了。\n执行完没有错误的话,就可以看到目录下又多出了一个gen-csharp的文件夹,里面有对应命名空间的文件夹,最后找到生成的.cs文件。将文件全部拷贝到ThriftSample项目目录下,并将它们添加到项目,编译生成库。\n服务端程序 在server项目下新建类MySampleService并实现接口SampleService.Iface,重写其中的两个服务接口函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace server { using System.Collections.Generic; using kira.Interface; public class MySampleService : SampleService.Iface { public ServiceVersion GetVersion() { return new ServiceVersion() { Name = \u0026#34;My Sample Service\u0026#34;, Version = \u0026#34;0.0.1.20\u0026#34; }; } public List\u0026lt;string\u0026gt; SayHello(string name) { return new List\u0026lt;string\u0026gt;() { $\u0026#34;你好 {name}\u0026#34;, $\u0026#34;Hello {name}\u0026#34;, $\u0026#34;Hola {name}\u0026#34;, $\u0026#34;Bonjour {name}\u0026#34;, $\u0026#34;こんにちは {name}\u0026#34;, $\u0026#34;hallo {name}\u0026#34; }; } } } 编写服务启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 namespace server { using Thrift.Server; using Thrift.Transport; using kira.Interface; class Program { static void Main(string[] args) { MySampleService service = new MySampleService(); SampleService.Processor processor = new SampleService.Processor(service); TServerTransport serverTransport = new TServerSocket(10240); TServer server = new TThreadPoolServer(processor, serverTransport); System.Console.WriteLine(\u0026#34;server listening at tcp://localhost:10240/\u0026#34;); server.Serve(); } } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 namespace client { using Thrift.Protocol; using Thrift.Transport; using kira.Interface; class Program { static void Main(string[] args) { TTransport transport = new TSocket(\u0026#34;localhost\u0026#34;, 10240); transport.Open(); TProtocol protocol = new TBinaryProtocol(transport); SampleService.Client cli = new SampleService.Client(protocol); ServiceVersion ver = cli.GetVersion(); System.Console.WriteLine(\u0026#34;Remote Service Version: {0} - v{1}\u0026#34;, ver.Name, ver.Version); var hellos = cli.SayHello(\u0026#34;Thrift\u0026#34;); foreach (string item in hellos) { System.Console.WriteLine(item); } System.Console.ReadKey(); transport.Close(); } } } 运行测试 老样子,先启动服务端程序,然后运行客户端程序,可以看到客户端输出\nRemote Service Version: My Sample Service - v0.0.1.20 你好 Thrift Hello Thrift Hola Thrift Bonjour Thrift こんにちは Thrift hallo Thrift 总结 对比Thrift和gRPC两个主流的RPC框架,个人感觉Thrift使用起来要更加灵活一些,当然这只是初步接触,较深层次的内容还没有去研究,实际项目的应用感觉两个都可以,有对比说Thrift框架的性能要优于gRPC,但对于小的项目来说已经完全够用了。\n","permalink":"https://kira-96.github.io/posts/thrift-c_-%E5%88%9D%E6%8E%A2/","summary":"简介 Thrift是由Facebook为“大规模跨语言服务开发”而开发的一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。目前","title":"Thrift C# 初探"},{"content":"简介 gRPC是Google开源的一个现代化、高性能的RPC框架,基于HTTP/2标准设计,同时提供多个语言版本,并支持跨语言调用,可以在任何环境中运行。\n创建项目 新建解决方案,包含3个项目\ngRpcSample\n类库,gRPC生成的接口,Server接口、Client接口等\nserver\n控制台程序,服务端\nclient\n控制台程序,客户端\n分别给3个项目安装Nuget程序包Grpc并安装所需依赖,然后为gRpcSample项目安装Grpc.Tools和Google.ProtoBuf程序包。\n同时,使项目server和client引用项目gRpcSample。\n定义服务接口 在gRpcSample项目的文件夹下新建Sample.proto文件,以文本方式打开,修改其中接口定义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 syntax = \u0026#34;proto3\u0026#34;; option csharp_namespace = \u0026#34;kira.Interface\u0026#34;; package sampleservice; service SampleService { rpc ServerVersion(VersionRequest) returns (VersionResponse) {} rpc SayHello(HelloRequest) returns (stream HelloResponse) {} } message VersionRequest {} message VersionResponse { string name = 1; string version = 2; } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } 这里指定了生成的类的命名空间,同时定义了两个服务函数,似乎每个函数都必须有参数(请求)和返回(响应),这一点不太清楚。具体的语法有条件的可以参考proto3 language guide。\n生成服务接口 在进行下面操作前,建议先将Grpc.Tools拷贝到解决方案目录下,不然的话下面的命令会很长很长\u0026hellip;\n具体操作是将解决方案下packages\\Grpc.Tools.2.23.0-pre1\\tools\\windows_x64\\里面的protoc.exe和grpc_csharp_plugin.exe拷贝到解决方案目录下,完成后就可以进行下一步。\n在解决方案目录下打开命令窗口,并执行下面命令\nTip: 直接进入到相应文件夹下,按住Shift键,在空白出单击鼠标右键,就可以看到菜单中多出了一项在此处打开命令窗口(W)\n1 $ protoc -IgRpcSample --csharp_out gRpcSample gRpcSample\\Sample.proto --grpc_out gRpcSample --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe 执行完没有错误的话,就可以看到gRpcSample项目下多出了两个文件Sample.cs和SampleGrpc.cs,将这两个文件添加到项目gRpcSample。\n编译gRpcSample通过。\n进行到这里,基本的工作就都完成了,剩下的就是编写服务端和客户端程序了。\n服务端程序 service项目新建类MySampleService,并继承SampleService.SampleServiceBase,重写刚刚定义的两个服务接口函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 namespace server { using System.Threading.Tasks; using Grpc.Core; using kira.Interface; public class MySampleService : SampleService.SampleServiceBase { public override Task\u0026lt;VersionResponse\u0026gt; ServerVersion(VersionRequest request, ServerCallContext context) { return Task.FromResult\u0026lt;VersionResponse\u0026gt;( new VersionResponse() { Name = \u0026#34;My Sample Service\u0026#34;, Version = \u0026#34;0.0.1.19\u0026#34; }); } public override async Task SayHello(HelloRequest request, IServerStreamWriter\u0026lt;HelloResponse\u0026gt; responseStream, ServerCallContext context) { string[] hellos = { \u0026#34;你好\u0026#34;, \u0026#34;Hello\u0026#34;, \u0026#34;Hola\u0026#34;, \u0026#34;Bonjour\u0026#34;, \u0026#34;こんにちは\u0026#34;, \u0026#34;hallo\u0026#34; }; foreach (string item in hellos) { await responseStream.WriteAsync(new HelloResponse() { Message = $\u0026#34;{item} {request.Name}\u0026#34; }); } } } } 编写服务启动程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace server { using Grpc.Core; using kira.Interface; class Program { static void Main(string[] args) { Server myServer = new Server() { Services = { SampleService.BindService(new MySampleService()) }, Ports = { new ServerPort(\u0026#34;localhost\u0026#34;, 10240, ServerCredentials.Insecure) } }; myServer.Start(); System.Console.WriteLine(\u0026#34;Sample Server listening on localhost:10240 \\nPress any key exit...\u0026#34;); System.Console.ReadKey(); myServer.ShutdownAsync().Wait(); } } } 客户端程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 namespace client { using System.Threading.Tasks; using Grpc.Core; using kira.Interface; class Program { static void Main(string[] args) { Program program = new Program(); program.TestService(); System.Console.ReadKey(); } async void TestService() { Channel channel = new Channel(\u0026#34;localhost:10240\u0026#34;, ChannelCredentials.Insecure); SampleService.SampleServiceClient cli = new SampleService.SampleServiceClient(channel); VersionResponse ver = cli.ServerVersion(new VersionRequest()); System.Console.WriteLine(\u0026#34;Remote Service Version: {0} - v{1}\u0026#34;, ver.Name, ver.Version); AsyncServerStreamingCall\u0026lt;HelloResponse\u0026gt; greetings = cli.SayHello(new HelloRequest() { Name = \u0026#34;gRPC\u0026#34; }); IAsyncStreamReader\u0026lt;HelloResponse\u0026gt; stream = greetings.ResponseStream; while (await stream.MoveNext()) { System.Console.WriteLine(stream.Current.Message); } } } } 运行测试 首先启动服务端程序,然后运行客户端程序,可以看到客户端输出\nRemote Service Version: My Sample Service - v0.0.1.19 你好 gRPC Hello gRPC Hola gRPC Bonjour gRPC こんにちは gRPC hallo gRPC OK,大功告成!\n","permalink":"https://kira-96.github.io/posts/grpc-c_-%E5%88%9D%E6%8E%A2/","summary":"简介 gRPC是Google开源的一个现代化、高性能的RPC框架,基于HTTP/2标准设计,同时提供多个语言版本,并支持跨语言调用,可以在任何","title":"gRPC C# 初探"},{"content":"进程间传递数据的方法 在进程间传递数据也就意味着两个不同的应用程序之间的通讯,大家可能会想到使用消息队列(Message Queue)来作为解决方案,当然这可能是最优解,然而这里我要讲的是另外一种方法,通过Windows的消息机制来传递数据,内容比较硬核。\n依旧用到了两个Windows的API,FindWindow和SendMessage,以及WPF如何和MFC窗口通讯,可以参考上一篇文章。\n传递数据的方式 这里需要先知道一个Window消息WM_COPYDATA 它在WinUser.h中的定义如下\n1 #define WM_COPYDATA 0x004A 这个消息就是这次要讲的内容,通过这个消息就可以在不同的窗口间传递数据了,它有两个参数,WPARAM是发送消息窗口的句柄,LPARAM是一个结构体的指针,这个结构体在WinUser.h里定义如下:\n1 2 3 4 5 6 7 8 /* * lParam of WM_COPYDATA message points to... */ typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData; DWORD cbData; _Field_size_bytes_(cbData) PVOID lpData; } COPYDATASTRUCT, *PCOPYDATASTRUCT; 嗯\u0026hellip;看起来也不是很复杂,为了能够正确处理这个指针,需要在C#中也定义一个相同的结构体\n1 2 3 4 5 6 7 8 9 10 11 12 /// \u0026lt;summary\u0026gt; /// COPYDATASTRUCT /// 对应 C++ 里的 COPYDATASTRUCT /// 不能更改 /// \u0026lt;/summary\u0026gt; [StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT { public IntPtr dwData; //可以是任意值 public int cbData; //指定lpData内存区域的字节数 public IntPtr lpData; //发送给目录窗口所在进程的数据 } 这样就可以了,其中最重要的就是lpData,它可以是任意对象的指针,只要C++和C#两边定义了一个相同的结构体,我们就可以用它来直接传递结构体对象\u0026#x1f62e;,这种方式比通过消息队列传递数据要快得多。\n那么先来定义一个简单的结构体吧:\n1 2 3 4 5 6 7 8 9 10 11 12 // C++ struct StructUser { char UserName[32]; char Password[32]; StructUser() { ZeroMemory(UserName, 32); ZeroMemory(Password, 32); } }; 1 2 3 4 5 6 7 8 9 // C# [StructLayout(LayoutKind.Sequential)] public struct StructUser { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string UserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string Password; } 这里定义了一个User的结构体,UserName和Password都是长度为32的字符串,注意C#中结构体的定义方式,这里指定了字符串的长度,就是为了和C++的结构体保持一致。\n先来看看C++中如何发送和处理WM_COPYDATA消息的:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 // 发送 StructUser user; char userName[32]; char password[32]; // GetDlgItem(IDC_EDIT_USERNAME)-\u0026gt;GetWindowTextA(userName, 32); // GetDlgItem(IDC_EDIT_PASSWORD)-\u0026gt;GetWindowTextA(password, 32); memcpy(user.UserName, userName, 32); memcpy(user.Password, password, 32); COPYDATASTRUCT copyData; copyData.dwData = 0; copyData.lpData = \u0026amp;user; copyData.cbData = sizeof(StructUser); HWND hWnd = nullptr; if (m_pTargerWnd == nullptr) { hWnd = ::FindWindow(nullptr, \u0026#34;WpfWindow\u0026#34;); } else { hWnd = m_pTargerWnd-\u0026gt;GetSafeHwnd(); } ::SendMessage(hWnd, WM_COPYDATA, (WPARAM)GetSafeHwnd(), (LPARAM)\u0026amp; copyData); // 接收 /* C++ 中相关代码 * 处理 WM_COPYDATA 消息 * Header File(.h) --------------------------------------------------------------------- ... afx_msg BOOL OnCopyData(CWnd *pWnd, COPYDATASTRUCT *pCopyDataStruct); ... DECLARE_MESSAGE_MAP() --------------------------------------------------------------------- * Source File(.cpp) BEGIN_MESSAGE_MAP(CxxxDlg, CDialogEx) ... ON_WM_COPYDATA() ... END_MESSAGE_MAP() ... */ BOOL CxxDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) { if (pWnd != nullptr) { m_pTargerWnd = pWnd; } if (pCopyDataStruct != nullptr) { StructUser* pUser = (StructUser*)(pCopyDataStruct-\u0026gt;lpData); // DWORD dwLen = pCopyDataStruct-\u0026gt;cbData; // GetDlgItem(IDC_EDIT_USERNAME)-\u0026gt;SetWindowTextA(pUser-\u0026gt;UserName); // GetDlgItem(IDC_EDIT_PASSWORD)-\u0026gt;SetWindowTextA(pUser-\u0026gt;Password); } return CDialogEx::OnCopyData(pWnd, pCopyDataStruct); } C#会比较复杂一些\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // 消息处理函数 private IntPtr WndProcFunc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { case WM_COPYDATA: // public const int WM_COPYDATA = 0x004A; IntPrt hWnd_Target = wParam; COPYDATASTRUCT param = Marshal.PtrToStructure\u0026lt;COPYDATASTRUCT\u0026gt;(lParam); StructUser user = Marshal.PtrToStructure\u0026lt;StructUser\u0026gt;(param.lpData); // UserName.Text = user.UserName; // Password.Text = user.Password; break; default: break; } return IntPtr.Zero; } // 发送消息 StructUser sctUser = new StructUser() { UserName = UserName.Text, Password = Password.Text }; IntPtr userPtr = Marshal.AllocHGlobal(Marshal.SizeOf\u0026lt;StructUser\u0026gt;()); Marshal.StructureToPtr\u0026lt;StructUser\u0026gt;(sctUser, userPtr, true); COPYDATASTRUCT copyData = new COPYDATASTRUCT() { dwData = IntPtr.Zero, cbData = Marshal.SizeOf\u0026lt;StructUser\u0026gt;(), lpData = userPtr, }; IntPtr copyDataPtr = Marshal.AllocHGlobal(Marshal.SizeOf\u0026lt;COPYDATASTRUCT\u0026gt;()); Marshal.StructureToPtr\u0026lt;COPYDATASTRUCT\u0026gt;(copyData, copyDataPtr, true); if (hWnd_Target == IntPtr.Zero) hWnd_Target = Win32Api.FindWindow(null, \u0026#34;MfcWindow\u0026#34;); hWnd_MainWnd = new WindowInteropHelper(this).Handle; if (hWnd_Target != IntPtr.Zero) Win32Api.SendMessage(hWnd_Target, WM_COPYDATA, hWnd_MainWnd, copyDataPtr); Marshal.FreeHGlobal(userPtr); // Marshal.FreeHGlobal(copyDataPtr); // 最后一定要释放掉非托管内存 处理消息的地方和上一篇一样,发送的地方比较复杂,总体来讲就是用Marshal开辟了两块非托管的内存来存放两个结构体的数据,等到消息被处理之后再将非托管内存释放掉,避免内存泄漏。\n那么,这次的内容就到这里了,主要内容都是代码,但其实也不复杂,只是需要慢慢消化。\n","permalink":"https://kira-96.github.io/posts/wpf%E5%92%8Cmfc%E8%BF%9B%E7%A8%8B%E9%97%B4%E4%BC%A0%E9%80%92%E6%95%B0%E6%8D%AE/","summary":"进程间传递数据的方法 在进程间传递数据也就意味着两个不同的应用程序之间的通讯,大家可能会想到使用消息队列(Message Queue)来作为解决","title":"WPF和MFC进程间传递数据"},{"content":"发送消息到指定窗口 发送消息相对来说比较简单,这里先讲,这里需要用到两个Windows的API\n1 2 3 4 5 6 7 8 9 10 11 12 // 查找指定窗口 [DllImport(\u0026#34;User32.dll\u0026#34;, EntryPoint = \u0026#34;FindWindow\u0026#34;, CharSet = CharSet.Auto)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); //消息发送API [DllImport(\u0026#34;User32.dll\u0026#34;, EntryPoint = \u0026#34;SendMessage\u0026#34;, CharSet = CharSet.Auto)] public static extern int SendMessage( IntPtr hWnd, // 信息发往的窗口的句柄 int Msg, // 消息ID IntPtr wParam, // 参数1 IntPtr lParam // 参数2 ); FindWindow就是根据窗口的名字去找到相应的窗口句柄,SendMessage就是发送消息到指定窗口了,写过MFC的应该不陌生\n1 2 3 4 IntPtr hWnd = FindWindow(null, windowName); if (hWnd == IntPtr.Zero) return; SendMessage(hWnd, WM_USER + 2, IntPtr.Zero, \u0026#34;msg\u0026#34;); 发送消息就讲这么多,剩下的需要自行摸索,下面是用WPF窗口接收消息\n获取WPF窗口句柄 Windows消息是通过窗口句柄来传递给指定窗口的,所以想要处理WPF窗口的收到消息,首先就需要获取自身的窗口句柄。\n1 HwndSource hWnd = PresentationSource.FromVisual(this) as HwndSource; 或者\n1 2 IntPtr hwnd = new WindowInteropHelper(this).Handle; HwndSource source = HwndSource.FromHwnd(hwnd); 这里的this就是WPF的窗口,当然也可以通过这种方式获取窗口任意控件的句柄\n添加钩子(AddHook) 得到窗口的句柄之后就可以为该窗口添加钩子来处理窗口收到的消息了\n1 2 3 4 5 6 7 8 9 10 11 12 protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); // Add Hook // HwndSource source = PresentationSource.FromVisual(this) as HwndSource; // source.AddHook(WndProcFunc); // 或者 HwndSource.FromHwnd(new WindowInteropHelper(this).Handle).AddHook(new HwndSourceHook(WndProcFunc)); } 或者\n1 2 3 4 5 6 7 public MainWindow() { InitializeComponent(); this.SourceInitialized += (s, e) =\u0026gt; { HwndSource.FromHwnd(new WindowInteropHelper(this).Handle).AddHook(new HwndSourceHook(WndProcFunc)); }; } 这个WndProcFunc就是我们的消息处理函数了,窗口在收到消息之后就会走到这个函数里面,它看起来像是这样\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { // Handle Msg Here switch (msg) { case WM_USER + 1: // 你的消息值 { // 处理 wParam, lParam break; } // ... 其它消息 default: break; } return IntPtr.Zero; } 这样就完了吗?是的,但是!目前还不能处理传递的参数,如果WPF程序和C++程序处于一个进程还好说,如果是两个进程,那么他们之间的内存是不共用的,所以即使WPF窗口拿到了指针也读不出指针里的内容。\n那么要怎样才能在WPF程序和C++程序之间传递值呢,有空在讲\u0026hellip;\n","permalink":"https://kira-96.github.io/posts/wpf%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86windows%E6%B6%88%E6%81%AF/","summary":"发送消息到指定窗口 发送消息相对来说比较简单,这里先讲,这里需要用到两个Windows的API 1 2 3 4 5 6 7 8 9 10 11 12 // 查找指定窗口 [DllImport(\u0026#34;User32.dll\u0026#34;, EntryPoint = \u0026#34;FindWindow\u0026#34;, CharSet","title":"WPF如何处理Windows消息"},{"content":"标题 1 2 3 4 5 6 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题 也可以使用闭合方式的标题,结尾的#可以不必和开头一致\n1 2 3 # 一级标题 # ## 二级标题 ## ... 另一种方式\n1 2 3 4 5 一级标题 ======= 二级标题 ------- 当然也可以用HTML的方式\n1 2 3 4 5 6 \u0026lt;h1\u0026gt;一级标题\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;二级标题\u0026lt;/h2\u0026gt; \u0026lt;h3\u0026gt;三级标题\u0026lt;/h3\u0026gt; \u0026lt;h4\u0026gt;四级标题\u0026lt;/h4\u0026gt; \u0026lt;h5\u0026gt;五级标题\u0026lt;/h5\u0026gt; \u0026lt;h6\u0026gt;六级标题\u0026lt;/h6\u0026gt; HTML的好处在于可以方便的使标题居中\n1 \u0026lt;h1 align=\u0026#34;center\u0026#34;\u0026gt;居中标题\u0026lt;/h1\u0026gt; 目录 可以使用[TOC]标记来自动生成目录,但兼容性貌似不怎么好\n1 [TOC] 分隔线 可以使用3个以上的*、-作为分隔线,中间也可以插入空格\n1 2 3 4 *** * * * --- - - - 字体 粗体\n在需要以粗体显示的文字前后各加两个*或_可以使文字加粗显示\n1 2 **粗体** __粗体__ 斜体\n在需要以斜体显示的文字前后各加一个*或_可以使文字已斜体显示\n1 2 *斜体* _斜体_ 删除线\n在文字前后各加两个~可以在文字上添加删除线\n1 ~~删除线~~ 当然也可以进行组合使用\n1 2 ***斜体加粗*** __~~粗体删除线~~__ 颜色 在写作过程中可能会遇到不少情况需要将文字用不同颜色标注,可以使用HTML的方式来实现,同时也可以设置字体和大小\n1 \u0026lt;font face=\u0026#34;微软雅黑\u0026#34; color=red size=12\u0026gt;落霞与孤鹜齐飞,秋水共长天一色。\u0026lt;/font\u0026gt; 段落 Markdown的换行有些奇特,直接Enter换行它好像不认,需要在段落结尾加两个空格+换行才可以,或者在上一段落和下一段落之间再加一行空行,即两次换行也可以。\n落霞与孤鹜齐飞,秋水共长天一色。 渔舟唱晚,响穷彭蠡之滨;\n雁阵惊寒,声断衡阳之浦。\n1 2 3 4 落霞与孤鹜齐飞,秋水共长天一色。 渔舟唱晚,响穷彭蠡之滨; 雁阵惊寒,声断衡阳之浦。 引用 写在\u0026gt;后的文字即可显示为引用,引用可以嵌套使用\n1 2 3 \u0026gt; 引用的文字 \u0026gt;\u0026gt; 嵌套引用的文字 \u0026gt;\u0026gt;\u0026gt; 更多嵌套 表格 表头 表头 表头 表头 内容 居左 居中 居右 1 2 3 |表头|表头|表头|表头| |---|:--|:--:|---:| |内容|居左|居中|居右| 第一行是表头,第二行代表对齐方式,默认是居左,在-左边加:即可居左对齐,在-右边加:可居右对齐,两边都加:表示居中对齐\n列表 有序列表\n1 2 3 1. 列表1 2. 列表2 3. 列表3 无序列表\n可以使用*、+或者-作为标记\n1 2 3 * 列表1 + 列表2 - 列表3 任务列表\n@mentions, #refs, links, formatting, and tags supported list syntax required (any unordered or ordered list supported) this is a complete item this is an incomplete item 1 2 - [x] 已完成的任务 - [ ] 未完成的任务 链接 可以直接输入网址,如:https://github.com/\n或者使用格式:[Text](url)\n点击这里返回主页\n1 点击[这里](https://kira-96.github.io/)返回主页 也可以使用HTML的方式\n1 点击\u0026lt;a href=\u0026#34;https://kira-96.github.io/\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;这里\u0026lt;/a\u0026gt;返回主页 还有一种就是使用索引的方式 例:谷歌、百度\n1 2 3 4 例:[谷歌][1]、[百度][2] [1]: https://www.google.com.hk/ \u0026#34;google\u0026#34; [2]: https://www.baidu.com/ \u0026#34;百度\u0026#34; 锚 主要用于在页面内跳转\n点击这里查看链接的用法\n1 点击[这里](#链接)查看链接的用法 图片 图片和链接的格式很像,url可以使用相对位置和绝对位置,当然网络位置也可以\n![Alt Text](url)\n1 ![图片](https://image-url.jpg) 也可以使用HTML的方式\n1 \u0026lt;img src=\u0026#34;https://image-url.jpg\u0026#34; width=\u0026#34;50%\u0026#34; height=\u0026#34;50%\u0026#34;\u0026gt; 设置对齐方式\n1 2 3 \u0026lt;div align=center\u0026gt; \u0026lt;img src=\u0026#34;https://image-url.jpg\u0026#34; width=\u0026#34;50%\u0026#34; height=\u0026#34;50%\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; 标注 这个用的并不多,看起来像是课本上文言文里面那种注释的感觉\n例:\n滕王阁序的作者是王勃1。\n1 2 3 滕王阁序的作者是王勃[^1]。 [^1]: 王勃(约650——676年),唐代诗人。汉族,字子安。绛州龙门(今山西河津)人。王勃与杨炯、卢照邻、骆宾王齐名,世称“初唐四杰”,其中王勃是“初唐四杰”之首。 行内代码 可以直接使用两个`(反引号)包裹行内代码\n例:我们学习的第一行代码通常都是printf(\u0026quot;Hello World!\u0026quot;)。\n1 我们学习的第一行代码通常都是`printf(\u0026#34;Hello World!\u0026#34;)`。 语法高亮 1 2 3 4 5 int main(void) { printf(\u0026#34;Hello World!\\n\u0026#34;); return 0; } 1 2 3 4 5 6 7 ``` cpp int main(void) { printf(\u0026#34;Hello World!\\n\u0026#34;); return 0; } ``` 公式 公式对于写论文的同学来说是非常有用的,Markdown的公式也比word的公式编辑方便多了。\n行内公式,使用$ $包括在内。\n如:$e=mc^2$\n1 $e=mc^2$ 单行公式,公式会单独占用一行,使用$$ $$包括在内。\n$$Fe+CuSO_4=FeSO_4+Cu$$\n1 $$Fe+CuSO_4=FeSO_4+Cu$$ 其中具体的符号和字母之类的需要的时候可以到网上去找,如Markdown 数学公式。\n转义字符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \\\\ 反斜杠 \\` 反引号 \\* 星号 \\_ 下划线 \\{\\} 大括号 \\[\\] 中括号 \\(\\) 小括号 \\# 井号 \\+ 加号 \\- 减号 \\. 英文句号 \\! 感叹号 注释 可以使用HTML的注释方式,会在生成的HTML中以注释的形式存在,不显示出来。\n1 \u0026lt;!-- 我是注释内容 --\u0026gt; 或者使用\n1 2 3 \u0026lt;div style=\u0026#39;display: none\u0026#39;\u0026gt; 我是注释内容 \u0026lt;/div\u0026gt; \u0026#x1f603; Emoji \u0026#x1f389; Markdown甚至支持Emoji\n\u0026#x1f60d;\u0026#x1f61c;\u0026#x1f620;\u0026#x1f4a2;\u0026#x1f637;\u0026#x1f47f;\u0026#x1f608;\u0026#x1f495;\nEmoji Cheat Sheet\n写在最后 自从接触了Markdown之后,我就很少使用Word这类工具了。日常工作和生活中用它来写文档和笔记真的是相当舒服,语法简单好记,完全可以满足需求,使用起来方便快捷,还可以借助HTML来实现一些比较复杂的功能。\n不过我们公司内部似乎没什么人使用,可能是由于我们公司并不是互联网企业,所以没有那么潮流,感觉可以借机会安利一波,对于提高整体的工作效率也有不小的帮助。\n王勃(约650——676年),唐代诗人。汉族,字子安。绛州龙门(今山西河津)人。王勃与杨炯、卢照邻、骆宾王齐名,世称“初唐四杰”,其中王勃是“初唐四杰”之首。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://kira-96.github.io/posts/markdown-%E5%B8%B8%E7%94%A8%E8%AF%AD%E6%B3%95%E5%A4%87%E5%BF%98/","summary":"标题 1 2 3 4 5 6 # 一级标题 ## 二级标题 ### 三级标题 #### 四级标题 ##### 五级标题 ###### 六级标题 也可以使用闭合方式的标题,结尾的#可以不必和开头一致 1 2 3 # 一级","title":"Markdown 常用语法备忘"},{"content":"桌面端 Typora 收费,支持 Mac, Windows, Linux\nMarkText 支持 Mac, Windows, Linux\niA Writer 收费, 支持 Mac, Windows\n移动端 iA Writer iOS - 收费 Android - 免费\n熊掌记 支持 Mac, iOS, 可免费使用,部分功能需要订购\n笔记软件 Joplin 支持 Windows, Mac, Linux, Android, iOS 真正的全平台,支持Markdown语法,可以配合OneDrive或者WebDav同步笔记 这里推荐使用坚果云做同步盘。\n","permalink":"https://kira-96.github.io/posts/%E5%A5%BD%E7%94%A8%E7%9A%84-markdown-%E7%BC%96%E8%BE%91%E5%99%A8/","summary":"桌面端 Typora 收费,支持 Mac, Windows, Linux MarkText 支持 Mac, Windows, Linux iA Writer 收费, 支持 Mac, Windows 移动端 iA Writer iOS - 收费 Android - 免费 熊掌记 支持 Mac, iOS, 可免费使用,部分功能需要订购 笔记软件 Joplin 支持 Windows, Mac,","title":"好用的 Markdown 编辑器 - 全平台"}]
\ No newline at end of file
diff --git a/index.xml b/index.xml
index 366c959..dcd6f58 100644
--- a/index.xml
+++ b/index.xml
@@ -1,4 +1,4 @@
-✨kiraの博客 https://kira-96.github.io/Recent content on ✨kiraの博客 Hugo -- gohugo.io zh Copyright © 2019-2024 [kira's blog](/) · Wed, 29 May 2024 14:52:04 +0800 个人常用软件分享 https://kira-96.github.io/posts/%E4%B8%AA%E4%BA%BA%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%88%86%E4%BA%AB/Sat, 22 Feb 2020 10:43:10 +0800 https://kira-96.github.io/posts/%E4%B8%AA%E4%BA%BA%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%88%86%E4%BA%AB/ 个人常用软件分享 EPICS IOC 访问安全 https://kira-96.github.io/posts/epics-ioc-access-security/Mon, 18 Mar 2024 09:52:05 +0800 https://kira-96.github.io/posts/epics-ioc-access-security/ EPICS IOC 数据访问安全配置 龙芯开发板移植 IgH EtherCAT Master https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/Fri, 23 Feb 2024 12:54:31 +0800 https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/ 龙芯2K0500开发板移植EtherCAT主站程序和EPICS EtherCAT模块 Hugo Diagrams https://kira-96.github.io/notes/hugo-diagrams/Tue, 30 Jan 2024 09:45:30 +0800 https://kira-96.github.io/notes/hugo-diagrams/ 在Hugo中使用示意图 EPICS的MODBUS模块的编译和使用 https://kira-96.github.io/posts/build-epics-module-modbus/Tue, 02 Jan 2024 14:38:38 +0800 https://kira-96.github.io/posts/build-epics-module-modbus/ 交叉编译EPICS的MODBUS模块 交叉编译 ACAI https://kira-96.github.io/notes/cross-compiling-acai/Tue, 26 Dec 2023 19:31:56 +0800 https://kira-96.github.io/notes/cross-compiling-acai/ 交叉编译 ACAI 交叉编译EPICS和IOC https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/Tue, 12 Dec 2023 16:26:35 +0800 https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/ Linux交叉编译EPICS Windows上使用MinGW编译安装EPICS https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/Wed, 29 Nov 2023 18:57:02 +0800 https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/ 需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。
+✨kiraの博客 https://kira-96.github.io/Recent content on ✨kiraの博客 Hugo -- gohugo.io zh Copyright © 2019-2024 [kira's blog](/) · Fri, 07 Jun 2024 14:51:36 +0800 个人常用软件分享 https://kira-96.github.io/posts/%E4%B8%AA%E4%BA%BA%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%88%86%E4%BA%AB/Sat, 22 Feb 2020 10:43:10 +0800 https://kira-96.github.io/posts/%E4%B8%AA%E4%BA%BA%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6%E5%88%86%E4%BA%AB/ 个人常用软件分享 EPICS IOC 访问安全 https://kira-96.github.io/posts/epics-ioc-access-security/Mon, 18 Mar 2024 09:52:05 +0800 https://kira-96.github.io/posts/epics-ioc-access-security/ EPICS IOC 数据访问安全配置 龙芯开发板移植 IgH EtherCAT Master https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/Fri, 23 Feb 2024 12:54:31 +0800 https://kira-96.github.io/posts/%E9%BE%99%E8%8A%AF%E5%BC%80%E5%8F%91%E6%9D%BF%E7%A7%BB%E6%A4%8Digh-ethercat-master/ 龙芯2K0500开发板移植EtherCAT主站程序和EPICS EtherCAT模块 Hugo Diagrams https://kira-96.github.io/notes/hugo-diagrams/Tue, 30 Jan 2024 09:45:30 +0800 https://kira-96.github.io/notes/hugo-diagrams/ 在Hugo中使用示意图 EPICS的MODBUS模块的编译和使用 https://kira-96.github.io/posts/build-epics-module-modbus/Tue, 02 Jan 2024 14:38:38 +0800 https://kira-96.github.io/posts/build-epics-module-modbus/ 交叉编译EPICS的MODBUS模块 交叉编译 ACAI https://kira-96.github.io/notes/cross-compiling-acai/Tue, 26 Dec 2023 19:31:56 +0800 https://kira-96.github.io/notes/cross-compiling-acai/ 交叉编译 ACAI 交叉编译EPICS和IOC https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/Tue, 12 Dec 2023 16:26:35 +0800 https://kira-96.github.io/posts/%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91epics%E5%92%8Cioc/ Linux交叉编译EPICS Windows上使用MinGW编译安装EPICS https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/Wed, 29 Nov 2023 18:57:02 +0800 https://kira-96.github.io/notes/windows%E4%B8%8A%E4%BD%BF%E7%94%A8mingw%E7%BC%96%E8%AF%91%E5%AE%89%E8%A3%85epics/ 需要使用的软件 Strawberry Perl for Windows EPICS Base 编译Base需要有gcc、g++、make、perl这些工具,但其实我们只需要安装Strawberry Perl就可以了,安装完成后就有了MinGW的编译环境,足够编译安装EPICS了。
这里使用MinGW环境编译EPICS,不使用MSVC编译器。
安装 Strawberry Perl 这里选择 Strawberry Perl 5.38.2.1。
直接安装即可,需要注意的是,安装路径不能有空格和中文,最好放在盘符的根目录下。
diff --git a/page/2/index.html b/page/2/index.html
index 62765ed..7488934 100644
--- a/page/2/index.html
+++ b/page/2/index.html
@@ -1,4 +1,4 @@
-✨kiraの博客
+✨kiraの博客
问题描述 在使用 WSL 更新软件包的时候经常会遇到这样一个报错 1 /sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link 意思是说 /usr/lib/wsl/lib/libcuda.so.1 不是一个符号链接。 问题分析 通过名字可以判断这应该是nVidi...
前言 由于这半年来一直在做嵌入式Linux系统软件开发工作,所以经常和嵌入式设备打交道,最早接触的嵌入式Linux应该就是树莓派了,而我的树莓...
开始一份新工作 好久没有更新博客了,最近几个月都在忙新工作的事情,虽然只是短短3个月,但回想起来已经是很久之前了。从4月份面试,然后体检,提交...
前言 The Medical Imaging Interaction Toolkit (MITK)是一个免费的开源软件,用于开发交互式医学影像处理软件。最近突然安排我做相关的一些工作,首先就要从编译开始,当然官网...
前言 今天偶然看到一个新的开源的git服务软件Gitea,一看到界面,瞬间就爱了,因为之前我自己用的是gitblit,界面比较简单,主要是用来...
说明 这里是个人工作时常用的一些git命令,现在越来越多了,小本本都快记不下了,这里稍微做一下整理。 工具下载 首先是git的下载地址: 官网:ht...
食用方法 Step 1 将ChineseSimplified.isl放到Inno Setup安装目录下的"Languages"文件夹里...
简介 Prism是一个用于WPF、Xamarin Forms、WinUI等的MVVM框架,刚刚学习,这里只是个人总结的一些知识点笔记。 IoC IContainerProvider 1 2 3...
简介 Inno Setup是一个免费的安装包生成软件,完全开源免费,使用起来也非常方便,文档也十分全面。与其它同类软件相比十分的小巧便携,功能也十分全...
简介 在编码的时候难免会遇到不同编程语言之间的接口调用,其中最通用的就是C的动态链接库,几乎所有语言都可以调用C的接口函数。那么这种关系能否反...