可惜后盖上的贴纸不见了, 不知道是哪家公司的. 先拆拆看.
目前市面上大大小小价签很多种, 拆解价签的方法大同小异. 拆价签翻车的案例太多, 尤其大屏幕, 一旦翻车就是一顿外卖的损失. 所以拆解前一定要做好功课, 了解屏幕的结构, 以及拆解的方法. 卡扣一般会特别紧, 需要较薄的刀片, 但又不能太薄, 否则可能会受伤. 比如这个:
另外加上软质塑料片, 这玩意文具店很多, 用来拆解屏幕背面和 PCB 的双面胶.
撬开后电池盖, 注意卡扣位置. 然后取掉电池. 方便之后取出 PCB.
然后就是拆卡扣, 从屏幕背面的一个开口处开始, 用刀片, 慢慢撬一圈卡扣, 注意不要伤到自己和屏幕边缘.
卡扣分离后, 这时候可以用软质塑料片, 伸进去, 慢慢拆开 PCB 和后盖的双面胶.
PCB 也是用双面胶贴在屏幕背后的, 这一步是最容易翻车的, 一定要慢慢来. 塑料片伸进去横向推, 一点一点的推开 PCB 和背板的双面胶.
PCB 丝印: Endor Telink1020 2021-07-22 change U5, 可见这是 BLOZI(保资) 的价签. 但是在今天, 他家官网都是挂的. 难道价签厂家也倒闭了…
24pin 屏幕排线丝印: HINK-E102A01-A1, 搜索可以找到全网唯一参考资料1, 虽然是个 CSDN, 但提供了不少线索, 里面说明了驱动 IC 是 SSD1677, 微雪 3in7 也使用了相同驱动 IC, 但那是一个黑白屏幕.
如果没有找到对应的驱动 IC, 那可能就需要参考 wenting 的方法逆向了. 请自行学习.
拆解完成后就可以尝试驱动了. 一般来说, 24pin 就是 AIO(All-In-One) 串口屏了, 驱动版都是通用的. 这里随便找了一个驱动板(咸鱼), 使用 RPi Pico(RP2040 MCU) 来驱动. 开发框架使用 Rust embassy.
Rust embedded-graphics 提供了非常方便的 Framebuffer API(注意, 没有屏幕旋转支持).
传统情况下, 我们只需要实现一个 Display
trait, 就可以使用 embedded-graphics 的各种绘图 API 了.
但这里, 我们再抽象一级, 直接预留一个 update_frame(raw: &[u8])
接口, 直接接收 Framebuffer::data()
作为参数.
这是在经历了 epd 项目之后, 尝试七八种屏幕之后发现最合适最通用的方法.
先写个骨架, 一切 SPI 串口 EPD 都可以这样搞, 唯一需要注意的是部分驱动 IC 的 BUSY 使用反逻辑:
struct EPD10in2<'a> {
spi: Spi<'a, SPI0, Blocking>,
dc: Output<'a, AnyPin>,
busy: Input<'a, AnyPin>,
}
impl EPD10in2<'_> {
fn send_command(&mut self, cmd: u8) {
self.dc.set_low();
self.spi.blocking_write(&[cmd]);
}
fn send_data(&mut self, data: &[u8]) {
self.dc.set_high();
self.spi.blocking_write(data);
}
fn send_command_data(&mut self, cmd: u8, data: &[u8]) {
self.send_command(cmd);
self.send_data(data);
}
pub fn busy_wait(&mut self) {
loop {
if self.busy.is_low() {
info!("busy out");
break;
}
}
}
// ...
pub fn init(&mut self) {}
pub fn update_frame(&mut self, raw: &[u8]) {}
pub fn refresh(&mut self) {}
}
微雪驱动使用了自定义黑白 LUT 和 4 阶灰度的 LUT 来驱动, 但个人经验是, 自定义 LUT 方法不适合三色 BWR 屏幕. 除非确定 LUT 来自厂家调教. 三色屏幕建议使用出厂的 OTP LUT(One-time-programming LUT). 这样可以保证屏幕的寿命, 也直接使用厂商调教过的颜色效果. 否则电子墨水屏在 LUT 表错误的情况下, 极容易永久性损坏, 例如我手头有若干永久性残影的屏幕.
当然, 的确是可以自己调教三色 LUT 逻辑, 相关论文有不少, 例如 Zeng, W.; Yi, Z.; Zhou, X.; Zhao, Y.; Feng, H.; Yang, J.; Liu, L.; Chi, F.; Zhang, C.; Zhou, G. Design of Driving Waveform for Shortening Red Particles Response Time in Three-Color Electrophoretic Displays. Micromachines 2021, 12, 578. https://doi.org/10.3390/mi12050578. 请沿着引文链自行探索.
一般来说, 三色墨水屏在墨囊黑白粒子之外额外加入了第三种颜色的粒子, 例如红色, 黄色. 彩色粒子的带电量和粘度(粒子物理运动特性)和黑色粒子可以通过较弱电压区分. 驱动过程大概是: 清屏, 激活(让黑色和彩色粒子尽可能分层而不是黏在一起), 然后利用较弱电压, 使得彩色粒子在屏幕上浮动, 形成彩色图像.
通读 SSD1677 的数据手册, 对照微雪的驱动代码, 找到核心修改点. 几乎所有 EPD 驱动 IC 的手册都是极其含糊, 这个也不例外. 其中一些关键词, 可能是需要你通读过其他同类型驱动 IC 才能理解.
SSD1677 有如上两种模式, 一种是 BWR 三色, 一种是黑白两色. 按照不同方式使用 LUT.
这两个含糊的 Command 描述文档, 隐藏了 BWR 驱动的细节.
那么这里, 我们直接加载 Display Mode 1 的 OTP LUT, 然后使用 Display Update Control 2(with Display Mode 1) 命令刷新.
由此修改 init()
函数:
pub fn init(&mut self) {
self.send_command(0x12); // Soft reset
Delay.delay_ms(20_u32);
self.send_command_data(0x46, &[0xF7]);
self.busy_wait();
self.send_command_data(0x47, &[0xF7]);
self.busy_wait();
// Driver output control
// 0x27F = 639
self.send_command_data(0x01, &[0x7F, 0x02, 0x00]);
// set gate voltage
self.send_command_data(0x03, &[0x00]);
// set source voltage
self.send_command_data(0x04, &[0x41, 0xA8, 0x32]); // POR
// set data entry sequence
self.send_command_data(0x11, &[0x03]);
// set border
self.send_command_data(0x3C, &[0x03]);
// set booster strength
self.send_command_data(0x0C, &[0xAE, 0xC7, 0xC3, 0xC0, 0xC0]);
// set internal sensor on
self.send_command_data(0x18, &[0x80]);
// set vcom value
self.send_command_data(0x2C, &[0x44]);
// setting X direction start/end position of RAM
// 640 -> 639 => 0x27F
// 960 -> 959 => 0x3BF
self.send_command_data(0x44, &[0x00, 0x00, 0xBF, 0x03]);
self.send_command_data(0x45, &[0x00, 0x00, 0x7F, 0x02]);
self.send_command_data(0x4E, &[0x00, 0x00]);
self.send_command_data(0x4F, &[0x00, 0x00]);
self.send_command_data(0x37, &[0x00; 10]); // Use Mode 1 !!!
// Load Waveform !!!
// 0x91, Load LUT with Mode 1
self.send_command_data(0x22, &[0x91]);
self.send_command(0x20);
self.busy_wait();
// Display Update Control 2
self.send_command_data(0x22, &[0xCF]);
}
除了屏幕大小设定的修改, 最核心的用 !!!
标注.
补齐写 RAM 函数和屏幕刷新函数:
pub fn update_bw_frame(&mut self, buf: &[u8]) {
// self.send_command_data(0x4E, &[0x00, 0x00]);
// self.send_command_data(0x4F, &[0x00, 0x00]);
self.send_command(0x24);
self.send_data(buf);
}
pub fn update_red_frame(&mut self, buf: &[u8]) {
// self.send_command_data(0x4E, &[0x00, 0x00]);
// self.send_command_data(0x4F, &[0x00, 0x00]);
self.send_command(0x26);
self.send_data(buf);
}
pub fn refresh(&mut self) {
let mut delay = Delay;
self.send_command(0x20); // Master activation
delay.delay_ms(100_u32); //must
self.busy_wait();
}
在屏幕大小正确设定的前提下, 0x4E/0x4F(RAM 当前 X/Y) 可以只写一次, 之后就不需要了, 自动增长.
为了测试效果, 我们找一张 LLM 生成的图. 由于屏幕是 960x640, 而一般 LLM 生成的图是正方形, 需要进行缩放. 这里可以使用 Context Aware Image Resizing(CAIR) 算法, 或者传统直接缩放.
考虑到我们的屏幕只有三种颜色, 无法体现图片的丰富彩色和灰度细节, 需要使用抖动 (Diffusion Dithering) 来模拟灰度. 然后提取 BW frame 和 Red frame, 分别写 RAM. 相关任务可以通过 ImageMagick 完成:
convert ~/Downloads/_34f9c9ae-d1c2-47a9-ab55-089ffc7cb626.jpeg -size 960x640 -dither FloydSteinberg -remap 3color.gif out.gif
这里 3color.gif
是只包含红黑白三色的索引色图, 用来提供调色板. out.gif
是经过抖动处理的图片.
之前突然想到, Rust 的过程宏不就是在编译期执行的吗? 于是就写了个过程宏, 用来自动加载图片, 提取 BW/Red frame. 项目在 text-image.
全部刷新大概需要 20 秒左右, 最终效果如下:
通过前面的描述, 其实大家都会发现, 首先 SSD1677 本身就支持三色和双色驱动两种模式, 且三色屏幕的彩色粒子和黑色粒子如果一同处理, 那完全是可以把三色屏幕当做双色屏幕来驱动的.
方法1: 依然使用三色模式, 只不过 RED RAM 永远置空. 这是最简单的, 不需要修改任何代码, 但需要忍受长大 20 秒的刷新时间.
方法2: 启用驱动 IC 的双色模式?
这里主要介绍双色模式, 它的好处是, 刷新速度可以调教到更快, 并且有可能支持灰度显示, 以及快速局部刷新. 黑白双色模式最主要的是驱动像素到新的状态, 需要拿到前一状态和目标状态, 然后执行对应的波形. 这需要驱动 IC 有对应的支持.
这里是驱动 IC 手册中含糊没有介绍清楚的部分:
可见 LUT 表和 上一篇文章 中的类似, 但似乎缺乏核心的 “AB” 概念? 其实不然. 我们大致按照 Display Mode 1 整理得到格式是:
而按照 Display Mode 2, 经过测试, 发现 LUT 是:
可见, 这里其实是有新旧 AB 转换的概念的, 实测发现在 Display Mode 2 下, 0x24 B/W RAM 表示当前(目标/NEW)显示状态, 0x26 Red RAM 则是上一(OLD)状态. 那么看起来就可以实现快速刷新了. 只需要我们在写入新内容到 NEW RAM 同时, 把旧内容写入 OLD RAM. 驱动 IC 将自动使用 OLD/NEW 信息执行对应的波形, 完成像素位的状态转换.
那么是不是有种方法可以让驱动 IC 自动完成这个过程呢? 答案是肯定的. 这个功能在不同驱动 IC 的叫法不同, 比如在 UCxxxx 系列手册中, 叫做 N2OCP (New to Old Copy), 在 SSD1677 中, 叫做 “RAM ping-pong”.
翻看上面 0x37 Write Display Option 命令的 F[6]
位, 可以看到 “RAM Ping-Pong for Display Mode 2” 的描述.
且同时告知, 只有 Display Mode 2(黑白双色) 支持这个功能.
简单对 init()
函数做修改, 这里只贴出修改的部分:
pub fn init(&mut self) {
// ....
// Display Option
#[rustfmt::skip]
self.send_command_data(0x37, &[
0x00,
0xFF, //B
0xFF, //C
0xFF, //D
0xFF, //E
// 0x0F, RAM ping-pong disable
// 0x4F, RAM ping-pong enable
0x4F, //F, RAM ping-pong enable. only in Display Mode 2
0xFF, //G
0xFF, //H
0xFF, //I
0xFF, //J
]); // MODE 2
self.send_command_data(0x22, &[0x99]); // Load LUT with Mode 2
self.send_command(0x20);
self.busy_wait();
// Display Update Control 2
self.send_command_data(0x22, &[0xCF]);
}
废话不多说, 我们编写一张快速刷新的波形, 前置条件是需要屏幕是纯白的, 也就是刚刚清屏后的状态. 这可以通过 OTP LUT 实现. 这里只关注我们需要的黑色和白色状态.
经过测试, 我们得到了如下 LUT, 简洁到惊人. 0x0F
是个人测试的值, 即 15 个周期. RP=0 表示只重复一次.
相关内容和上一篇文章介绍的大同小异.
pub fn configure_partial_update(&mut self) {
#[rustfmt::skip]
const LUT: &[u8] = &[
0b00_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT0, B2B
0b10_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT1, B2W
0b01_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT2, W2B
0b00_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT3, W2W
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT5, reserved
// TP[xA, xB, xC, xD], RP
0x0F,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,//7
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,//9
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
// FR
0x22,0x22,0x22,0x22,0x22
];
self.send_command_data(0x32, LUT);
}
而此时只需要写入 0x24 B/W RAM, 就可实现快速刷新, 实测大概 1s 左右即可完成刷屏. 且无闪动.
看着 0x0F 这个时间周期值, 是不是手痒. 没错, 我们可以尝试调整这个值, 从而实现灰度显示. 灰度显示对于阅读器类应用意义重大, 矢量字体的抗锯齿渲染, 以及图片的灰度显示, 都可以大大提升用户体验.
这里假设我们需要实现 16 级别灰度, 正好对应 0x00 ~ 0x0F.
废话不多说, 直接上代码:
/// Level 0 to 15
fn configure_gray_update_level(&mut self, level: u8) {
#[rustfmt::skip]
let lut: &[u8] = &[
0b01_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT0, B2B
0b00_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT1, B2W
0b01_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT2, W2B
0b00_00_00_00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//LUT3, W2W
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//5
// TP[xA, xB, xC, xD], RP
level,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,//7
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,//9
0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,
// FR
0x22,0x22,0x22,0x22,0x22
];
self.send_command_data(0x32, lut);
}
pub fn refresh_gray4_image(&mut self, buf: &[u8]) {
for level in (0..4).rev() {
// level: (8, 4, 2, 1)
self.configure_gray_update_level(1 << level);
self.send_command(0x24);
for chunk in buf.chunks(4) {
let mut n = 0;
for b in chunk {
if b & (0x10 << level) != 0 {
n = (n << 1) | 1;
} else {
n = n << 1;
}
if b & (1 << level) != 0 {
n = (n << 1) | 1;
} else {
n = n << 1;
}
}
// 0xFF is white, 0x00 is black
self.send_data(&[n]);
}
self.refresh();
}
}
pub fn refresh_gray2_image(&mut self, buf: &[u8]) {
for level in [1, 0] {
// level: 9, 5
self.configure_gray_update_level(1 << (level + 2) + 1);
self.send_command(0x24);
for chunk in buf.chunks(2) {
let mut n = 0;
for b in chunk {
if b & (0b01_00_00_00 << level) != 0 {
n = (n << 1) | 1;
} else {
n = n << 1;
}
if b & (0b00_01_00_00 << level) != 0 {
n = (n << 1) | 1;
} else {
n = n << 1;
}
if b & (0b00_00_01_00 << level) != 0 {
n = (n << 1) | 1;
} else {
n = n << 1;
}
if b & (0b00_00_00_01 << level) != 0 {
n = (n << 1) | 1;
} else {
n = n << 1;
}
}
// 0xFF is white, 0x00 is black
self.send_data(&[n]);
}
self.refresh();
}
}
以上代码中, refresh_gray4_image
和 refresh_gray2_image
分别是 4 级和 2 级灰度的刷新函数.
这里对灰度的每一位的权重映射做了微调.
格式兼容 embedded-graphics 中的 Framebuffer 和 Image 类型, 可以直接使用.
其中的 bit 操作, 其实基本上是 Github Copilot 写的, 我只是稍微修改了下边界情况和编译错误,
实现不是最优, 但好理解.
显示效果, 这里以互联网 UGC 时代的化石, 徐静蕾手写体为例:
你猜我抗锯齿字体怎么渲染的? 没错, 还是 text-image 过程宏, 使用方法:
let (w, h, raw) = text_image::text_image!(
text = "北京市发布持续低温黄色预警\n北京市发布道路结冰橙色预警\n\n-12.5℃ \n-16℃ -- -7℃\n相对湿度 36%\n东北风 1级",
font = "./徐静蕾手写体.ttf",
font_size = 48.0,
line_spacing = 0,
inverse,
Gray4,
);
info!("w: {}, h: {}", w, h);
epd.set_partial_refresh(Rectangle::new(Point::new(128, 160), Size::new(w, h)));
epd.refresh_gray4_image(raw);
当然这里用到了 “局部刷新” 函数, 我们这就介绍.
局部刷新是指, 只刷新屏幕的一个矩形部分, 而不是整屏刷新. 这在阅读器类应用中, 是非常重要的功能. 包括弹出式菜单, 局部 UI 元素等都可以用到. 在日历天气中, 局部刷新可以只刷新时间或天气的一小部分, 而不是整屏刷新, 从而大大提升观感.
实际上局部刷新只需要找驱动 IC 手册中的 RAM X/Y Start/End 关键词即可, UCxxxx 系列的驱动 IC, 也会直接提供 Partial Update 的相关命令. 这里一笔带过直接上代码:
fn clear_as_bw_mode(&mut self) {
// set X/Y ram counter
self.send_command_data(0x4E, &[0x00, 0x00]);
self.send_command_data(0x4F, &[0x00, 0x00]);
const NBUF: usize = 960 * 640 / 8;
self.send_command(0x24);
for i in 0..NBUF {
self.send_data(&[0xFF]); // W
}
// reset X/Y ram counter
self.send_command_data(0x4E, &[0x00, 0x00]);
self.send_command_data(0x4F, &[0x00, 0x00]);
self.send_command(0x26);
for i in 0..NBUF {
self.send_data(&[0xFF]); // Red off
}
}
pub fn set_partial_refresh(&mut self, rect: Rectangle) {
// clear old buf
self.clear_as_bw_mode();
let x0 = (rect.top_left.x as u16);
let x1 = rect.bottom_right().unwrap().x as u16;
let y0 = rect.top_left.y as u16;
let y1 = rect.bottom_right().unwrap().y as u16;
self.send_command_data(0x44, &[(x0 & 0xff) as u8, (x0 >> 8) as u8, (x1 & 0xff) as u8, (x1 >> 8) as u8]);
self.send_command_data(0x45, &[(y0 & 0xff) as u8, (y0 >> 8) as u8, (y1 & 0xff) as u8, (y1 >> 8) as u8]);
// set X/Y ram counter
self.send_command_data(0x4E, &[(x0 & 0xff) as u8, (x0 >> 8) as u8]);
self.send_command_data(0x4F, &[(y0 & 0xff) as u8, (y0 >> 8) as u8]);
}
需要注意的是:
这里只是简单介绍了如何驱动这块价签, 以及如何实现局部刷新和灰度显示. 其中若干技术可以混用, 例如局部刷新 + 灰度显示, 局部刷新 + 三色显示等等. 屏幕也可以在不同状态下重新初始化, 最终实现的效果, 取决于你的想象力.
原理有了, 剩下的就是点子了. 之前做了一个 Bing Image Creator 加随机每日诗词的小工具. 效果还不错, 但一堆粗糙脚本未整理.
一些可能用到的技术再提一遍:
本文主要介绍如何使用 Rust 语言的 Embassy 嵌入式框架实现 STM32WL LoRa 数据传输. 过年回老家, 随身带的东西不多, 只有一个迷你 BMP280 (大气压温度)传感器模块, 所以本文使用 BMP280 传感器数据作为例子.
门槛率高, 还是从点灯开始搞起.
最终相关代码位于 Github: andelf/lm401-pro-kit.
快递于 [[2023-01-08]] 收到, 里面的评估板, 天线, 数据线均是两份, 方便开发使用.
LM401-Pro-Kit 是基于 STM32WLE5CBU6 的 Lora 评估板. 支持 SubGHz 无线传输. LM401 模组内嵌高性能 MCU 芯片 STM32WLE5CBU6, 芯片内部集成了 SX1262. 开发板板载 ST-Link(上传下载程序, UART 转 USB). ST-Link 通过跳线帽和模块核心部分连接, 方便单独供电使用模块. 开发板提供了若干 LED 状态灯, 复位按钮和一个用户按钮.
日常屯的(吃灰)板子也有大几十上百了, 拿到新板子, 需要查资料, 看手册, 电路图, 读例程, 找到一些核心信息, 其中一些信息可能需要读例程的 C 代码库才能获得, 这里列出整理的部分:
使用 Rust 嵌入式开发大概大概有如下几层(只是粗略分类, 实际项目使用中, 可能会混合使用):
svd2rust
工具从 .svd
文件生成embedded-hal
生态Embassy 框架是基于 Rust 语言的嵌入式异步框架. 考虑到相关框架还在开发中, 本文的代码仓库使用的是最新的 embassy master 分支. Commit hash 为 f98ba4ebac192e81c46933c0dc1dfb2d907cd532
, 通过 Cargo.toml
中设置依赖 path
的方式引入. 其他可选方案还可有 git submodule
或 直接 git
依赖远程版本等.
绕开 C HAL/BSP 库开发, 是需要踩不少坑的, 例如, RCC 时钟初始化, 需要查阅 BSP 代码才能确认, 48MHz 主时钟通过 MSI range11 获得, 而 embassy 对应 MCU 的示例代码使用的是 HSE, 这些都给 Rust 嵌入式开发带来一定的门槛.
安装 Rust 工具链, 本文使用 rustup nightly. 请参考 https://rustup.rs/ .
安装 Rust thumbv7em-none-eabi
target, 对应 Cortex-M4:
rustup target add thumbv7em-none-eabi
安装 Rust 嵌入式开发烧录/运行工具 probe-run
, 也可以使用 OpenOCD 或其他烧录工具:
> cargo install probe-run
...(install log)
> probe-run --list-chips | grep STM32WL
STM32WL Series
STM32WLE5J8Ix
STM32WLE5JBIx
STM32WLE5JCIx
STM32WL55JCIx
检查发现支持列表没有 STM32WLE5CBU6, 不过可以拿 STM32WLE5JCIx 替代, 问题不大.
安装任意串口调试工具, 这里我使用 picocom
. 其他可以使用的替代有 PuTTy, Teraterm 等等.
通过 USB 数据线连接开发板, 通过 picocom
连接串口, 通过 probe-run
烧录程序.
测试连接
> lsusb
Bus 001 Device 008: ID 0483:374b STMicroelectronics STM32 STLink Serial: xxxx
考虑到从初识 Rust 嵌入式开发直接跨越到 LoRa 无线传输门槛较高, 我们从简单的点灯例子开始:
我们直接依赖 embassy 的 master 分支进行开发, 为方便调试, 直接 clone 到本地用相对路径引入依赖:
git clone git@github.com:embassy-rs/embassy.git
# or
git clone https://github.com/embassy-rs/embassy.git
# 在同层目录直接创建我们的项目, 起板子名就可以. 相当于一个 BSP 模板可以扩充
cargo new --lib lm401-pro-kit
# 进入项目目录, 以下命令均在此执行
cd lm401-pro-kit
Rust 嵌入式项目的初始设置需要请参考项目代码
.cargo/config.toml
thumbv7em-none-eabi
cargo run
的执行方式为调用 probe-run ...
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-run --chip STM32WLE5JCIx"
[build]
target = "thumbv7em-none-eabi"
build.rs
link.x
/memory.x
链接过程中所用配置, 编译过程中由 embassy 自动按照芯片选择生成defmt
链接参数支持Cargo.toml
embassy
相关依赖, 并通过 features
设置相关参数opt-level = "z"
, 最小化编译二进制大小# part of Cargo.toml
[dependencies]
# ...
embassy-stm32 = { version = "0.1.0", path = "../embassy/embassy-stm32", features = [
"nightly",
"defmt",
"stm32wle5cb",
"time-driver-any",
"memory-x",
"subghz",
"unstable-pac",
"exti",
] }
# ...
[profile.dev]
opt-level = "z" # Optimize for size.
[profile.release]
lto = true
opt-level = "z" # Optimize for size.
stm32wle5cb
用于选择 STM32WLE5CBU6 的芯片配置, subghz
用于选择 SubGHz 驱动.memory-x
自动生成链接所需的 memory.x
文件(FLASH, SRAM 的大小和内存位置).src/lib.rs
项目初始文件, 用 #![no_std]
替代几乎所有的 Rust 嵌入式项目都是 no_std
的, 这意味着无法简单地使用所有带内存分配类型. 本例中, 我们使用 heapless
crate 中提供的栈分配类型来替代 String
.
注意到, 创建项目时候使用了 cargo new --lib
, 相当于我们创建的是一个 library 项目. 这不需要担心, cargo run
会自动识别 src/bin/xxx.rs
为 “可执行” 二进制目标. 通过 cargo run --bin xxx
即可运行对应程序. 也可以通过 examples/xxx.rs
的方法管理多个可执行二进制目标.
我们先通过一个最简单的闪灯例子来熟悉 Rust Embassy 的使用. 创建 src/bin/blinky.rs
.
// blinky.rs
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::{Duration, Timer};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
info!("Hello World!");
let mut led = Output::new(p.PB4, Level::High, Speed::Low);
loop {
info!("high");
led.set_high();
Timer::after(Duration::from_millis(1000)).await;
info!("low");
led.set_low();
Timer::after(Duration::from_millis(1000)).await;
}
}
#![no_main]
用于告诉 Rust 编译器, 我们不使用 Rust 提供的 main
函数做程序入口. #[embassy_executor::main]
是一个宏, 用于包装 async fn main()
函数, 由 embassy-executor 提供了一个 futures runtime, 所以可以使用 async
和 await
语法. 底层实现中, .await
通过 STM32 的 WFE/SEV 等待指令和中断唤醒指令实现, 实现了程序逻辑在等待时候的低功耗.
Spawner
还可以用来启动其他 async fn
函数, 实现了多任务的功能.
#![feature(type_alias_impl_trait)]
在 embassy 中被广泛使用, 需要开启. Embassy 中经常能看到形如 irq: impl Peripheral<P = T::Interrupt> + 'd
的类型签名.
let p = embassy_stm32::init(Default::default());
直接初始化了所有的外设, 并返回一个 Peripherals
对象.
通过 Rust 的 move 语义保证不同外设使用之间不会出现竞争.
let mut led = Output::new(p.PB4, Level::High, Speed::Low);
创建了一个 Output
对象, 用于控制 PB4 引脚.
Output
对象是一个 Pin
的 trait, 通过 set_high
和 set_low
方法可以控制引脚电平. 这里会自动完成对 GPIOB PB4 的所有初始化和设置, 包括外设时钟使能, 状态设置等.
info!
, warn!
等都是 defmt
的宏, 用于通过 ST-Link 提供的 Debug 通道打印调试信息. 强烈推荐使用, 否则嵌入式开发中, 只能用串口打印信息.
Timer::after(Duration::from_millis(1000)).await
是一个异步等待 1 秒的方法, 通过 embassy-time
crate 实现. 在 Cargo.toml
中的 time-driver-any
feature 选择了任意可用 timer 实现, 默认是 TIM2, 由 embassy-stm32 提供给 embassy-time
.
确保板子连接正常, 直接运行:
> cargo run --bin blinky
Finished dev [optimized + debuginfo] target(s) in 0.32s
Running `probe-run --chip STM32WLE5JCIx target/thumbv7em-none-eabi/debug/blinky`
(HOST) INFO flashing program (14 pages / 14.00 KiB)
(HOST) INFO success!
────────────────────────────────────────────────────────────────────────────────
0.000000 DEBUG rcc: Clocks { sys: Hertz(4000000), apb1: Hertz(4000000), apb1_tim: Hertz(4000000), apb2: Hertz(4000000), apb2_tim: Hertz(4000000), apb3: Hertz(4000000), ahb1: Hertz(4000000), ahb2: Hertz(4000000), ahb3: Hertz(4000000) }
└─ embassy_stm32::rcc::set_freqs @ ./embassy/embassy-stm32/src/fmt.rs:125
0.000113 INFO Hello World!
└─ blinky::____embassy_main_task::{async_fn#0} @ src/bin/blinky.rs:14
0.000552 INFO high
└─ blinky::____embassy_main_task::{async_fn#0} @ src/bin/blinky.rs:19
1.001157 INFO low
└─ blinky::____embassy_main_task::{async_fn#0} @ src/bin/blinky.rs:23
2.001811 INFO high
probe-run
烧录到 MCU 并执行, 持续获取 defmt 打印信息rcc: Clocks
调试时钟信息由 embassy-stm32
库 embassy_stm32::rcc::set_freqs
打印cargo run
dev 模式下均附加了代码行, 非常方便main
函数显示为 blinky::____embassy_main_task::{async_fn#0}
, 由 #[embassy_executor::main]
宏生成defmt 固然方便, 但很多时候依然需要用到 UART, 通过串口获取调试信息或收集数据. LM401-Pro-Kit 正好通过 ST-Link 提供了到 USART2 的访问.
Blinky 例子中, 由 defmt 调试信息可知, 我们使用的系统时钟只有 4MHz, 但 STM32WL 的最大时钟频率是 48MHz. 所以需要通过初始化 init()
方法设置时钟参数:
// sys clk init, with LSI support
let mut config = embassy_stm32::Config::default();
config.rcc.enable_lsi = true;
config.rcc.mux = embassy_stm32::rcc::ClockSrc::MSI(embassy_stm32::rcc::MSIRange::Range11); // 48MHz
let p = embassy_stm32::init(config);
Embassy UART 使用非常简单, 可以单独用 UartTx/UartRx 只初始发送/接收部分. 这里是一个发送 Hello world 和 MCU 内部 “时间” 的简单示例:
// USART2 tx
use embassy_stm32::dma::NoDma;
use embassy_stm32::usart::UartTx;
use embassy_time::Instant;
use heapless::String;
// Default: 115200 8N1
let mut usart = UartTx::new(p.USART2, p.PA2, NoDma, Default::default());
let mut msg: String<64> = String::new();
let i = Instant::now();
core::write!(msg, "Hello world, device time: {}\r\n", i.as_millis()).unwrap();
usart.blocking_write(msg.as_bytes()).unwrap();
msg.clear();
UartTx
初始化时需要传入 USART2
, PA2
, 分别对应 USART2 外设和 TX 引脚, DMA 通道是可选的. 默认串口参数是 115200 8N1. 外设初始化会自动处理对应引脚的 AF 设置.
串口打印需要字符串拼接格式化, 由于 no_std
, 标准库的 String
类型不可用, 这里使用 heapless::String
, 初始化时候需要指定分配大小. core::write!
即标准库中的 write!
, core::
前缀是为了避免和 defmt::write!
名字冲突.
完整代码请参考 代码仓库.
执行代码确认, 可以看到系统时钟被正确设置为 48MHz.
> cargo run --bin uart
0.000000 DEBUG rcc: Clocks { sys: Hertz(48000000), apb1: Hertz(48000000), apb1_tim: Hertz(48000000), apb2: Hertz(48000000), apb2_tim: Hertz(48000000), apb3: Hertz(48000000), ahb1: Hertz(48000000), ahb2: Hertz(48000000), ahb3: Hertz(48000000) }
└─ embassy_stm32::rcc::set_freqs @ /Users/mono/Elec/embassy/embassy-stm32/src/fmt.rs:125
0.000011 INFO Hello World!
└─ uart::____embassy_main_task::{async_fn#0} @ src/bin/uart.rs:21
0.000064 INFO tick
在另一命令行打开串口监视工具, 查看串口输出:
> picocom -b 115200 /dev/tty.usbmodem11103
Hello world, device time: 1000
Hello world, device time: 3002
Hello world, device time: 5005
Hello world, device time: 7008
Hello world, device time: 9011
Hello world, device time: 11013
....
BMP280 是来自 Bosch 的气压传感器, 通过 I2C 接口读取气压和温度数据, 所以需要在板子上找到未被占用的 I2C SCL/SDA 引脚资源, 通过查阅芯片手册, 最后选择了空闲的 I2C2, SCL pin PA12, SDA pin PA11. 开发板上一排跳线帽正好提供了 VCC, GND.
接线:
+--------+ VCC GND
| BMP280 | | |
| VCC>----------+ |
| GND>--------------+
| [.] SCL>-------------------->PA12
| SDA>-------------------->PA11
| | (LM401-Pro-Kit)
+--------+
Rust Embassy 完美兼容 embedded-hal
相关生态, 相关外设类型均支持对应的 embedded-hal
trait,
考虑到 BMP280 的使用略微复杂, 需要初始化, 读取校准数据, 测量后还需要通过校准数据计算最终测量结果. 所以 BMP280 直接寻找对应驱动即可. 但 Rust 嵌入式生态有个问题, 弃坑项目太多. 寻找第三方依赖时候需要注意阅读代码, 查看依赖版本, 必要时更新.
这么说, 其实是之前我有个弃坑项目里面有个 BME280 驱动库, BME280 和 BMP280 基本兼容, 只是多了湿度测量. 驱动代码使用 embedded-hal
提供的 trait 类型访问设备, 完成传感器初始化和测量. 稍微改了改, 直接 Copy embedded-drivers: bme280.rs 到项目 src/
下使用即可.
修改 src/lib.rs
增加:
pub mod bme280;
创建 BMP280 传感器项目 src/bin/i2c-bmp280.rs
. 完整代码请参考 代码仓库, 以下只选择关键部分介绍.
// BMP280 init
use embassy_stm32::i2c::I2c;
use embassy_stm32::interrupt;
use embassy_stm32::time::Hertz;
use embassy_time::Delay;
use lm401_pro_kit::bme280::BME280;
let irq = interrupt::take!(I2C2_EV);
let i2c = I2c::new(
p.I2C2,
p.PA12,
p.PA11,
irq,
NoDma,
NoDma,
Hertz(100_000),
Default::default(),
);
let mut delay = Delay;
let mut bmp280 = BME280::new_primary(i2c);
unwrap!(bmp280.init(&mut delay));
Embassy 中访问设备时, 一般会需要中断, 虽然理论上阻塞访问外设时不需要中断.
但是为了保证接口的一致性, 一般都会要求提供中断参数. interrupt::take!
用于获取对应中断对象.
BME280::new_primary
直接使用设备主地址 0x76
访问 I2C 总线上的 BMP280.
初始化设备时候由于需要软复位, 需要传递 Delay
对象, 用于延时(delay_ms
).
默认的 embassy_time::Delay
使用循环比较 “设备当前时间” 的方法实现.
unwrap!
宏由 defmt
提供, 等价于 .unwrap()
调用, 但是会在 panic 时候通过 defmt 打印信息.
完成设备初始化后, 可以访问传感器信息:
let raw = unwrap!(bmp280.measure(&mut delay));
info!("BMP280: {:?}", raw);
传感器执行测量时候, 按照手册, 依然需要延时, 所以也同样需要传递 Delay
对象.
BME280::measure
方法返回 Measurements
类型, 为了方便调试使用, 用 derive macro 增加了 defmt 支持, 可以直接做格式化参数:
#[derive(Debug, defmt::Format)]
pub struct Measurements {
/// temperature in degrees celsius
pub temperature: f32,
/// pressure in pascals
pub pressure: f32,
/// percent relative humidity (`0` with BMP280)
pub humidity: f32,
}
执行代码:
> cargo run --bin i2c-bmp280
0.000011 INFO I2C BMP280 demo!
└─ i2c_bmp280::____embassy_main_task::{async_fn#0} @ src/bin/i2c-bmp280.rs:23
0.009314 INFO measure tick
└─ i2c_bmp280::____embassy_main_task::{async_fn#0} @ src/bin/i2c-bmp280.rs:45
0.051652 INFO BMP280: Measurements { temperature: 23.689554, pressure: 88391.13, humidity: 0.0 }
└─ i2c_bmp280::____embassy_main_task::{async_fn#0} @ src/bin/i2c-bmp280.rs:48
temperature: 23.689554, pressure: 88391.13
传感器数据正常.
LoRa 是一种无线传输协议, 适合长距离(km), 少量数据传输. 尤其适合传感器数据. 因为手头没有 LoRaWAN 基站, 所以暂时没法测试 LoRaWAN. 这里使用 LoRa 调制模式点对点传输 BMP280 传感器数据.
详细实现请参考 代码仓库 里的 src/bin/subghz-bmp280-tx.rs
和 src/bin/subghz-bmp280-rx.rs
.
LM401-Pro-Kit x2, 天线, 数据线.
其中一个开发板作为传感器采集端, 按照上一示例链接到 BMP280 传感器模块, 另一个作为接收端, 两个开发板之间通过 LoRa 无线传输数据. 接收端通过 UART 与电脑连接, 通过串口调试工具查看传感器数据.(实际上也可以直接通过 ST-Link + defmt 获取数据)
使用开发板射频功能, 需要处理射频开关逻辑. 相关逻辑从 BSP C 代码获得. 可以直接作为 BSP 的工具类型, 写入到 src/lib.rs
中:
use embassy_stm32::{
gpio::{AnyPin, Level, Output, Pin, Speed},
peripherals::{PA15, PA8, PB0},
};
pub struct RadioSwitch<'a> {
ctrl1: Output<'a, AnyPin>,
ctrl2: Output<'a, AnyPin>,
ctrl3: Output<'a, AnyPin>,
}
impl<'a> RadioSwitch<'a> {
pub fn new_from_pins(ctrl1: PB0, ctrl2: PA15, ctrl3: PA8) -> Self {
Self {
ctrl1: Output::new(ctrl1.degrade(), Level::Low, Speed::VeryHigh),
ctrl2: Output::new(ctrl2.degrade(), Level::Low, Speed::VeryHigh),
ctrl3: Output::new(ctrl3.degrade(), Level::Low, Speed::VeryHigh),
}
}
pub fn new(
ctrl1: Output<'a, AnyPin>,
ctrl2: Output<'a, AnyPin>,
ctrl3: Output<'a, AnyPin>,
) -> Self {
Self {
ctrl1,
ctrl2,
ctrl3,
}
}
pub fn set_off(&mut self) {
self.ctrl3.set_low();
self.ctrl1.set_low();
self.ctrl2.set_low();
}
}
impl<'a> embassy_lora::stm32wl::RadioSwitch for RadioSwitch<'a> {
fn set_rx(&mut self) {
self.ctrl3.set_low();
self.ctrl1.set_high();
self.ctrl2.set_low();
}
fn set_tx(&mut self) {
self.ctrl3.set_high();
self.ctrl1.set_low();
self.ctrl2.set_low();
}
}
非常简单的 GPIO 操作, GPIO 的强类型 PAn
/PBn
/.. 可以通过 .degrade()
方法转换为 AnyPin
类型, 方便使用.
let mut rfs = lm401_pro_kit::RadioSwitch::new_from_pins(p.PB0, p.PA15, p.PA8);
为简单展示, 传感器节点只负责发送, 接受节点只接受 LoRa 报文, 不回传 ACK 信号.
报文格式为 24 字节:
头 | 设备地址 | 设备时间戳 | 温度 | 大气压 | checksum |
b”MM” | u32 | u64 | f32 | f32 | u16 |
其中设备地址使用 STM32 系列的 chip id 实现, 保证一定的唯一性:
// Device ID in STM32L4/STM32WL microcontrollers
pub fn chip_id() -> [u32; 3] {
unsafe {
[
core::ptr::read_volatile(0x1FFF7590 as *const u32),
core::ptr::read_volatile(0x1FFF7594 as *const u32),
core::ptr::read_volatile(0x1FFF7598 as *const u32),
]
}
}
let chip_id = chip_id();
let dev_addr = chip_id[0] ^ chip_id[1] ^ chip_id[2];
设备时间戳直接读取 Instant::now()
并转为 millis. 保证每个数据报文的差异性. checksum
校验和字段通过计算 [2..22]
所有字节之和得到. 所有数据字段均按照大端序列化(BigEndian).
LM401 的射频功能由 STM32WLE5 内置的 SX1262 提供, 设备内部通过 SPI3(SUBGHZSPI) 访问. SX1262 初始化需要较多参数, 且发送端接收端若干参数需要一致.
这里选用 490.500MHz, LoRa SF7, 4/5 编码率, 125kHz 带宽, 24 字节数据长度. 接收端和发送端设置一致.
参数定义:
use embassy_stm32::subghz::*;
const DATA_LEN: u8 = 24_u8;
const PREAMBLE_LEN: u16 = 0x8 * 4;
const RF_FREQ: RfFreq = RfFreq::from_frequency(490_500_000);
const TX_BUF_OFFSET: u8 = 128;
const RX_BUF_OFFSET: u8 = 0;
const LORA_PACKET_PARAMS: LoRaPacketParams = LoRaPacketParams::new()
.set_crc_en(true)
.set_preamble_len(PREAMBLE_LEN)
.set_payload_len(DATA_LEN)
.set_invert_iq(false)
.set_header_type(HeaderType::Fixed);
// SF7, Bandwidth 125 kHz, 4/5 coding rate, low data rate optimization
const LORA_MOD_PARAMS: LoRaModParams = LoRaModParams::new()
.set_bw(LoRaBandwidth::Bw125)
.set_cr(CodingRate::Cr45)
.set_ldro_en(true)
.set_sf(SpreadingFactor::Sf7);
// see table 35 "PA optimal setting and operating modes"
const PA_CONFIG: PaConfig = PaConfig::new()
.set_pa_duty_cycle(0x4)
.set_hp_max(0x7)
.set_pa(PaSel::Hp);
const TX_PARAMS: TxParams = TxParams::new()
.set_power(0x16) // +22dB
.set_ramp_time(RampTime::Micros200);
设备初始化, 部分内容从 BSP C 代码转换得到:
let mut radio = SubGhz::new(p.SUBGHZSPI, NoDma, NoDma);
// from demo code: Radio_SMPS_Set
unwrap!(radio.set_smps_clock_det_en(true));
unwrap!(radio.set_smps_drv(SmpsDrv::Milli40));
unwrap!(radio.set_standby(StandbyClk::Rc));
// in XO mode, set internal capacitor (from 0x00 to 0x2F starting 11.2pF with 0.47pF steps)
unwrap!(radio.set_hse_in_trim(HseTrim::from_raw(0x20)));
unwrap!(radio.set_hse_out_trim(HseTrim::from_raw(0x20)));
unwrap!(radio.set_regulator_mode(RegMode::Smps)); // Use DCDC
unwrap!(radio.set_buffer_base_address(TX_BUF_OFFSET, RX_BUF_OFFSET));
unwrap!(radio.set_pa_config(&PA_CONFIG));
unwrap!(radio.set_pa_ocp(Ocp::Max60m)); // current max
unwrap!(radio.set_tx_params(&TX_PARAMS));
unwrap!(radio.set_packet_type(PacketType::LoRa));
unwrap!(radio.set_lora_sync_word(LoRaSyncWord::Public));
unwrap!(radio.set_lora_mod_params(&LORA_MOD_PARAMS));
unwrap!(radio.set_lora_packet_params(&LORA_PACKET_PARAMS));
unwrap!(radio.calibrate_image(CalibrateImage::ISM_470_510));
unwrap!(radio.set_rf_frequency(&RF_FREQ));
中断信号量处理, 由于发送接收循环需要涉及到中断处理, 这里直接用 Signal
类型的信号量处理中断:
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::signal::Signal;
static IRQ_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
let radio_irq = interrupt::take!(SUBGHZ_RADIO);
radio_irq.set_handler(|_| {
IRQ_SIGNAL.signal(());
unsafe { interrupt::SUBGHZ_RADIO::steal() }.disable();
});
这样, 在 async fn main()
中使用 IRQ_SIGNAL.wait().await
就可以随时等待中断信号量.
首先拼接报文, 这里直接手动拼接组合字节:
let mut payload = [0u8; 24];
let now = Instant::now();
let measurements = unwrap!(bmp280.measure(&mut delay));
payload[0] = b'M';
payload[1] = b'M';
payload[2..6].copy_from_slice(dev_addr.to_be_bytes().as_slice());
payload[6..14].copy_from_slice(now.as_millis().to_be_bytes().as_slice());
payload[14..18].copy_from_slice(measurements.temperature.to_be_bytes().as_slice());
payload[18..22].copy_from_slice(measurements.pressure.to_be_bytes().as_slice());
let checksum = payload[2..22]
.iter()
.fold(0u16, |acc, x| acc.wrapping_add(*x as u16));
info!("checksum: {:04x}", checksum);
payload[22..24].copy_from_slice(checksum.to_be_bytes().as_slice());
然后开始发送:
rfs.set_tx();
unwrap!(radio.set_irq_cfg(&CfgIrq::new().irq_enable_all(Irq::TxDone)));
unwrap!(radio.write_buffer(TX_BUF_OFFSET, &payload[..]));
unwrap!(radio.set_tx(Timeout::DISABLED));
radio_irq.enable();
IRQ_SIGNAL.wait().await;
rfs.set_off();
let (_, irq_status) = unwrap!(radio.irq_status());
if irq_status & Irq::TxDone.mask() != 0 {
defmt::info!("TX done");
}
unwrap!(radio.clear_irq_status(irq_status));
总结起来发送过程需要如下步骤:
TxDone
Timeout
这里是接收端逻辑, src/bin/subghz-bmp280-rx.rs
, 其中配置部分和发送端相同:
let mut buf = [0u8; 256];
rfs.set_rx();
unwrap!(radio.set_irq_cfg(
&CfgIrq::new()
.irq_enable_all(Irq::RxDone)
.irq_enable_all(Irq::Timeout)
.irq_enable_all(Irq::Err)
));
unwrap!(radio.read_buffer(RX_BUF_OFFSET, &mut buf));
unwrap!(radio.set_rx(Timeout::from_duration_sat(Duration::from_millis(5000))));
radio_irq.unpend();
radio_irq.enable();
IRQ_SIGNAL.wait().await;
led_rx.set_low();
let (_, irq_status) = unwrap!(radio.irq_status());
unwrap!(radio.clear_irq_status(irq_status));
if irq_status & Irq::RxDone.mask() != 0 {
let (_st, len, offset) = unwrap!(radio.rx_buffer_status());
let packet_status = unwrap!(radio.lora_packet_status());
let rssi = packet_status.rssi_pkt().to_integer();
let snr = packet_status.snr_pkt().to_integer();
info!(
"RX done: rssi={}dBm snr={}dB len={} offset={}",
rssi, snr, len, offset
);
let payload = &buf[offset as usize..offset as usize + len as usize];
// Parse payload here
}
发送步骤如下:
RxDone
, Timeout
, Err
Timeout
5 秒rx_buffer_status
获取 buffer 状态lora_packet_status
获取报文 rssi, snr 信息发送端上电之后, 每2秒采集一次传感器数据并发送.
接收端上电之后, 持续接收数据并同时打印在 defmt 调试和串口输出.
> cargo run --bin subghz-bmp280-rx --release
1.226162 INFO begin rx...
3.292868 INFO RX done: rssi=-42dBm snr=14dB len=24 offset=0
3.292969 DEBUG got BMP280 node raw=[0x4d, 0x4d, 0x72, 0x2e, 0x67, 0x28, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x58, 0x3a, 0x41, 0xad, 0x10, 0xa2, 0x47, 0xac, 0x8c, 0x2a, 0x5, 0xa]
3.293173 INFO dev addr=722e6728 dev tick=22586 temp=21.633121'C pressure=883.4433hPa
3.299479 INFO stats: Stats { status: Status { mode: Ok(StandbyRc), cmd: Ok(Avaliable) }, pkt_rx: 2, pkt_crc: 0, pkt_len_or_hdr_err: 0, ty: LoRaStats }
3.299622 INFO begin rx...
串口输出, CSV 格式:
> picocom -b 115200 /dev/tty.usbmodem11203
addr=722e6728,rssi=-44,snr=14,temperature=16.304043,pressure=87621.96
addr=722e6728,rssi=-44,snr=14,temperature=16.306524,pressure=87621.96
addr=722e6728,rssi=-44,snr=13,temperature=16.309006,pressure=87621.83
addr=722e6728,rssi=-45,snr=13,temperature=16.311487,pressure=87621.81
addr=722e6728,rssi=-45,snr=13,temperature=16.313969,pressure=87621.66
Rust Embassy 是一个非常好的嵌入式 Rust 开发框架, 通过它可以快速开发嵌入式应用.
Rust Embassy 把 async
, await
关键字带到了 Rust 嵌入式开发中, 其还有丰富的多任务支持, 多种同步元语支持. 通过它们, 我们可以很方便的开发多任务应用.
但它依然是一个很早期的框架, 还不够完善, 例如目前在 STM32WL 上缺乏 ADC 支持. 文档不够丰富, 部分库函数会随着开发进度有所变更, 给维护项目带来不小的困难.
在开发过程中, 往往能看到 move 语义, ownership, 类型系统等 Rust 的特性, 虽然这些特性在嵌入式开发中并不是必须的, 但是它们确实能带来更好的开发体验. 例如 move/borrow 保证对设备资源的唯一访问所有权. 通过类型安全的寄存器类型访问避免 C 语言中错误的寄存器访问, 经过 Rust 编译器优化后, 和 C 中的 bit mask 写法是等价的. 通过 “associated types” 保证设备和对应引脚的状态匹配.
Rust Embassy 隐藏了大部分嵌入式设备细节, 开发者不需要过多的关注设备初始化细节, 应用代码短小.
实际使用过程中, 也遇到了一些坑, 例如在写一个 PWM 例子时候, embassy_time::Delay
怎么都不工作, 添加了若干 debug 打印之后才发现, embassy_time::Delay
内部使用 embassy_time::Instant
实现, 默认情况下会使用 TIM2
.
而选择的 PWM 输出 pin 正好是 TIM2_CH2
, 两者互相干扰, 导致 Delay
不工作.
目前类型系统还不能保证 Delay
和 Pwm
不会使用同一个 TIM
设备.
最终的解决方法是使用 cortex_m::delay::Delay
, 这是一个基于 SYSTICK 的实现.
本位未介绍 Embassy 的多任务功能, 在代码仓库里有一个简单的按钮控制闪灯频率的例子 src/bin/button-control-blinky.rs
.
多任务的时候需要有 .await
调用让出时间片.
在一个小村子的路口小卖店,看到一位略残疾腿脚不方便的阿姨极其熟练地单指打字聊天,操作着在线聊天室服务端软件, 同时管理虚拟摄像头播放的擦边球女主播视频。可想当时我有多震惊。于是有了这遍访谈。
title: 施阿姨的互联网
时间: 7月22日 下午
地点: XQ村村口小卖店
访谈人: Mono
被访谈人: 施阿姨
整理汇总: Mono
整理时间: 7/26
版本: v3
施阿姨。XQ村路口小卖店老板,56岁,似乎残疾。中午买水时见到她对着两台电脑的屏幕熟练操作,很是惊异,于是下午去做问卷和访谈。
访谈时,他的老公刚要外出。她卧坐在椅子上,眼睛时不时回头看两台电脑的屏幕(一台台式机,一台笔记本,均WinXP操作系统),打开的是视频聊天室的应用程序,能看到长长的在线用户列表和正在直播的视频窗口,她时不时用右手单手中指打出一句话和聊天室的网友互动,打字速度还挺快。每隔一会她还切换到一个类似老虎机到网络赌博程序,去查看状态。
访谈中了解到,施阿姨今年56岁,村里的普通农户。她弟弟在上海市的一家医院里工作,收入还可以,各方面也有一定的人脉,给施阿姨棒过不少忙。施阿姨的丈夫今年59岁,似乎不太会说话,不在家里做主的样子,没几分钟就出门去村北的蔬菜大棚里做工。丈夫的父母都已去世,施阿姨的母亲还健在,每天在家里给施阿姨和她老公做饭,平日里他们两口回家时间都略晚。施阿姨的儿子在上海上班。
早在80年代末,施阿姨就开始做裁缝,用缝纫机帮人加工衣服,她自豪地说,最多时候一年能有一万多收入,96年家里建的3层楼房就是用自己做衣服的钱盖的。但是95年后,人们的消费习惯逐渐发生变化,更多直接购买品牌衣服而不是买布料找裁缝加工,施阿姨的裁缝生意一天不如一天。
这时候,施阿姨借用了亲戚家在村路口的地,盖了一间屋子做小卖店,当时卖香烟很赚钱,但香烟需要烟草局的许可,施阿姨等了很久也没消息,一两年后听说上海市区比她晚申请烟草专卖许可的都办了下来,就托弟弟去崇明县烟草局去质问,之后才办了下来,但这时候烟草利润已经没有当年高,生意依旧一般。近几年施阿姨的小卖店每年大概有6000左右的收入。
同时在95年左右,施阿姨的股骨头出了问题,具体哪年做的置换已记不得,右腿(猜是)无法抬起正常走路,残疾人助力车停在小卖店里。阿姨说她当时申请残疾证也是费了好大周折,最后不得已通过弟弟出面,找人花了一万多才办了下来。谈到此,施阿姨感叹说,其实上面政府的政策是好的,残疾(人)补助不少,民政部门每月还发粮油,但到了下面,就不为老百姓考虑了,明明家里有钱开着车天天浪的人办下来了残疾证,真正残疾的人却办不下来。
施阿姨的互联网+,要从她弟弟说起,她弟弟家境宽裕,早在九十年代就置办了电脑,后来因更新换代,把旧电脑淘汰给了她,而她只有初中文化水平,且不会普通话(打字拼音完全不会),所以电脑就放在自己的小卖店里,供自己儿子使用。
后来村里有小孩看到她这里有电脑,就来玩,给她按小时计费,在她的电脑上装了很多游戏,也因此电脑经常中病毒出故障。那时候相当于只有一台电脑的黑网吧。后来施阿姨觉得对这些小孩的家长有愧,同时电脑也经常坏,就停下了黑网吧的生意。儿子后来出去上学,也用不到电脑,所以电脑就闲置了一段时间。(2000年左右)
而后,村里有一个在中学教书的王老师,看到施阿姨家里有电脑能上网,就给施阿姨介绍了QQ,教她网上聊天。一开始施阿姨不会打字,就用语音或者半天打一个字(因为普通话不会,所以拼音输入往往找不到字),同时也买了摄像头。
施阿姨是一个很爱学习东西的人。为了克服打字困难,她找来自己的外甥,将常用字和拼音写下来,挂在电脑背面的墙上,每天对着学习。没过几月,王老师再和施阿姨聊天,惊讶于施阿姨流畅的打字速度(虽然只是用一个指头敲键),敬佩不已。
此外,施阿姨的电脑每次出故障或是有问题,都是邻居家一个学电脑的小伙子解决的,小伙子很热心,说阿姨你会用就行了,这些我给你修。但是阿姨不愿意,比如装系统的时候,就要看着,一步步把步骤记下来,自己摸索。施阿姨还说,自己还会拆装电脑,电脑不亮的时候,什么显卡内存啊都拆了下来然后擦擦灰装起来就能好。(我当时听到这些也是惊到了)
07、08年股市疯涨的时候,施阿姨在弟弟的鼓动下,学起了炒股,当时也是每天盯K线,她以试探的心态投了2万,最多的时候涨到了6、7万,但后来被套牢。也就再没去折腾炒股。
说起视频聊天室,施阿姨说是一开始因为聊天打字太慢,语音视频聊天更方便,所以买了摄像头、耳机话筒。后来QQ上有一个网友,上海的,让她下载个软件,说带她去视频聊天室玩玩,她觉得很新鲜。一开始,都是别人教她怎么下载,注册,进入聊天室。在聊天室里可以排麦(聊天室同时一般只有很少的人可以语音,所以其他人需要按顺序排麦克风,由聊天室房主负责管理),唱歌,送花(虚拟道具)等等。最好玩的时候,一晚上她们一帮人能唱三四十首歌。
后来施阿姨就自己一个去各种聊天室逛,也逐渐萌生了自己做房主开聊天室的念头。她在网上聊天室里认识了一个71岁的老阿姨,在这方面是高手,经验丰富,这位老阿姨教施阿姨申请到了聊天室房间(需要一笔钱,才能拥有自己的房间),还协助施阿姨配置了新的电脑(视频聊天室需要转编码,需要性能稍微好的电脑)。
刚开始的时候,施阿姨的聊天室没多少人。而这类视频聊天室的模式是这样:网站提供视频直播服务器和视频直播软件,集中管理视频直播室的帐号和虚拟道具。虚拟道具即聊天室中用于相互赠送的鲜花、跑车等礼物。虚拟道具需要从网站的平台充值获取。每个聊天室都需要房主(在聊天室内被称为老大)来开启,房主需要向聊天室平台一次性缴纳费用,获得开聊天室的资格。聊天室中所有人的充值消费房主从中抽成,获得收益。部分视频聊天室还有在线赌博功能,虚拟货币流水更高。
视频聊天室往往具有集聚效应,即原本热闹的聊天室会更热闹。所以吸引参与者成了聊天室房主的首要任务。所以有的聊天室房主就会通过色情内容吸引成员,增加用户量。而施阿姨一开始是让老阿姨帮忙“挂聊天室”,赚取人气。所谓“挂聊天室”其实是用“马甲”帐号,从网上下载漂亮女主播的视频片段,然后通过模拟摄像头软件,在自己的聊天室内虚拟出一个漂亮的女主播,同时用帐号和房间内的用户互动,拉高人气。
那位71岁的老阿姨在这方面更有经验,帮施阿姨挂没多久后,聊天室的人气就来了,每天高峰大概有几百人在线。施阿姨也能通过道具充值提成获取一些收益。但施阿姨说,她和别人不一样,她获得到的金币(其中某平台,100万金币能换60元人民币)最后又通过各种方式返给了聊天室的参与者,发发小礼物等等,所以她的聊天室人气不错,大家都觉得她是个实在人,都叫她老大,或者老阿姨。
这时候来看,施阿姨更多把这样一个打监管擦边球的、具有获利性质的网络赌博平台当作了自己另一个社交圈子交流的工具。她在网上会和各种年龄段的人闲聊,比如和同时残疾人的网友聊聊政府补助的事情一类。
但较早较广接触网络,明显让施阿姨有了更大的眼界。谈起网络色情,阿姨说,有些小伙子来问她聊天室有没有不穿衣服女人那种视频,她说,都是人身上长得有什么好看的。谈起网络病毒,阿姨讲了她的一次经历,曾经有网友给她发了个文件,叫聚会照片,她打开后,桌面就弹出一个披长头发流血的女人照片,当时把她吓得半死,而且电脑之后也无法启动了。找来邻居家小伙子修好了电脑之后,她从此再也不接陌生人的文件,熟人也要看是不是本地的才会去看。
她家附近有个邻居也很好奇,但是人经常在上海住,想找机会让她教怎么开视频聊天室。而其他邻居对她的评价是,就知道买电脑玩电脑。
很多时候在考虑,互联网的即时、多媒体的社交方式到达农村到底会催生一种什么样的亚文化现象?例如前段时间很火爆的“快手”App,山东老阿姨吃日光灯管、年轻小伙子用鞭炮炸裤裆、残疾姑娘晒自己的打扮、搞怪装精神病的各种丑角、维修农机的小伙子自制各种新奇玩意、叠扑克牌、叠银币等等等等。实际上大部分是基于交流和“被注意到”的需求,当然也不排除部分也被一些小集团控制,编排节目,制作视频,炒作环节一应俱全的产业链。
(快手App已经是全国网络流量前几名的应用了,它的早期推广方式是和华为、酷派、步步高等国产手机在县城及以下的营销网点合作,直接预装。)
施阿姨也许只是孤例,纯流水帐记录,供大家思考。为何视频聊天室这种似乎已经在大中城市过气很久的互联网应用,却可以占有部分三四线城镇及农村的市场,得到生存。
参考:两个网站 ht tp: / /w ww.99 dzr.com/
大自然娱乐、 ht tp:/ /w ww.78 90 xy.com/
丰彩人生,还有个未知来源的游戏大厅,实际上是类似在线老虎机的样式,不停地转动切换下一个中奖的商标,可以看到下注量其实很高,单个商标下注在百万金币左右。网站均是未备案,注册地在二线城市。
无聊逛咸鱼,发现有便宜的电子墨水屏,这玩意正常价格大几十,而咸鱼一片 2.13 寸模块只需要 15 块钱人民币。
是的,还等啥,先来几片凑个包邮。
了解到之所以这么便宜,是因为实际上是拆机屏,拆的是电子价签。来自一茬又一茬倒闭的超市和便利店。
电子墨水屏,也叫 E-ink, 墨水屏(瓶),电子纸,也简写为 EPD(Electronic Paper Display)。
和你用来压泡面的 Kindle 的屏幕是一个东西。只不过你的 Kindle 屏幕更大素质更高,而超市价签要小很多,相对低成本。
如上来自维基,原理一目了然。
不需要扯什么 gate 什么的,每个像素是一个小胶囊,顶部是公共 \(V_{com}\) 电压,透明。 底面是驱动芯片在屏幕每一行每一列像素的输出电压,可正可负(相对于 \(V_{com}\)),胶囊内部是对电场方向有反应的带电颜色微粒。 不同电压不同时长作用下,胶囊顶部上的微粒分布情况不同,肉眼看到的像素深浅就不同。
屏幕的驱动芯片,和我们常见的 IC 芯片那种黑色块带引脚是不同的,屏幕驱动芯片一般和屏幕一起封装, 对应屏幕的行列有输出。对外暴露接口,物理上一般是排线。
屏幕模块到手。显示“微雪电子”,那当然是不可能的,这只是因为默认用了微雪的示例代码出厂测试。 墨水屏的特点就是断电画面驻留,也可以说保持显示状态不需要供电。
合影是一只凑单的 STM32F4 板子(本例未用到),一只 ESP8266(就准备用它来驱动屏幕了),和屏幕模块(主角)。
一共需要八根线驱动。熟悉的 SPI + CS + DC 式,和多数 SPI 接口的 LCD / TFT-LCD 屏幕接口类似。
不同的是多了一个 BUSY pin,这是墨水屏特有的输出信号,表示屏幕正在刷新,其他操作需要 MCU 延后。
告知该模块兼容微雪 2.13 寸黑白屏幕 v1 版。参数如下:
查询模块手册,得知该显示屏驱动芯片是 IL3895, 来自 Good Display(大连佳显)。 屏幕排线上有型号 HINK-E0213A04-G01(HINK-E0213-G01).
ESP8266 算是较推荐的墨水屏之友,IO 接口不多但够用,带 WiFi 功能, 适合做这类显示屏小制作。 可以轻易找到各类天气时钟等代码。
推荐编程环境 Arduino. 省事。需要安装 ESP8266 Board Support 库.
我这里的 ESP8266 板子是一只 NodeMCU DevKit 兼容板。最普通不过,但比较麻烦的是它的管脚标签和标准的 ESP8266 GPIO 之间有一个映射关系。
(ref: https://www.electronicwings.com/nodemcu/nodemcu-gpio-with-arduino-ide)
搞清楚映射关系,写代码就不会错了。
官方有出售专门的 ESP8266 驱动板,集成了 ESP8266, 只需要按照相同的接线,即可使用官方例程。
接线方式:
EPD board | NodeMCU pin | ESP8266 pin | description |
---|---|---|---|
BUSY | D1 | GPIO5 | 屏幕刷新忙 |
RES/RST | D4 | 2 | 复位 |
DC | D2 | 4 | Data/Command 信号 |
CS | D8 | 15 | 片选 |
CLK/SCK | D5 | 14 | SPI 时钟 |
DIN/SDA | D7 | 13 | SPI MOSI |
常识 VCC = 3.3V, GND 接地。
虽然不是官方正版,只是个拆机屏模块,但好在屏幕型号一致,全兼容微雪官方例程。
官方例程解压后子目录复制到 Arduino libraries 目录。
然后就可以直接从 Arduino 的 File->Examples 菜单打开例程。
例程压缩包中的 src/
, extras/
目录,其实就是官方驱动,所有的不同型号屏幕的例程,都依赖驱动库。
该款屏幕的例程是 waveshare-e-Paper/epd2in13-demo
.
设备选择 NodeMCU 或 Generic EPS8266, 编译上传例程。
屏幕噌噌闪动几下,清屏后,开始执行例程。
屏幕右下角的时间显示,秒位在不停变动。
例程中可以看到基础绘图,中英文数字显示,时间显示(局部刷新功能)。照着改改可以整出不少好玩的。 再加上 ESP8266 的 WiFi 功能,想象力足够。
至此,屏幕跑通。画画图,改改文字,皆大欢喜。🤪
以下是干货部分。需要知识预备:
看到例程中直接有中文显示语句,暗爽不是?
Paint_DrawString_CN(140, 60, "你好abc", &Font12CN, BLACK, WHITE);
Paint_DrawString_CN(5, 65, "**电子", &Font24CN, WHITE, BLACK);
于是改成“你好世界”,然而只见“你好”不见“世界”。
是的,官方例程(驱动)里没有完整字库,只有测试时候屏幕上出现的那几个汉字。所以需要加字库!
我们能接触到的绝大多数屏幕,都是点阵屏。 所谓字库,就是字符编码到图形象素点的映射。所以这里要增加缺失的字型,怎么整?
先看看官方怎么实现的。
当然,字库还有其他意思,在手机维修界,字库也指 ROM 芯片。 这是历史遗留问题了,当年字库都存在专门的芯片里。 字库和字库芯片在很多场合不区分。这里叫字库,其实是字模,即汉字的模型,对应的二进制数据。
找到驱动目录 ~/Documents/Arduino/libraries/esp8266-waveshare-epd
.
在 src/
目录下,找到若干 font*.cpp
, font*.h
文件就是字库了。
例如 font12CN
字体,微软雅黑 12:
const CH_CN Font12CN_Table[] =
{
/*-- 文字: 你 --*/
/*-- 微软雅黑12; 此字体下对应的点阵为:宽x高=16x21 --*/
{"你",
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1D,0xC0,0x1D,0x80,0x3B,0xFF,0x3B,0x07,
0x3F,0x77,0x7E,0x76,0xF8,0x70,0xFB,0xFE,0xFB,0xFE,0x3F,0x77,0x3F,0x77,0x3E,0x73,
0x38,0x70,0x38,0x70,0x3B,0xE0,0x00,0x00,0x00,0x00},
// ...
{"A",
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x1F,0x00,0x1F,0x00,
0x1F,0x00,0x3B,0x80,0x3B,0x80,0x71,0x80,0x7F,0xC0,0x71,0xC0,0xE0,0xE0,0xE0,0xE0,
0xE0,0xE0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
};
cFONT Font12CN = {
Font12CN_Table,
sizeof(Font12CN_Table)/sizeof(CH_CN), /*size of table*/
11, /* ASCII Width */
16, /* Width */
21, /* Height */
};
明显看到中文字库分两部分,一部分是字型表 Font12CN_Table
, 一部分是配置结构体 Font12CN
.
字型表即字的象素点二进制表示。因为中文字符较多,为节省空间字库只有例程所需汉字,
所以每条记录第一个元素是中文汉字,用于索引。
而对于英文字库来说,字符是连续的,且总体占用空间较小,一般使用连续字节块表示。
从配置结构我们可以得知,该字型宽 16 位,高 21 位,即两个字节表示一行象素,一共 21 行。 我们可以写个脚本展示下:
char = [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1D,0xC0,0x1D,0x80,0x3B,0xFF,0x3B,0x07,
0x3F,0x77,0x7E,0x76,0xF8,0x70,0xFB,0xFE,0xFB,0xFE,0x3F,0x77,0x3F,0x77,0x3E,0x73,
0x38,0x70,0x38,0x70,0x3B,0xE0,0x00,0x00,0x00,0x00]
# group by 2 -> to 0-1 -> padding with 0
lines = ["%08d%08d" % (int(bin(l)[2:]), int(bin(r)[2:])) for (l,r) in zip(char[::2], char[1::2])]
print('\n'.join(lines).replace('0', '.').replace('1', '*'))
得到命令行下输出:
................
................
................
................
...***.***......
...***.**.......
..***.**********
..***.**.....***
..******.***.***
.******..***.**.
*****....***....
*****.*********.
*****.*********.
..******.***.***
..******.***.***
..*****..***..**
..***....***....
..***....***....
..***.*****.....
................
................
一个汉字被解析了出来,以点阵的方式展示。上方和下方的 0, 用于行间距。 在实际应用中可以省去,节约空间。
而配置中的 ASCII Width 11 表示该中文字体中的 ASCII 字符宽度。因为半角全角的关系, 中文字体中的半角英文字符(ASCII)相对来说宽度都不足一字,为了显示效果,不留过多字间距, 丢弃多余位不用,所以这里单独有一个配置项。
char = [0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x1F,0x00,0x1F,0x00,
...: 0x1F,0x00,0x3B,0x80,0x3B,0x80,0x71,0x80,0x7F,0xC0,0x71,0xC0,0xE0,0xE0,0xE0,0xE0,
...: 0xE0,0xE0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]
lines = ["%08d%08d" % (int(bin(l)[2:]), int(bin(r)[2:])) for (l,r) in zip(char[::2], char[1::2])]
print('\n'.join(lines).replace('0', '.').replace('1', '*'))
# outputs:
"""
................
................
................
................
................
....***.........
...*****........
...*****........
...*****........
..***.***.......
..***.***.......
.***...**.......
.*********......
.***...***......
***.....***.....
***.....***.....
***.....***.....
................
................
................
................
"""
搞明白了原理和格式,接下来就是生成所需的字库了。网上有非常多的 Windows 下小工具可以做。 但我这么肝,就自己写了。代码来自之前给 TFT-LCD 写的抠字模小脚本儿。
原理很简单,用 Pillow/PIL
即可。先把需要的字画在图片上,然后读取象素点,移位生成对于字节表示。
最后最好直接生成 C 代码,就万事大吉。
而字体选择,一般使用点阵字体而非矢量字体,矢量字体在渲染的时候边缘都是带灰阶的, 如果忽略灰阶直接二值化,会导致最终字型锯齿严重,丑。
为了方便处理,这里选用开源的等宽字体文泉驿点阵字体 Unibit. 以中文点阵最常见的 16x16 输出。正好两个字节宽,16 行高,一个汉字 32 字节。 英文字符也正好是中文字符宽度的一半。
当然你可以随便从系统找个中文字体。需要注意的是,为了在黑白(无灰度)屏幕上达到最好效果,需要位图字体(bitmap font, raster font, pixel font), 否则矢量字体在渲染的过程中会有灰阶边缘,最终屏幕效果锯齿明显。
# 安装依赖
pip3 install Pillow
# 用于字体缩放的依赖库
brew install libraqm
from PIL import Image, ImageDraw, ImageFont
import PIL.features
# brew install libraqm
assert PIL.features.check('raqm'), "libraqm required"
# 画布大小
size = (320, 16)
# 黑白格式
FORMAT = '1'
BG = 0
FG = 1
# Y offset, 多数字体有自带行间距,可以用此参数消除行间距
YOFF = 0 # or -1
CHARS = "晴天卧槽可以了!你好世界最怕你一生碌碌无为,还安慰自己平凡可贵。雾霾"
CHARS = ''.join(list(set(CHARS)))
im = Image.new(FORMAT, size, BG)
font = ImageFont.truetype("Unibit.ttf", size=16, index=0)
draw = ImageDraw.Draw(im)
# 代码段用于检查字体渲染
# draw.text((0, YOFF), CHARS, font=font, fill=FG, language='zh-CN')
# im.save('font.png')
# im.show()
draw.rectangle([(0, 0), size], fill=BG)
for i, c in enumerate(CHARS):
charmap = []
draw.text((0, YOFF), c, font=font, fill=FG)
for y in range(16):
v = 0
for x in range(0, 16):
b = im.getpixel((x, y))
v = (v << 1) + b
charmap.append(v >> 8)
charmap.append(v & 0xFF)
draw.rectangle([(0, 0), size], fill=BG)
print("{", end='')
print('"{}", {}'.format(c, ', '.join(map(lambda c: "0x%02x" % c, charmap))), end="")
print("},")
直接输出 C 代码片段:
{"一", 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
{"生", 0x01, 0x00, 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x3f, 0xfc, 0x21, 0x00, 0x41, 0x00, 0x81, 0x00, 0x01, 0x00, 0x3f, 0xf8, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0xff, 0xfe, 0x00, 0x00},
{"碌", 0x00, 0x00, 0x01, 0xf8, 0xf8, 0x08, 0x20, 0x08, 0x21, 0xf8, 0x40, 0x08, 0x78, 0x08, 0x4b, 0xfe, 0xc8, 0x20, 0x4a, 0x22, 0x49, 0x74, 0x48, 0xa8, 0x79, 0x24, 0x4a, 0x22, 0x00, 0xa0, 0x00, 0x40},
{"碌", 0x00, 0x00, 0x01, 0xf8, 0xf8, 0x08, 0x20, 0x08, 0x21, 0xf8, 0x40, 0x08, 0x78, 0x08, 0x4b, 0xfe, 0xc8, 0x20, 0x4a, 0x22, 0x49, 0x74, 0x48, 0xa8, 0x79, 0x24, 0x4a, 0x22, 0x00, 0xa0, 0x00, 0x40},
{"无", 0x00, 0x00, 0x3f, 0xf0, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x7f, 0xfc, 0x04, 0x80, 0x04, 0x80, 0x04, 0x80, 0x08, 0x80, 0x08, 0x80, 0x10, 0x84, 0x20, 0x84, 0x40, 0x7c, 0x80, 0x00},
{"为", 0x01, 0x00, 0x21, 0x00, 0x11, 0x00, 0x11, 0x00, 0x01, 0x00, 0x7f, 0xf8, 0x02, 0x08, 0x02, 0x08, 0x02, 0x88, 0x04, 0x48, 0x04, 0x48, 0x08, 0x08, 0x10, 0x08, 0x20, 0x08, 0x40, 0x50, 0x80, 0x20},
要想使用字体,我们需要新建一个字体 .h
文件,就叫 ch_font.h
,在项目目录即可,不需要修改驱动库:
#include "fonts.h"
const CH_CN Font16CN_Table[] =
{
{"一", 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
{"生", 0x01, 0x00, 0x11, 0x00, 0x11, 0x00, 0x11, 0x00, 0x3f, 0xfc, 0x21, 0x00, 0x41, 0x00, 0x81, 0x00, 0x01, 0x00, 0x3f, 0xf8, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0xff, 0xfe, 0x00, 0x00},
// ... 这里写入剩余字型
};
cFONT Font16CN = {
Font16CN_Table,
sizeof(Font16CN_Table)/sizeof(CH_CN), /*size of table*/
8, /* ASCII Width */
16, /* Width */
16, /* Height */
};
使用:
// ...
#include "cn_font.h"
void setup() {
// ...
Paint_DrawString_CN(140, 70, "卧槽可以了!", &Font16CN, BLACK, WHITE);
EPD_2IN13_Display(BlackImage);
// ...
}
// ...
效果大概是(忘记拍照了,这里是另外一个开源字体 Sarasa):
是的,毒鸡汤。至此中文字体搞定,其他 CJK 字体同理。
而英文字体就更简单了,参考其他驱动中的 font*
文件即可。字型生成代码略改即可使用。
具体制作中可以混合中英文大小字体,还可以选择例如数码管字体等方案,完成布局。
墨水屏最烂大街的应用,大概就是天气时钟了,各式各样,各种尺寸。
显示文字信息的事情,上面我们已经通过自定义字库搞定了, 发挥想象力可以搞出诸如数码管字体,手写字体,等各种适合在墨水屏上实现的显示效果。
但问题来了,我想搞个天气图标符号显示,比如,中国天气网 那样的。
官方例程里其实有全屏图片显示例子,可以看到相关的调用函数 Paint_DrawBitMap
。
同时驱动库里也提供了 Paint_DrawImage(buf, x_start, y_start, img_width, img_height)
函数用于在任意位置显示任意大小图片。
看一眼驱动库里 Paint_DrawImage
代码,好像哪里不对,只支持宽度象素是 8 倍数的图片。
改也简单,用 Paint_SetPixel
函数替换即可,简单的位运算。
现在问题是怎么生成一张用于显示的图片。其实和上面的字库非常类似,就是用二进制位去映射象素点。 然后生成 C 数组。甚至核心代码逻辑也差不多。
需要注意的是图片只能是黑白二值图。为方便库函数识别,也提前将图片宽度处理成 8 的整数倍。
图源,就取天气网那堆图标,原图是用 CSS offset 方式显示的,也就是所有图标在一张图片上,需要切下。
完整代码见 gist: crop-blue30.py. 这里贴要点
# RGBA 到 RGB 的转换
pix = r, g, b, a = im1.getpixel((x, y))
BG = 255 # 背景是白色
r = (BG * (255 - a) + r * a) // 255
g = (BG * (255 - a) + g * a) // 255
b = (BG * (255 - a) + b * a) // 255
a = 255 # alpha 通道置空,不透明度
im1.putpixel((x, y), (r, g, b, a))
# 图像二值化,
im1 = im1.resize((64, 64), Image.LANCZOS)
im1 = im1.filter(ImageFilter.SHARPEN)
im1 = im1.convert('1', dither=Image.NONE)
当然,你要是用 ImageMagick 或者 Photoshop 也一样可以。
然后把生成的二进制装入 C uint8_t[]
即可。
显示一个太阳:
因为二值化和缩放的关系,象素周围略有点腐蚀的感觉。 效果还行,考虑考虑界面元素布局,做个天气时钟足够了。
正常情况下在显示文字和图案的时候屏幕会连续从最黑到最白来回闪动几下,参考显示原理, 这里是为了避免残留墨水粒子影响显示效果,即消除残影的影响。 有使用过 Kindle 的同学对这点会比较清楚,一般是翻页若干次全部刷新一次。
官方例程中右下角有个时间显示,用到了局部刷新技术:
EPD_2IN13_Init(EPD_2IN13_PART);
Paint_SelectImage(BlackImage);
// ...
Paint_ClearWindows(140, 90, 140 + Font20.Width * 7, 90 + Font20.Height, WHITE);
Paint_DrawTime(140, 90, &sPaint_time, &Font20, WHITE, BLACK);
EPD_2IN13_Display(BlackImage);
首先以 EPD_2IN13_PART
方式重新初始化屏幕(不用清屏),然后就通过 Paint_ClearWindows
清除需要局部更新的矩形区域,
之后就可以使用各种绘图绘字符函数填写屏幕的这部分。最后调用 Display 上屏。
局部刷新的效果因屏幕体质不同各异,经常会遇到残影特别严重的时候。做小制作的时候也可以学习电纸书,局部刷新多次后全局刷新一次。
以上,基本介绍完了墨水屏的常见功能。已满足绝大部分需求。
回头再看显示原理,有了一开始介绍的小胶囊阵列结构,驱动怎么通过搞定显示的呢?这里以 2.13 寸显示屏的驱动 IC IL3895 为例。
LUT, 即 Waveform Look Up Table(LUT), 是很多介绍电子墨水驱动文章的离不开的话题。
所谓的 LUT 功能,其实是驱动芯片的“可编程驱动电压波形”功能。即通过若干寄存器字节, 设置像素在不同状态转换情况下使用的底板电压高低的时序。该设置全局有效,针对全屏幕的任何一个像素的变动。 整个波形通过更新屏幕指令(MasterActivation = 0x20, Activate Display Update Sequence)触发, 此过程中 BUSY 信号有效,更新逻辑完成后, BUSY 信号结束。
例如数据手册中:
IL3895 芯片支持 10 个 phase 的波形,分别是 phase0A phase0B phase1A phase1B phase2A phase2B phase3A phase3B phase4A phase4B, 其中每个 phase 可以指定维持状态的时间周期数 TP. 每两个波形可以设置一个重复次数 RP. 在每个 phase, 都可以指定像素底板电压 VS 高低。不同电压级别驱动颜色微粒向不同方向运动,维持不同的时间,最终实现像素的黑白变化。
图表右侧的 XY=LL, XY=LH, … 表示不同的像素值变动情形。例如 HL 表示像素从白色变动到黑色, LL 表示像素在此次刷新中值没有变化。
以上的所有配置项按照固定格式,最终形成了 LUT 表,可以通过命令设置:
而例程中的 EPD_2IN13_Init(EPD_2IN13_FULL/PART)
, 其中最重要也是唯一的区别就是 FULL 和 PART 初始化时使用的 LUT 表不同。
const unsigned char EPD_2IN13_lut_full_update[] = {
0x22, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x11,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00
};
const unsigned char EPD_2IN13_lut_partial_update[] = {
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
初始化后,每次显示更新,都会执行对应的 LUT 表时序。也就是说,全局刷新情况下的屏幕从全黑到全白好几次清空屏幕的动作,
就定义在这个 FULL 对应的 LUT 中。有兴趣的小伙伴可以解析下 EPD_2IN13_lut_full_update
.
正因为有了 LUT 定义,屏幕以 EPD_2IN13_PART
方式初始化时,新显示内容不再需要全屏刷新后再显示,直接在原始状态进行绘制。
这里以 EPD_2IN13_lut_partial_update
为例介绍下实际 LUT 执行时候发生的动作。简单得多,以至于整个表只有 3 个非空字节,
对应 phase0A, phase0B, 其余 phase 设置因对应的 TP(period) 为 0, 不生效。所以只有两个 phase 的波形。
/// LUT for partial update.
#[rustfmt::skip]
pub const LUT_PARTIAL_UPDATE: [u8; 30] = [
// VS, voltage in phase n
// <<VS[0A-HH]:2/binary, VS[0A-HL]:2/binary, VS[0A-LH]:2/binary, VS[0A-LL]:2/binary>>
// <<VS[0B-HH]:2/binary, VS[0B-HL]:2/binary, VS[0B-LH]:2/binary, VS[0B-LL]:2/binary>>
// HL: white to black
// LH: black to white
// e.g. 0x18 = 0b00_01_10_00
0x18, 0x00, // phase 0
0x00, 0x00, // phase 1
0x00, 0x00,
0x00, 0x00,
0x00, 0x00, // phase 4
// padding
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// RP, repeat counter, 0 to 63, 0 means run time = 1
// TP, phase period, 0 to 31
// <<RP[0]_L:3/binary, TP[0A]:5/binary>>
// <<RP[0]_H:3/binary, TP[0B]:5/binary>>
0x0F, 0x01, // phase 0
0x00, 0x00,
0x00, 0x00,
0x00, 0x00,
0x00, 0x00, // phase 4
// padding
0x00, 0x00, 0x00, 0x00
];
如上,改写为 Rust 代码,加入详细注释,注释混搭假 Erlang 语法。
先看 VS 部分,即驱动电压部分。0x18 = 0b00_01_10_00
, 按照格式拆出:
VS[0A-HL] = 01
VS[0A-LH] = 10
# 其他情况为 00
手册中有介绍 VS 格式:
00–VSS
01–VSH
10–VSL
分别是三种电压输出级别,其中 VSS 与顶版 VCOM 相等。相当于无电场存在。
VSH, VSL 分别会导致是两种不同的电场方向。
所以解读 phase0A 的配置即:
再来看 RP, TP 部分。需要将字节的高低位组合:
0x0F = 0b000_01111
0x01 = 0b000_00001
所以得到
RP[0] = 0b000_000 = 0
TP[0A] = 0b01111 = 15
TP[0B] = 0b00001 = 1
如上提到, TP 为 0 时表示该 phrase 无效,所以上面说只有 phase0A, phase0B 两个 phase 有效。 RP = 0 是表示重复 1 次。
总结以上那么该 LUT 的逻辑是:
所以似乎明了,所谓局部刷新,就是在像素点不变的时候,不做任何操作。 在像素翻转的情况下,执行一次给电压操作,随后静置少量时间。
而全刷新,可以很明显看到,前几个周期都是全置黑全置白的统一操作,为的是清屏。 之后才是处理不同像素状态变更的波形。
持续电压 15 个周期,那么少一点会怎么样?
测试发现这样出图效果没那么黑,甚至是灰色。
所以似乎就有了在黑白墨水屏上实现灰度显示的方案。这里的黑白墨水屏,特指驱动手册中单个像素为 1 位的屏幕。即 1bpp(bit per pixel).
本文题头图,即为测试效果。
灰度显示的内容,另开坑讲。
A framebuffer (frame buffer, or sometimes framestore) is a portion of random-access memory (RAM) containing a bitmap that drives a video display. It is a memory buffer containing data representing all the pixels in a complete video frame. – Wikipedia
回到例程,可以看到屏幕初始化调用大概是:
EPD_2IN13_Init(EPD_2IN13_FULL/PART);
EPD_2IN13_Clear();
UBYTE *BlackImage = malloc(...);
Paint_NewImage(BlackImage, EPD_2IN13_WIDTH, EPD_2IN13_HEIGHT, 270, WHITE);
Paint_SelectImage(BlackImage);
// ...
EPD_2IN13_Display(BlackImage);
阅读对应函数得知,这里创建了一个 BlackImage
字节数组做墨水屏的 framebuffer. 所有的绘图操作都只对这个 framebuffer 进行,
不和设备进行交互,直到调用 Display 才会进行实际的设备交互。
framebuffer 在 Display 被调用时,按照驱动芯片所设定的方向,比如按行,按列的方式, 一个个字节被从 MCU 传输到驱动芯片的内部的“显存”中,随后执行 LUT 更新逻辑。 驱动芯片会对比像素点的当前状态和目标状态,执行对应的 LH, HL, LL, HH 波形。 最终像素出现在屏幕上,完成显示。
之后抽时间写写灰度显示相关的折腾过程。
和 Rust embedded-grahics 驱动的情况。
其实更主要的原因是,整个世纪初的前几年,不曾拍过几张照片。或者出游,也是拍别人。
想讲讲 2003 的故事。其实这么久,能记得的,也都模糊到失真了。
那年初三。整个寒假在 Uncle Wang 家补课,说是补课,其实更像是托管。有英语和数学课。封闭管理。 他家用水不便,就记得那个寒假好像只洗了一两次头? 洗头这事,好说,当不洗的天数到达一定境界后,就已经感觉不到头发的存在了。
说起来万幸,还好有这一寒假的补课,不然中考大概率会考扯。
开学后就是紧张的中考前复习。依旧玩得昏天黑地。学校为了提高上线率,重新给两个实验班分了班, 二班一部分到我们一班,我班成绩靠后的去二班上课,两个班教学不同步,进度和教学内容都不太一样。
然后位置自己选,这可搞笑了,和一朋友自选搭了同桌,一起坐第一排。陕北方言把闲聊叫“谝”, 于是爱自习课说话的同学们各自封什么“谝王”,“谝圣”一类。形容的原话往往是,“那(ne)xxx太能谝了(liao4),纯粹一谝x”。
那时候兼任物理课代表。整个初三下近乎所有的课都是做卷子加讲卷子的无聊组合,卷子大概是科任老师自己找的, 比如物理,我们于是每个人要交几块钱的试卷费。收试卷费的任务就落到我头上。
于是一个百无聊赖的晚自习,拉着十几号人,去心灵在线(离学校最近的网吧),用试卷费请大家上网。 近乎包了网吧二楼。那时候玩啥的都有,与几个好友那段时间玩的最多是暗黑破坏神,我玩刺客。
所以试卷费就这么被我散掉了。后来物理老师总碎碎念这大几十块钱,还了他。
初三,整个初中没有比我们更资历老的学生了,所以你看周围人总多少带点痞气,放学回家路上逗逗学妹一类的。 那时候初中生男女之间的感情,大概基本上就是写纸条,送xx回家之类。 “送xx回家”这可能要以后细了说。
突然有天,电视说什么 SARS, 什么非典,那时候口号最多的是叫“众志成城,抗击非典”。 倒是印象中没要求啥口罩,记得最深的是每天到教室那浓重的消毒水味。
于是乎网吧游戏厅台球室都关掉了,但朋友们间的小道消息说的是,因为中考前家长投诉关的。 然后黑网吧黑游戏厅成了同学们口口传的秘密,曾跟同学一起翘下午自习翻墙去一家黑游戏厅打 PS(就叫打索尼), 玩的是 FIFA, 但其实我不太会玩这类,被虐就是了。只是热闹。
非典对于小县城来说,也就茶余饭后的小话题,周围没有人真得了非典,那其实大家的消息源还是新闻联播。 依稀记得那年板蓝根的疯狂。但小县城,比板蓝根牛逼的让人啼笑皆非。比如,那时候县里时兴送领导求办事送免疫球蛋白, 或者干扰素。嗯,这个带劲儿。
因为非典的关系,我们那届毕业生没有毕业联欢晚会。很遗憾。之前一年的毕业联欢会上,还出了个小品, 自毁形象那种。
但突然非典就没了。大概也就是6月中考后,就再没有这个话题了。
那时候全校近乎每个班都有认识的人。曾自大地笑称,没有借不来的东西。
所以很多故事,也就从借东西开始发生。一借一还,看起来简单的事情,却不知,借的人或被借的人那一节课有没有各种胡思乱想。 一胡思乱想,诶,很多事就不一样了。
各种机缘巧合又错过,回头再看,真是捉弄人。
中考比想像中来得快很多。所有人都是突然间发现就要中考了。也不知复习没复习完事。
不曾想到,那时候遇到的人儿,成了后来很长一段时间里的重要的人,也最终成了一不可说的大遗憾。
嗨,苦就苦在,等你想明白这一切,这一切却已经过去了。
以至于成了很长时间都解不开的结,也无从再去回头解释自己的行为逻辑。空留下那么多年和 SA 的来回邮件, 总回看,总内牛满面。
中考就是这样一件事。有时候会梦到在这故事的某一刻,却问的是你这几年还好吗。
陌生的学校,陌生的环境,不多的几个老同学。
日常交际出问题,所以每周末就是单调和无聊。
就交际来说,现在看,可能自己是极被动的那种。总在等。但实际上又较依赖亲密关系的存在。
想起那时候的 IC 卡电话和校门外的话吧。某段时间里,也曾是常客。
这段高一,每年都会出现在梦里,那是种容易让人分不清现实的梦,总愿意在梦里永远呆下去不醒来。
]]>“总是这样,来不及相认就失散。”
]]>是的我又回来了.
]]>这里,不谈 CI/CD,只谈作为对用户产品的一键部署,无论是叫 one-click, single-click, one-key, one-press, one-button, one-command, single-command… 还是只是对外发布的某种快速部署工具(集)。部署,俗称上线,不过细究概念,上线似乎更侧重日常功能更新,而部署的概念,更侧重首次的初始环境搭建。无论何,传统上,部署都是运维(OP)同学的日常工作。
所谓一键,大都是虚指。做过运维的同学知道,部署怎么可能是一件容易的事情。考虑到现代服务端软件集的庞大,从数据库/缓存,到后端逻辑,到前端服务,到监控系统,到报警系统,各组件相互配合,才完成最终对用户提供服务。更不用说机房服务器环境,云主机环境,现代分布式服务的多样和复杂性,导致 trouble shooting 极其复杂,哪怕是饱经事故的老 OP,也不容易。
正因有了这些复杂性,可信赖的部署工具就更显得重要。好用的工具不只为虎添翼,还给萌新在黑夜里点了盏明灯,在产品拉新的层面更有极其重要的作用。垃圾的工具,处处是坑不说,让人骂娘的心都有,且有工具还不如没有工具。没有工具的时候,OP 拿着命令行一顿敲,不也成功部署了么?
是的,关于一键部署,这里要从命令行说起。
最原始的典型服务部署过程,不过是登录到服务器,通过 scp/wget 下载到最近的软件包,解压编译,必要时候还需要下载若干编译依赖,然后修改配置,最后启动程序完成部署。
潮一点的容器部署方式,拉几个容器镜像下来,加上参数 run 起来,也是略苦逼。
是命令,就有可能出错,相信多数 OP 都有一个命令行小本本,记录着常见操作需要执行的命令。
看起来传统的命令行式部署很容易自动化,把这些命令集合在一起,于是就诞生了最原始的,一键部署脚本儿。脚本丢到服务器上,一执行,漫长或短暂的等待过去,服务 ready.
而容器化部署,也可以自动化成 docker-compose 等待,外部包一个处理配置的脚本,依旧好使。
随后 OP 同学为该脚本增加了更多的命令行参数,比如软件包版本号,部署路径,配置文件的某条常修改的参数,看起来这个时候,已经可以交付外部使用了。
然而多数服务端软件比以上流程复杂得多,OP 们往往面对的是一个集群,每台机器的配置或命令都有所不同,且服务的启停过程往往具有某种依赖顺序。这时候,简单的命令堆叠小脚本就不能满足需求了。
开源届倒是提供了不少解决方案,比如 Ansible, Puppet, Chef 等待,以及若干虚拟化解决方案,实现了配置,执行,部署,交付的完整流程。此外还有若干轻量工具,靠更多的人工配置环境提供更高的自由度,例如 fabric.
这时候的一键部署,可能就是某个强大工具的配置文件,描述了部署的步骤,然后加上目标机器环境的配置文件,通过执行一条命令,完成整个环境检查,环境初始化,部署应用,启动应用的全流程。
例如 ansible-playbook -i inventory install.yml 这样一条命令,加载 install.yml 描述的安装流程,然后在 inverntory 指定的服务器列表上执行这些安装流程。
但是这样的工具有一个很大的缺点,那就是运行异常可能需要工具的专家介入。专家大概是能给工具写插件级别的。以 Ansible 为例,能讲清楚机器登陆时候,不同发行版报错原因以及解决方法的人,并不多。
对于容器化部署,那当然要诉诸于各种编排工具,不在此讨论了。
严格地说,命令行也是 UI 的一种,原教旨主义会告诉你,UI 就是 User Interface,就好比 explorer.exe 也是 SHELL 一样。这里说的 UI, 特指 GUI,大概包括说有的图形应用程序,尤其指浏览器 Web UI.
之前在推上有句吐槽,“每个傻逼的后端产品 PM,都有一颗给命令行写 GUI 的心“,这话放在一键部署领域,依旧是合适无比。
做好 UI 的第一步,大概是不做 UI.
你可以认为 UI 在大部分时候是伪需求,问问自己的内心:在已经有较完善的一键部署工具集或者脚本的情况下,为什么做 UI?
其实答案很简单,UI 不是给 OP 或者一线工程师用的。它目标是为了把一件事情的门槛降低到尽可能低,低到只是恰好理解这个服务是干什么有什么要素的人,也能操作。
然而这是不可能的。所以所谓的你看到到的一众 UI 只能将大部分晦涩的配置选项隐藏起来,然后让你填写一个机器列表然后”一键“部署。然后失败了,弹出简洁的“部署失败”四个大字,或是晦涩的冗长的没人会去看的错误日志。
迄今为止,见过可以说的上能用的一键部署 UI, 可能是 Ansible-Tower,即 Ansible AWX, 但它真的只是包装了 Ansible 的命令行,所以足够简洁,足够完备。然而它的 trouble shooting 难度依旧是 Ansible 级的。除非你对 Ansible 足够熟悉,否则还是找足够经验丰富的人去追查背后到底发生了什么错误。
大概会有若干种特殊情况。例如大型公司的自研(或致敬某开源项目的)内部系统,例如云平台服务商的某些自动部署工具等。共同点是,受控底层环境,用户有天然身份门槛。
所以这个时候,不乏内部优秀工具或是云平台优秀工具。
然而该做不该做的东西,总是要做的。所以谈谈一键部署到底要做啥吧?这里主要谈分布式系统集群的一键部署。一个应用一个二进制文件一台机器,就真没必要折腾。
这里的 inventory, 泛指一切目标资产,比如服务器,云主机或者虚拟机。其中的属性信息繁杂,和后续的部署过程有着强依赖,比如机器上的配置,例如磁盘空间,内存,CPU 时间等。通过 inventory 的自动检查和初始化脚本,获取各种信息,为部署过程提供方便。
部分支持云主机的 Inventory 管理,还包括按需动态创建主机实例。
再广义些,还可能包括外部可用的服务资源,例如公共的 redis 服务。
Credential 是指登陆机器进行操作的用户权限,比如 SSH 私钥,或是机器的用户名密码,或是云平台的访问所需密钥 KEY.
所谓应用,就是将要部署的服务(往往包含多个不同子应用),它们之间通过特定的依赖关系互联,最终对外界提供服务。除核心服务应用之外,还可能包括监控应用(含报警应用),管理应用(adminstrative dashboard),工具应用(例如备份/恢复工具)等。
应用的管理,主要是应用元信息的管理,应用之间的依赖关系管理,应用对资源的关系,应用配置管理。
应用元信息的管理,往往是描述一个应用的诸如版本,二进制等等信息,往往是部署的第一步,先准备好将要部署的应用。其中涉及到产品二进制分发的问题,则是额外的话题了。
应用之间依赖,应用对资源的依赖关系的管理,往往通过配置管理的形态实现。
配置管理不只包括应用配置管理,往往还包括部署的配置管理,但两者之间往往存在交叉融合的地方,比如说指定某个机器的上部署的应用 A 需要一个特殊的配置项。
管理各应用的配置参数,最终可能以配置文件,应用启动的命令行参数,或是应用的环境变量等方式存在。其中部分配置暗含各应用各部署目标机器之间的互联关系,往往是动态生成得到。例如
部署配置管理其实就是所部署的应用到具体的部署目标之间的映射关系。例如:
部署配置管理的高级形态,是声明式。即机器声明我支持某资源,由一键部署系统自动选择依赖关系。这也稍许牵扯到高级形态的部署。
应用的正常运行,不只依赖配置,往往还有若干依赖数据。传统的静态依赖数据之外,就是动态数据了,往往以数据库或是数据目录的方式存在。
状态数据的管理,是整个部署届的难题。越是庞大的状态数据,在部署,环境变更,灾难恢复,以及迁移的时候就越成为问题。
现代分布式系统一般通过底层分布式数据库的方式解决状态数据管理,然而这带来了一个鸡生蛋蛋生鸡的问题,底层分布式数据库的部署,又是一个状态数据管理问题。
是的。我们还有分布式文件系统。233。
部署管理,即管理具体的部署操作任务。往往通过任务队列的方式实现,是整个一键部署最核心的部分。
在部署过程中,尤其要提供相对较清晰的进度展示,并输出合理的操作日志供时候追查问题。
部署往往也包含了变更管理,即应用版本或某依赖更新后,再次触发部署。所以部署不应是一次性任务,而是可重入任务。
部署任务的触发一般提供手动和自动两种模式。所谓的“一键”就是这里点击的“部署”按钮。
而自动触发,就隶属持续部署或是持续交付的范畴了,一般通过某种触发器或是任务计划实现。
服务的正常运行,离不开监控。在一键部署系统中,监控往往作为单独的应用存在,所以将之化解为另一个应用管理的问题。
监控往往包括监控数据的收集,监控数据的查询展示,日志收集等问题。高级形态还包括动态调试等,加入了诊断系统的功能。
监控系统一般还会提供通知(notification)的功能,报警或者状态日报信息通过通知系统发送给关注者。
服务管理相对较简单,即一个部署完成的集群,其中服务的启动,停止,删库跑路等操作。其中还可能牵扯到服务存活检测,服务自启动,服务异常自动重启(保活)等知识点。
服务管理可以作为特殊的部署操作来实现,比如设定特定的部署动作,检查环境后启停对应服务。
如上是核心功能。然后 PM 往往会提其他需求。例如:
一键部署,终将要被云平台或是云平台的容器编排消灭的吧。
但到时候,又是给容器编排做 UI 了。
]]>stdout capture is missing
when restoring from previous backup.
Or run from docker?
(得,不装 B 英语了)
长话短说,之前要把 Ansible Tower 拆到 Docker 里,结果发现总不能正常执行。任务界面会提示:
stdout capture is missing
检查发现是 celery 进程出错,用 root 启动 celery 倒是正常的。
最后发现是 docker 中的 supervisord 启动时缺乏部分环境变量,解决方法:
change supervisor/conf.d/tower.conf
ADD:
[program:awx-celeryd]
......
environment=HOME="/var/lib/awx",USER="awx"
......
是的,为找到原因,逆向了整个 Ansible Tower。
Ref: GitHub Issue
]]>了解到 CircleCI 是不错的替代品,所以打算迁移 Rust 项目过去。当然说起来, CircleCI 的野心更大,是要来替代 jenkins 的。
目前官方支持语言其实都比较落后,包括 go 也只是 1.6 版本,但似乎不是问题,而且据介绍, CircleCI 2.0 支持自定义 build image,支持语言的版本当然不在话下。
每天面对各种 IaaS, PaaS,免不了写配置是,这也是 yaml 程序员的日常。
dependencies:
pre:
- curl https://sh.rustup.rs -sSf | sh
test:
override:
- cargo build
- cargo test
如上。然而不 work。报错:
cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
warning: spurious network error (2 tries remaining): [12/-12] Malformed URL 'ssh://git@github.com:/rust-lang/crates.io-index'
warning: spurious network error (1 tries remaining): [12/-12] Malformed URL 'ssh://git@github.com:/rust-lang/crates.io-index'
error: failed to fetch `https://github.com/rust-lang/crates.io-index`
To learn more, run the command again with --verbose.
cargo build returned exit code 101
Action failed: cargo build
神了。原来, CircleCI 自作聪明在 .gitconfig
里修改了映射配置,强制用它自己的 ssh key 去访问 github,rewrite 了 https://github.com
的所有仓库。
这恰恰和 cargo 的 registry 机制冲突。所以报错。
CircleCI has rewrite
https:://github.com
tossh://git@github.com:
in.gitconfig
. And this made cargo fail with above error message.
找到了原因,就可以搞了:
machine:
pre:
- sed -i 's/github/git-non-exist-hub/g' ~/.gitconfig
dependencies:
pre:
- curl https://sh.rustup.rs -sSf | sh
test:
override:
- cargo build
- cargo test
嗯, Ugly but works.
]]>两年前苹果发布 Swift 语言的同时,新增了 HomeKit,当时用工具 dump 过最老版本的 Swift 声明。传送门:HomeKit.swift。目前所有官方相关的资料位于 HomeKit - Apple。
好消息是期待很久的 HomeKit 应用终于上线,屏幕上多了“家庭(Home)”应用,控制中心(从屏幕下方滑动)、 Siri 均对此有支持。 iOS 10 终于强化了推出已有两年智能家居平台,提供了官方 App,有不少硬件厂商支持。
简单说,HomeKit 就是苹果官方的智能家居平台解决方案,包括移动设备 SDK,智能家居硬件通信协议(HAP: HomeKit Accessory Protocol)、以及 MFi(Made for iPhone/iPod/iPad) 认证等等。通过 WiFi 或蓝牙连接智能家居设备(或 bridge 设备),也可以利用 Apple TV(4代) 或闲家中的置 iPad 实现设备的远程控制(HAP over iCloud)。
Home App 的维度划分:
众所周知苹果是卖数据线等硬件的公司(嗯,假设你数据线也坏过不少),HAP 协议部分是需要加入 MFi Program 才能获取文档,而且 MFi Program 无法以个人开发者身份加入。
好在有好心人逆向了 HAP 的服务端协议(对于智能硬件来说,硬件是服务端,手机App是客户端)。
对于折腾党来说,机会来了,自己动手改造家居!本文不涉及 App 开发,只涉及如何自制支持 HomeKit 的设备。
设备列表:
考察了两个比较靠谱的 HAP 实现:
最终选择使用 golang 的 brutella/hc
,准备环境。
需要保证树莓派和手机位于统一子网,因为 HAP 底层是基于 Apple mDNS(RFC 6762)。
brutella/hc
要求 golang >= 1.4,而 Debian jessie 版本较低,
需要配置 jessie-backports 源:
deb ftp://ftp.cn.debian.org/debian jessie-backports main contrib non-free
同时导入源的 GPG Key。方法参考 这里。
安装好 golang 1.6.2,建立开发目录。
# 似乎直接 install golang 会出点小问题,所以折衷用了如下方法:
> sudo apt-get install -t jessie-backports golang-1.6 golang-1.6-go golang-1.6-src golang-1.6-doc
> sudo apt-get install -t jessie-backports golang
跑通官方示例代码:
package main
import (
"github.com/brutella/hc"
"github.com/brutella/hc/accessory"
"log"
)
func main() {
info := accessory.Info{
Name: "Lamp",
SerialNumber: "051AC-23AAM1",
Manufacturer: "Apple",
Model: "AB",
}
acc := accessory.NewSwitch(info)
acc.Switch.On.OnValueRemoteUpdate(func(on bool) {
if on == true {
log.Println("Client changed switch to on")
} else {
log.Println("Client changed switch to off")
}
})
config := hc.Config{Pin: "00102003"}
t, err := hc.NewIPTransport(config, acc.Accessory)
if err != nil {
log.Fatal(err)
}
hc.OnTermination(func() {
t.Stop()
})
t.Start()
}
编译执行.
$ AppleHome> # current dir
$ AppleHome> go get
...
$ AppleHome> go build
...
$ AppleHome> ./AppleHome
...
随后打开手机的 Home App,添加设备,选择 Lamp,输入 PIN 00102003,完成配对,即可使用。
树莓派外接小音箱一只,用来放电台,尝试用 HomeKit 控制树莓派的禁音。命令:
amixer set PCM on
amixer set PCM off
代码:
package main
import (
"os/exec"
"github.com/brutella/hc"
"github.com/brutella/hc/accessory"
"log"
)
func main() {
info := accessory.Info{
Name: "Radio",
SerialNumber: "051AC-23AAM2",
Manufacturer: "Apple",
Model: "RPI3",
}
acc := accessory.NewSwitch(info)
acc.Switch.On.OnValueRemoteUpdate(func(on bool) {
log.Println("Toggled PCM!")
if on == true {
exec.Command("amixer", "set", "PCM", "on").Run()
log.Println("Client changed switch to on")
} else {
exec.Command("amixer", "set", "PCM", "off").Run()
log.Println("Client changed switch to off")
}
})
config := hc.Config{Pin: "00102004"}
t, err := hc.NewIPTransport(config, acc.Accessory)
if err != nil {
log.Fatal(err)
}
hc.OnTermination(func() {
t.Stop()
})
t.Start()
}
HAP 将智能家居分为以下维度:
TODO
]]>https://github.com/kylef/swiftenv
以下内容来自 Swift 语言提案1。
Swift 3.0 发布计划
https://github.com/donald-pinckney/swift-packages
代码位于 include/swift/AST
和 lib/AST
。
ModuleDecl 模块(单个库或是可执行文件)。编译的最小单元,由多个文件组成。
FileUnit(抽象类) 文件作用域,是代码组织的最小单元。
入口函数 tools/driver/driver.cpp
,main
函数。
集成多个子工具。同时若 PATH
下有名为 swift-foobar
的可执行文件,则可通过 swift foobar
调用。
swift -frontend
编译。同时支持打印出各种编译时中间结果。
swift -apinotes
参考信息位于 https://github.com/apple/swift/tree/master/apinotes .
简单说,API Notes 机制就是通过 .apinotes
文件(YAML格式)描述 Objective-C Framework 和对应 Swift API 的关系。最终生成 .apinotesc
文件,与 .swiftmodule
文件一起作为 Swift 的模块。
主要功能包括且不限于:
SwiftBridge
:设置对应的 Bridge 类型,例如 NSArray 对应与 Swift.Array
Nullability
/NullabilityOfRet
: 类的属性、方法的参数、返回值对应类型是否可以为 null,即对应与 Swift 的 T 还是 T?Availability
:方法是否在 Swift 中暴露,并给出 availability messageSwiftName
:方法 Selector 在 Swift 中的重命名,例如 filteredArrayUsingPredicate:
替换为 filtered(using:)
Dump 为YAML文件:
$> swift -apinotes -binary-to-yaml /path/to/lib/swift/macosx/x86_64/Dispatch.apinotesc -o=-
swift -modulewrap
// Wraps .swiftmodule files inside an object file container so they
// can be passed to the linker directly. Mostly useful for platforms
// where the debug info typically stays in the executable.
// (ie. ELF-based platforms).
用法:
swift -modulewrap ObjectiveC.swiftmodule -o objc.o
实际发现是在 .o
里定义了 ___Swift_AST
符号。
Swift 提供了两个 REPL(Read-Evaluate-Print Loop),一个是 Swift 本身内置,另一个集成到了 lldb 命令行下。前者只有基本功能,即将废弃,后者功能更强大。
子命令分别是:
swift -deprecated-integrated-repl
swift -lldb-repl
。swift -repl
子命令选择可用的 REPL 进入,一般是 lldb-repl
,除非找不到 lldb 时。这也是 Swift 命令不带任何参数的默认行为。
(apple/swift-evolution)[https://github.com/apple/swift-evolution] ↩
汉语字典中对“模式”的解释是:事物的标准样式。在计算机科学中,它指特定类型的数据(往往是序列或是树形结构)满足某一特定结构或格式。“匹配”本身是指一个判断寻找过程。最早的模式匹配用于文本编辑器中的正则字符串搜索,之后才作为编程语言特性。
模式匹配在计算机科学领域有两层意思。其一,可以特指字符串匹配算法,例如为人熟知的 KMP 字符串匹配算法、命令行工具 grep 等。 其二,特指在一些语言中作为一种以结构的方式处理数据的工具,此时的匹配过程往往是树形匹配,与此相伴的往往还有一个特性叫 guard(守卫)。
Rust 中模式匹配随处可见,例如在let
变量绑定语句、match
匹配语句中等。利用好模式匹配这一特性可以使代码更简洁易懂。Rust
支持模式匹配中的变量绑定、结构体/元组解构、守卫条件判断、数值范围匹配等特性。
match
语句中可以直接匹配字面常量,下划线_
匹配任意情形。
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
以上代码会打印出one
。
match
用于匹配一个表达式的值,寻找满足条件的子分支(arm
)并执行。每个子分支包含三部分:一系列模式、可选的守卫条件以及主体代码块。
每个子分支可以是多个模式,通过 |
符号分割:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
以上代码打印出one or two
。
通过if
引入子分支的守卫条件:
enum OptionalInt {
Value(i32),
Missing,
}
let x = OptionalInt::Value(5);
match x {
OptionalInt::Value(i) if i > 5 => println!("Got an int bigger than five!"),
OptionalInt::Value(..) => println!("Got an int!"),
OptionalInt::Missing => println!("No such luck."),
}
其实进阶,不如直接从libsyntax
源码看看到底模式匹配是如何实现。syntax::ast::Pat
。
从AST源码中寻找语法要素屋外户两个要点,其一,语法要素是如何表达为对应AST的;其二,对应AST在哪些父AST中出现。
Rust中使用syntax::ast::Pat
枚举来表示一个模式匹配。
pub struct Pat {
pub id: NodeId,
pub node: PatKind,
pub span: Span,
}
pub enum PatKind {
/// Represents a wildcard pattern (`_`)
/// 表示通配,下划线
Wild,
/// A `PatKind::Ident` may either be a new bound variable,
/// or a unit struct/variant pattern, or a const pattern (in the last two cases
/// the third field must be `None`).
///
/// In the unit or const pattern case, the parser can't determine
/// which it is. The resolver determines this, and
/// records this pattern's `NodeId` in an auxiliary
/// set (of "PatIdents that refer to unit patterns or constants").
Ident(BindingMode, SpannedIdent, Option<P<Pat>>),
/// A struct or struct variant pattern, e.g. `Variant {x, y, ..}`.
/// The `bool` is `true` in the presence of a `..`.
Struct(Path, Vec<Spanned<FieldPat>>, bool),
/// A tuple struct/variant pattern `Variant(x, y, z)`.
/// "None" means a `Variant(..)` pattern where we don't bind the fields to names.
TupleStruct(Path, Option<Vec<P<Pat>>>),
/// A path pattern.
/// Such pattern can be resolved to a unit struct/variant or a constant.
Path(Path),
/// An associated const named using the qualified path `<T>::CONST` or
/// `<T as Trait>::CONST`. Associated consts from inherent impls can be
/// referred to as simply `T::CONST`, in which case they will end up as
/// PatKind::Path, and the resolver will have to sort that out.
QPath(QSelf, Path),
/// A tuple pattern `(a, b)`
Tup(Vec<P<Pat>>),
/// A `box` pattern
Box(P<Pat>),
/// A reference pattern, e.g. `&mut (a, b)`
Ref(P<Pat>, Mutability),
/// A literal
Lit(P<Expr>),
/// A range pattern, e.g. `1...2`
Range(P<Expr>, P<Expr>),
/// `[a, b, ..i, y, z]` is represented as:
/// `PatKind::Vec(box [a, b], Some(i), box [y, z])`
Vec(Vec<P<Pat>>, Option<P<Pat>>, Vec<P<Pat>>),
/// A macro pattern; pre-expansion
Mac(Mac),
}
以上AST定义,即说明,到底什么被认为是一个“模式”。
以下介绍Pat
在哪些AST中出现。
全局 Item 中,使用模式匹配的均为函数参数。
Fn
全局函数 -> FnDecl
函数声明 -> [Arg]
函数头参数声明。
Trait
-> [TraitItem]
-> TraitItemKind::Method
-> MethodSig
-> FnDecl
方法声明,同上。
Impl
-> [ImplItem]
-> ImplItemKind::Method
-> MethodSig
-> FnDecl
。
Decl
-> DeclKind::Local
。
即 let
语句 let <pat>:<ty> = <expr>;
。
见下。
除match
外,if let
、while let
、for
控制语句支持同时进行模式匹配。具体实现是一种desugared
过程,即,去语法糖化。
同时类似于函数定义,闭包参数也支持模式匹配。
IfLet(P<Pat>, P<Expr>, P<Block>, Option<P<Expr>>)
if let pat = expr { block } else { expr }
This is desugared to a match expression.
WhileLet(P<Pat>, P<Expr>, P<Block>, Option<Ident>)
'label: while let pat = expr { block }
ForLoop(P<Pat>, P<Expr>, P<Block>, Option<Ident>)
'label: for pat in expr { block }
Match(P<Expr>, Vec<Arm>)
match
语句,在 Arm
中出现,其中 Arm
定义为
pub struct Arm {
pub attrs: Vec<Attribute>,
pub pats: Vec<P<Pat>>,
pub guard: Option<P<Expr>>,
pub body: P<Expr>,
}
Closure(CaptureBy, P<FnDecl>, P<Block>)
闭包,例如 move |a, b, c| {a + b + c}
。
advanced_slice_patterns
- See the match expressions section for discussion; the exact semantics of slice patterns are subject to change, so some types are still unstable.
slice_patterns
- OK, actually, slice patterns are just scary and completely unstable.
box_patterns
- Allows box patterns, the exact semantics of which is subject to change.
https://doc.rust-lang.org/book/patterns.html
]]>Github: guangzhou-realtime-bus
base64封装过程:先打包字符串长度,然后是原始字符串(JSON),然后是0x10
(md5字符串长度),
然后是 md5 校验值。整个二进制字符串用 base64 转码,POST 给服务器。
具体的登录注册过程还需要进一步抓包分析,不过暂时兴趣不在这里了。
]]>protocol ErrorType {
var _domain: String { get }
var _code: Int { get }
}
@asmname("swift_bridgeErrorTypeToNSError") func _bridgeErrorTypeToNSError(e: ErrorType) -> AnyObject
@asmname("swift_stdlib_getErrorCode") func _stdlib_getErrorCode<T : ErrorType>(x: UnsafePointer<T>) -> Int
@asmname("swift_stdlib_getErrorDomainNSString") func _stdlib_getErrorDomainNSString<T : ErrorType>(x: UnsafePointer<T>) -> AnyObject
protocol _ObjectiveCBridgeableErrorType : ErrorType {
init?(_bridgedNSError: NSError)
}
struct NSCocoaError : RawRepresentable, _BridgedNSError, _ObjectiveCBridgeableErrorType, ErrorType, __BridgedNSError, Hashable, Equatable {
let rawValue: Int
init(rawValue: Int)
static var _NSErrorDomain: String {
get {}
}
typealias RawValue = Int
}
infix func ==(a: _GenericObjCError, b: _GenericObjCError) -> Bool
infix func ==(a: _GenericObjCError, b: _GenericObjCError) -> Bool
func ==<T : __BridgedNSError where T.RawValue : SignedIntegerType>(lhs: T, rhs: T) -> Bool
@available(OSX 10.11, iOS 9.0, *)
func resolveError(error: NSError?) throws
enum _GenericObjCError : ErrorType {
case NilError
var hashValue: Int {
get {}
}
var _domain: String {
get {}
}
var _code: Int {
get {}
}
}
@asmname("swift_stdlib_bridgeNSErrorToErrorType")
func _stdlib_bridgeNSErrorToErrorType<T : _ObjectiveCBridgeableErrorType>(error: NSError, out: UnsafeMutablePointer<T>) -> Bool
@asmname("swift_convertNSErrorToErrorType") func _convertNSErrorToErrorType(error: NSError?) -> ErrorType
@objc enum NSURLError : Int, _BridgedNSError, _ObjectiveCBridgeableErrorType, ErrorType, __BridgedNSError { ... }
protocol __BridgedNSError : RawRepresentable {
static var _NSErrorDomain: String { get }
}
@asmname("swift_convertErrorTypeToNSError") func _convertErrorTypeToNSError(error: ErrorType) -> NSError
func ~=(match: NSCocoaError, error: ErrorType) -> Bool
protocol _BridgedNSError : __BridgedNSError, _ObjectiveCBridgeableErrorType, Hashable {
static var _NSErrorDomain: String { get }
}
ErrorType 在 Swift 中表示。
extension NSError : ErrorType {
@objc dynamic var _domain: String {
@objc dynamic get {}
}
@objc dynamic var _code: Int {
@objc dynamic get {}
}
}
每辆车,每15秒更新一次 GPS,
整理成为 Repo andelf/beijing-realtime-bus.
]]>以 Alamofire 为例,
cd Path-To-Alamofire-Src-Dir
mkdir -p 32 64
# 创建动态链接库,及对应 Swift 模块,32/64版本
xcrun swiftc -sdk $(xcrun --show-sdk-path --sdk iphoneos) Alamofire.swift -target arm64-apple-ios7.1 -target-cpu cyclone -emit-library -emit-module -module-name Alamofire -v -o libswiftAlamofire.dylib -module-link-name swiftAlamofire -Xlinker -install_name -Xlinker @rpath/libswiftAlamofire.dylib
mv Alamofire.swiftdoc Alamofire.swiftmodule libswiftAlamofire.dylib ./64
xcrun swiftc -sdk $(xcrun --show-sdk-path --sdk iphoneos) Alamofire.swift -target armv7-apple-ios7.1 -target-cpu cyclone -emit-library -emit-module -module-name Alamofire -v -o libswiftAlamofire.dylib -module-link-name swiftAlamofire -Xlinker -install_name -Xlinker @rpath/libswiftAlamofire.dylib
mv Alamofire.swiftdoc Alamofire.swiftmodule libswiftAlamofire.dylib ./64
# 创建 universal lib
lipo -create ./{32,64}/libswiftAlamofire.dylib -output ./libswiftAlamofire.dylib
# 创建模拟器用 lib
xcrun swiftc -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) Alamofire.swift -target i386-apple-ios7.1 -target-cpu yonah -emit-library -emit-module -module-name Alamofire -v -o libswiftAlamofire.dylib -module-link-name swiftAlamofire -Xlinker -install_name -Xlinker @rpath/libswiftAlamofire.dylib
其他相关 target
-target armv7-apple-ios7.1 -target-cpu cortex-a8
-target arm64-apple-ios7.1 -target-cpu cyclone
-target i386-apple-ios7.1 -target-cpu yonah
-target x86_64-apple-ios7.1 -target-cpu core2
其实你了解 Swift 模块结构的化,应该回想到,将第三方模块创建为 swiftmodule 应该是最靠谱的选择。不过实际操作发现, 编译命令无法很方便地调整,主要是因为 xcodebuild 系统,和编译命令不知道怎么导出。也是略纠结。
实际上,如果使用 Carthage 的话,即把第三方扩展作为 Framework 引入,会导致无法支持 iOS 7,但是 Swift 本身是支持 iOS 7 的, 在编译命令和生成的文件中检查发现,对于 iOS 7,Swift 使用了纯静态模块编译的方法。所以其实我们引入第三方扩展的时候也可以这样做。
以下是静态编译所需命令:
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) SwiftyJSON.swift -c -parse-as-library -module-name SwiftyJSON -v -o SwiftyJSON.o
ar rvs libswiftSwiftyJSON.a SwiftyJSON.o
如何使用?
将编译结果扔到:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift_static
下对应目录。
然后在 Xcode 里,直接 import。
]]>Swift version 1.0 (swift-600.0.34.4.8)
到 beta3 Swift version 1.0 (swift-600.0.38.7)
的变化。
对了,补充下。 beta1 Swift version 1.0 (swift-600.0.34.4.5)
到 beta2 几乎没有什么变化。
nil
成为关键字。
[KeyType : ValueType]
可以表示字典类型 Dictionary<KeyType, ValueType>
。
[Type]
用于表示原 Array 类型 Type[]
,等价 Array<T>
,原用法会导致警告。
增加 @noinline 属性
..
运算符改为 ..<
,不容易和 ...
混淆。
原 sort()
改名为 sorted()
。新增 sort()
函数,参数为 inout
。
Index 类型中的 .succ()
变为 .successor()
、 .pred()
变为 .predecessor()
。
增加 UnsafeMutableArray<T>
类型。
增加 CFunctionPointer<T>
类型。
删除 CConstVoidPointer
、 CMutableVoidPointer
。替换为 UnsafePointer<()>
、ConstUnsafePointer<Int32>
。
删除 CConstPointer<T>
、CMutablePointer<T>
。替换为 UnsafePointer<T>
、ConstUnsafePointer<T>
。
这么一来指针操作简单了好多。原有会出现 COpaquePointer
的不合理情况,也都对应到适合的类型。
CString
可以从 UnsafePointer<UInt8>
和 UnsafePointer<CChar>
两种类型构造获得,之前只支持 UInt8
。
module.map 中头文件声明转换为 Swift 声明不再使用 C 兼容类型,直接使用 Swift 相应类型。原有 CInt
,现在成为 Int32
。
结构体会自动添加构造函数 init(field1:field2:...)
这样。
去掉了 NilType
,增加了 NilLiteralConvertible
, nil
成为关键字。可以认为是 nil 常量。
protocol NilLiteralConvertible {
class func convertFromNilLiteral() -> Self
}
除了 Optional 、上面所提到的指针类型外,RawOptionSet
也实现了该协议。
去掉了 .copy()
、unshare()
方法。
增加了以下方法:
func makeUnique(inout buffer: ArrayBuffer<T>, e: T, index: Int)
func sorted(isOrderedBefore: (T, T) -> Bool) -> Array<T>
看起来 Array
对底层容器的引用有了更好的控制 ArrayBufferType
增加了判断方法 func isMutableAndUniquelyReferenced() -> Bool
。
Array 目前可以认为是真正的值类型。
_Pointer
protocolprotocol _Pointer {
var value: RawPointer { get }
init(_ value: RawPointer)
}
表示一个类型可以对应到原生指针。
同时成为内部桥接类型,编译器内部在转换时使用它(取出 RawPointer, 构造具体指针类型)。
增加了 StdlibUnittest 模块。 声明代码。单元测试终于有了。
]]>目前已经有了很多非常棒的 Swift 第三方库, JSON 处理啊、 HTTP 访问啊、 UIView 插件啊等等。
如何科学地引用这些第三方库呢?
CocoaPods 由于完全使用静态链接解决方法,过度依赖 Objective-C ,目前应该是官方 repo 有提到是 -Xlinker
error , 这个问题之前我也遇到过,无解。除非手工执行 ar
不用 ld
和 libtool
。
小伙伴有用子目录的方法引用代码,貌似不错,还有就是直接用 git submodule
,看起来维护性也可以。
一个良好的第三方库应该实现为 Cocoa Touch Framework (实际内容为 Header + 动态链接库)。而不是直接把 Swift 代码 Copy 过来放入自己的项目。这里以一个简单项目为例,介绍如何科学使用。
用 Swift 创建一个 Demo ,使用 SwiftyJSON 和 LTMorphingLabel 库。
项目的名字叫 DemoApp 。
创建一个 Workspace ,名字随意,位置能找到就好。这个 Workspace 主要用来管理我们的项目及其依赖的第三方库。
在 Workspace 创建一个 App ,因为是测试所以我选了 Single View Application 。
SwiftyJSON 是一个 Cocoa Touch Framework ,可以直接使用, git clone
后,添加项目到 Workspace 即可。
尝试操作发现。。最容易最不会出错的方法就是直接从 Finder 里把 .xcodeproj
文件拖动到 Workspace 。
LTMorphingLabel 是一个 App Deme 式项目。其中 Label View 的实现在一个子目录中。可以采用创建 Cocoa Touch Framework 的方法来引入这几个文件。
当然也可以直接把目录拖到我们的 DemoApp 里,不过太原始粗暴了。
在 DemoApp 的 Genral 选项卡中,添加 Linked Frameworks and Libraries 。选择 Workspace 中 SwiftyJSON 和
LTMorphingLabel 两个 .framework
。
如果是直接选择来自其他项目的 .framework
而不是同一 Workspace ,那么这里也许还要同时加入 Embedded Binaries
。
添加好依赖后,就可以在 DemoApp 项目代码中 import SwiftyJSON
或者 import LTMorphingLabel
来使用对应的库。同时还可以用 Command + 鼠标点击的方法查看声明代码。
比较坑爹的是,实际上按照以上方法, LTMorphingLabel
并不能正常使用,查看报错信息发现是自动生成的 LTMorphingLabel-Swift.h
有处语法无法被识别,编辑器找到 .h
文件,注释掉这行诡异代码即可。
看起来目前的 Bridge Header 和 -emit-objc-header 实现还是有问题的。小伙伴一定要淡定。
如果不喜欢使用 Workspace ,也可以将第三方库的编译结果,一个 .framework
目录拖到项目文件里,然后添加 Embedded Binaries
。
创建 Cocoa Touch Framework 选项中,可以使用 Swift 代码,此时编译结果(默认)会包含 module.modulemap
文件,
之前有介绍过它的作用,通过它, Swift 可以使用第三方模块。参考 Module System of Swift (简析 Swift 的模块系统) 。
实际上这个解决方案绕了一大圈,通过 Swift 文件导出 ProjName-Swift.h
、然后 module.modulemap
模块描述文件引入、然后再由 Swift 导入。
其实 .framework
同时也包含了 ProjName.swiftmodule/[ARCH].swiftmodule
不过看起来没有使用到,而且默认在 IDE 下也不支持 Swift 从 .swiftmodule
文件导入,比较坑。希望以后版本能加入支持。
.framework
包含了所有 Swift 标准库的动态链接库,小伙伴可能会以为这会导致编译后的 App 变大。其实大可放心,任何 Swift 语言的 App 都会包含这些动态链接库,而且只会包含一个副本。此方法对 App 最终的大小几乎无影响。
注: 个人测试了下,发现这个 .swiftmodule
是可以通过其他方法使用的,绕过 module.modulemap
,应该是更佳的解决方案,但是需要控制命令行参数。
至于静态链接库,过时了。抛弃吧。
电子书上介绍的 default function parameter 这里都不好意思拿出来写。
咳咳。持续更新。
Keywards as variable name.
// escaped variable name
let `let` = 1000
dump(`let`, name: "variable named let")
new
关键字The new
keyword.
快速初始化数组。
let an_array_with_100_zero = new(Int)[100]
use protocol<Protocol1, Protocol2, ...>
as a type.
瞎试出来的。
]]>Swift 本身的特性,导致它在一些用法上和 Objective-C 上有所不同,比如 ObjC 的 struct 单纯和 C 的一样,但是在 Swift 中的 struct 则要强大得多。
个人认为比如 CGPointMake
这样的函数,理论上不应该出现在 Swift 代码中。而是应该用 CGPoint(x:y:)
。
本文可以作为参考手册使用。
值得注意的是 Selector 相关方法,实现了 StringLiteralConvertible
。也可以从 nil
获得。
这里忽略之前介绍过的 _BridgedToObjectiveC
相关内容。
Sequence 协议
NSMutableArray NSSet NSArray NSMutableDictionary NSMutableSet NSDictionary
所有以上这些类型都可以通过 for-in 操作。
*LiteralConvertible
NSNumber NSString NSArray NSDictionary
CF 几乎都对应到了 NS 类型。这里略去
NilType
-> NSZone
Dictionary<KeyType: Hashable, ValueType>
-> NSDictionary
NSDictionary
-> Dictionary<NSObject, AnyObject>
String
<-> NSString
NSArray
-> AnyObject[]
A[]
-> NSArray
Float Double Int UInt Bool
-> NSNumber
NSRange
-> Range<Int>
// 比较有意思的一个// let s = NSSet(objects: 12, 32, 23, 12)
extension NSSet {
convenience init(objects elements: AnyObject...)
}
extension NSOrderedSet {
convenience init(objects elements: AnyObject...)
}
// 这里注意,NSRange 和 Swift Range 对 range 结束的表述方法不同
// NSRange 保存 range 元素个数
// Swift Range 保存的是结束元素
// let r = NSRange(0..20)
extension NSRange {
init(_ x: Range<Int>)
}
// let prop = NSDictionary(objectsAndKeys: "Feather", "name", "Programming", "hobby")
extension NSDictionary {
convenience init(objectsAndKeys objects: AnyObject...)
}
extension NSObject : CVarArg {
@objc func encode() -> Word[]
}
字符串的扩展方法非常多。
static func availableStringEncodings() -> NSStringEncoding[]
static func defaultCStringEncoding() -> NSStringEncoding
static func localizedNameOfStringEncoding(encoding: NSStringEncoding) -> String
static func localizedStringWithFormat(format: String, _ arguments: CVarArg...) -> String
static func pathWithComponents(components: String[]) -> String
static func stringWithContentsOfFile(path: String, encoding enc: NSStringEncoding, error: NSErrorPointer = default) -> String?
static func stringWithContentsOfFile(path: String, usedEncoding: CMutablePointer<NSStringEncoding> = default, error: NSErrorPointer = default) -> String?
static func stringWithContentsOfURL(url: NSURL, encoding enc: NSStringEncoding, error: NSErrorPointer = default) -> String?
static func stringWithContentsOfURL(url: NSURL, usedEncoding enc: CMutablePointer<NSStringEncoding> = default, error: NSErrorPointer = default) -> String?
static func stringWithCString(cString: CString, encoding enc: NSStringEncoding) -> String?
static func stringWithUTF8String(bytes: CString) -> String?
func canBeConvertedToEncoding(encoding: NSStringEncoding) -> Bool
var capitalizedString: String { get }
func capitalizedStringWithLocale(locale: NSLocale) -> String
func caseInsensitiveCompare(aString: String) -> NSComparisonResult
func commonPrefixWithString(aString: String, options: NSStringCompareOptions) -> String
func compare(aString: String, options mask: NSStringCompareOptions = default, range: Range<String.Index>? = default, locale: NSLocale? = default) -> NSComparisonResult
func completePathIntoString(_ outputName: CMutablePointer<String> = default, caseSensitive: Bool, matchesIntoArray: CMutablePointer<String[]> = default, filterTypes: String[]? = default) -> Int
func componentsSeparatedByCharactersInSet(separator: NSCharacterSet) -> String[]
func componentsSeparatedByString(separator: String) -> String[]
func cStringUsingEncoding(encoding: NSStringEncoding) -> CChar[]?
func dataUsingEncoding(encoding: NSStringEncoding, allowLossyConversion: Bool = default) -> NSData
var decomposedStringWithCanonicalMapping: String { get }
var decomposedStringWithCompatibilityMapping: String { get }
func enumerateLines(body: (line: String, inout stop: Bool) -> ())
func enumerateLinguisticTagsInRange(range: Range<String.Index>, scheme tagScheme: String, options opts: NSLinguisticTaggerOptions, orthography: NSOrthography?, _ body: (String, Range<String.Index>, Range<String.Index>, inout Bool) -> ())
func enumerateSubstringsInRange(range: Range<String.Index>, options opts: NSStringEnumerationOptions, _ body: (substring: String, substringRange: Range<String.Index>, enclosingRange: Range<String.Index>, inout Bool) -> ())
var fastestEncoding: NSStringEncoding { get }
func fileSystemRepresentation() -> CChar[]
func getBytes(inout buffer: UInt8[], maxLength: Int, usedLength: CMutablePointer<Int>, encoding: NSStringEncoding, options: NSStringEncodingConversionOptions, range: Range<String.Index>, remainingRange: CMutablePointer<Range<String.Index>>) -> Bool
func getCString(inout buffer: CChar[], maxLength: Int, encoding: NSStringEncoding) -> Bool
func getFileSystemRepresentation(inout buffer: CChar[], maxLength: Int) -> Bool
func getLineStart(start: CMutablePointer<String.Index>, end: CMutablePointer<String.Index>, contentsEnd: CMutablePointer<String.Index>, forRange: Range<String.Index>)
func getParagraphStart(start: CMutablePointer<String.Index>, end: CMutablePointer<String.Index>, contentsEnd: CMutablePointer<String.Index>, forRange: Range<String.Index>)
var hash: Int { get }
static func stringWithBytes(bytes: UInt8[], length: Int, encoding: NSStringEncoding) -> String?
static func stringWithBytesNoCopy(bytes: CMutableVoidPointer, length: Int, encoding: NSStringEncoding, freeWhenDone flag: Bool) -> String?
init(utf16CodeUnits: CConstPointer<unichar>, count: Int)
init(utf16CodeUnitsNoCopy: CConstPointer<unichar>, count: Int, freeWhenDone flag: Bool)
init(format: String, _ _arguments: CVarArg...)
init(format: String, arguments: CVarArg[])
init(format: String, locale: NSLocale?, _ args: CVarArg...)
init(format: String, locale: NSLocale?, arguments: CVarArg[])
var lastPathComponent: String { get }
var utf16count: Int { get }
func lengthOfBytesUsingEncoding(encoding: NSStringEncoding) -> Int
func lineRangeForRange(aRange: Range<String.Index>) -> Range<String.Index>
func linguisticTagsInRange(range: Range<String.Index>, scheme tagScheme: String, options opts: NSLinguisticTaggerOptions = default, orthography: NSOrthography? = default, tokenRanges: CMutablePointer<Range<String.Index>[]> = default) -> String[]
func localizedCaseInsensitiveCompare(aString: String) -> NSComparisonResult
func localizedCompare(aString: String) -> NSComparisonResult
func localizedStandardCompare(string: String) -> NSComparisonResult
func lowercaseStringWithLocale(locale: NSLocale) -> String
func maximumLengthOfBytesUsingEncoding(encoding: NSStringEncoding) -> Int
func paragraphRangeForRange(aRange: Range<String.Index>) -> Range<String.Index>
var pathComponents: String[] { get }
var pathExtension: String { get }
var precomposedStringWithCanonicalMapping: String { get }
var precomposedStringWithCompatibilityMapping: String { get }
func propertyList() -> AnyObject
func propertyListFromStringsFileFormat() -> Dictionary<String, String>
func rangeOfCharacterFromSet(aSet: NSCharacterSet, options mask: NSStringCompareOptions = default, range aRange: Range<String.Index>? = default) -> Range<String.Index>
func rangeOfComposedCharacterSequenceAtIndex(anIndex: String.Index) -> Range<String.Index>
func rangeOfComposedCharacterSequencesForRange(range: Range<String.Index>) -> Range<String.Index>
func rangeOfString(aString: String, options mask: NSStringCompareOptions = default, range searchRange: Range<String.Index>? = default, locale: NSLocale? = default) -> Range<String.Index>
var smallestEncoding: NSStringEncoding { get }
func stringByAbbreviatingWithTildeInPath() -> String
func stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters: NSCharacterSet) -> String
func stringByAddingPercentEscapesUsingEncoding(encoding: NSStringEncoding) -> String
func stringByAppendingFormat(format: String, _ arguments: CVarArg...) -> String
func stringByAppendingPathComponent(aString: String) -> String
func stringByAppendingPathExtension(ext: String) -> String
func stringByAppendingString(aString: String) -> String
var stringByDeletingLastPathComponent: String { get }
var stringByDeletingPathExtension: String { get }
var stringByExpandingTildeInPath: String { get }
func stringByFoldingWithOptions(options: NSStringCompareOptions, locale: NSLocale) -> String
func stringByPaddingToLength(newLength: Int, withString padString: String, startingAtIndex padIndex: Int) -> String
var stringByRemovingPercentEncoding: String { get }
func stringByReplacingCharactersInRange(range: Range<String.Index>, withString replacement: String) -> String
func stringByReplacingOccurrencesOfString(target: String, withString replacement: String, options: NSStringCompareOptions = default, range searchRange: Range<String.Index>? = default) -> String
func stringByReplacingPercentEscapesUsingEncoding(encoding: NSStringEncoding) -> String
var stringByResolvingSymlinksInPath: String { get }
var stringByStandardizingPath: String { get }
func stringByTrimmingCharactersInSet(set: NSCharacterSet) -> String
func stringsByAppendingPaths(paths: String[]) -> String[]
func substringFromIndex(index: Int) -> String
func substringToIndex(index: Int) -> String
func substringWithRange(aRange: Range<String.Index>) -> String
func uppercaseStringWithLocale(locale: NSLocale) -> String
func writeToFile(path: String, atomically useAuxiliaryFile: Bool, encoding enc: NSStringEncoding, error: NSErrorPointer = default) -> Bool
func writeToURL(url: NSURL, atomically useAuxiliaryFile: Bool, encoding enc: NSStringEncoding, error: NSErrorPointer = default) -> Bool
几个常用基本类型都有了 Swift-style 的构造函数。其中 CGRect
有很多的相关运算都被封装为方法,很不错。
extension CGPoint : Equatable {
static var zeroPoint: CGPoint
init()
init(x: Int, y: Int)
}
extension CGSize {
static var zeroSize: CGSize
init()
init(width: Int, height: Int)
}
extension CGVector {
static var zeroVector: CGVector
init(_ dx: CGFloat, _ dy: CGFloat)
init(_ dx: Int, _ dy: Int)
}
extension CGRect : Equatable {
// 全为 0
static var zeroRect: CGRect
// 原点为无穷大,表示空
static var nullRect: CGRect
// 原点无穷小,宽高无穷大
static var infiniteRect: CGRect
init()
init(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat)
init(x: Int, y: Int, width: Int, height: Int)
var width: CGFloat
var height: CGFloat
var minX: CGFloat
var minY: CGFloat
// 中点
var midX: CGFloat
var midY: CGFloat
var maxX: CGFloat
var maxY: CGFloat
var isNull: Bool
var isEmpty: Bool
var isInfinite: Bool
var standardizedRect: CGRect
func standardize()
var integerRect: CGRect
func integerize()
func rectByInsetting(#dx: CGFloat, dy: CGFloat) -> CGRect
func inset(#dx: CGFloat, dy: CGFloat)
func rectByOffsetting(#dx: CGFloat, dy: CGFloat) -> CGRect
func offset(#dx: CGFloat, dy: CGFloat)
func rectByUnion(withRect: CGRect) -> CGRect
func union(withRect: CGRect)
func rectByIntersecting(withRect: CGRect) -> CGRect
func intersect(withRect: CGRect)
func rectsByDividing(atDistance: CGFloat, fromEdge: CGRectEdge) -> (slice: CGRect, remainder: CGRect)
func contains(rect: CGRect) -> Bool
func contains(point: CGPoint) -> Bool
func intersects(rect: CGRect) -> Bool
}
extension NSGradient {
convenience init(colorsAndLocations objects: (AnyObject, CGFloat)...)
}
extension UIDeviceOrientation {
var isPortrait: Bool
// also isLandscape isValidInterfaceOrientation isFlat
}
extension UIInterfaceOrientation {
var isPortrait: Bool
var isLandscape: Bool
}
这个模块是交叉编译的。。不太容易获得信息。不过好在扩展内容不多。
extension SKNode {
@objc subscript (name: String) -> SKNode[] { get }
}
NSSet NSDate NSArray NSRange NSURL NSDictionary NSString
CGPoint CGRect CGSize
NSView
UIView
SKTextureAtlas SKTexture SKSpriteNode SKShapeNode
单独添加了自己的 Mirror
类型,单独实现。
Mirror
类型其实是为 QuickLookObject
准备的,也就是在 Xcode Playground 中快速查看。
本文主要介绍 Swift 所有标准库类型的层次结构,及所有标准类型。本文可作为参考手册使用。
本人不保证内容及时性和正确性,请善于怀疑并反馈。谢谢。
本文探索 Swift 所有基础类型和高级类型,以及所有协议和他们之间的继承关系。
为了简化问题,某些类型略去了中间的过渡类型,人肉保证不歧义。
Bit
只有一位,实现为 enum
, .zero
或 .one
。简单明了。
协议: RandomAccessIndex IntegerArithmetic
有符号:
Int Int8 Int16 Int32 Int64
协议:SignedInteger RandomAccessIndex BitwiseOperations SignedNumber CVarArg
无符号:
UInt UInt8 UInt16 UInt32 UInt64
协议:UnsignedInteger RandomAccessIndex BitwiseOperations
别名:
IntMax = Int64
UIntMax = UInt64
IntegerLiteralType = Int
Word = Int // 字长
UWord = UInt
Float Double Float80
别名:
FloatLiteralType = Double
Float64 = Double
协议:FloatingPointNumber
。
只有一个 Bool
。
实例: true
、false
协议:LogicValue
。
只有一个 NilType
。
唯一实例 nil
。
String
Character
Unicode 字符UnicodeScalar
相当于 C 中的 wchar_t
CString
用于表示 C 中的 const char *
,请参考相关文章StaticString
静态字符串,内部使用,例如 fatalError
别名:
StringLiteralType = String
ExtendedGraphemeClusterType = String
官方文档
Character
represents some Unicode grapheme cluster as defined by a canonical, localized, or otherwise tailored segmentation algorithm.
String
实现协议:Collection ExtensibleCollection OutputStream TargetStream
。
Array<T>
ContiguousArray<T>
实现协议 ArrayType
。
内部容器:
ArrayBuffer<T>
ContiguousArrayBuffer<T>
这两个类型看起来是 Array 的内部容器,一般不应该直接使用。
Dictionary<KeyType : Hashable, ValueType>
只实现了 Collection
。
除正常元祖外,还有个特殊的别名
Void = ()
其实很多语言都这么定义的,比如 Haskell 。
Optional<T>
即 T?
ImplicitlyUnwrappedOptional<T>
即 T!
实现协议: LogicValue
,行为是判断是否为 .None
。
另外 Swift 的隐式类型转换
有提到,为什么 nil
可以给 Optional
类型赋值的问题。
CBool = Bool
CFloat = Float
CDouble = Double
CChar = Int8
CSignedChar = Int8
CUnsignedChar = UInt8
CChar16 = UInt16
CWideChar = UnicodeScalar
CChar32 = UnicodeScalar
CInt = Int32
CUnsignedInt = UInt32
CShort = Int16
CUnsignedShort = UInt16
CLong = Int
CUnsignedLong = UInt
CLongLong = Int64
CUnsignedLongLong = UInt64
具体使用参考 C 交互的几篇文章,基本没区别。
AnyObject
// 别名
Any = protocol<>
AnyClass = AnyObject.Type
还有个用在函数定义的类型签名上, Any.Type
。
顺便这里看到一个奇异的语法 protocol<>
,这个也是 Swift 一种用来表示类型限制的方法,可以用在类型的位置,尖括号里可以是协议的列表。
UnsafePointer<T>
CMutableVoidPointer
CConstVoidPointer
COpaquePointer
CConstPointer<T>
AutoreleasingUnsafePointer<T>
CVaListPointer
CMutablePointer<T>
参考 C 交互文章。
多了去了。比如 for-in 实现时候的 Generator 、比如反射时候用的 *Mirror
、比如切片操作用的 Range<T>
。比如内部储存类。
还有储存辅助类 OnHeap<T>
等等。以后有机会再探索。
Printable DebugPrintable
protocol Printable {
var description: String { get }
}
protocol DebugPrintable {
var debugDescription: String { get }
}
用于打印和字符串的 Interpolation 。
*LiteralConvertible
从字面常量获取。
ArrayLiteralConvertible
IntegerLiteralConvertible
DictionaryLiteralConvertible
CharacterLiteralConvertible
FloatLiteralConvertible
ExtendedGraphemeClusterLiteralConvertible
StringLiteralConvertible
其中字符串和字符的字面常量表示有所重合,也就是说 "a"
可以是字符串也可以是字符。简析 Swift 中的 Pattern Match 一文中就是遇到了类似的情况。
LogicValue
相当于重载 if
、while
的行为。
protocol LogicValue {
func getLogicValue() -> Bool
}
Sequence
相当于重载 for-in 。和 Generator
联用。
protocol Sequence {
typealias GeneratorType : Generator
func generate() -> GeneratorType
}
protocol Generator {
typealias Element
mutating func next() -> Element?
}
// for .. in { }
var __g = someSequence.generate()
while let x = __g.next() {
...
}}
这些协议都是用来表示容器类型的索引、及相关的索引运算。
这里略去了部分私有内容。略去了 Printable
等。
一般用来表示二进制的选项,类似于 C enum ,很多 Cocoa 的 flag 被映射到它。相当于一个 Wrapper 的作用。
可以看到要求被 Wrap 的对象支持 BitwiseOperations
。
图中用虚线标注了和 Generator
的关系。
Array<T>
类型实现了 ArrayType
协议。
Dictionary
类型实现了 Collection
协议。
包括 Mirror
、MirrorDisposition
、Reflectable
。
请参考 Swift 的反射。
只有一个 FloatingPointNumber
。单独存在。是为了定义完整而存在。看官自己搞定。
protocol Streamable {
func writeTo<Target : OutputStream>(inout target: Target)
}
Streamable
表示可以被写入到输出流中,比如字符串、字符等。
protocol OutputStream {
func write(string: String)
}
OutputStream
表示一个输出流,比如标准输出(stdout
),也可以表示一个伪输出流,例如字符串 String
。
标准输出的获取方法
var stdout = _Stdout()
看起来是私有结构,某一天不能用的话,别怪我。调用时候用 inout
引用语法。
protocol CVarArg {
func encode() -> Word[]
}
用于处理 C 函数的可变参数,参考 简析 Swift 和 C 的交互,Part 二。
这里有个疑问就是编译过程中这些 Bridge 协议有没有参与。目前还没办法确定。
_BridgedToObjectiveC
_ConditionallyBridgedToObjectiveC
具体内容可以参考 Swift 与 Objective-C 之间的交互一文。
Sink
看起来是一个容器,可能是用来编码时使用。
ArrayBufferType
用于表示 ArrayType
的内部储存,看起来似乎也可以直接用。
UnicodeCodec
用于处理编码。有 UTF8
、UTF16
、UTF32
可用。
ArrayBound
用来处理数组边界,详细原理和作用过程未知。
无。
参考:
]]>纯个人观点。
一开始大家还不太了解的时候,可能会有很多误解。现在好歹一个月了。误解终于少了。
是的, Swift 是系统编程语言,原因是因为它 ABI 兼容 C (不包括 name mangling 部分)。基于强大的 llvm 生成具体平台代码。不是翻译为 Objective-C 的。
编译器参数还显示, Swift 文件的中间编译结果(介于 Swift 代码和 llvm ir )是 SIL ,猜测是 Swift Intermediate Language 。好像和 llvm ir 有所联系。而且至少有两个 stage 。
不是脚本语言,也不是胶水语言。但是它的标准库 (import Swift 库) 几乎不含任何 IO 网络 等内容,随便做个功能强依赖 Cocoa 框架。也可以 import Darwin
用 C 语言的标准库来写。
猜测写个 Python C 模块这种任务是可以轻易胜任的。
而 Golang 、 Rust 本身 ABI 是和 C 不兼容的。虽然 Rust 通过 extern "C"
可以修改单个函数为兼容。
自动类型推导、泛型、 LLVM 。当然语言研究党都知道这些都是几十年前的“新东西”。
这么说主要是指 Swift 对 Cocoa 的库实在是太半吊子了。只是 Foundation 有 Bridge 支持,其他库中,明显的列表都无法支持 subscript 、 for-in 这样简单的操作。原因很简单,这些库都是自动转换 ObjC 头文件而来(参考模块那篇文章)。没有额外的封装代码。
所以其实真要用起来,可能会很纠结。或者可以预计很快就有第三方的 Bridge 库给这些类型加上舒服的 Swift 支持。
另外命令行没有静态链接库支持。只能用其他命令拼装。也侧面说明, Apple 希望开发者更多用动态链接库, Framework 。
另外目前的编译器 coredump 、 stackoverflow 太多太多。错哪都不知道。
就对应到 Foundation 类型这个特性太说,太多黑魔法,隐式类型转换、 BridgeToObjectiveC 协议、指针类型转换。
这些隐藏的特性多少都会成为 Swift 的坑。
要知道定义在 ObjC 库的 NSString
参数某些情况下在 Swift 中被转换为 String
。 NSArray
都被转换为 AnyObject[]
。即使有隐式类型转换,某些极端情况下,还是会有编译时错误。
我没做过测试,但就语言特性来说, Swift 是比 ObjC 快的,因为静态类型使得他在编译时就已经知道调用函数的具体位置。而不是 Objective-C 的消息发送、 Selector 机制。
目前来看, Swift 性能略差原因主要是编译器还没足够优化、还有就是 Cocoa 拖了后腿, Cocoa 本身有大量的 AnyObject
返回值。所以实际写 Swift 代码时,多用 as
一定是好习惯。明确类型。
我不知道。至少好像感觉很多培训机构都看到了前途开始疯狂的做视频。
倒是觉得什么时候 Cocoa for Swift 出了才算它完全完成任务。
总觉得 Cocoa 拖后腿,不然放到其他平台也不错。
对了,之前不是在 App 开发领域,这才知道原来这个地盘水很深,太多唯利的培训机构,太多嗷嗷待哺等视频教程的新人。觉得挺有意思。就拿 ? ! 这个 Optional 为例,太多介绍的文章。可惜能说明白的太少太少。糊里糊涂做开发就是当前现状吧。
]]>之前说那是最后一篇。可惜越来越发现有很多东西还没介绍到。事不过三。再坑一篇。
本文解决如下问题
补充之前没解决的一些问题,比如提到 CMutablePointer
的 sizeof
是两个字长,那么在函数调用中是如何对应到 C 的指针的?
预备内容:
以下内容均适合 Objective-C 。第一部分适合 C 。
以下内容再 Xcode6-beta3 中不适用 请参考 Swift 在 Xcode6-beta3 中的变化。
函数、枚举、结构体、常量定义、宏定义。
结构体定义支持:
typedef struct Name {...} Name;
typedef struct Name_t {...} Name;
struct Name { ... };
其中无法处理的结构体、函数类型、 varargs 定义不导出。预计以后版本会修复。带 bit field 的结构体也无法识别。
仔细分析发现,诡异情况还很多。基础类型请参考上几篇。
在函数定义参数中:
类型 | 对应为 |
---|---|
void * |
CMutableVoidPointer |
Type * 、Type[] |
CMutablePointer<Type> |
const char * |
CString |
const Type * |
CConstPointer<Type> |
const void * |
CConstVoidPointer |
在函数返回、结构体字段中:
类型 | 对应为 |
---|---|
const char * |
CString |
Type * 、const Type * |
UnsafePointer<Type> |
void * 、const void * |
COpaquePointer |
无法识别的结构指针 | COpaquePointer |
另外还有如下情况:
全局变量、全局常量(const
)、宏定义常量(#define
) 均使用 var
,常量不带 set
。
结构体中的数组,对应为元祖,例如 int data[2]
对应为 (CInt, CInt)
,所以也许。。会很长。数组有多少元素就是几元祖。
ObjC 明显情况要好的多,官方文档也很详细。
除了 NSError **
转为 NSErrorPointer
外,需要注意的就是:
函数参数、返回中的 NSString *
被替换为 String!
、NSArray *
被替换为 AnyObject[]!
。
而全局变量、常量的 NSString *
不变。
CMutablePointer
的行为上回说到 CMutablePointer
、CConstPointer
、CMutableVoidPointer
、CConstVoidPointer
四个指针类型的字长是 2,也就是说,不可以直接对应为 C 中的指针。但是前面说类型对应关系的时候, C 函数声明转为 Swift
时候又用到了这些类型,所以看起来自相矛盾。仔细分析了 lldb 反汇编代码后发现,有如下隐藏行为:
在纯 Swift 环境下,函数定义等等、这些类型字长都为 2,不会有任何意外情况出现。
当一个函数的声明是由 Bridge Header 或者 LLVM Module 隐式转换而来,且用到了这四个指针类型,那么代码编译过程中类型转换规则、隐式转换调用等规则依然有效。只不过在代码最生成一步,会插入以下私有函数调用之一:
@transparent func _convertCMutablePointerToUnsafePointer<T>(p: CMutablePointer<T>) -> UnsafePointer<T>
@transparent func _convertCConstPointerToUnsafePointer<T>(p: CConstPointer<T>) -> UnsafePointer<T>
@transparent func _convertCMutableVoidPointerToCOpaquePointer(p: CMutableVoidPointer) -> COpaquePointer
@transparent func _convertCConstVoidPointerToCOpaquePointer(p: CConstVoidPointer) -> COpaquePointer
这个过程是背后隐藏的。然后将转换的结果传参给对应的 C/ObjC 函数。实现了指针类型字长正确、一致。
作为程序员,需要保证调用 C 函数的时候类型一致。如果有特殊需求重新声明了对应的 C 函数,那么以上规则不起作用,所以重声明 C 中的函数时表示指针不可以使用这四个指针类型。
虚线表示直接隐式类型转换。其中 UnsafePointer<T>
可以通过用其他任何指针调用构造函数获得。
CMutablePointer<T>
和 CMutableVoidPointer
也可以通过 Array<T>
的引用隐式类型转换获得 ( &arr
)。
椭圆表示类型 sizeof
为字长,可以用于声明 C 函数。
四大指针可以用 withUnsafePointer
操作。转换为 UnsafePointer<T>
。上一节提到的私有转换函数请不要使用。
之前的文章已经介绍过怎么从 CString
获取 String
(静态方法 String.fromCString
)。
从 String
获取 CString
也说过, 是用 withCString
。
也可以从 CString(UnsafePointer.alloc(100))
来分配空数组。
本文挖掘 Swift 标准库中的诡异操作符 ~>
波浪箭头的作用。
查看标准库定义的时候,发现了一个奇怪的运算符 ~>
,看起来高大上,所以探索下它到底起什么作用。
标准库对 ~>
用到的地方很多,我取最简单的一个来做说明。
protocol SignedNumber : _SignedNumber {
func -(x: Self) -> Self
func ~>(_: Self, _: (_Abs, ())) -> Self
}
func ~><T : _SignedNumber>(x: T, _: (_Abs, ())) -> T
func abs(_: CInt) -> CInt
func abs<T : SignedNumber>(x: T) -> T
这是对有符号整型的一个协议,我去掉了额外的属性。事实上 _Abs
类型是一个空结构, sizeof
为 0 。
写个测试程序,计算下 abs(-100)
看看情况,发现 top_level_code()
调用了 SignedNumber
版本的 abs()
:
callq 0x100001410 ; Swift.abs <A : Swift.SignedNumber>(A) -> A
反汇编这个库函数,发现一个有意思的调用:
callq 0x10000302a ; symbol stub for: Swift._abs <A>(A) -> (Swift._Abs, A)
这个 _abs()
函数是私有函数, Swift 中把很多私有的函数、成员变量、结构、协议都以下划线开头,意思就是不希望我们去调用或者访问的函数,在缺乏成员访问控制的语言中,其实这么做也不错。大家可以借鉴。
而 _abs()
函数很简单,将任意类型 T 直接封装成 (_Abs, T)
元组,返回。
然后代码的逻辑就是用这个元祖解开重新组装,调用 ~>
。逻辑如下:
// logic of abs() funciton
let (operation, val) = _abs(-100)
val ~> (operation, ()) // 返回 100
到这里就清楚了。实际上 ~>
将一个简单的操作复杂化。多调用了层,实际开销主要在元祖的解开和重组装(实际开销理论上在优化模式下应该可以忽略,因为包含 _Abs
, size 为 0)。
到这里很多朋友应该已经知道怎么回事了。 SignedNumber
中的 ~>
操作是为我们提供了一个方法可以 hook 到标准库的 abs()
函数。来自 Haskell 的同学应该会见过这种单纯地用类型签名来实现函数分发调用的方式。
暂时正在考虑。想明白会发出来。
其实很多标准库函数都用到了类似的方法实现。都用到了 ~>
运算符。包括:
countElements()
// _countElements() 工具函数 _CountElements 结构
underestimateCount()
// _underestimateCount() 、 _UnderestimateCount
advance()
// _advance() 、 _Advance
等。
这里列出部分定义:
protocol Sequence : _Sequence_ {
typealias GeneratorType : Generator
func generate() -> GeneratorType
func ~>(_: Self, _: (_UnderestimateCount, ())) -> Int
func ~><R>(_: Self, _: (_PreprocessingPass, ((Self) -> R))) -> R?
func ~>(_: Self, _: (_CopyToNativeArrayBuffer, ())) -> ContiguousArrayBuffer<Self.GeneratorType.Element>
}
protocol Collection : _Collection, Sequence {
subscript (i: Self.IndexType) -> Self.GeneratorType.Element { get }
func ~>(_: Self, _: (_CountElements, ())) -> Self.IndexType.DistanceType
}
protocol ForwardIndex : _ForwardIndex {
func ~>(start: Self, _: (_Distance, Self)) -> Self.DistanceType
func ~>(start: Self, _: (_Advance, Self.DistanceType)) -> Self
func ~>(start: Self, _: (_Advance, (Self.DistanceType, Self))) -> Self
}
相关更多声明代码信息请参考 我的 Github : andelf/Defines-Swift 。
通过 ~>
和 protocol
可以自定义编译器的行为。相当于 hook 标准库函数。由于内部实现未知,还不能继续断言它还有什么作用。
但是和直接用 extension 实现协议的方法相比,这个有什么好处呢?待考。
可以避免 protocol
中的静态函数混淆空间,如果用全局函数,那么相当于全局函数去调用静态函数。
还有就是在使用操作符的时候,如果定义多个,那么需要编译器去寻找可用的一个版本。
仔细查看目前的 protocol
实现,发现还是有点 BUG ,类型限制还是不清楚,表述高阶类型的时候。
为了描述 ~>
的用法,我写了个 Monad.swift 。
本文的发现基于个人研究。请尊重原创。
本文提出了一种可以编译 Swift 静态链接模块的方法,通过对 swift 编译命令行参数的控制,生成可以自由分发的静态链接库和 swift module 描述文件。同时还提出了导出 objC 头文件供 Objective-C 调用的可能。
关键词: Swift 模块 静态链接库
上次一篇文章 Module System of Swift (简析 Swift 的模块系统) 中提到:
静态链接库 .a 目前还没有找到方法, -Xlinker -static 会报错。
最近摸索了下用 Swift 创建静态链接库的方法。有所收获,这里记录下。
我们中的很多人都知道,编译器编译的最后一个步骤一般都是链接,一般都是调用 ld
。经过仔细分析,之前为什么不能生成 .a
静态链接库的原因,发现有如下问题:
-Xlinker -static
参数传递的时候, swift 命令本身不能识别,讲 -dylib
与 -static
一起传递(这倒不是问题,参数优先级,静态盖掉了动态)-lSystem
时候,这个库没有静态链接。所以总会报错。
实际上之前的方法是走了弯路,根本没有必要去调用 ld
,作为一个合格的 .a
静态链接库,只要有对应的 .o
就可以了,没必要去链接 -lSystem
,也许是 swift 本身没有编译为静态链接库的参数支持。
检查 Swift 标准库中的静态链接库,果然只包含对应 .swift
代码编译后的 .o
文件。(检查方法是用 ar -t libName.a
)
说到底, Swift 静态链接库的目标很简单,就是包含对应 Swift 模块的所有代码,这样就避免了对应动态链接库的引入。和什么 -lSystem
没啥相干。
以 lingoer 的 SwiftyJSON 为例。
我们的目标很简单,就是生成 ModName.swiftmodule
、ModName.swiftdoc
(可选)、libswiftModName.a
三个文件。
.swiftmodule
.swiftdoc
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) SwiftyJSON.swift -emit-library -emit-module -module-name SwiftyJSON -v -o libswiftSwiftyJSON.dylib -module-link-name swiftSwiftyJSON
.o
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) -c SwiftyJSON.swift -parse-as-library -module-name SwiftyJSON -v -o SwiftyJSON.o
.a
ar rvs libswiftSwiftyJSON.a SwiftyJSON.o
大功告成。
同时应该也可以用 lipo
来合成不同平台下的 .a
链接库。
和静态链接库类似,需要 -I
包含 .swiftmodule
所在目录, -L
包含 .a
所在目录。
如果动态链接库和静态链接库两者同时存在,可以依靠不同目录来区分。
可能不少人要群嘲,你这意义是啥。你丫闲的。
其实在分发 library 的时候,很多时候我们需要二进制分发,希望别人可以方便地使用。这种情况下,静态链接更佳(虽然新的 iOS 8 支持动态链接,但是看起来是基于 Framework 的,略复杂些。)
甚至我们可以用 lipo
创建全平台可用的静态链接库。多赞。
多个 Swift 文件可以分别编译为 .o
然后用 ar
合并。
对于 CocoaPods ,也许可以按照这个逻辑将 Swift 模块暴露出去。需要多加一个参数 -emit-objc-header
(以及 -emit-objc-header-path
)即可。
本文的发现基于个人研究。请尊重原创。已授权 CocoaChina 转载个人文章。
本文介绍如何在 Swift 项目中使用 CocoaPods 。如果你已经精通 Bridging Header 的方法,请直接跳到 “扩展 CocoaPods” 一节。
CocoaPods is the dependency manager for Objective-C projects. It has thousands of libraries and can help you scale your projects elegantly. 1
从介绍看,它是主要给 Objective-C 项目用的,但是我们可以很容易地混合 Objective-C 和 Swift 到同个项目,从而利用大量的 CocoaPods 库和 Swift 漂亮舒服的语法。
作为 iOS 开发新手,一定是要紧跟前人脚步,学习使用 CocoaPods 。
这里简单略过,请参考其他无数的文章。
系统默认安装,可以参考其他教程2 。在命令行下执行。
sudo gem install cocoapods
我的环境是 HomeBrew
# 添加 taobao Mirror 不然被墙掉没办法下载
gem sources -a http://ruby.taobao.org/
# 安装
gem install cocoapods
# 更新命令
rbenv rehash
# 执行
pod
# 此时一般会下载官方的所有 PodSpec 库,也可以用 pod setup 初始化环境
本文不打算在安装部分耗费太多时间。希望看到这里保证你的命令行下有可用的 pod
命令。
假设我们已经有个项目,叫 ProjName ,需要使用一些注明的 CocoaPods 库,比如 AFNetworking3.
首先,命令行 cd 到我们的项目目录,一般 ls 命令会看到如下几个文件夹:
ProjName
ProjName.xcodeproj
ProjNameTests
赞,就是这里,创建一个 Podfile
文本文件,写入如下内容
platform :ios, "8.0"
pod "AFNetworking", "~> 2.0"
一般这么简单的文件都是直接 nano 写。 :)
直接创建 Podfile
, CocoaPods 会创建一个项目同名的 WorkSpace ,然后添加一个叫 Pods 的项目,这个项目编译结果是一个叫 libPods.a
的链接库,
它会添加到我们之前的 ProjName 项目中作为编译依赖。
当然,通过命令行执行 pod init
也可以自动创建 Podfile
,而且可以自动分析当前项目的 target ,相对来说更好,也更优雅。具体请参考官方手册。这样的好处是更细致,还可以区分多个子项目子 target 。原理大同小异。
然后接下来,命令行执行 open ProjName.xcworkspace
,注意这个可不是 .xcodeproj
,这个是 CocoaPods 为我们创建的一个 WorkSpace ,包含我们之前的项目,和 Pods 依赖。
开始编码过程。直接在代码里调用,比如写在某个按钮的 @IBAction
里:
let manager = AFHTTPRequestOperationManager()
let url = "http://api.openweathermap.org/data/2.5/weather"
println(url)
let params = ["lat": 39.26, "lon": 41.03, "cnt":0]
println(params)
manager.GET(url,
parameters: params,
success: { (operation: AFHTTPRequestOperation!,
responseObject: AnyObject!) in
println("JSON: " + responseObject.description!)
},
failure: { (operation: AFHTTPRequestOperation!,
error: NSError!) in
println("Error: " + error.localizedDescription)
})
这里直接抄了 JakeLin 的 SwiftWeather 代码4,就一小段,希望他不会打我。
看起来貌似我们已经可以在 Swift 中使用 AFNetworking
了。结果刚写几句代码一堆类和变量找不到定义,而且坑爹的是很多时候我们只能靠猜测,判断这些 Objective-C 的定义转换成 Swift 定义是什么样子,用起来就是完全靠蒙!
这不科学!
这都三礼拜了,所以大家都摸索出了调用的方法,那就是按照和 Objective-C 代码混编的例子,添加 Bridging Header !
之前简单介绍过和 Objective-C 交互的内容5,大家可以去围观。
一般说来,你在 Swift 项目新建 Objective-C 类的时候,直接弹出是否创建 Bridge Header 的窗口,点 YES 就是了,这时候一般多出来个 ProjectName-Bridging-Header.h
。然后删掉这个类, Bridging Header 头文件还在。
在这个 Bridging Header 文件里写入要导入的 CocoaPods 库,就可以在 Swift 中使用了。
#import <AFNetworking/AFNetworking.h>
如果没有自动创建头文件的话,这个配置在项目的 Build Settings 中的 Swift Compiler - Code Generation 子项里。
创建一个头文件,指定为 Bridging Header 也可以。
然后编译,成功执行!
实际上,前两天刚写一篇 Swift 的模块系统 , 把任意 Objective-C 库当做 Swift Module 是可行的。当时就觉得这个东西应该是可能完全进入 CocoaPods 的,但是在官方 repo 找了下发现,以前有人提过增加 module.map
支持,结果 CocoaPods 的人认为这个是 llvm 内部特性, issue 被关闭了。#2216 最近又被提起,我在后面提了下 Swift 支持,希望官方靠谱。
所以下面的内容,就是,我们是否可以在 CocoaPods 上加入 module.map
支持,然后直接在 Swift 中 import ModuleName
?
考虑了多种方式,最后选择了 Hook 的方式。如果 Ruby 技术足够好,或许可以直接写个插件。或者直接改官方代码给官方提交。但是实在能力有限。相关的 module.map
语法参考 llvm 官方手册 Modules – Clang 3.5 documentation。用了最简单的功能。也许遇到复杂的 PodSpec 就不起作用了,但是原理如此,相信小伙伴们已经知道怎么做了。
目前我的 Podfile
大概是这个样子:
platform :ios, "8.0"
pod "AFNetworking", "~> 2.0"
pod "Baidu-Maps-iOS-SDK", "~> 2.0"
post_install do |installer|
File.open("#{installer.sandbox_root}/Headers/module.map", 'w') do |fp|
installer.pods.each do |pod|
normalized_pod_name = pod.name.gsub('-', '')
fp.write <<EOF
module #{normalized_pod_name} [system] {
umbrella "#{pod.name}"
export *
}
EOF
puts "Generating Swift Module #{normalized_pod_name.green} for #{pod} OK!"
end
end
end
post_install
是 Podfile
的一种 hook 机制,可以用来加入自定义操作。我在这里的写的逻辑就是,针对所有的 Pod 生成一个 module.map
文件。
位于 Pods/Headers/
,这个目录被 CocoaPods 自动设置为项目的 Header Search Path 所以不需要额外处理。默认我们的 Swift 文件就找得到。
其中 normalized_pod_name
用于处理百度地图 API SDK 这一类名字带减号的库,因为他们不能作为 Module Name ,实际上或许有更好的方法来处理。
实测发现完全没有问题,直接 import AFNetworking
或者 import BaiduMapsiOSSDK
都可以。
而且很不错的一点是,按住 Command 键,然后鼠标点击模块名、类名等,会跳转到 Swift 定义。
遇到提示 .pcm
文件 outdate 的情况下需要你删除 $HOME/Library/Developer/Xcode/DerivedData/ModuleCache
目录,这个目录保存的是预编译模块,类似于预编译头文件。
目前 Swift 还是有很多 BUG 的,调用 NSObject
也许会让编译器直接 segment fault ,不带任何出错信息。很伤情。此时请第一时间检查语法是否有诡异,其次将所有用到字符串或者 Optional
的地方都额外用变量处理,避免用字面常量。(个人经验)
如果多次调用 pod install
并在其中修改过 Podfile
,那么有可能你的项目依赖会乱掉,多了不存在的 .a
文件到依赖或者多次包含。手工在项目树和项目选项里删除就可以了。此类编译错误都是链接错误。
本文提出了一种 Bridging Header 之外的使用 CocoaPods 库的方法。利用有限的 Ruby 知识写了个 Hook 。目前测试 OK 。
CocoaPods Offical Site CocoaPods 官网 ↩
CocoaPods - CocoaChina CocoaChina 对 CocoaPods 的介绍 ↩
The metatype of a class, structure, or enumeration type is the name of that type followed by .Type. The metatype of a protocol type—not the concrete type that conforms to the protocol at runtime—is the name of that protocol followed by .Protocol. For example, the metatype of the class type SomeClass is SomeClass.Type and the metatype of the protocol SomeProtocol is SomeProtocol.Protocol.
You can use the postfix self expression to access a type as a value. For example, SomeClass.self returns SomeClass itself, not an instance of SomeClass. And SomeProtocol.self returns SomeProtocol itself, not an instance of a type that conforms to SomeProtocol at runtime.
metatype-type -> type .Type |
type .Protocol |
type
.self其中 metatype-type 出现在代码中需要类型的地方, type-as-value 出现在代码中需要值、变量的地方。
Any.Type
类型大家可以猜下它表示什么。
反射信息用 Mirror
类型表示,类型协议是 Reflectable
,但实际看起来 Reflectable
没有任何作用。
protocol Reflectable {
func getMirror() -> Mirror
}
protocol Mirror {
var value: Any { get }
var valueType: Any.Type { get }
var objectIdentifier: ObjectIdentifier? { get }
var count: Int { get }
subscript (i: Int) -> (String, Mirror) { get }
var summary: String { get }
var quickLookObject: QuickLookObject? { get }
var disposition: MirrorDisposition { get }
}
实际上所有类型都实现了 Reflectable
。
Mirror
协议相关字段:
value
相当于变量的 as Any
操作valueType
获得变量类型objectIdentifier
相当于一个 UInt 作用未知,可能是 metadata 表用到count
子项目个数(可以是类、结构体的成员变量,也可以是字典,数组的数据)subscript(Int)
访问子项目, 和子项目的名字summary
相当于 description
quickLookObject
是一个枚举,这个在 WWDC 有讲到,就是 Playground 代码右边栏的显示内容,比如常见类型,颜色,视图都可以disposition
表示变量类型的性质,基础类型 or 结构 or 类 or 枚举 or 索引对象 or … 如下enum MirrorDisposition {
case Struct // 结构体
case Class // 类
case Enum // 枚举
case Tuple // 元组
case Aggregate // 基础类型
case IndexContainer // 索引对象
case KeyContainer // 键-值对象
case MembershipContainer // 未知
case Container // 未知
case Optional // Type?
var hashValue: Int { get }
}
通过函数 func reflect<T>(x: T) -> Mirror
可以获得反射对象 Mirror
。它定义在 Any
上,所有类型均可用。
.valueType
处理Any.Type
是所有类型的元类型,所以 .valueType
属性表示类型。实际使用的时候还真是有点诡异:
let mir = reflect(someVal)
swith mir.valueType {
case _ as String.Type:
println("type = string")
case _ as Range<Int>.Type:
println("type = range of int")
case _ as Dictionary<Int, Int>.Type:
println("type = dict of int")
case _ as Point.Type:
println("type = a point struct")
default:
println("unkown type")
}
或者使用 is
判断:
if mir.valueType is String.Type {
println("!!!type => String")
}
勘误: 这里之前笔误了。遗漏了 .valueType
。
is String
判断变量是否是 String 类型,而 is String.Type
这里用来判断类型是否是 String 类型。
subscript(Int)
处理实测发现直接用 mir[0]
访问偶尔会出错,也许是 beta 的原因。
for r in 0..mir.count {
let (name, subref) = mir[r]
prtln("name: \(name)")
// visit sub Mirror here
}
通过上面的方法,基本上可以遍历大部分结构。
.count
为字段个数。subscript(Int)
返回 (字段名,字段值反射 Mirror
) 元组summary
为 mangled name.count
为元组子元素个数subscript(Int)
的 name 为 “.0”, “.1” …包括数字、字符串(含 NSString
)、函数、部分 Foundation 类型、 MetaType 。
很奇怪一点是测试发现枚举也被反射为基础类型。怀疑是没实现完。
.count
为 0包括 Array<T>
, T[]
, NSArray
等。可以通过 subscript 访问。
.count
为元组子元素个数subscript(Int)
的 name 为 “[0]”, “[1]” …包括 Dictionary<T, U>
、NSDictionary
.count
为元组子元素个数subscript(Int)
的 name 为 “[0]”, “[1]” … 实际访问是 (name, (reflect(key), reflect(val)))
只包括 Type?
,不包括 Type!
。
.count
为 0 或者 1 (对应 nil 和有值的情况)subscript(Int)
, name 为 “Some”本文的发现基于个人研究。请尊重原创。
你之所以认为 Swift 最像 Scala, 那是因为你还没学过 Rust. —- 猫·仁波切
Swift 中模块是什么?当写下 Swift 中一句 import Cocoa
的时候到底整了个什么玩意?官方 ibook 很含糊只是提了半页不到。
本文解决如下问题
Clang 模块是来自系统底层的模块,一般是 C/ObjC 的头文件。原始 API 通过它们暴露给 Swift ,编译时需要链接到对应的 Library。
例如 UIKit
、Foundation
模块,从这些模块 dump 出的定义来看,几乎是完全自动生成的。当然, Foundation
模块更像是自动生成 + 人工扩展(我是说其中的隐式类型转换定义、对 Swift 对象的扩展等,以及 @availability
禁用掉部分函数。)。相关函数声明可以从 我的 Github andelf/Defines-Swift 获得。
我可不觉得这些定义全部都是官方生成后给封装进去的。所以在整个 Xcode-6 beta2 目录树里进行了探索。
在 Xcode 目录寻找相关信息,最后目标锁定到了一个特殊的文件名 module.map
。
原来这个文件叫 Module map(这个名字还真是缺乏想象力),属于 llvm 的 Module 系统。本来是用来颠覆传统的 C/C++/Objc 中的 #include
和 #import
。最早在 2012 年 11 月的 LLVM DevMeeting 中由 Apple 的 Doug Gregor 提出 1。相关内容 CSDN 也有文章介绍,不过是直译版,没有提出自己见解 2。
2012 年提出概念,所以其实这个东西已经很早就实现了 。简单说就是用树形的结构化描述来取代以往的平坦式 #include
, 例如传统的 #include <stdio.h>
现在变成了 import std.io;
, 逼格更高。主要好处有:
所以这么好的一个东西, Apple 作为 llvm 的主力,在它的下一代语言中采用几乎是一定的。
算了,我是个半路出家的,之前没接触过 iOS / MacOSX 开发,其实 2013 年的 WWDC, Apple 为 Objective-C 加入的 @import
语法就是它。可以认为,这是第一次这个 Module 系统得到应用。
module.map
文件就是对一个框架,一个库的所有头文件的结构化描述。通过这个描述,桥接了新语言特性和老的头文件。默认文件名是 module.modulemap
,module.map
其实是为了兼容老标准,不过现在 Xcode 里的还都是这个文件名,相信以后会改成新名字。
文件的内容以 Module Map Language
描述,大概语法我从 llvm 官方文档 3 摘录一段,大家体会一下:
module MyLib {
explicit module A {
header "A.h"
export *
}
explicit module B {
header "B.h"
export *
}
}
类似上面的语法,描述了 MyLib
、MyLib.A
、MyLib.B
这样的模块结构。
官方文档 3 中有更多相关内容,可以描述框架,描述系统头文件,控制导出的范围,描述依赖关系,链接参数等等。这里不多叙述,举个 libcurl 的例子:
module curl [system] [extern_c] {
header "/usr/include/curl/curl.h"
link "curl"
export *
}
将此 module.map
文件放入任意文件夹,通过 Xcode 选项或者命令行参数,添加路径到 import search path (swift 的 -I 参数)。
然后就可以在 Swift 代码里直接通过 import curl
导入所有的接口函数、结构体、常量等,(实测,发现 curl_easy_setopt
无法自动导入,看起来是声明语法太复杂导致)。甚至可以直接从 swift repl 调用,体验脚本语言解释器般的快感(因为我们已经指定了链接到 curl 库)。
Xcode 选项位于 Build Settings 下面的 Swift Compiler - Search Paths 。添加路劲即可。
再举个复杂点的 SDL2.framework
的例子,看看如何实现树形的模块结构,这个需要把 module.map
放到 .framework
目录里
framework module SDL2 [system] {
umbrella header "SDL.h"
link -framework SDL2
module Version {
header "SDL_version.h"
export *
}
module Event {
header "SDL_events.h"
export *
}
// ....
export *
module * {
export *
}
}
Swift 的 C 模块(也是它的标准库部分)完全就是 llvm 的 Module 系统,在 import search path 的所有 module.map 中的模块都可以被识别,唯一缺点可能是如果有过于复杂用到太多高级 C 或者黑暗 C 语法的函数,无法很好识别,相信以后的版本会有所改善。
所以当有人问 Swift 到底有多少标准库的时候,答案就是,基本上系统里所有的 Objective-C 和 C 头文件都可以调用。自 iOS 7 时代,这些头文件就已经被组织为 Module 了,包括标准 C 库 Darwin.C
。同样因为 Module 系统来自于传统的 C/C++/Objc 头文件,所以 Swift 虽然可以有 import ModA.ModB.ModC
的语句,但是整个模块函数名字空间还是平坦的。
一些有意思的模块可以探索探索,比如 simd
,比如 Python
(没错是的,直接调用 Python 解释器)等。
另外 Swift 的 -module-cache-path
参数可以控制这类模块预编译头的存放位置( .pcm 文件: pre compiled module)。
Xcode 项目的 Build Settings , Apple LLVM 6.0 - Language - Modules 有项目对 Module 支持的相关选项,默认是打开的。
说完了系统模块,该说 Swift 模块了。 Swift 自身的这个系统还是很赞的。
本节介绍怎样用 Swift 创建一个可 import 的模块。
先清楚几个文件类型。假设 ModName.swift
是我们的 Swift 源码文件。
ModName.swiftmodule
Swift 的模块文件,有了它,才能 importModName.swiftdoc
保存了从源码获得的文档注释
///
开头libswiftModName.dylib
动态链接库libswiftModName.a
静态链接库TODO: 目前有个疑问就是 .swiftmodule
和链接库到底什么时候用哪个,以及具体作用。
先明确一个概念,一个 .swift 文件执行是从它的第一条非声明语句(表达式、控制结构)开始的,同时包括声明中的赋值部分(对应为 mov 指令或者 lea 指令),所有这些语句,构成了该 .swift 文件的 top_level_code()
函数。
而所有的声明,包括结构体、类、枚举及其方法,都不属于 top_level_code()
代码部分,其中的代码逻辑,包含在其他区域,top_level_code()
可以直接调用他们。
程序的入口是隐含的一个 main(argc, argv)
函数,该函数执行逻辑是设置全局变量 C_ARGC
C_ARGV
,然后调用 top_level_code()
。
不是所有的 .swift 文件都可以作为模块,目前看,任何包含表达式语句和控制控制的 .swift 文件都不可以作为模块。正常情况下模块可以包含全局变量(var
)、全局常量(let
)、结构体(struct
)、类(class
)、枚举(enum
)、协议(protocol
)、扩展(extension
)、函数(func)、以及全局属性(var { get set }
)。这里的全局,指的是定义在 top level 。
这里说的表达式指 expression ,语句指 statement ,声明指 declaration 。可能和有些人对相关概念的定义不同。实际上我特无奈有些人纠结于概念问题,而不是问题本身,本来翻译过来的舶来品就有可能有误差,当你明白那指的是什么的时候,就可以了。
这里先以命令行操作为例,
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) ModName.swift -emit-library -emit-module -module-name ModName -v -o libswiftModName.dylib -module-link-name swiftModName
执行后获得 ModName.swiftdoc
、ModName.swiftmodule
、libswiftModName.dylib
.
这三个文件就可以表示一个可 import 的 Swift 模块。目前看起来 dylib 是必须得有的,否则链接过程报错。实际感觉 .swiftmodule
文件所包含的信息还需要继续挖掘挖掘。
多个源码文件直接依次传递所有文件名即可。
静态链接库 .a
目前还没有找到方法, -Xlinker -static
会报错。
相关命令行参数:
-module-name <value>
Name of the module to build 模块名-emit-library
编译为链接库文件-emit-module-path <path>
Emit an importable module to -emit-module
Emit an importable module-module-link-name <value>
Library to link against when using this module 该模块的链接库名,就是 libswiftModName.dylib
,这个信息会直接写入到 .swiftmodule
使用模块就很简单了,记住两个参数:
-I
表示 import search path ,前面介绍过,保证 .swiftmodule
文件可以在 import search path 找到(这点很类似 module.map 文件,找得到这个就可以 import 可以编译)
-L
表示 链接库搜索路径,保证 .dylib
文件可以在其中找到,如果已经在系统链接库目录中,就不需要这个参数。
例如:
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) mymodtest.swift -I. -L.
此时表示所有 module 文件都在当前目录。
这两个选项都可以在 Xcode 中指定,所以如果你有小伙伴编译好的 module 想在你的项目里用是完全 ok 的。
很不幸,没能在 Xcode 中找到编译模块的相关方法。等我发现如何搞定的时候我会补上这个坑。
不过在任何含 Swift 项目的编译过程中, .swiftmodule
文件总是伴随着 .o
文件传递。
简单分析下一个 .swiftmodule 所包含的信息。
这里先以标准库的 Foundation.swiftmodule
下手。
用 hexdump 查看发现它包含所有导出符号,以及 mangled name 。还有个文件列表,表示它是从哪些文件获得的(可以是 .swift 也可以是 .swiftmodule )。
用 strings 列出内容,发现 Foundation 库有如下特征:
...
Foundation
LLVM 3.5svn
/SourceCache/compiler_KLONDIKE/compiler_KLONDIKE-600.0.34.4.8/src/tools/swift/stdlib/objc/Foundation/Foundation.swift
/SourceCache/compiler_KLONDIKE/compiler_KLONDIKE-600.0.34.4.8/src/tools/swift/stdlib/objc/Foundation/KVO.swift
/SourceCache/compiler_KLONDIKE/compiler_KLONDIKE-600.0.34.4.8/src/tools/swift/stdlib/objc/Foundation/NSStringAPI.swift
CoreFoundation
Foundation
Swift
swiftFoundation
...
可以大胆猜测对应下:
-module-name
=> Foundation
CoreFoundation
, Foundation
, Swift
-module-link-name
=> swiftFoundation
我由此猜测, Foundation
的确是只有少量 Swift 代码做桥接。然后通过 Clang 模块将剩下工作交到底层。
分析其他类似模块也得到相同结果。
接下来有点好奇标准库 Swift 是怎么实现的。得到如下结果。
节选重要部分到 我的 Gist
里面有些很有意思的信息,有兴趣的同学可以去看看。
依赖模块 SwiftShims 是一个 module.map
定义的模块,桥接的部分头文件。源文件有相关信息和注释。大致意思是用来实现几个底层接口对象,比如 NSRange
邓。
其中-module-link-name
是 swift_stdlib_core
。
LLVM Module 作为 Apple 提出的特性,已经被 Swift 完全采用,直接在它基础上建立了自己的模块系统。我相信它会影响到我们处理第三方库的方式方法。相信不久就会有相关工具基于它来管理依赖关系,比如老的 cocoapods4 可以加入新特性。
用 Swift 写模块目前并没有很好的 IDE 支持,所以不是很方便。基于猜测验证,上面的方法可以实现在 Swift 里 import Swift 模块,方法和结果看起来完全和官方模块相同。
Swift 的标准库完全是上面两种模块的结合体,用 Swift 模块封装 Clang 模块。这就解决了文章一开始提出的问题:为什么标准库大部分看起来是自动生成代码,少部分又好像是人工写的接口代码。
本文的发现基于个人研究,目测是官方外的首创。请尊重原创。
本文是讲述 Swift 与 C 交互操作系列文章的第二部分。解决之前的所有遗留问题。
第一部分请参考 Swift and C Interop 简析Swift和C的交互
本文将介绍实际应用过程中会遇到的各类情况。
标准类型这里就不提了,上面的文章讲的很明白了。
从代码看,我认为 Swift 对应 C 的指针时候,存在一个最原始的类型 RawPointer
,但是它是内部表示,不可以直接使用。所以略过。但它是基础,可以认为它相当于 Word
类型(机器字长)。
以下内容再 Xcode6-beta3 中不适用 请参考 Swift 在 Xcode6-beta3 中的变化。
不透明指针。之前我以为它很少会用到,不过现在看来想错了,虽然类型不安全,但是很多场合只能用它。它是直接对应 RawPointer
的。字长相等。
In computer programming, an opaque pointer is a special case of an opaque data type, a datatype declared to be a pointer to a record or data structure of some unspecified type. - 来自 Wikipedia
几乎没有任何操作方法,不带类型,主要用于 Bridging Header 中表示 C 中的复杂结构指针
比如一个例子, libcurl 中的 CURL *
的处理,其实就是对应为 COpaquePointer
。
泛型指针。直接对应 RawPointer
。字长相等。
处理指针的主力类型。常量中的 C_ARGV
的类型也是它 UnsafePointer<CString>
。
支持大量操作方法:
.memory
属性 { get set }
操作指针指向的内容C_ARGV[1]
alloc(num: Int)
分配数组空间initialize(val: T)
直接初始化.succ()
.pred()
COpaquePointer
之外的任意一种指针之前特地写文介绍过这个指针类型。NSError
的处理就主要用它。传送门: Swift NSError Internals(解析 Swift 对 NSError 操作)
内部实现用了语言内置特性,从名字也可以看出来,这个应该是非常棒的一个指针,可以帮助管理内存,逼格也高。内存直接对应 RawPointer
可以传递给 C 函数。
.memory
属性 { get set }
操作指针指向的内容&T
类型获得,使用方法比较诡异,建议参考文章分别对应于 C 中的 T *
、const T *
。不可直接传递给 C 函数,因为表示结构里还有一个 owner
域,应该是用来自动管理生命周期的。sizeof
操作返回 16。但是可以有隐式类型转换。
操作方法主要是 func withUnsafePointer<U>(f: UnsafePointer<T> -> U) -> U
,用 Trailing Closure 语法非常方便。
分别对应于 C 中的 void *
、const void *
。其他内容同上一种。
以上 7 种指针类型可以分未两类,我给他们起名为 第一类指针 和 第二类指针 。(你看我在黑马克思耶,算了这个梗太深,参考马克思主义政治经济学)
COpaquePointer
UnsafePointer<T>
AutoreleasingUnsafePointer<T>
RawPointer
的封装,直接对应于 C 的指针,它们的 sizeof
都是单位字长CMutablePointer<T> CConstPointer<T> CMutableVoidPointer CConstVoidPointer
owner
字段,可以管理生命周期,理论上在 Swift 中使用.withUnsafePointer
方法调用所有指针都实现了 LogicValue
协议,可以直接 if a_pointer
判断是否为 NULL
。
nil
类型实现了到所有指针类型的隐式类型转换,等价于 C 中的 ``NULL`,可以直接判断。
什么时候用什么?这个问题我也在考虑中,以下是我的建议。
COpaquePointer
例如 CURL *
UnsafePointer<T>
AutoreleasingUnsafePointer<T>
用于处理 C 语言中的可变参数 valist 函数。
protocol CVarArg {
func encode() -> Word[]
}
表示该类型可以作为可变参数,相当多的类型都实现了这个。
struct CVaListPointer {
var value: UnsafePointer<Void>
init(fromUnsafePointer from: UnsafePointer<Void>)
@conversion func __conversion() -> CMutableVoidPointer
}
对应于 C,直接给 C 函数传递,声明、定义时使用。
class VaListBuilder {
init()
func append(arg: CVarArg)
func va_list() -> CVaListPointer
}
工具类,方便地创建 CVaListPointer
。
还有一些工具函数:
func getVaList(args: CVarArg[]) -> CVaListPointer
func withVaList<R>(args: CVarArg[], f: (CVaListPointer) -> R) -> R
func withVaList<R>(builder: VaListBuilder, f: (CVaListPointer) -> R) -> R
非常方便。
struct UnsafeArray<T> : Collection, Generator {
var startIndex: Int { get }
var endIndex: Int { get }
subscript (i: Int) -> T { get }
init(start: UnsafePointer<T>, length: Int)
func next() -> T?
func generate() -> UnsafeArray<T>
}
处理 C 数组的工具类型,可以直接 for-in 处理。当然,只读的,略可惜。
struct Unmanaged<T> {
var _value: T
init(_private: T)
func fromOpaque(value: COpaquePointer) -> Unmanaged<T>
func toOpaque() -> COpaquePointer
static func passRetained(value: T) -> Unmanaged<T>
static func passUnretained(value: T) -> Unmanaged<T>
func takeUnretainedValue() -> T
func takeRetainedValue() -> T
func retain() -> Unmanaged<T>
func release()
func autorelease() -> Unmanaged<T>
}
顾名思义,手动管理 RC 的。避免 Swift 插入的 ARC 代码影响程序逻辑。
数字常量 CInt, CDouble (带类型后缀则为对应类型,如 1.0f ) 字符常量 CString 其他宏 展开后,无定义
创建 enum 类型,并继承自 CUnsignedInt
或 CInt
(enum 是否有负初始值)
可以通过 .value
访问。
创建 struct 类型,只有默认 init ,需要加上所有结构体字段名创建。
转为 CVaListPointer
。手动重声明更好。这里举 Darwin
模块的例子说。
func vprintf(_: CString, _: CVaListPointer) -> CInt
只能调用函数。
之前说过,用 @asmname("name")
指定 mangled name 即可。
然后 C 语言中人工声明下函数。很可惜自动导出头文件不适用于 C 语言,只适用于 Objective-C 。
目测暂时无法导出结构体,因为 Swift 闭源不提供相关头文件。靠猜有风险。
全局变量不支持用 @asmname("name")
控制导出符号名。目测可以尝试用 mangled name 访问,但是很不方便。
我尝试调用了下 libcurl 。
项目地址在 andelf/curl-swift 包含编译脚本(就一句命令)。
Bridging Header 只写入 #include<curl/curl.h>
即可。
@asmname("curl_easy_setopt") func curl_easy_setopt(curl: COpaquePointer, option: CURLoption, param: CString) -> CURLcode
@asmname("curl_easy_setopt") func curl_easy_setopt(curl: COpaquePointer, option: CURLoption, param: CBool) -> CURLcode
let handle = curl_easy_init()
// this should be a const c string. curl_easy_perform() will use this.
let url: CString = "http://www.baidu.com"
curl_easy_setopt(handle, CURLOPT_URL, url)
curl_easy_setopt(handle, CURLOPT_VERBOSE, true)
let ret = curl_easy_perform(handle)
let error = curl_easy_strerror(ret)
println("error = \(error)")
值得注意的是其中对单个函数的多态声明, curl_easy_setopt
实际上第三个参数是 void *
。
以及对 url
的处理,实际上 libcurl 要求设置的 url 参数一直保持到 curl_easy_perform
时,所以这里用 withUnsafePointer
或者
withCString
是不太可取的方法。实际上或许可以用 Unmanaged<T>
来解决。
我觉得说这么多。。。
调用 C 已经再没有别的内容可说了。其他的就是编程经验的问题,比如如何实现 C 回调 Swift 或者 Swift 回调 C 。可以参考其他语言的做法。解决方法不只一种。