PVD: 真实的虚拟键盘鼠标

广大工矿企业在日常生产生活中,经常会遇到需要虚拟键盘和鼠标的场景。通常的解决方法是使用软件进行模拟。但是软件模拟经常会遇到安全软件误杀等等情况。为了解决这种问题,这次带来的制作是一个于 CH552开发的虚拟键盘鼠标项目。它是基于CH554 Arduino 环境开发的设备,插上之后,系统 中会出现一个 USB 串口,一个USB 键盘,一个USB 鼠标,我们将数据从串口送进设备,然后设备将收到的串口数据直接转发到键盘鼠标对应的端口上,从而实现鼠标键盘操作。

首先进行硬件的设计,电路图如下:

图片中上方是一个 CH554 的最小电路图,他是WCH 出品的一款兼容MCS51 指令集的增强型E8051内核单片机。带有256 字节内部iRAM,可以用于快速数据暂存以及堆栈;1KB 片内xRAM,可以用于大量数据暂存以及DMA直接内存存取。16KB 容量的可多次编程的非易失存储器ROM,可以全部用于程序存储空间;或者可以分为14KB 程序存储区和2KB引导代码BootLoader/ISP程序区。更特别的是其内嵌USB 控制器和USB 收发器,支持USB-Host 主机模式和USB-Device 设备模式,支持USB type-C主从检测,支持USB 2.0全速12Mbps或者低速1.5Mbps。支持最大64字节数据包,内置FIFO,支持DMA。

这里使用的型号为CH554E , MSOP-10 封装,体积非常小便于整体设备小型化。

为了调试方便,预留了P2 是UART输出。此外,还有一个 WS2812B LED 可以实现多种颜色的灯效。

PCB 设计如下:

3D 预览如下

图片3 正面预览

在实际使用中,只需要焊接CH554最小系统部分即可实现USB 串口转键盘鼠标功能,其余部分可以不上件。

这次的设计尺寸是根据透明U盘外壳来的,在制作时选择 0.8mm PCB, 刚好能够放入外壳中。

硬件设计完成之后就可以进行代码的编写了。主要代码如下:

#include <WS2812.h>
#include "src/CdcHidCombo/USBCDC.h"
#include "src/CdcHidCombo/USBHIDKeyboardMouse.h"

#define NUM_LEDS 1
#define COLOR_PER_LEDS 3
#define NUM_BYTES (NUM_LEDS*COLOR_PER_LEDS)

__xdata uint8_t ledData[NUM_BYTES];

#define KeyboardReportID 0x01
#define MouseReportID    0x02
#define OnBoardLED       0x03

// Data format
// Keyboard(Total 9 bytes): 01(ReportID 01) + Keyboard data (8 Bytes)
// Mouse(Total 5 bytes): 02(ReportID 02) + Mouse Data (4 Bytes)
uint8_t recvStr[9];
uint8_t recvStrPtr = 0;
unsigned long Elsp;

void setup() {
  USBInit();
  Serial0_begin(115200);
  delay(1000);
  Serial0_print("start");
  Elsp=0;
}

void loop() {
  while (USBSerial_available()) {
    uint8_t serialChar = USBSerial_read();
    recvStr[recvStrPtr++] = serialChar;
    if (recvStrPtr == 10) {
      for (uint8_t i = 0; i < 9; i++) {
        Serial0_write(recvStr[i]);
      }      
      if (recvStr[0] == KeyboardReportID) { // Keyboard
        USB_EP3_send(recvStr, 9);
      }
      if (recvStr[0] == MouseReportID) {
        USB_EP3_send(recvStr, 5); // Mouse
      }
      if (recvStr[0] == OnBoardLED) {
        set_pixel_for_GRB_LED(ledData, 0, recvStr[0], recvStr[1], recvStr[2]); 
        neopixel_show_P1_5(ledData, NUM_BYTES);  
      }
      recvStrPtr = 0;
    }
    Elsp=millis();
  }
  // If there is no data in 100ms, clear the receive buffer
  if (millis()-Elsp>100) {
      recvStrPtr = 0;
      Elsp=millis();
    }
}

每次收取10字节串口数据,如果第一字节为KeyboardReportID,那么直接将9个字节从端点3发送给主机;如果第一字节是MouseReportID,那么直接将5个字节从端点3发送给主机;如果第一字节为 OnBoardLED ,那么将后续3个字节合成为一个颜色信息,然后通过neopixel_show_P1_5() 函数将数据从 P1.5 引脚发送给WS2812。

USB 设备信息在USBconstant.c 文件中有描述。其中包含了 USB CDC 设备/USB 键盘/USB鼠标的描述符。其中的USB 键盘/USB鼠标使用端点3 OUTPUT。

对于键盘鼠标的 HID 描述符,在USBconstant.c文件的ReportDescriptor[]的结构体中。使用的是标准的键盘鼠标描述符。

键盘数据为8字节长:

Byte0Byte1Byte2Byte3Byte4Byte5Byte6Byte7
特殊按键NA数据0数据1数据2数据3数据4数据5

鼠标数据为4字节长:

Byte0Byte1Byte2Byte3
左右中键X轴数据Y轴数据滚轮数据

