[ESPHOME]自定义开发

esphome提供的框架偏向于让我们尽量在yaml配置文件中定义逻辑,它提供了很多基本的功能,这样可以减少自己需要写的代码,也可以减少出问题的概率。

如果框架提供的部分满足不了自己的全部需求的话,就需要自己写c++代码。另外再强调一下,esphome基于platformIO工具链,再底层可以选Arduino,也可以是esp-idf,在开发时要考虑兼容性的话(比如在esp8266和esp32上都可以运行),就尽量使用esphome和arduino的api。

esphome提供了几种方式混入外部的代码:

  1. 在配置文件中使用”includes”选项直接一同编译指定的文件。
  2. 在配置文件中使用”libraries”选项引入外部的库。
  3. 使用外部组件,外部组件支持直接指定仓库地址,它在编译时会去把仓库克隆下来。当指定的是本地路径时,也就是把本地的一个目录当成一个组件库来处理。

要自定义代码我认为最好的方式就是在项目目录里定义一个本地的外部组件,虽然使用外部组件需要写一个python初始化脚本(这是我极其讨厌的,还好有AI帮我写),但这种组件的形式可以更容易地嵌入和调用我们的代码(因为如果你不用这种形式,就要禁用main.cpp自动生成,然后自己去手写入口文件,否则你的代码就没有地方可以调用。esphome框架默认会生成一份它自己的入口和loop,你就不能再在全局定义你的了。)。

继续阅读[ESPHOME]自定义开发

[ESPHOME]本地部署开发环境

这是一篇笔记,记录我开始尝试使用esphome做一些小工具的过程,之后可能还会有一些相关的笔记。
如果你要问为什么我不用home assistant里的esphome builder插件的话,原因是我需要插入我自己的项目代码,如果使用esphome builder 我就必须先把代码上传到某处然后把部件的地址写在配置文件里才能下载并编译,会很麻烦,所以需要一个本地开发环境用于快速测试更改。

安装环境

使用esphome主要是为了接入home assistant,esphome依赖platformIO(以下都简称为pio)作为底层框架,因此需要先安装它。在windows平台,我们可以直接使用vscode安装platformIO插件,这个插件将会帮我们把pio安装好,在安装pio的时候需要确保网络爬墙,否则大概率会安装失败,在实际使用时我们并不需要直接去用这个插件,之后的步骤都将靠esphome的命令完成。

然后是安装esphome工具链,这其实是一个python包,按照官网的方式安装:https://esphome.io/guides/installing_esphome

pip3 install wheel
pip3 install esphome

然后使用`esphome version`命令检查能否正确显示版本号即可。

项目初始化

接下来进行一个基础的固件编译测试,首先确定要使用的esp8266或者esp32的模块,然后用以下命令来初始化一个配置文件:

esphome wizard 配置文件名字.yaml

执行后将会引导你选择板子以及进行一些设置,这里就不赘述了,完成后会生成一个配置文件。

我在写这篇笔记时使用的测试板是一个esp8266的开发板,选择的是’nodemcuv2’。

固件编译

有了配置文件之后就可以使用以下命令来进行编译,编译出来的固件会在当前目录下的”.esphome\build”文件夹里,这里面还有其它的缓存之类的东西,如果之后的编译出现了奇怪的错误,可以尝试删除这个文件夹。

esphome compile 配置文件名字.yaml

固件烧录

编译好固件之后就可以用以下命令上传你的固件,第一次上传需要使用串口连接,以后的固件就可以通过网络OTA的方式进行更新了(也是这个命令,执行时会让你选择)。

esphome upload 配置文件名字.yaml

固件写完之后重启开发板,它将会开始连接到wifi,此时同一局域网里的home assistant应该就会提示发现了新的设备,问是否要添加,添加时把配置里的api密钥填进去连接即可。

额外测试

接下来配置一个简单的控制开关用来点亮开发板上的led,在配置文件里加入以下顶层项目:

# LED控制配置
output:
- platform: gpio
pin: D4
id: led_gpio
inverted: true #led在引脚低电平时是亮,所以要反转一下

switch:
- platform: output
name: "开发板LED"
output: led_gpio

然后再编译并上传,设备联网后在home assistant的该设备页面上应该会自动多出来一个控制开关,现在就可以通过这个开关控制led的亮灭了。


继续阅读[ESPHOME]本地部署开发环境

Home Assistant 访问返回400

Home Assistant默认不允许通过反代访问,直接配置反代会返回400,需要修改配置文件”/homeassistant/configuration.yaml”来进行设置。

