Initial commit: IP-KVM server implementation with complete functionality

Core Features:
- Web server based on Mongoose framework listening on port 82
- WebSocket support for real-time mouse and keyboard control
- Serial communication via /dev/ttyS5 (115200 baud rate)
- GPIO-based hardware control (GPIO 34/35 for shutdown/reboot, GPIO 139 for reset)
- User authentication and management system
- Network configuration management with dynamic updates
- MJPG-streamer integration for video streaming (port 8080, 1920x1080)
- File upload support with chunked transfer capability
- Hardware reset functionality via GPIO button

Technical Implementation:
- Mouse protocol conversion to serial data format {0x57, 0xAB, ...}
- Keyboard input handling with proper HID report generation
- Real-time configuration updates without service restart
- Thread-based GPIO monitoring for hardware events
- Authentication integration with MJPG-streamer
- Default configuration restoration on hardware reset

Configuration Files:
- /mnt/user.txt - User credentials storage
- /etc/init.d/xnet.conf - Network configuration
- /www/webcam - Web interface files

Documentation:
- Complete functional specification in Chinese (功能说明.md)
- Detailed WebSocket message protocol documentation
- GPIO mapping and hardware control specifications

This is a complete standalone IP-KVM server that enables remote
keyboard, video, and mouse control over IP networks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
fml
2025-10-13 13:29:13 +08:00
commit a344f2fd8d
8 changed files with 10478 additions and 0 deletions

94
Makefile Normal file
View File