需要特别注意的是:键盘鼠标都是通过端点3发送给主机的,他们使用 Report ID 进行区分,在 ReportDescriptor[] 中有如下定义:

    0x09, 0x06,       // USAGE (Keyboard)
    0xa1, 0x01,       // COLLECTION (Application)
    0x85, 0x01,       //   REPORT_ID (1)
    0x05, 0x07,       //   USAGE_PAGE (Keyboard)
0x19, 0xe0,       //   USAGE_MINIMUM (Keyboard LeftControl)
…….
    0x09, 0x02,       // USAGE (Mouse)
    0xa1, 0x01,       // COLLECTION (Application)
    0x09, 0x01,       //   USAGE (Pointer)
    0xa1, 0x00,       //   COLLECTION (Physical)
    0x85, 0x02,       //   REPORT_ID (2)
    0x05, 0x09,       //     USAGE_PAGE (Button)

就是说,设备通过端点3发送给主机的数据,如果是 0x01 + 8个字节的数据,主机会当作键盘数据来处理;如果是0x02+4个字节的数据,主机则会当作鼠标数据来处理。

上面的代码中,USB串口,收到数据之后,将9字节和5字节发送给主机即可实现键盘和鼠标的操作了。

端点3的发送代码在 USB_EP3_send() 函数中,可以看到对于 CH554 来说,USB的发送操作非常明确简单,要发送的数据直接填充到Ep3Buffer[]中,然后设定  UEP3_T_LEN 寄存器要发送的数据长度,再设定 UEP3_CTRL 寄存器就完成了了发送。这对于USB初学者非常友好,代码直接对应硬件动作,通俗易懂。

  // 将所有数据放入 EP3 准备发送
  for (__data uint8_t i = 0; i < Len; i++) { // load data for upload
    Ep3Buffer[i] = Data[i];
  }

  UEP3_T_LEN = Len; // data length
  UpPoint3_Busy = 1;
  UEP3_CTRL = UEP3_CTRL & ~MASK_UEP_T_RES |
              UEP_T_RES_ACK; // upload data and respond ACK

上位机代码:

// CDC_vKBMSTest.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <windows.h>
#include <SetupAPI.h>
#include <tchar.h>
#include <iostream>
#include <cstring>
#include <atlstr.h>

#pragma comment(lib, "Setupapi.lib")

#define MY_USB_PID_VID	_T("VID_1209&PID_C55C")

class ComPortException : public std::exception {
public:
	ComPortException(DWORD errorCode) : errorCode(errorCode) {}

	DWORD getErrorCode() const {
		return errorCode;
	}

private:
	DWORD errorCode;
};

// 对串口portName 发送数据
// 无法打开串口返回 1
// 无法设置串口参数 2
void SendToComPort(const int port, const char* data) {
	int Result = 0;
	HANDLE hCom = INVALID_HANDLE_VALUE;
	TCHAR portName[10]; // 用于存储 "COMp" 字符串

// 将整数 p 转换为 "COMp" 字符串
#ifdef UNICODE
	swprintf(portName, 10, _T("\\\\.\\COM%d"), port);
#else
	sprintf(portName, "COM%d", port);
#endif
	try {
		// 打开串口
		hCom = CreateFile(portName,
			GENERIC_READ | GENERIC_WRITE,
			0,
			NULL,
			OPEN_EXISTING,
			0,
			NULL);

		if (hCom == INVALID_HANDLE_VALUE) {
			throw ComPortException(GetLastError());
		}

		// 设置串口参数
		DCB dcb;
		SecureZeroMemory(&dcb, sizeof(DCB));
		dcb.DCBlength = sizeof(DCB);

		if (!GetCommState(hCom, &dcb)) {
			throw ComPortException(GetLastError());
		}

		dcb.BaudRate = CBR_115200;  // 波特率
		dcb.ByteSize = 8;         // 数据位
		dcb.StopBits = ONESTOPBIT; // 停止位
		dcb.Parity = NOPARITY;    // 校验位

		if (!SetCommState(hCom, &dcb)) {
			throw ComPortException(GetLastError());
		}

		// 设置超时参数
		COMMTIMEOUTS timeouts;
		timeouts.ReadIntervalTimeout = 50;
		timeouts.ReadTotalTimeoutConstant = 50;
		timeouts.ReadTotalTimeoutMultiplier = 10;
		timeouts.WriteTotalTimeoutConstant = 50;
		timeouts.WriteTotalTimeoutMultiplier = 10;

		if (!SetCommTimeouts(hCom, &timeouts)) {
			throw ComPortException(GetLastError());
		}

		// 发送数据
		DWORD bytesWritten;
		if (!WriteFile(hCom, data, 10, &bytesWritten, NULL)) {
			std::cerr << "Failed to write to COM port." << std::endl;
		}
		else {
			std::cout << "Successfully sent data to COM port: [" << port << "]" << std::endl;
		}
		// 关闭串口
		if (hCom != INVALID_HANDLE_VALUE) {
			CloseHandle(hCom);
		}
	}
	catch (const ComPortException& ex) {
		std::cerr << "Error: " << ex.getErrorCode() << std::endl;
		if (hCom != INVALID_HANDLE_VALUE) {
			CloseHandle(hCom);
		}
	}

}