在该文件中添加以下内容:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 10.0.0.0/16  #你要允许的反代服务的ip或CIDR

然后重启Home Assistant服务即可(重新加载配置没用,要重启服务)。

open-webui 分支剧情助手

在生成剧情的时候,我发现AI很容易被之前它自己提供的选项和用户提交的选项或者要求干扰,所以我借助deepseek写了这个脚本(我不会python),并修正了一些逻辑错误。

虽然叫这个名字可能不太准确,不过我也不确定到底应该怎么叫,所以我就直接取得直白一点。

这个脚本的有两个模式:

  1. 在向服务器发送消息时把AI已经生成的剧情合并成一个大的回复,剔除其中已经没用的选项(除了最后一次回复的选项会被保留,其它回复中的选项都会被删除),并删除用户之前所有的选项,只保留最后一次回复的选项。
  2. 和前一个模式的区别是仅移除之前消息中的选项,但不合并历史消息,用户的路线选择也会保留在消息历史中。

注意,这个脚本只是修改发送出去的内容,所以不会修改你可以看到的消息历史,这样是为了方便你发现选择错路线了之后还可以回去重新再来,以及回顾自己的历史选项。

这样处理之后,AI就不会再被用户实际上没有选择的选项干扰。

脚本代码在这里https://gist.github.com/JiaJiaJiang/c0bb2e8ec3fe5d9a2cd2ec997fe4f042

用法:直接添加到管理员面板的函数中即可,名字自己取,记得打开该脚本的“全局”选项。当用户发送的第一个消息以“分支剧情场景定义:”开头时,这个插件就会起作用。为了更好的效果,建议在使用模式2时不要使用选项字母,而是把AI提供的选项复制发送,这样消息历史中的内容会更加连贯,因为AI消息中提供的选项内容会被剔除。

切换两个模式的开关是函数选项按钮中的“Merge Assistant Messages”,默认开启,即模式1。

建议的系统提示词

# Task
* AI正在为用户提供分支剧情剧情生成服务。
* 用户是故事的主人公,剧情中把用户成为"你"。
* AI需要根据用户的场景定义补全其它要素,开始一个新的剧情,用户将以“分支剧情场景定义:”为开头进行场景的定义,然后AI直接开始回复用户400到500字的剧情开端。
* 续写剧情文本的内容需要充实,人物和场景描写细致生动。不要出现和前文重复性的描述,对同一个现象的描写用词要有新意。
* 续写到情景最后时使用代码块给用户提供分支选项,向用户提供A、B、C、D四个选项。
* 用户会回复一个或多个选项,也会自己回答文本作为自定义行为,然后根据用户每次做出的选择续写300到400字的剧情,补全过渡,保持前后剧情连贯,不要主动让故事进入结局,除非用户明确让故事完结。不要对用户的选择做额外的解释,只要直接回复续写的剧情。
* 向用户提供的选项必须符合下面"选项代码块模板"的格式。
* 向用户提供了选项之后就不能再输出任何内容。
* 由于后续提交的剧情将被排除选项文本后进行合并,不要输出除了剧情和选项以外的任何解释性文本。

# 选项代码块模板
```options
A. xxx
B. xxx
C. xxx
D. xxx
```

以上是AI必须遵守的要求,不要在下文中输出以上所有内容,AI一旦回答就表示完全明白和接受以上所有要求。

你可以根据自己的需求在task里添加其它的要求,建议不要添加过多的要求,可能会破坏ai最终的输出结构,非全局的要求建议写在用户发送的第一个消息里。

继续阅读open-webui 分支剧情助手

TrueNAS设置ipv6网关

之前我的TrueNAS系统里网络无论怎么设置ipv6的默认网关,在默认路由那一栏里显示的始终是fe80::1,后来问了下deepseek,知道了要在网关地址后面加个接口名称,比如fe80::1%enp6s18这种形式,其中的”enp6s18″就是我这里的网络接口名称,这样设置好之后在默认路由那里的网关地址终于变了。

处理口腔溃疡

根据我常年口腔溃疡得出的经验,写一篇笔记 ,希望能帮到大家。

首先我觉得口腔溃疡的主要起因是创伤和物质残留,无论是口腔壁、舌头还是嘴唇上应该都是这两个原因。

创伤就比如经常用舌头舔牙齿缝的残渣导致舌头的磨损然后溃疡,不小心咬到舌头、嘴唇或者口腔壁造成溃疡、因习惯或生理问题导致牙齿经常磨到嘴唇或口腔壁造成的反复溃疡。

