最近碰到的糟心电池

这两年移动电源的概念在不少应用里面开始普及,一块一度电的电池价格大概降到了 500$ 以下,业界正在完成从三元锂到磷酸铁锂的转变。早些时候买了块 EcoFlow River 2,算是基本入了个门,比较欣赏他家的快充以及 app 支持,256Wh 的电池给 Raspberry Pi 和一个相机供电可以完成两天多的 timelapse 没什么问题,只是这个价格没有达到 0.5$/Wh 的水平(售价得到 ~120$,一般只有 150+),有点让人觉得膈应。后来网上搜索的时候发现 TenergyGolabs 有款 300Wh 的电池,可以比较一下外形,这款这款,感觉同一个厂贴的牌,为了避免误伤,我这里只说 Golabs R300,与 Tenergy 无关(我并没有购买或者拆开过),售价时不时会下探到 100$,符合这里提到的售价,于是这次机缘巧合购入了台。主要看了这个人的详细评测,觉得至少用料不算太差,软件上没有 EcoFlow 家好,但是作为一个用途比较小众的电源,app 的支持可能也用不上。

一个让我觉得可以试试这个电源的原因是它本质上是 LFP 电池,直接可以输出 12V DC,很多灯具供电其实就够了,而且它直接提供了三个 12V 的输出,其中两个都是 5.5/2.1mm 常见的插头。这意味着不需要 buck converter 之类的升压降压,效率高了很多。而 River 2 竟然只提供了车充一个头,还得转,这点有点对不起它的售价。

不过令我震惊的事情是收到了这款电池之后发现的情况。其中一个摇晃了一下发现有东西在里面滚动,我第一反应不会是什么螺丝在里面吧。询问了卖家之后,卖家小哥最后提供了一个替换的电池,因为我本打算把这玩意放车上,万一真的有螺丝在里面短路还是什么,这可不是损失几十块的事情。稳妥起见,我简单的测试电池可以工作后就在没有打开了。后来卖家小哥同意再给一台之后,下决心一探究竟。这玩意用的螺丝还不是那么常见 torx T10 型号(不是常见的梅花、一字或者六角,而是六角星),一般的螺丝刀里面可能带了个这个类型的接头,但这货的螺丝孔非常非常的深,一开始以为随便买个套装就能够到,结果寄过来的 T10 是个 60mm 杆,仍然探不到里面的螺丝,后来买了个 100mm 的,勉强够到绝大部分的螺丝,但是感觉可能要 150mm 才比较安全。

拧螺丝的时候其实就有点嘀咕,为啥有的螺丝似乎非常松,这个带磁吸的螺丝刀没拧一两圈就可以把它弄出来了。等到可以打开一半的时候其实已经能窥视到内部一二,它前后两块塑料模具之间螺丝承力的部分是塑料,而这种圆柱形的结构已经碎裂了,这更加让我担心里面滚动的是螺丝。经过几天的尝试,最后终于完整的打开了这块 Golabs R300 电池,令我大跌眼镜的是,这 11 个用来闭合前后两块部件的螺丝,有两个在里面,几乎所有的螺丝孔都有不同程度的损伤,也许是二手货前面的买家拆过?也许是出厂之前的装配工人拧螺丝太用力,把里面的塑料已经弄坏掉了,运输过程中加上颠簸,已经无法承受这重量?

Golabs R300 内部结构

一般这种电池都有几个主要组成部分:

  • 锂电池本体:这部分看起来与 YT 上所说吻合,的确是用的 LFP 电池
  • 直流部分,主要是从电池把 12V 接过来,通过不同的端口输出出去
  • 交流部分:需要一个硕大的 300W 的逆变器,直流进去交流出来,然后接到面板上的插头

我发现电池塑料壳里面一共捞出来 4 颗螺丝,其中两颗是前面固定前后面板的螺丝较大,另有两颗较小的还不知道哪里来的,后来发现竟然是逆变器上面固定逆变器的四颗螺丝,其中两颗消失了(正好匹配),另外两颗也摇摇欲坠。我的天哪,这电池是吃了一记降龙十八掌震的全身经脉已断了么?

红框内是机身内取出来的螺丝,靠左的两颗是固定逆变器中四颗中两颗,右上黑色破碎的塑料是这些固定这些螺丝的部分