/************************************************************************/
/* 根据USB描述信息字符串中读取
/************************************************************************/
int MTGetPortFromVidPid(CString strVidPid)
{
	// 获取当前系统所有使用的设备
	int					nPort = -1;
	int					nStart = -1;
	int					nEnd = -1;
	int					i = 0;
	CString				strTemp, strName;
	DWORD				dwFlag = (DIGCF_ALLCLASSES | DIGCF_PRESENT);
	HDEVINFO			hDevInfo = INVALID_HANDLE_VALUE;
	SP_DEVINFO_DATA		sDevInfoData;
	TCHAR				szDis[2048] = { 0x00 };// 存储设备实例ID
	TCHAR				szFN[MAX_PATH] = { 0x00 };// 存储设备实例属性
	DWORD				nSize = 0;

	// 准备遍历所有设备查找USB
	hDevInfo = SetupDiGetClassDevs(NULL, L"USB", NULL, dwFlag);
	if (INVALID_HANDLE_VALUE == hDevInfo)
		goto STEP_END;

	// 开始遍历所有设备
	memset(&sDevInfoData, 0x00, sizeof(SP_DEVICE_INTERFACE_DATA));
	sDevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
	for (i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &sDevInfoData); i++)
	{
		nSize = 0;

		// 无效设备
		if (!SetupDiGetDeviceInstanceId(hDevInfo, &sDevInfoData, szDis, sizeof(szDis), &nSize))
			goto STEP_END;

		// 根据设备信息寻找VID PID一致的设备
		strTemp.Format(_T("%s"), szDis);
		strTemp.MakeUpper();
		if (strTemp.Find(strVidPid, 0) == -1)
			continue;

		// 查找设备属性
		nSize = 0;
		SetupDiGetDeviceRegistryProperty(hDevInfo, &sDevInfoData,
			SPDRP_FRIENDLYNAME,
			0, (PBYTE)szFN,
			sizeof(szFN),
			&nSize);

		// "XXX Virtual Com Port (COM7)"
		strName.Format(_T("%s"), szFN);
		if (strName.IsEmpty())
			//goto STEP_END;
			continue;

		// 寻找串口信息
		nStart = strName.Find(_T("(COM"), 0);
		nEnd = strName.Find(_T(")"), 0);
		if (nStart == -1 || nEnd == -1)
			//goto STEP_END;
			continue;

		strTemp = strName.Mid(nStart + 4, nEnd - nStart - 2);
		nPort = _ttoi(strTemp);

	}
STEP_END:

	// 关闭设备信息集句柄
	if (hDevInfo != INVALID_HANDLE_VALUE)
	{
		SetupDiDestroyDeviceInfoList(hDevInfo);
		hDevInfo = INVALID_HANDLE_VALUE;
	}

	return nPort;
}

int main()
{
	int Port;
	char Data[10];
	printf("Virutal KB MS Test\n");
	Port = MTGetPortFromVidPid(MY_USB_PID_VID);
	if (Port == -1) {
		printf("No device is found\n");
		goto EndProgram;
	}
	else {
		printf("Found COM%d\n",Port);
	}
	Sleep(5000);

	// 测试鼠标移动
	memset(Data,0,sizeof(Data));
	Data[0] = 2; // 鼠标
	Data[2] = 50;
	SendToComPort(Port,(char *)&Data);
	Sleep(300);
	Data[2] = 0; Data[3] = 50;
	SendToComPort(Port, (char*)&Data);
	Sleep(300);
	Data[2] = -50; Data[3] = 0x00;
	SendToComPort(Port, (char*)&Data);
	Sleep(300);
	Data[2] = 0; Data[3] = -50;
	SendToComPort(Port, (char*)&Data);
	Sleep(300);

	// 测试键盘数据
	memset(Data, 0, sizeof(Data));
	Data[0] = 1; // 键盘
	// 发送GUI信息
	Data[1] = 0x08;
	SendToComPort(Port, (char*)&Data);
	Sleep(1000);
	memset(Data, 0, sizeof(Data));
	Data[0] = 1; // 键盘
	SendToComPort(Port, (char*)&Data);
	Sleep(300);

	// 测试键盘数据
	memset(Data, 0, sizeof(Data));
	Data[0] = 1; // 键盘
	// 发送按键信息
	Data[2] = 0x04; 
	Data[3] = 0x0F; //'l'
	Data[4] = 0x04; //'a ' 
	Data[5] = 0x05; //'b'
	Data[6] = 0x1d; //'z'
	SendToComPort(Port, (char*)&Data);
	Sleep(300);
	memset(Data, 0, sizeof(Data));
	Data[0] = 1; // 键盘
	// 抬起按键
	SendToComPort(Port, (char*)&Data);
	Sleep(300);



	// 测试LED
	memset(Data, 0, sizeof(Data));
	Data[0] = 3; // LED
	Data[1] = 0xFF;
	SendToComPort(Port, (char*)&Data);
	Sleep(500);
	memset(Data, 0, sizeof(Data));
	Data[0] = 3; // LED
	Data[2] = 0xFF;
	SendToComPort(Port, (char*)&Data);
	Sleep(500);
	memset(Data, 0, sizeof(Data));
	Data[0] = 3; // LED
	Data[3] = 0xFF;
	SendToComPort(Port, (char*)&Data);
	Sleep(500);
	memset(Data, 0, sizeof(Data));
	Data[0] = 3; // LED
	SendToComPort(Port, (char*)&Data);
	Sleep(500);

EndProgram:
	printf("End\n");
}

