组装好NAS之后,我一直在考虑如何让其实现定时开机与关机,其中后者可以通过cron或者systemd-timer轻松解决,问题是开机,想到的方案主要有2个:
- 在机箱内安装一块Arduino/ESP开发板,配合时钟模块和锂电,定时接通PWR。
- 依靠网卡唤醒,也就是WOL(Wake-On-Lan),通过路由器实现定时唤醒(OpenWRT)。
对于我个人而言,2个方案都可以实现,但后者显然耗时更少,近期忙着毕业,没法抽出大段时间折腾。在编写LuCI插件过程中遇到了一点麻烦,单纯的cbi无法满足需求,所有这部分内容只能滞后了。
WOL很简单,只需要知道以下几点内容即可:
- 采用的是UDP协议
- 需要广播
- 特定的报文结构
WOL报文的组成为:6 * 0xFF + 16 * MAC(6B),我给出了程序源码可以直接编译运行,其中有一半代码是在解析MAC地址,并且程序参数处理部分存在一个bug,我没有fix。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <unistd.h>
#define PORT 53
#define MAC_STRLEN 18
static inline int is_hex(char ch) {
if(ch >= '0' && ch <= '9' ||
ch >= 'a' && ch <= 'f' ||
ch >= 'A' && ch <= 'F') {
return 1;
}
return 0;
}
int parse_mac(const char* mac_str, char buf[]) {
static uint8_t HEX[128] = {
['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4,
['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15,
['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15,
};
if(strlen(mac_str) != 17) {
return 0;
}
const char* p = mac_str;
uint8_t i = 0;
do {
if(p[2] != ':') {
return 0;
}
buf[i++] = (HEX[p[0]] << 4) + HEX[p[1]];
p = mac_str + i * 3;
} while(i < 5);
if(p[2] != '\0') {
return 0;
}
buf[i] = (HEX[p[0]] << 4) + HEX[p[1]];
return 1;
}
void mac_to_string(char* buf, char res[]) {
snprintf(res, MAC_STRLEN, "%02hhX:%02hhX:%02hhX:%02hhX:%02hhX:%02hhX",
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]);
}
int create_socket() {
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s == -1) {
perror("socket");
return -1;
}
int optval = 1;
int ret = setsockopt(s, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(int));
if (ret == -1) {
perror("setsockopt");
return -1;
}
return s;
}
void init_sockaddr(struct sockaddr_in* addr) {
memset(addr, 0, sizeof(struct sockaddr_in));
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = htonl(INADDR_BROADCAST);
addr->sin_port = htons(PORT);
}
int parse_args(int argc, char** argv, char* mac) {
int ch;
while((ch = getopt(argc, argv, "hm:")) != -1) {
switch(ch) {
case 'h':
fprintf(stdout,
"Usage: \n"
" ./wol -h\n"
" ./wol -m XX:XX:XX:XX:XX:XX\n"
);
return -1;
case 'm':
if(parse_mac(optarg, mac) != 1) {
fprintf(stderr, "Invalid MAC!\n");
return -1;
}
break;
default:
fprintf(stderr, "Invalid option!\n");
return -1;
}
}
return 0;
}
int main(int argc, char **argv) {
char mac[6] = {0};
if(parse_args(argc, argv, mac) == -1) {
return -1;
}
printf("Creating Socket...");
int s = create_socket();
if(s == -1) {
return -1;
}
printf("Done\n");
struct sockaddr_in addr;
init_sockaddr(&addr);
printf("Creating Packet...");
char pkt[6 + 16 * 6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
for(uint8_t i = 0; i < 16; ++i) {
memcpy(pkt + 6 + i * 6, mac, 6);
}
printf("Done\n");
printf("Sending Packet...");
int len = sendto(s, pkt, sizeof(pkt), 0, (struct sockaddr*)&addr, sizeof(addr));
if (len == -1) {
perror("sendto");
return -1;
}
printf("Done\n");
char mac_str[MAC_STRLEN] = {0};
mac_to_string(mac, mac_str);
printf("Wake %s OK!\n", mac_str);
return 0;
}
WOL唤醒的缺点在于,需要网卡支持唤醒功能,例如对于树莓派则是没有效果的。此外也没法确认报文发送后机器是否有被唤醒,除非能够ping MAC。。。
如果BIOS支持,可以设置RTC Alarm Power On,这个时间是以BIOS内的时钟为准的。