这么深的螺丝孔,但是螺丝却这么短,也没有金属加固件,这也许真的是个设计缺陷?也许电池还能拼回来继续用用,可是想到内部如此状况不禁担忧,这公司不会已经倒闭了吧

也许这个还能装回去,在相对安全的环境下使用,但是可能真心不想再碰这个牌子的设备了。

最近碰到的糟心电池

Servo 的 tasmota 控制

之前不少 tasmota 的玩法都是传感器与 relay。这里讨论一下 servo 的基本控制,参考这篇文档,并且更新其中几处错误。从原理上来说他们都是依靠方波(PWM)来控制,因此重要的参数有三个

  • 方波信号的频率(PWM frequency),见到过 50Hz 跟 200Hz 两种
  • 在一个周期上 1 的长度决定了不同的行为,因此有一个可接受的宽度范围,很显然这个范围不会超过周期,如果周期是 200Hz = 5ms 的话,这个宽度可能在几百微秒到 2500 微秒之间(该文档中提到的单位 ms 应该为 μs,可能作者觉得 microsecond 缩写是 ms,但是实际上 millisecond 才是 ms)

从分类上来说,servo 其实有两个种类:

  • 角度控制(positional servo):设定 PWM 方波宽度影响的是旋转臂的位置,最小/大宽度对应的是最小/大角度的位置
  • 速度控制(continuous rotation servo):设定 PWM 方波宽度影响的是转速,一般中间的宽度对应的是 0 转速,而最小最大宽度对应的是顺时针/逆时针的最大转速

参考这里的 video 可能更明确一点。中文里面似乎把 servo 称为“舵机”,不晓得是不是因为角度控制更多的是用来处理(飞机、船的)“舵”这类驱动。

文档里面提到的 servo 可能是某些 RC car 之类里面更为常见的型号,而市面上比较便宜对某些轻量级应用更为友好的是 SG90(也是上面视频里面作为例子的型号),9g 的重量,可以被常见的 3-5V DC 电源驱动(MG90 需要的是 12V,所以文档里面的图片都使用了直流降压芯片将电源的 12V 转换到 5V 来驱动控制芯片,实际使用的时候需要找 12V 电源比较麻烦),简单的项目可能直接拿 USB 的 5V 输出来搞定,直接可以为控制器与 servo 供电,方便许多。

依照文档上面的设置(注意 D5 对应的是 relay2)

PINGPIOConfigServo
D30Relay1EN
D42PWM1PLS
D514Relay2DIR

实际接线的时候可以将 ESP8266 开发板上的 5V 连接红线,GND 连接棕线,D4 引脚连接橙线,对应的 D3/D5 其实不需要任何连接(只是为了 UI 上显示按钮来控制开合)。