工作的测试视频在

电路图:

Arduino代码:

VC2019工程

OpenSSL 差异

理论上 OpenSSL 在进行摘要运算时应该有着相同的结果,比如,对于同一个文件abc.bin 计算 md5 之后结果应该相同。但是我最近就遇到了 OpenSSL 计算结果相同,但是返回调用值不同的情况。

例如执行如下命令:

openssl dgst -binary -sha256 labz.bin > result.bin

运行之后, openssl.exe 显示执行正常,但是使用 echo %ERRORLEVEL% ,有一个版本的返回值为1,接下来会导致批处理报错。

乐东 九所 小东北

发小送我们去了乐东黎族自治县的九所镇,它和三亚同处热带地区。海南是一个岛,但是因为山脉和气流的影响不同地区气候相差较大。

海南岛地处北纬19°附近(网上很多图片显示北纬18°是错误的。北纬18°是在海南岛南侧海中)

详细的来说这一天的行程是从右下的凤凰机场到了左上的乐东站:

下榻的宾馆是“乐东迎宾馆”(这个名字听起来有些奇怪,以至于我不得不一次又一次确认。相当于起名:上海水浴场,肇东方红机修厂这种),外观大气

稍微晚一点,夕阳照耀着椰子树富有热带风情

走在这边的街头,感觉和三亚的街景完全不同,有可能是因为我们在三亚居住在靠海的旅游区,完全不同于这边扑面而来的生活的气息,更准确的说是似曾相似的东北气息。

席地而坐椰子七元一个:

顾客选中之后现场切开,根据卖家说,他们无法保证每个椰子质量,水多少,甜度如何是要看运气了:

几刀之后,椰子顶部切出一个小口,露出果肉插上吸管在路边的摊位上即可享用。仔细观察一下,切开之后还仔细刮干净品尝每一块椰蓉的顾客看起来多是北方人的面相;喝光椰子水之后随意丢弃的应该就是本地人了。照片是大年初三拍摄的,这里现在的气温相当于东北的夏天。

当地房价不贵,但是在这里买房并不是很好的选择,主要因为租房更有性价比。租房可以选择更喜欢的户型和位置。房屋通常都是以年或者半年为单位进行出租的。最不济的情况,等到合同到期可以更换其他地方。

我的五叔在这边租房居住,进门之后就是一间房屋,这里被称作“开间”,楼内是井字布局的,我是第一次看到这样的造型。中空的结构,最顶层的玻璃让阳光能够倾泻而下。可以看到,许多房屋完全都是朝北的房间,这在北方是完全无法想象的。

从栏杆俯视让我有着恐高的感觉。这边人非常忌讳“4”字,因此电梯用 5A 和 15A 取代了 4层和14层的称呼。

从十六楼窗口眺望外面。反光的不是水面而是塑料大棚,这里还有着大片的农田。照片上出现在地平线中间的凸起是海上的一个岛屿。

这边的人们,下午2点多就开始了各种户外活动,比如,新疆舞和八段锦这种。因为刚好是过年期间,小区门口不时有人在放鞭炮和烟花。

路过一栋楼房时,抬头看到住家阳台上的灯笼和彩灯,更让我想起来在东北过年时的装扮。在冬季,东北外部只有银装素裹一种景色,因此东北人需要五颜六色的彩灯彩纸填充视觉上的单调。

回到酒店后,经历了难忘的夜晚。熄灯之后,无数的蚊虫仿佛轰炸机一般呼啸而至。半夜实在受不了之后我检查了酒店的液体蚊香,虽然指示灯提示在工作,但是似乎加热棒并没有工作。最终只得把衬衣蒙在头上,熬过漫长的夜晚。

如果你问我去年的第一个包是什么时候被叮咬的,我绝对回答不了,而2025年我可以清楚的记得是在1月的最后一个夜晚,在乐东迎宾馆获得的。

第二天一早,老婆特地给我盘点一番,我头上被咬了十个。我儿子耳朵肿起来了,手指中招肿胀得无法弯曲,这正好给他一个不去完成作业的借口。甚至还想以此为由再次享受被人喂饭得待遇,当然这个被我们及时识破了,他值得练习起来左手使用筷子。母亲讲她半夜起来去找前台,索要到了蚊香,回来用了一段自行熄灭了。又去前台,大约是惊扰了他们的清梦,对方非常不耐烦,直接拒绝了索要打火机的要求,只是将没有烧完的蚊香继续点燃。

总结下来:好消息,酒店包免费,坏消息,蚊子包免费。

离开酒店之后,根据结果推测,咬我们的并不是普通蚊子,而是当地特有的:槟榔虫。如果有到海南旅游的朋友一定要特别注意保护好自己和家人。如果有可能最好自行携带蚊香,这样也比被蚊子折磨一夜强的多。

早晨,五叔他们招待我们在外面吃了一顿东北特色的早餐,有:豆腐脑(我特地留心了一下,没有甜豆腐脑。我老婆是湖北人,她完全无法接收咸的豆腐脑。在这个问题上我们彼此视作异端);油条,大茬子粥(玉米),咸菜,茶叶蛋(个头挺大的鸡蛋,实惠)。