物质残留就比如塞在牙缝里的菜,喝完饮料没有在一定时间内清洁口腔,残留在口腔里的物质会导致局部细菌大量繁殖,从而产生溃疡。有时睡前喝过可乐忘了漱口,第二天醒来必定会有溃疡。

如何解决呢?

首先最重要的是保持口腔清洁,口腔清洁是加快溃疡愈合的基本条件,也是预防溃疡产生的主要因素。

在发生溃疡之后,一定不要用舌头去舔创口,这一点非常重要,如果溃疡在舌头上,就尽量不要用溃疡处去碰到任何东西,如果溃疡在别处,也一定要忍住别用舌头去舔,这会延缓愈合甚至变得更加严重。如果是因为被牙齿磨损导致的溃疡,首先要确定为什么会产生这样的磨损,是否是习惯问题,知道导致的问题之后主动对类似诱因进行规避,如果不知道为什么会产生,就先用一些口腔溃疡凝胶之类的把创口和牙齿隔开来。

如何恢复?

到目前为止,我发现最有效的方法就是溃疡之后经常张嘴呼吸,具体原理不祥,但真的很有效。

在发现有一点溃疡的苗头出现时就开始经常张嘴呼吸甚至可以把这个溃疡直接憋回去,让它没有发展成溃疡的机会。即使是已经成型的溃疡,用这个办法也可以快速恢复,不用上药。

当然以上方法还是要在保持口腔清洁的情况下才是最有效的。

 

Sony WF-1000XM5

之前的WF-1000XM4是2021年买的了,左右两只我也都自己换过电池了,其实现在还能正常用,就是对一些小毛病不太满意,比如触摸功能可以被饭的热蒸汽触发,另外不知道为什么右耳的有时候从耳机盒里拿出来会处于无法触控的状态,必须摆回去再拿出来才行,以前不会这样的,这种情况还经常出现就挺麻烦的,于是趁现在XM5的价格还可以就买了,另外给小周也买了一副。

继续阅读Sony WF-1000XM5

[ZFS]给Zpool中的硬盘附加镜像

刚刚在处理truenas里一块问题镜像盘的时候本想把它下线,结果不小心直接把它删除了,于是这个pool直接就被降级成了条带池。

然后我插进另一块盘之后想要再把它添加为镜像盘,Truenas的ui竟然不让,于是只能用命令,这里做个笔记。

首先用 zpool status 池名称 看一下想要镜像的盘在池中的id,这里我的是”df872c84-8689-44f1-8a1d-5f0ee673c34d”。

然后用以下命令直接把新的硬盘添加进池中即可。

zpool attach 池名称 df872c84-8689-44f1-8a1d-5f0ee673c34d /新盘的路径

 

拦截微软电脑管家的安装或执行

首先不要指望通过调整windows自己的设置来禁止这个流氓行为,微软已经沉迷于给用户喂屎,不会给你关闭的选项的(甚至你反复删除之后它依然会反复自动安装)。

所以这里用的是杀毒软件拦截,比如通过在火绒添加以下自定义规则即可拦截其安装或运行。

这里用通配符匹配微软电脑管家的程序目录名称,并且禁止该目录下一切的创建和修改以及执行行为,只保留删除操作,这样即使它已经被以各种奇怪的方式装进了你的电脑里,也可以确保它里面的程序不会被执行。

如果要问我为什么要对这个软件这么恨之入骨,我只能说对于未经许可偷偷安装的软件(简称流氓软件)就要用这样的态度对待它。我也不是没有尝试过打开它看看,但打开之后发现它确实就是一坨屎,没有任何实际的功能,完全就是个其它程序的启动器,但是它却还起了好几个webview来浪费我们的内存,这不值得。

 

[Zig]使用 std.Thread.Pool 线程池

我发现Zig的官方文档里似乎没有写这个`std.Thread.Pool`的具体用例,所以写一篇笔记给大家参考一下。

这个Zig标准库中的线程池主要用到两个部分,一个是线程池本身的结构体`std.Thread.Pool`,一个是用于等待任务结束的`std.Thread.WaitGroup`(等待组)结构体。等待组的作用是把一组相关的任务放在一起,之后可以使用特定方法等待它们全部完成。

流程:

  • 在线程池初始化好了之后在创建线程任务时给pool.spawnWg方法传入指定的WaitGroup和要执行的函数以及参数
  • 分配任务时线程池就会开始执行任务
  • →随时使用`pool.waitAndWork`方法阻塞当前线程,等待WaitGroup中所有任务完成
  • ↘或者在任何时候使用`wait_group.isDone()`方法检查任务是否全部完成,如果没有完成可以让主线程去做其他事情。