购买 SG90 的时候有若干个版本(外观居然一毛一样):

  • 180 度版本一般是角度控制,我拿到的频率设置为 200,波形宽度大约在 500-2500 微秒(参看 datasheet
  • 360 度版本一般是速度控制

按照文档配置好了之后,HA 里面这个设备就会以 cover 的形式出现

Home Assistant 整合 tasmota 控制的 servo

有了这个玩意,我们拿来做什么用呢?

  • 控制物理开关,thingiverse 上有不少控制电灯开关的例子
  • 速度控制的感觉可以驱动个什么轮子之类的东西

看看后面能不能找到点不一样的用途

Servo 的 tasmota 控制

Coral TPU

其实一直比较纳闷为什么 Google 没有更大程度上开源 TPU 这个生态系统,的确 GCP 是一个不错的“卖点”,但是现在 AI 的天下几乎都是 NVIDIA 的硬件一家支棱起来,这个事情就像当年的 MapReduce,Google 做好了实现、发了篇论文,工业界选择了 hadoop,到头来在 GCP 上还要兼容 hadoop 的 API。感觉现在的 TPU 捣鼓出来了这么多代,一直在告诉大家 TPU 更有效率(参看这里),但是现在事实上除了 Midjourney 这一家比较有名的 AIGC 公司在 GCP 上,OpenAI 是 NVIDIA 的客户,不少大一点的公司(Amazon、Meta 之类)也不会在 GCP 上购买 TPU 算力,TF 一度输给 PyTorch 的情况(参看这里),而其中赢面比较大的算是部分公开了 Coral TPU 作为 edge computing 的这部分,正是因为它,使得我们可以通过 tf-lite 将一些小型模型能够部署到一些工业应用的场景里:

  • 比如部署了摄像头,可以同时部署一个 edge computing 的机器用于一些比较复杂的计算:估算人流、车流
  • 比如部署了麦克风,可以部署类似的设备处理声音的识别

不过 Coral 设备并不便宜,一款 USB 接口的 accelerator 卖到 60$,还经常买不到(现在一块二手的 GTX 960 差不多也这个价格,感觉是不是一下子就觉得贵了)。这次大概等了两个月终于拿到了。另有一款多 TPU 的 PCI-E 接口的加速卡竟然卖到了 1000$,不晓得谁会是冤大头。其实吧如果早些时候拿出来卖,像学术界这种比较在意成本的很可能会跟进,TF 到后面其他的 JAX 也好也不至于输这么大的用户群体吧。当然一家之言,不足为信,历史的进程往往就是让人难以预料吧

上手

from pi3g.com

其实 Coral 设备的软件(datasheet)做的确实挺好,上手很快,比如跟着这篇文章就能很快在 Raspberry Pi 上运行起来一个识别鸟类的 MobileNet 模型,而这里可以找到一些已经训练好、并且可以部署在 Coral 设备上的模型,对于 AI 小白来说非常友好。其实对应的 python 接口也比较容易,我们这里摘一段

from PIL import Image
from pycoral.adapters import classify
from pycoral.adapters import common
from pycoral.utils.dataset import read_label_file
from pycoral.utils.edgetpu import make_interpreter

def main():
  # ...
  labels = read_label_file(args.labels) if args.labels else {}
  interpreter = make_interpreter(*args.model.split('@'))
  interpreter.allocate_tensors()

  # Model must be uint8 quantized
  if common.input_details(interpreter, 'dtype') != np.uint8:
    raise ValueError('Only support uint8 input type.')

  size = common.input_size(interpreter)
  image = Image.open(args.input).convert('RGB').resize(size, Image.ANTIALIAS)

  # Image data must go through two transforms before running inference:
  # 1. normalization: f = (input - mean) / std
  # 2. quantization: q = f / scale + zero_point
  # The following code combines the two steps as such:
  # q = (input - mean) / (std * scale) + zero_point
  # However, if std * scale equals 1, and mean - zero_point equals 0, the input
  # does not need any preprocessing (but in practice, even if the results are
  # very close to 1 and 0, it is probably okay to skip preprocessing for better
  # efficiency; we use 1e-5 below instead of absolute zero).
  params = common.input_details(interpreter, 'quantization_parameters')
  scale = params['scales']
  zero_point = params['zero_points']
  mean = args.input_mean
  std = args.input_std
  if abs(scale * std - 1) < 1e-5 and abs(mean - zero_point) < 1e-5:
    # Input data does not require preprocessing.
    common.set_input(interpreter, image)
  else:
    # Input data requires preprocessing
    normalized_input = (np.asarray(image) - mean) / (std * scale) + zero_point
    np.clip(normalized_input, 0, 255, out=normalized_input)
    common.set_input(interpreter, normalized_input.astype(np.uint8))

  # Run inference
  interpreter.invoke()
  classes = classify.get_classes(interpreter, args.top_k, args.threshold)

其实还蛮直接的,需要注意的是这里在 CPU 上还是需要干一点活的,比如图像的预处理(缩放、归一化),TPU 处理的图片仅有 224x224x3 的大小。简单的改写一下,估计可以作为家里面的摄像头数据处理的服务器用用了。

tf-lite 模型

比较常见的做法是将一个 TF/JAX 生成的 saved model(通常使用 proto buffer 保存相关的数据)通过 tf.lite.TFLiteConverter.from_*_model API 转换成为一个 tf-lite 模型(.tflite 后缀),这是使用 flatbuffer 存储的格式,同时还会做一些 quantization 的操作。我们后面拿一个具体例子来试试。

TF 的操作关系

不是所有的模型都可以在 TF-lite 里面工作的,其实转换本身是将一系列 tf.ops 转换到 tfl.ops,当有一些不能转换的操作时就会失败,比如 tf.float16、字符串这些数据类型,这意味着我们很多情况下需要一定的预处理(以上例子中分类的结果不能直接输出 string 而要通过一个单独的查询获得实际的类别名称大概就这个原因了)。

Coral TPU