最后,我们上了高铁,前往了下一个目的地:昌江黎族自治县下面的石碌镇。

三亚:购物走马观花

稍微有点时间,我陪同母亲和孩子去了一下当地的海旅免税店。这个免税店是比较小的,据说国旅比他大好几倍。只是这里距离我们的酒店比较近只有12公里,因此过去转了一圈。

这种免税店和大型商场一样,内部也是一家家的品牌商户,有些店铺标明是免税店,结账的时候需要出示身份证验证离岛航班才能有享有优惠。此外,很多店铺直接标注并无免税优惠。

进入之后,我母亲就自行游览去了。孩子直接拉着我前往标注着“儿童玩具”的四楼。让人意想不到的是,四楼只有一家儿童玩具:乐高店。好奇心驱使下我根据玩具的型号搜索了一下网上的价格,结果发现网上价格只有店铺的 2/3。意思是对于店铺中标价300元的,同样型号(每个乐高产品都有自己的编号。比如,乐高City 消防编号就是60004),在淘宝网200即可拿下。

孩子一遍一遍在在乐高店中观看每样玩具,我在背后跟着有些无聊就在乐高店门口找到一个凳子坐下了。偶然瞥见旁边牌子写着男士衬衣打折,三件只要239。衣服看起来材质还不错,起身拉长脖子看清牌子之后立刻打消了仔细看看的意愿,因为牌子上写的是一件1199, 三件折扣力度巨大,只要 2399。刚才的一瞥间被挡住了最后一位。

两个多小时后,母亲也空手而归,因为价格实在让人无法提起购买的冲动。转头,我们就去了1.6公里外的“大悦城”。相比之下,这里场地更大更符合普通人的消费水准。

一楼的Pop Mart , 人气旺盛。许多可爱的玩偶,价格适中。

大悦城中的儿童玩具店颇多,售卖的玩具大部分是国产。

同样的这里的店铺也售卖乐高积木。

它货架对面是国产 的积木,价格更加实惠。

玩具,同样可以成为文化影响力的一部分。比如,伴随着芭比娃娃商业版图的扩张,金发碧眼美女形象在潜在影响着儿童的审美。如果有可能,乐高可以受到英国政府委托推出纪念布尔战争的题材,纪念英国人发明集中营这种组织形式。历史上,创造“福尔摩斯”形象的英国作家柯南道尔,曾经编写了一本名为《在南非的战争:起源与行为》(The War in South Africa: Its Cause and Conduct)的小册子,为英国在南非的布尔战争进行辩护。这本书被翻译成多种文字发行,有很大影响。这本书使他在1902年获得了一生引以为傲的爵士头衔。

大悦城的四楼,有电玩城,一边是儿童的各种游戏机,有射击(开飞机),格斗(拳皇),体感(比如,扮演奥特曼和怪兽决斗),音游,驾驶竞速(汽车/摩托车);另外一边则是成人游戏:推币机。不同的年龄,享受着不同的欲望和刺激。

我们在这里消磨了半天时间,最后在门口通过滴滴叫到出租车回了酒店。

VC Header 引用顺序导致错误

最近遇到一个问题,Visual C++不同头文件的引用顺序会导致错误。例如下面这个代码在 VS2019 中编译时会遇到一堆错误:

#include <iostream>
#include "windows.h"
#include "WinSock2.h"


int main()
{
    std::cout << "Hello World!\n";
}

解决方法很简单,调整如下即可:

#include "WinSock2.h"
#include "windows.h"

参考:

1.https://blog.csdn.net/ldadadaaaa/article/details/140998537

上海到三亚:从冬天到夏天

1月26日,今年上海的冬季与往年大有不同。继续连续一个多月都没有降水,让我有着在上海有使用加湿器的冲动;另外今年的冬季气温偏高,老婆还没有叫过冷,并且将此归结于锻炼的结果。大约是前几年,曾经有气象专家对于当年的冬季做出了一些预测,最终的结论是:是否是一个暖冬必须等到冬天结束才能知道。因为这个结论,专家被喷了很多,从此之后再也不见有人对于是否暖冬做出预测了。

飞机的目的是是海口,下机之后有发小接站。这是我到过的祖国最南端。不知道未来是否还有机会前往祖国更远的南方。

起飞的时候因为阴天,落地之后才注意到航班的涂装是“玩具总动员”,机头是巴斯光年:

发小开车带我们从中线横穿海南岛,一路上的景色和内陆大有不同。在东北道路两边通常是高大的白杨树守护着道路,而在这里经常是挺拔的椰子树在充当着道路守护者的责任。

途中休息时发小带我们到本地特色的餐厅,或者说是富有本地特色的餐饮一条街:“会跳舞的牛肉”。道路的一侧是各种牛肉店铺。挂在钩子上的牛肉非常新鲜,许多还在不停的抖动,故此得名。

牛肉店铺对面则是以新鲜牛肉为卖点的餐厅。食客下单后,餐厅老板马上到对面购买牛肉,十分钟之内即可端上餐桌。