@ -0,0 +1,94 @@
#
# Copyright (C) 2006-2015 OpenWrt.org
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice (including the next
# paragraph) shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# ---
#
# The above is the version of the MIT "Expat" License used by X.org:
#
# http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
#
include $(TOPDIR)/rules.mk
include $(BUILD_DIR)/package.mk
#include ../sunxifb.mk
PKG_NAME:=xtell_server
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_BUILD_DIR := $(COMPILE_DIR)/$(PKG_NAME)
define Package/$(PKG_NAME)
SECTION:=xtell
CATEGORY:=xtell
DEPENDS:=
TITLE:=xtell server 1.0.0
MAINTAINER:=edison<edison@xtell.cn>
endef
define Package/$(PKG_NAME)/config
endef
define Package/$(PKG_NAME)/Default
endef
define Package/$(PKG_NAME)/description
xtell server v1.0.0
endef
define Build/Prepare
$(INSTALL_DIR) $(PKG_BUILD_DIR)/
$(CP) ./src $(PKG_BUILD_DIR)/
# $(CP) ../../../../take_photo $(PKG_BUILD_DIR)/src
endef
define Build/Configure
endef
TARGET_CFLAGS+=-I$(PKG_BUILD_DIR)/src
define Build/Compile
@echo "===================================================================="
$(MAKE) -C $(PKG_BUILD_DIR)/src\
ARCH="$(TARGET_ARCH)" \
AR="$(TARGET_AR)" \
CC="$(TARGET_CC)" \
CXX="$(TARGET_CXX)" \
CFLAGS="$(TARGET_CFLAGS)" \
LDFLAGS="$(TARGET_LDFLAGS)" \
INSTALL_PREFIX="$(PKG_INSTALL_DIR)" \
all
@echo PATH=$(PATH)
@echo STAGING_DIR=$(STAGING_DIR)
endef
#define Build/InstallDev
# $(INSTALL_DIR) $(1)/usr/lib
#endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/src/$(PKG_NAME) $(1)/usr/bin/
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

26
src/Makefile Normal file
View File

@ -0,0 +1,26 @@
PROG ?= xtell_server # Program we are building
DELETE = rm -rf # Command to remove files
OUT ?= -o $(PROG) # Compiler argument for output file
SOURCES = main.c mongoose.c # Source code files
CFLAGS = -W -Wall -Wextra -g -I. # Build options
# Mongoose build options. See https://mongoose.ws/documentation/#build-options
#CFLAGS_MONGOOSE += -DMG_ENABLE_LINES
#ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe
# PROG ?= example.exe # Use .exe suffix for the binary
# CC = gcc # Use MinGW gcc compiler
# CFLAGS += -lws2_32 # Link against Winsock library
# DELETE = cmd /C del /Q /F /S # Command prompt command to delete files
# OUT ?= -o $(PROG) # Build output
#endif
all: $(PROG) # Default target. Build and run program
# $(RUN) ./$(PROG) $(ARGS)
$(PROG): $(SOURCES) # Build program from sources
$(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT)
clean: # Cleanup. Delete built program and all build artifacts
$(DELETE) $(PROG) *.o *.obj *.exe *.dSYM

469
src/main.c Normal file
View File

@ -0,0 +1,469 @@
// test the upload by using following cmd in linux
// curl http://localhost:8000/upload?name=a.txt --data-binary @large_file.txt
#include "mongoose.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <errno.h>
#define GPIO_PIN 139
#define FILE_PATH "/mnt/user.txt"
int global_serial_fd = -1;
int read_gpio() {
char buffer[256];
char value_str[3];
int fd;
snprintf(buffer, sizeof(buffer), "/sys/class/gpio/gpio%d/value", GPIO_PIN);
fd = open(buffer, O_RDONLY);
if (fd == -1) {
perror("GPIO read failed");
return -1;
}
read(fd, value_str, 3);
close(fd);
return atoi(value_str);
}
void *gpio_handler(void *arg) {
bool prev_gpio_state = false; // 保存上一次按键状态
char command[1024];
FILE *conf_file;
while(1) {
bool current_gpio_state = (read_gpio() == 0); // 获取当前按键状态
if (current_gpio_state && !prev_gpio_state) {
// 按键按下且上一次按键状态为未按下
printf("GPIO triggered: Low to High\n");
system("killall mjpg_streamer");
sleep (2);
// 在这里执行删除文件的操作
if (remove(FILE_PATH) == 0) {
conf_file = fopen("/etc/init.d/xnet.conf", "w");
if (conf_file != NULL) {
fprintf(conf_file, "ip 192.168.6.100\n");
fprintf(conf_file, "port 8700\n");
fprintf(conf_file, "mask 255.255.255.0\n");
fprintf(conf_file, "gw 192.168.6.1\n");
fprintf(conf_file, "dns1 1.2.3.4\n");
fprintf(conf_file, "dns2 8.8.8.8\n");
fclose(conf_file);
printf("IP configuration updated and saved.\n");
system("/etc/init.d/xnet_init.sh");
system("sync");
} else {
printf("Failed to open xnet.conf file for writing.\n");
}
printf("File %s removed successfully.\n", FILE_PATH);
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam\" &");
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mnt/output_http.so -p 8080 -w /www/webcam\" &");
system(command);
// mg_ws_send(c, "refresh", 7, WEBSOCKET_OP_TEXT);
} else {
conf_file = fopen("/etc/init.d/xnet.conf", "w");
if (conf_file != NULL) {
fprintf(conf_file, "ip 192.168.6.100\n");
fprintf(conf_file, "port 8700\n");
fprintf(conf_file, "mask 255.255.255.0\n");
fprintf(conf_file, "gw 192.168.6.1\n");
fprintf(conf_file, "dns1 1.2.3.4\n");
fprintf(conf_file, "dns2 8.8.8.8\n");
fclose(conf_file);
printf("IP configuration updated and saved.\n");
system("/etc/init.d/xnet_init.sh");
system("sync");
} else {
printf("Failed to open xnet.conf file for writing.\n");
}
perror("File removal failed");
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam\" &");
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mnt/output_http.so -p 8080 -w /www/webcam\" &");
system(command);
// mg_ws_send(c, "refresh", 7, WEBSOCKET_OP_TEXT);
}
}
prev_gpio_state = current_gpio_state; // 更新上一次按键状态
usleep(100 * 1000); // 休眠一段时间,避免过于频繁地检测按键状态
}
}
void init_gpio() {
char buffer[256];
int fd;
// 导出 GPIO
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd == -1) {
perror("GPIO export failed");
exit(EXIT_FAILURE);
}
snprintf(buffer, sizeof(buffer), "%d", GPIO_PIN);
write(fd, buffer, strlen(buffer));
close(fd);
// 设置 GPIO 方向为输入
snprintf(buffer, sizeof(buffer), "/sys/class/gpio/gpio%d/direction", GPIO_PIN);
fd = open(buffer, O_WRONLY);
if (fd == -1) {
perror("GPIO direction set failed");
exit(EXIT_FAILURE);
}
write(fd, "in", 2);
close(fd);
}
int init_serial() {
int fd = open("/dev/ttyS5", O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
printf("Error %i from open: %s\n", errno, strerror(errno));
return -1;
}
struct termios tty;
memset(&tty, 0, sizeof tty);
cfsetospeed(&tty, B115200);
cfsetispeed(&tty, B115200);
// ... 其他串口配置 ...
tty.c_cflag |= (CLOCAL | CREAD); // Ignore modem controls and enable reading
tty.c_cflag &= ~CSIZE;
tty.c_cflag |= CS8; // 8-bit characters
tty.c_cflag &= ~PARENB; // No parity bit
tty.c_cflag &= ~CSTOPB; // Only need 1 stop bit
tty.c_cflag &= ~CRTSCTS; // No hardware flowcontrol
// Setup for non-canonical mode
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Shut off xon/xoff ctrl
tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // Disable echo etc
tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes
// Fetch bytes as they become available
tty.c_cc[VMIN] = 1;
tty.c_cc[VTIME] = 1;
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
return -1;
}
return fd;
}
// 以下函数提取 `src` 中存在的字符
// 在 `m` 和 `n` 之间(不包括 `n`)
char* substr(const char *src, int m, int n){
// 获取目标字符串的长度
int len = n - m;
// 为目标分配 (len + 1) 个字符(+1 用于额外的空字符)
char *dest = (char*)malloc(sizeof(char) * (len + 1));
// 从源字符串中提取第 m 和第 n 索引之间的字符
// 并将它们复制到目标字符串中
for (int i = m; i < n && (*(src + i) != '\0'); i++){
*dest = *(src + i);
dest++;
}
// 以空值结束目标字符串
*dest = '\0';
// 返回目标字符串
return dest - len;
}
int chunk_num=0; //文件分块序号
FILE *fp;
// HTTP request handler function.
static void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
/////////////////////////////////////////////////////
// 处理上传的文件
if (ev == MG_EV_HTTP_CHUNK && mg_http_match_uri(hm, "/upload")) { //上传用的网址
chunk_num++;
if (chunk_num==1){
fp = fopen("/mnt/UDISK/gcode.nc","w"); //上传文件固定路径
}
// printf("."); //log
printf("%d\n", chunk_num++);
// MG_INFO(("Got chunk len %lu", (unsigned long) hm->chunk.len));
// MG_INFO(("Query string: [%.*s]", (int) hm->query.len, hm->query.ptr));
// MG_INFO(("Chunk data:\n%.*s", (int) hm->chunk.len, hm->chunk.ptr));
fwrite(hm->chunk.ptr, 1, hm->chunk.len, fp); //上传的数据写入文件
mg_http_delete_chunk(c, hm); //释放内存,允许后续数据块上传
if (hm->chunk.len == 0) {
chunk_num=0;
fclose(fp);
mg_http_reply(c, 200, "", "ok\n"); //上传结束
MG_INFO(("Last chunk received, sending response"));
}
} else {
if (ev == MG_EV_OPEN) {
// c->is_hexdumping = 1;
} else if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
if (mg_http_match_uri(hm, "/websocket")) {
// Upgrade to websocket. From now on, a connection is a full-duplex
// Websocket connection, which will receive MG_EV_WS_MSG events.
printf("websocket connected\n");
mg_ws_upgrade(c, hm, NULL);
}
} else if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
printf("Received WebSocket message: %.*s\n", (int) wm->data.len, wm->data.ptr);
if (strncmp(wm->data.ptr, "mouse,", 6) == 0) {
int num[6];
sscanf(wm->data.ptr + 6, "%d,%d,%d,%d,%d,%x", &num[0], &num[1], &num[2], &num[3], &num[4], &num[5]);
unsigned char data[13] = {0x57, 0xAB, 0x00, 0x04, 0x07, 0x02, 0x00, 0x40, 0x01, 0x15, 0x02, 0x00, 0x67};
data[6] = num[0];
data[7] = num[1];
data[8] = num[2];
data[9] = num[3];
data[10] = num[4];
data[11] = num[5];
unsigned char sum = 0;
for (int i = 0; i < 12; i++) {
sum += data[i];
}
data[12] = sum;
printf("Data to be sent: ");
for (int i = 0; i < sizeof(data); i++) {
printf("%02X ", data[i]);
}
printf("\n");
if (global_serial_fd >= 0) {
write(global_serial_fd, data, sizeof(data));
}
} else if (strncmp(wm->data.ptr, "keyboard,", 9) == 0) {
int num[2];
sscanf(wm->data.ptr + 9, "%x,%x", &num[0], &num[1]);
unsigned char data[14] = {0x57,0xAB,0x00,0x02,0x08,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x10};
data[5] = num[1];
data[7] = num[0];
unsigned char sum = 0;
for (int i = 0; i < 13; i++) {
sum += data[i];
}
data[13] = sum;
if (global_serial_fd >= 0) {
write(global_serial_fd, data, sizeof(data));
}
printf("Data to be sent: ");
for (int i = 0; i < sizeof(data); i++) {
printf("%02X ", data[i]);
}
printf("\n");
}else if (strncmp(wm->data.ptr, "newuser,", 8) == 0) {
char oldUsername[256];
char oldPassword[256];
char username[256];
char password[256];
// 解析收到的数据
sscanf(wm->data.ptr + 8, "%[^,],%[^,],%[^,],%s", oldUsername, oldPassword, username, password);
printf("Received WebSocket message: oldUsername=%s, oldPassword=%s, username=%s, password=%s\n", oldUsername, oldPassword, username, password);
// 读取文件中的旧用户名和密码
FILE *file = fopen("/mnt/user.txt", "r");
char fileUsername[256], filePassword[256];
if (file != NULL) {
fscanf(file, "Username: %[^,], Password: %s", fileUsername, filePassword);
fclose(file);
// 比较用户名和密码
if (strcmp(oldUsername, fileUsername) == 0 && strcmp(oldPassword, filePassword) == 0) {
// 用户名和密码匹配,更新文件
file = fopen("/mnt/user.txt", "w");
if (file != NULL) {
fprintf(file, "Username: %s, Password: %s\n", username, password);
fclose(file);
printf("User data updated and saved: %s %s\n", username, password);
system("sync"); // 确保数据被写入磁盘
system("killall mjpg_streamer");
sleep(1);
char command[1024];
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam -c %s:%s\" &", username, password);
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mnt/output_http.so -p 8080 -w /www/webcam -c %s:%s\" &", username, password);
system(command);
printf("Command executed: %s\n", command);
mg_ws_send(c, "refresh", 7, WEBSOCKET_OP_TEXT);
} else {
printf("Failed to open file for writing.\n");
}
} else {
if (strcmp(oldUsername, fileUsername) != 0 || strcmp(oldPassword, filePassword) != 0) {
// 发送一条消息给客户端,通知用户名错误
mg_ws_send(c, "oldusererror", 12, WEBSOCKET_OP_TEXT);
}
printf("oldusernameerror xtell\n");
// printf("Username or password does not match.\n");
}
} else {
if (access("/mnt/user.txt", F_OK) == -1) {
// 文件不存在,执行默认操作
printf("No existing user file, treating as new setup.\n");
file = fopen("/mnt/user.txt", "w");
if (file) {
fprintf(file, "Username: %s, Password: %s\n", username, password);
fclose(file);
printf("Initial user data saved: %s %s\n", username, password);
system("sync");
system("killall mjpg_streamer");
sleep(1);
char command[1024];
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam -c %s:%s\" &", username, password);
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mnt/output_http.so -p 8080 -w /www/webcam -c %s:%s\" &", username, password);
system(command);
printf("Command executed: %s\n", command);
mg_ws_send(c, "refresh", 7, WEBSOCKET_OP_TEXT);
} else {
printf("Failed to create new user file.\n");
}
} else {
printf("Failed to open existing user file.\n");
}
}
}else if (strncmp(wm->data.ptr, "newip,", 6) == 0) {
char ipAddress[256];
char mask[256];
char gateway[256];
char dns1[256];
char dns2[256];
// 解析收到的数据
sscanf(wm->data.ptr + 6, "%[^,],%[^,],%[^,],%[^,],%s", ipAddress, mask, gateway, dns1, dns2);
printf("Received WebSocket message: ipAddress=%s, mask=%s, gateway=%s, dns1=%s, dns2=%s\n", ipAddress, mask, gateway, dns1, dns2);
// 保存到文件
FILE *file = fopen("/etc/init.d/xnet.conf", "w");
if (file != NULL) {
fprintf(file, "ip %s\n", ipAddress);
fprintf(file, "port 8700\n");
fprintf(file, "mask %s\n", mask);
fprintf(file, "gw %s\n", gateway);
fprintf(file, "dns1 %s\n", dns1);
fprintf(file, "dns2 %s\n", dns2);
fclose(file);
printf("IP configuration updated and saved.\n");
system("/etc/init.d/xnet_init.sh");
system("sync");
} else {
printf("Failed to open xnet.conf file for writing.\n");
}
}else if (strncmp(wm->data.ptr, "control,", 8) == 0) {
char action[256];
// 解析收到的控制指令
sscanf(wm->data.ptr + 8, "%s", action);
printf("Received control command: %s\n", action);
if (strcmp(action, "shutdown") == 0) {
// 执行关机操作 控制 GPIO 35 开关机
system("echo 1 > /sys/class/gpio/gpio35/value");
usleep(500000);
system("echo 0 > /sys/class/gpio/gpio35/value");
printf("Shutting down the system...\n");
} else if (strcmp(action, "reboot") == 0) {
// 执行重启操作 控制 GPIO 34 重启
system("echo 1 > /sys/class/gpio/gpio34/value");
usleep(500000);
system("echo 0 > /sys/class/gpio/gpio34/value");
printf("Rebooting the system...\n");
} else {
printf("Unknown control command: %s\n", action);
}
}
}
}
(void) fn_data;
}
int main(void) {
char command[1024];
FILE *file = fopen("/mnt/user.txt", "r");
if (file != NULL) {
char username[256], password[256];
if (fscanf(file, "Username: %[^,], Password: %s", username, password) == 2) {
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam -c %s:%s\" &", username, password);
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mnt/output_http.so -p 8080 -w /www/webcam -c %s:%s\" &", username, password);
} else {
printf("Failed to parse user credentials.\n");
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam\" &");
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mntoutput_http.so -p 8080 -w /www/webcam\" &");
}
fclose(file);
} else {
if (access("/mnt/user.txt", F_OK) != -1) {
printf("File exists but could not be opened.\n");
} else {
printf("No credentials file found, starting mjpg_streamer with default settings.\n");
}
// sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"output_http.so -p 8080 -w /www/webcam\" &");
sprintf(command, "mjpg_streamer -i \"input_uvc.so -d /dev/video0 -r 1920x1080\" -o \"/mnt/output_http.so -p 8080 -w /www/webcam\" &");
}
system("killall mjpg_streamer"); // Ensure no previous instance is running
sleep(1); // Wait a bit to make sure the process has been terminated
system(command); // Execute the command
printf("Command executed: %s\n", command);
//读gpio重置IP和用户名+密码
system("echo 139 > /sys/class/gpio/export");
system("echo in > /sys/class/gpio/gpio139/direction");
//开关机
system("echo 34 > /sys/class/gpio/export");
system("echo out > /sys/class/gpio/gpio34/direction");
//重启
system("echo 35 > /sys/class/gpio/export");
system("echo out > /sys/class/gpio/gpio35/direction");
system("ifconfig eth0 up");
pthread_t client_thread2;
if (pthread_create(&client_thread2, NULL, gpio_handler, NULL) != 0) {
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
// Detach the thread to free its resources automatically
pthread_detach(client_thread2);
struct mg_mgr mgr;
global_serial_fd = init_serial();
if (global_serial_fd < 0) {
return 1;
}
mg_mgr_init(&mgr);
mg_log_set(MG_LL_DEBUG); // Set debug log level
mg_http_listen(&mgr, "http://0.0.0.0:82", cb, NULL);
for (;;) mg_mgr_poll(&mgr, 50);
mg_mgr_free(&mgr);
if (global_serial_fd >= 0) {
close(global_serial_fd);
}
return 0;
}