以下直接写一个示例,内容依然是在我前面的笔记中已经出现了好几次的多线程数据竞争示例。

const std = @import("std");
const debug = std.debug;
const Pool = std.Thread.Pool;
const Thread = std.Thread;
const WaitGroup = std.Thread.WaitGroup;

const print = debug.print;

const NUM_THREADS = 2;

var sum: u64 = 0;//让几个线程分别把这个sum自增,完成后sum应当由于非加锁自增导致其值为一个随机数
pub fn sumTask(idx: usize) void {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Thread {} (idx:{}) : starting...\n", .{ tid, idx });
    var timer = std.time.Timer.start() catch unreachable; //获取一个计时器,用于之后计算线程执行时间
    const y = ∑
    for (0..50000000) |_| {
        y.* += 1;
    }
    print("Thread {} (idx:{}) : done in {}ms.\n", .{ tid, idx, timer.read() / 1000000 });
}
test "threadpool" {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Main thread {} start\n", .{tid});
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var thread_pool: Pool = undefined;
    try thread_pool.init(.{ //初始化线程池
        .allocator = allocator,
        .n_jobs = NUM_THREADS, //线程数
    });
    defer thread_pool.deinit();
    var wait_group: WaitGroup = undefined;
    wait_group.reset(); //初始化等待组
    //分配10个任务,线程池内的空闲线程会分别完成这些任务
    for (0..10) |idx| {
        thread_pool.spawnWg(
            &wait_group,//传入等待组
            sumTask,//传入任务函数
            .{idx},//任务函数的参数元组
        );
    }
    thread_pool.waitAndWork(&wait_group); //等待以上分配的任务全部完成
    print("Main: sum = {}\n", .{sum});
    print("Main: program completed. Exiting.\n", .{});
}

需要注意的是,如果在分配完任务后立刻使用`thread_pool.waitAndWork`等待任务完成(像上面的示例中一样,也就是在任务没有全部完成时就开始等待),你会发现虽然设置的线程数是2,理论上任务应该两个两个一起执行,但实际上却是3个任务一起执行的,因为调用这个等待方法的线程也执行了任务。我当时也疑惑了一会儿,然后想明白了,既然等待线程已经等在那里了,那么与其干等不如一起把任务做完,这样的逻辑确实是合理的,但这样实际并行执行的线程就会多一个,这是要注意的地方。

另一个要注意的是,目前的线程池实现中每一个线程启动之后就会开始从任务队列的开头取任务,但添加任务的时候也是添加到队列的开头的,所以虽然不严格保证顺序(因为你后续还可以继续添加任务),但任务队列大致上是后进先出的,所以不要连续不断地添加任务,至少要等线程池任务清空了才可以继续添加新的任务。(我觉得这样不太合理)

配置VSCode CodeGeeX插件连接本地语言模型的提示词

CodeGeeX插件支持连接本地语言模型服务来提供补全功能,可以避免发送出去的源码泄露的风险。

我使用LM Studio起了一个服务端,然后配置插件去连接,虽然连接可以正常连上,但是补全代码时完全不会返回正确的内容,要么就是复读,要么就是连着发送的原始内容一起返回回来。

然后我发现了,“参数配置”里的两个“使用默认提示词”选项,并不是使用插件自带的默认提示词的意思,而是使用服务端配置的提示词的意思,所以如果服务端没有配置系统提示词的话,那么补全的时候就会乱答。

但即使我把“使用默认提示词”取消勾选,使用插件设置里自带的提示词,依然会进行错误的补全,然后我认为是不是这个插件提供的提示词其实是不对的,于是我改了一下,把代码补全的提示词改为如下内容:

你正在执行编辑器的文本或代码内容补全任务,编辑器将按以下结构向你提供用户正在编辑的文件内容:
"###LANGUAGE:当前文件所用的语言
###MODE:BLOCK
<|code_suffix|>光标之后的内容<|code_prefix|>光标之前的内容<|code_middle|>"
你需要预测在"光标之前的内容"和"光标之后的内容"之间应该插入的内容。
你只要输出你预测要插入的内容,不要附加其它任何内容。

然后再进行补全,这下补全的内容终于正常了。

额外提示,提示词模板需要按照下面的设置,否则也无法正确生成结果:

Before System:     <|system|>\n
After System:      \n
Before User:       <|user|>\n
After User:        \n
Before Assistant:  <|assistant|>\n
After Assistant:   \n