店铺类似大排档,是发小经当地人推荐经常光顾的,除了正常的点餐之外,发小还直接委托老板帮着购买了几百块的牛肉在我们吃饭之后带走。海南人对于食材新鲜程度的追求让发小感到惊讶,从要求各种肉类的新鲜屠宰,到绿叶菜必须当日现摘。诸多当地人品尝之后都能准备分辨出食材为当日生产。

牛肉的烹饪也颇具当地特色,灶上的锅是分作两种烹饪方式:中间是涮着吃的(中间),外围一圈则是类似于铁板烧的方式。

发小也分享了区分牛肉是否冷冻过的经验:在煎制过程如果有水出现,那么一定是冷冻过,最新鲜牛肉在这个烹制过程中只会有油析出。

蘸料非常特别,据说每一家都有自己的秘方。蘸料带有了除了酸味之外的所有味道,而刺激食欲的酸味是通过一颗颗小青柠檬来提供的。挤出来的柠檬汁,取代了醋的作用,自己挤压滴入蘸料中。

几刀下去,椰子露出来白色的椰蓉,插入吸管,椰子就变成了现成的饮料。如同茄子有着长短粗细不同品种,不同品种的椰子也有着巨大的差异。据说下面这种椰子只是用来喝的,并不会吃其中的椰蓉。和所有的农产品一样,椰子之间也有不同,我们点了4个椰子,品尝起来彼此之间汁水味道和甜度差别挺大。

我们沿着中线穿越了整个海南岛,路上有着连绵的高山,山顶被云雾覆盖着。据说正是因为山脉的原因,使得整个海南的气候差别很大。比如,有些地方经常阴雨,有些地方没有热带的感觉。同样,截断的水汽,也成为岛上许多河流的源头。

比较有趣的是,即便地处热带的三亚地区,经常出现身着羽绒服和赤足穿着拖鞋的人同框的情形。

路上接近乐东的时候,我们遇到了大雨。据说这样的大雨在这个季节非常少见,在我到来之前已经一月有余没有降水。

开进了三亚,雨水逐渐平息。最终,到达此行的目的地:三亚。推开车门,暖湿的气息铺面而来,仿佛踏进了八九月份的上海。地面和建筑也被这场突如其来的大雨冲刷得愈发干净。

酒店旁边就是大海,虽然无法听到波涛的声音,但是出门10分钟左右就能到达海滩。海滩的一侧都是这样酒店,跟随着海滩延伸而去。

Visual C++ 计算一个函数耗时的代码

下面的代码可以用来计算一个函数的耗时。

   LARGE_INTEGER frequency;
    LARGE_INTEGER start;
    LARGE_INTEGER end;

    // 获取高精度计时器的频率
    QueryPerformanceFrequency(&frequency);

    // 获取开始时间
    QueryPerformanceCounter(&start);

   // EnumerateComPorts();
    EmuCom();

    // 获取结束时间
    QueryPerformanceCounter(&end);

    // 计算耗时(秒)
    double elapsedTime = static_cast<double>(end.QuadPart - start.QuadPart) / frequency.QuadPart;

    std::cout << "Time taken by foo(): " << elapsedTime << " seconds" << std::endl;

故乡的水塔

前一段看新闻,忽然意识到现在生活的城市已经很少见到这样的庞然巨物了。

我是出生和成长在黑龙江大庆市的。“大庆”这个城市名称始终和“工业学大庆”这个口号联系在一起的,同时和“大油田”深深的绑定。小时候有一段时间,我非常反感有文章用“小油苗”来称呼本地的少年儿童。甚至深深的怀疑第一个编造出这个称呼的人是没有见过“原油”的。那是一种黏糊糊散发着异常味道的物质,稍微有一些化学知识的人都会了解其中含有大量的“苯”“烃””烯“致癌成分,更不会有人产生可爱的联想。

长大之后,我去成都求学,在那里也曾经看到过很多水塔,通常都是下面飞盘造型,蓝白相间的颜色在阴郁的天空下会显得无比深沉。

大庆的水塔没有如此的艺术感,完全像一个木柄手榴弹,外立面不会有任何的装饰,赤裸着红砖努力展现着质朴和实用的主题。估计建设之初一定是本着“又不是不能用”的原则。

这样的水塔下方通常都是机房,靠近能听到其中机器的嘶吼,电力驱动的泵机将水送到顶层,用于维持这一片区供水压力。小时候站在下面,仰望水塔,会有一阵阵的眩晕感,高大的水塔仿佛随时会扑下来的巨人一般。

很长一段时间,大庆的居民楼,甚至是学校都是统一造型的。比如,下面这种样式就是典型的居民楼。从飞机上看下去都是类似的火柴盒。若干年后,我大约知道这种楼被称作“赫鲁晓夫楼”,是一种风格统一的工业化设计,能够在短期内满足改善当地人居住条件的建筑。

更早期大庆的典型住宅是被称作“干打垒”的房子。在东北,保暖性能是第一需要考虑的问题,冬季最冷的时候会达到零下三四十度。只有足够厚度的墙体才能抵御外部的寒冷。干打垒有很大一部分处于地以下,这样能够显著的节省建筑材料。也是因为这样的原因,听说早年间遇到特别大的雪天,早晨门会被积雪掩盖,通常需要从窗口跳出去,铲开门口的雪才能打开房门。