8124
src/mongoose.c Normal file

File diff suppressed because it is too large Load Diff

1635
src/mongoose.h Normal file

File diff suppressed because it is too large Load Diff

9
src/my_build.sh Normal file
View File

@ -0,0 +1,9 @@
#!/bin/sh
export PATH=/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/host/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/staging_dir/toolchain/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/usr/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/staging_dir/toolchain/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/usr/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/host/bin:/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/host/bin:/home/edison/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
export STAGING_DIR=/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/staging_dir/target
make -C . ARCH="arm" AR="arm-openwrt-linux-muslgnueabi-ar" CC="arm-openwrt-linux-muslgnueabi-gcc" CXX="arm-openwrt-linux-muslgnueabi-g++" CFLAGS="-Os -pipe -march=armv7-a -mtune=cortex-a7 -mfpu=neon -fno-caller-saves -Wno-unused-result -mfloat-abi=hard -Wformat -Werror=format-security -fPIC -fstack-protector -D_FORTIFY_SOURCE=2 -Wl,-z,now -Wl,-z,relro -DUSE_SUNXIFB_DOUBLE_BUFFER -DUSE_SUNXIFB_CACHE -DCONF_G2D_VERSION_NEW -I/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/compile_dir/target/lv_examples/src" LDFLAGS="-L/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/staging_dir/target/usr/lib -L/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/staging_dir/target/lib -L/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/usr/lib -L/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/lib -specs=/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/build/hardened-ld-pie.specs -znow -zrelro" INSTALL_PREFIX="/home/xtell_projects_dev/allwinner/mangopi/t113/Tina-Linux/out/t113-mq_r/compile_dir/target/lv_examples/ipkg-install" all