所有的学校也是相同的造型【参考1】。这是我初中的学校,之前原名是大庆市第二十四中学,原来自己独占一个大院的,后来挤进来五十六中学,再后来二者合并改名为“三十六中学”。

参考:

1.https://baike.baidu.com/item/%E5%A4%A7%E5%BA%86%E5%B8%82%E7%AC%AC%E4%B8%89%E5%8D%81%E5%85%AD%E4%B8%AD%E5%AD%A6/2428328

自制Windows 11精简版

先说最后制作出来的结果:镜像文件2.78GB.虚拟机中安装后内存占用1.6GB,硬盘占用 10GB

接下来介绍制作方法:

  1. 项目是来自https://github.com/ntdevlabs/tiny11builder , 项目通过运行脚本文件来提供Windows 11安装镜像中的文件再重新生成一个安装 ISO
  2. 运行方法是首先挂载一个 Windows 11安装镜像,比如,这里我挂接到 f:
  3. 以管理员权限打开Windows Power  Shell 首先运行如下命令
Set-ExecutionPolicy unrestricted

4.运行 tiny11Coremaker.ps1,这个过程中会要求你选择制作的类型,推荐选择  Windows 11 pro

5.接下来等待即可,结束之后会有提示

原来的项目中会需要联网下载 oscdimg.exe 文件的,这里我直接都打包在一起,可以离线使用。

Windows Service 服务框架例子

这里提供一个Service 的例子,实现对c:\log.txt 每隔一段写入当前时间。


// MyService.cpp : 定义控制台应用程序的入口点。
//


#include <Windows.h>
#include <tchar.h>
#include <iostream>

using namespace std;

/*
BOOL IsInstalled();
BOOL Install();
BOOL Uninstall();
void LogEvent(LPCTSTR pszFormat, ...);
void WINAPI ServiceMain();
void WINAPI ServiceStrl(DWORD dwOpcode);
TCHAR szServiceName[] = _T("MyService");
BOOL bInstall;
SERVICE_STATUS_HANDLE hServiceStatus;
SERVICE_STATUS status;
DWORD dwThreadID;
SC_HANDLE hSCM;
SC_HANDLE hService;
*/

/*
OpenSCManager 用于打开服务控制管理器;
CreateService 用于创建服务;
OpenService用于打开已有的服务,返回该服务的句柄;
ControlService则用于控制已打开的服务状态,这里是让服务停止后才删除;
DeleteService 用于删除指定服务。
RegisterServiceCtrlHandler 注册服务控制
*/

//定义全局函数变量  
void Init();
BOOL IsInstalled();
BOOL Install();
BOOL Uninstall();
void LogEvent(LPCTSTR pszFormat, ...);
void WINAPI ServiceMain();
void WINAPI ServiceStrl(DWORD dwOpcode);

TCHAR szServiceName[] = _T("MyService");
BOOL bInstall;
SERVICE_STATUS_HANDLE hServiceStatus;
SERVICE_STATUS status;
DWORD dwThreadID;

int APIENTRY _tWinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPTSTR    lpCmdLine,
	int       nCmdShow)
{
	Init();
	dwThreadID = ::GetCurrentThreadId();
	SERVICE_TABLE_ENTRY st[] =
	{
		{ szServiceName, (LPSERVICE_MAIN_FUNCTION)ServiceMain },
		{ NULL, NULL }
	};

	if (_tcscmp(lpCmdLine, _T("/install")) == 0)
	{
		Install();
	}
	else if (_tcscmp(lpCmdLine, _T("/uninstall")) == 0)
	{
		Uninstall();
	}
	else
	{
		if (!::StartServiceCtrlDispatcher(st))
		{
			LogEvent(_T("Register Service Main Function Error!"));
		}
	}

	return 0;
}

//初始化
void Init()
{
	hServiceStatus = NULL;
	status.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
	status.dwCurrentState = SERVICE_START_PENDING;
	status.dwControlsAccepted = SERVICE_ACCEPT_STOP;
	status.dwWin32ExitCode = 0;
	status.dwServiceSpecificExitCode = 0;
	status.dwCheckPoint = 0;
	status.dwWaitHint = 0;
}

//服务主函数,这在里进行控制对服务控制的注册
void WINAPI ServiceMain()
{
	status.dwCurrentState = SERVICE_START_PENDING;
	status.dwControlsAccepted = SERVICE_ACCEPT_STOP;

	//注册服务控制  
	hServiceStatus = RegisterServiceCtrlHandler(szServiceName, ServiceStrl);
	if (hServiceStatus == NULL)
	{
		LogEvent(_T("Handler not installed"));
		return;
	}
	SetServiceStatus(hServiceStatus, &status);

	status.dwWin32ExitCode = S_OK;
	status.dwCheckPoint = 0;
	status.dwWaitHint = 0;
	status.dwCurrentState = SERVICE_RUNNING;
	SetServiceStatus(hServiceStatus, &status);

	//模拟服务的运行。应用时将主要任务放于此即可  
	//可在此写上服务需要执行的代码,一般为死循环  
	while (1)
	{
		FILE* p=NULL;
		errno_t  err =_tfopen_s(&p,_T("c:\\log.txt"), _T("ab+"));
		if (err != 0) {
			TCHAR errMsg[256];
			_wcserror_s(errMsg, 256, err);
			_tprintf(_T("err! %s"), errMsg);
			return ;
		}
		SYSTEMTIME st;
		GetSystemTime(&st);
		TCHAR time[100] = { 0 };
		_stprintf_s(time, 100, _T("%4d-%02d-%02d %02d:%02d:%02d\r\n"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
		if (p == NULL) {
			return;
		}
		fwrite(time, sizeof(TCHAR), _tcsclen(time), p);
		fclose(p);

		Sleep(5000);
	}
	status.dwCurrentState = SERVICE_STOPPED;
	SetServiceStatus(hServiceStatus, &status);
}

//Description:          服务控制主函数,这里实现对服务的控制,  
//                      当在服务管理器上停止或其它操作时,将会运行此处代码  
void WINAPI ServiceStrl(DWORD dwOpcode)
{
	switch (dwOpcode)
	{
	case SERVICE_CONTROL_STOP:
		status.dwCheckPoint = 1;
		status.dwCurrentState = SERVICE_STOP_PENDING;
		SetServiceStatus(hServiceStatus, &status);
		Sleep(500);
		status.dwCheckPoint = 0;
		status.dwCurrentState = SERVICE_STOPPED;
		SetServiceStatus(hServiceStatus, &status);

		PostThreadMessage(dwThreadID, WM_CLOSE, 0, 0);
		break;
	case SERVICE_CONTROL_PAUSE:
		break;
	case SERVICE_CONTROL_CONTINUE:
		break;
	case SERVICE_CONTROL_INTERROGATE:
		break;
	case SERVICE_CONTROL_SHUTDOWN:
		exit(0);
		break;
	default:
		LogEvent(_T("Bad service request"));
	}
}

//判断服务是否已经被安装
BOOL IsInstalled()
{ 
	BOOL bResult = FALSE;

	//打开服务控制管理器  
	SC_HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

	if (hSCM != NULL)
	{
		//打开服务  
		SC_HANDLE hService = ::OpenService(hSCM, szServiceName, SERVICE_QUERY_CONFIG);
		if (hService != NULL)
		{
			bResult = TRUE;
			::CloseServiceHandle(hService);
		}
		::CloseServiceHandle(hSCM);
	}
	return bResult;
}

//安装服务函数
BOOL Install()
{
	//检测是否安装过
	if (IsInstalled())
		return TRUE;

	//打开服务控制管理器  
	SC_HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (hSCM == NULL)
	{
		MessageBox(NULL, _T("Couldn't open service manager"), szServiceName, MB_OK);
		return FALSE;
	}

	//获取程序目录
	TCHAR szFilePath[MAX_PATH];
	::GetModuleFileName(NULL, szFilePath, MAX_PATH);

	//创建服务  
	SC_HANDLE hService = ::CreateService(hSCM, szServiceName, szServiceName,
		SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
		szFilePath, NULL, NULL, _T(""), NULL, NULL);

	//检测创建是否成功
	if (hService == NULL)
	{
		::CloseServiceHandle(hSCM);
		MessageBox(NULL, _T("Couldn't create service"), szServiceName, MB_OK);
		return FALSE;
	}

	//释放资源
	::CloseServiceHandle(hService);
	::CloseServiceHandle(hSCM);
	return TRUE;
}

//删除服务函数
BOOL Uninstall()
{
	//检测是否安装过
	if (!IsInstalled())
		return TRUE;

	//打开服务控制管理器
	SC_HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
	if (hSCM == NULL)
	{
		MessageBox(NULL, _T("Couldn't open service manager"), szServiceName, MB_OK);
		return FALSE;
	}

	//打开具体服务
	SC_HANDLE hService = ::OpenService(hSCM, szServiceName, SERVICE_STOP | DELETE);
	if (hService == NULL)
	{
		::CloseServiceHandle(hSCM);
		MessageBox(NULL, _T("Couldn't open service"), szServiceName, MB_OK);
		return FALSE;
	}

	//先停止服务
	SERVICE_STATUS status;
	::ControlService(hService, SERVICE_CONTROL_STOP, &status);

	//删除服务  
	BOOL bDelete = ::DeleteService(hService);
	::CloseServiceHandle(hService);
	::CloseServiceHandle(hSCM);

	if (bDelete)  return TRUE;
	LogEvent(_T("Service could not be deleted"));
	return FALSE;
}

//记录服务事件
void LogEvent(LPCTSTR pFormat, ...)
{
	TCHAR    chMsg[256];
	HANDLE  hEventSource;
	LPTSTR  lpszStrings[1];
	va_list pArg;

	va_start(pArg, pFormat);
	_vsntprintf_s(chMsg, sizeof(chMsg),_TRUNCATE,pFormat, pArg);
	va_end(pArg);

	lpszStrings[0] = chMsg;

	hEventSource = RegisterEventSource(NULL, szServiceName);
	if (hEventSource != NULL)
	{
		ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*)&lpszStrings[0], NULL);
		DeregisterEventSource(hEventSource);
	}
}

完整代码:

参考:

1.https://cloud.tencent.com/developer/article/1857390

2https://learn.microsoft.com/zh-cn/windows/win32/services/writing-a-service-program-s-main-function

3.https://blog.csdn.net/hsy12342611/article/details/133557759