BIN
src/xtell_server Normal file

Binary file not shown.

121
功能说明.md Normal file
View File

@ -0,0 +1,121 @@
# xtell_server 功能说明
## 概述
xtell_server 是一个基于 Mongoose 库的嵌入式 Web 服务器,用于 IP-KVM (Keyboard-Video-Mouse over IP) 远程控制系统。
## 主要功能
### 1. Web 服务器功能
- 监听端口 82提供 HTTP 服务
- 支持 WebSocket 连接和文件上传
- 使用 Mongoose 库处理 HTTP 请求
### 2. 远程控制功能
- **鼠标控制**: 接收 WebSocket 鼠标指令,转换为串口协议数据
- 数据格式: `{0x57, 0xAB, 0x00, 0x04, 0x07, 0x02, ..., checksum}`
- 通过 `/dev/ttyS5` 串口通信 (波特率 115200)
- **键盘控制**: 接收 WebSocket 键盘指令,转换为串口协议数据
- 数据格式: `{0x57,0xAB,0x00,0x02,0x08,0x00, ..., checksum}`
### 3. 用户管理系统
- 用户名密码修改功能
- 认证信息存储在 `/mnt/user.txt`
- 支持 MJPG-streamer 认证配置
- 默认配置检测和创建
### 4. 网络配置管理
- IP 地址、子网掩码、网关、DNS 配置
- 配置文件存储在 `/etc/init.d/xnet.conf`
- 支持网络配置动态更新
- 默认 IP: 192.168.6.100, 端口: 8700
### 5. 系统控制功能
- **关机控制**: 通过 GPIO 35 控制系统关机
- **重启控制**: 通过 GPIO 34 控制系统重启
- 支持远程关机和重启操作
### 6. 视频流管理
- 启动和管理 MJPG-streamer 服务
- 支持认证的视频流传输 (端口 8080)
- 分辨率: 1920x1080
- 动态用户认证配置
### 7. GPIO 按键处理
- GPIO 139 按键检测线程
- 按键触发时恢复默认网络配置
- 硬件重置功能
- 自动重启 MJPG-streamer 服务
### 8. 文件上传功能
- 支持分块文件上传
- 上传端点: `/upload`
- 上传路径: `/mnt/UDISK/gcode.nc`
- 支持大文件分块传输
## 技术架构
### 核心依赖
- **Mongoose**: 嵌入式 Web 服务器框架
- **MJPG-streamer**: 视频流服务
- **Linux GPIO**: 硬件控制接口
- **串口通信**: `/dev/ttyS5`
### 配置文件
- `/mnt/user.txt`: 用户认证信息
- `/etc/init.d/xnet.conf`: 网络配置
- `/www/webcam`: Web 界面文件
### GPIO 映射
- GPIO 34: 系统重启控制
- GPIO 35: 系统关机控制
- GPIO 139: 硬件重置按键
## WebSocket 消息协议
### 鼠标控制
```
mouse,x1,y1,x2,y2,buttons,scroll
```
### 键盘控制
```
keyboard,key_code,modifier
```
### 用户管理
```
newuser,oldUser,oldPass,newUser,newPass
```
### 网络配置
```
newip,ip,mask,gw,dns1,dns2
```
### 系统控制
```
control,shutdown|reboot
```
## 运行流程
1. **初始化阶段**:
- 读取用户配置
- 启动 MJPG-streamer
- 配置 GPIO
- 初始化串口
- 启动 GPIO 监控线程
2. **服务阶段**:
- 监听 HTTP/WebSocket 连接
- 处理客户端指令
- 转发键鼠数据到串口
- 管理系统配置
3. **硬件重置**:
- GPIO 139 按键触发
- 恢复默认网络配置
- 重启相关服务
## 独立性
xtell_server 是一个完整的独立应用程序,不需要其他组件配合即可正常运行所有功能。