Compare commits
9 Commits
99dc7b52cc
...
c70cc46aa9
Author | SHA1 | Date |
---|---|---|
|
c70cc46aa9 | |
|
0f850c3ae7 | |
|
474151d412 | |
|
a28ae9be97 | |
|
938ee80979 | |
|
4af7da6344 | |
|
4b140ef683 | |
|
533e2561ab | |
|
2b1ee90af7 |
|
@ -4,6 +4,19 @@ import fetch from 'node-fetch';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
// Windows 支持函数
|
||||||
|
function getCommand(command: string): string {
|
||||||
|
// dotnet 在 Windows 上不需要 .cmd 后缀
|
||||||
|
if (command === 'dotnet') {
|
||||||
|
return 'dotnet';
|
||||||
|
}
|
||||||
|
return process.platform === 'win32' ? `${command}.cmd` : command;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpawnOptions() {
|
||||||
|
return process.platform === 'win32' ? { stdio: 'pipe', shell: true } : { stdio: 'pipe' };
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise<boolean> {
|
async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise<boolean> {
|
||||||
for (let i = 0; i < maxRetries; i++) {
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
try {
|
try {
|
||||||
|
@ -28,9 +41,7 @@ let webProcess: ChildProcess | null = null;
|
||||||
async function startWeb(): Promise<ChildProcess> {
|
async function startWeb(): Promise<ChildProcess> {
|
||||||
console.log('Starting Vite frontend...');
|
console.log('Starting Vite frontend...');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const process = spawn('npm', ['run', 'dev'], {
|
const process = spawn(getCommand('npm'), ['run', 'dev'], getSpawnOptions() as any);
|
||||||
stdio: 'pipe'
|
|
||||||
});
|
|
||||||
|
|
||||||
let webStarted = false;
|
let webStarted = false;
|
||||||
|
|
||||||
|
@ -75,10 +86,10 @@ async function startWeb(): Promise<ChildProcess> {
|
||||||
async function startServer(): Promise<ChildProcess> {
|
async function startServer(): Promise<ChildProcess> {
|
||||||
console.log('Starting .NET server...');
|
console.log('Starting .NET server...');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const process = spawn('dotnet', ['run', '--property:Configuration=Release'], {
|
const process = spawn(getCommand('dotnet'), ['run', '--property:Configuration=Release'], {
|
||||||
cwd: 'server',
|
cwd: 'server',
|
||||||
stdio: 'pipe'
|
...getSpawnOptions()
|
||||||
});
|
} as any);
|
||||||
|
|
||||||
let serverStarted = false;
|
let serverStarted = false;
|
||||||
|
|
||||||
|
@ -175,7 +186,12 @@ async function stopServer(): Promise<void> {
|
||||||
|
|
||||||
// 额外清理:确保没有遗留的 dotnet 进程
|
// 额外清理:确保没有遗留的 dotnet 进程
|
||||||
try {
|
try {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
// Windows: 使用 taskkill 清理进程
|
||||||
|
await execAsync('taskkill /F /IM dotnet.exe').catch(() => {
|
||||||
|
// 忽略错误,可能没有匹配的进程
|
||||||
|
});
|
||||||
|
} else {
|
||||||
// 只清理与我们项目相关的进程
|
// 只清理与我们项目相关的进程
|
||||||
await execAsync('pkill -f "dotnet.*run.*--property:Configuration=Release"').catch(() => {
|
await execAsync('pkill -f "dotnet.*run.*--property:Configuration=Release"').catch(() => {
|
||||||
// 忽略错误,可能没有匹配的进程
|
// 忽略错误,可能没有匹配的进程
|
||||||
|
@ -242,7 +258,12 @@ async function stopWeb(): Promise<void> {
|
||||||
|
|
||||||
// 额外清理:确保没有遗留的 npm/node 进程
|
// 额外清理:确保没有遗留的 npm/node 进程
|
||||||
try {
|
try {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
// Windows: 清理可能的 node 进程
|
||||||
|
await execAsync('taskkill /F /IM node.exe').catch(() => {
|
||||||
|
// 忽略错误,可能没有匹配的进程
|
||||||
|
});
|
||||||
|
} else {
|
||||||
// 清理可能的 vite 进程
|
// 清理可能的 vite 进程
|
||||||
await execAsync('pkill -f "vite"').catch(() => {
|
await execAsync('pkill -f "vite"').catch(() => {
|
||||||
// 忽略错误,可能没有匹配的进程
|
// 忽略错误,可能没有匹配的进程
|
||||||
|
@ -257,7 +278,8 @@ async function stopWeb(): Promise<void> {
|
||||||
async function generateApiClient(): Promise<void> {
|
async function generateApiClient(): Promise<void> {
|
||||||
console.log('Generating API client...');
|
console.log('Generating API client...');
|
||||||
try {
|
try {
|
||||||
await execAsync('npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts');
|
const npxCommand = getCommand('npx');
|
||||||
|
await execAsync(`${npxCommand} nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts`);
|
||||||
console.log('✓ API client generated successfully');
|
console.log('✓ API client generated successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to generate API client: ${error}`);
|
throw new Error(`Failed to generate API client: ${error}`);
|
||||||
|
|
|
@ -447,4 +447,61 @@ public class VideoStreamController : ControllerBase
|
||||||
return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
|
return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行一次自动对焦 (GET方式)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>对焦结果</returns>
|
||||||
|
[HttpGet("Focus")]
|
||||||
|
[EnableCors("Users")]
|
||||||
|
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||||
|
public async Task<IResult> Focus()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.Info("收到执行一次对焦请求 (GET)");
|
||||||
|
|
||||||
|
// 检查摄像头是否已配置
|
||||||
|
if (!_videoStreamService.IsCameraConfigured())
|
||||||
|
{
|
||||||
|
logger.Warn("摄像头未配置,无法执行对焦");
|
||||||
|
return TypedResults.BadRequest(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "摄像头未配置,请先配置摄像头连接",
|
||||||
|
timestamp = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _videoStreamService.PerformAutoFocusAsync();
|
||||||
|
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
logger.Info("对焦执行成功");
|
||||||
|
return TypedResults.Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
message = "对焦执行成功",
|
||||||
|
timestamp = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.Warn("对焦执行失败");
|
||||||
|
return TypedResults.BadRequest(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "对焦执行失败",
|
||||||
|
timestamp = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "执行对焦时发生异常");
|
||||||
|
return TypedResults.InternalServerError($"执行对焦失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,9 +61,8 @@ class Camera
|
||||||
var resetResult = await Reset();
|
var resetResult = await Reset();
|
||||||
if (!resetResult.IsSuccessful) return resetResult;
|
if (!resetResult.IsSuccessful) return resetResult;
|
||||||
|
|
||||||
// 步骤2: 休眠
|
var wakeupResult = await WakeUp();
|
||||||
var sleepResult = await Sleep();
|
if (!wakeupResult.IsSuccessful) return wakeupResult;
|
||||||
if (!sleepResult.IsSuccessful) return sleepResult;
|
|
||||||
|
|
||||||
// 步骤3: 配置基础寄存器
|
// 步骤3: 配置基础寄存器
|
||||||
var basicResult = await ConfigureBasicRegisters();
|
var basicResult = await ConfigureBasicRegisters();
|
||||||
|
@ -138,13 +137,14 @@ class Camera
|
||||||
if (!testResult.IsSuccessful) return testResult;
|
if (!testResult.IsSuccessful) return testResult;
|
||||||
|
|
||||||
// 步骤21: 配置分辨率(默认640x480)
|
// 步骤21: 配置分辨率(默认640x480)
|
||||||
// var resolutionResult = await ConfigureResolution640x480();
|
var resolutionResult = await ConfigureResolution640x480();
|
||||||
var resolutionResult = await ConfigureResolution1280x720();
|
// var resolutionResult = await ConfigureResolution1280x720();
|
||||||
if (!resolutionResult.IsSuccessful) return resolutionResult;
|
if (!resolutionResult.IsSuccessful) return resolutionResult;
|
||||||
|
|
||||||
// // 步骤22: 开始流
|
// var startResult = await WakeUp();
|
||||||
var startResult = await StartStreaming();
|
// if (!startResult.IsSuccessful) return startResult;
|
||||||
if (!startResult.IsSuccessful) return startResult;
|
var sleepResult = await Sleep();
|
||||||
|
if (!sleepResult.IsSuccessful) return sleepResult;
|
||||||
// var resetResult2 = await Reset();
|
// var resetResult2 = await Reset();
|
||||||
// if (!resetResult2.IsSuccessful) return resetResult2;
|
// if (!resetResult2.IsSuccessful) return resetResult2;
|
||||||
|
|
||||||
|
@ -245,33 +245,6 @@ class Camera
|
||||||
return result.Value;
|
return result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 读取摄像头寄存器
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="registerAddr">寄存器地址</param>
|
|
||||||
/// <returns>读取到的寄存器值</returns>
|
|
||||||
private async ValueTask<Result<byte>> ReadRegister(UInt16 registerAddr)
|
|
||||||
{
|
|
||||||
var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.taskID, this.timeout);
|
|
||||||
|
|
||||||
// 地址高低字节
|
|
||||||
var addrBytes = new byte[2];
|
|
||||||
addrBytes[0] = (byte)(registerAddr >> 8);
|
|
||||||
addrBytes[1] = (byte)(registerAddr & 0xFF);
|
|
||||||
|
|
||||||
// 先写寄存器地址
|
|
||||||
var writeResult = await i2c.WriteData(CAM_I2C_ADDR, addrBytes, CAM_PROTO);
|
|
||||||
if (!writeResult.IsSuccessful)
|
|
||||||
return new(writeResult.Error);
|
|
||||||
|
|
||||||
// 再读一个字节
|
|
||||||
var readResult = await i2c.ReadData(CAM_I2C_ADDR, 1, CAM_PROTO);
|
|
||||||
if (!readResult.IsSuccessful)
|
|
||||||
return new(readResult.Error);
|
|
||||||
|
|
||||||
return readResult.Value[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 批量配置I2C寄存器
|
/// 批量配置I2C寄存器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -325,21 +298,33 @@ class Camera
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 使用默认延时逻辑
|
// 默认延时逻辑:每个寄存器写入后延时1毫秒
|
||||||
if (address == 0x3008 && cmd.Length >= 2 && cmd[1] == 0x82)
|
await Task.Delay(1);
|
||||||
{
|
|
||||||
// 复位命令,等待5MS
|
|
||||||
await Task.Delay(5);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Task.Delay(5); // 其他命令延时3ms
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取I2C寄存器字节值
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="register">要读取的寄存器地址 (16位)</param>
|
||||||
|
/// <returns>ret</returns>
|
||||||
|
public async ValueTask<Result<byte>> ReadRegister(UInt16 register)
|
||||||
|
{
|
||||||
|
var i2c = new Peripherals.I2cClient.I2c(this.address, this.port, this.taskID, this.timeout);
|
||||||
|
|
||||||
|
// Convert 16-bit register address to byte array
|
||||||
|
var registerBytes = new byte[] { (byte)(register >> 8), (byte)(register & 0xFF) };
|
||||||
|
|
||||||
|
var ret = await i2c.ReadData(CAM_I2C_ADDR, registerBytes, 1, CAM_PROTO);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
return new(ret.Error);
|
||||||
|
|
||||||
|
return new Result<byte>(ret.Value[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置摄像头分辨率和相关参数
|
/// 配置摄像头分辨率和相关参数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -511,6 +496,7 @@ class Camera
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await WakeUp();
|
||||||
logger.Info($"正在切换摄像头分辨率到 {width}x{height}");
|
logger.Info($"正在切换摄像头分辨率到 {width}x{height}");
|
||||||
|
|
||||||
Result<bool> result;
|
Result<bool> result;
|
||||||
|
@ -534,7 +520,7 @@ class Camera
|
||||||
_currentFrameLength = (UInt32)(width * height * 2 / 4); // RGB565格式,按4字节对齐
|
_currentFrameLength = (UInt32)(width * height * 2 / 4); // RGB565格式,按4字节对齐
|
||||||
logger.Info($"摄像头分辨率已切换到 {width}x{height}");
|
logger.Info($"摄像头分辨率已切换到 {width}x{height}");
|
||||||
}
|
}
|
||||||
|
await Sleep();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -588,7 +574,7 @@ class Camera
|
||||||
[0x3008, 0x42] // 休眠命令
|
[0x3008, 0x42] // 休眠命令
|
||||||
};
|
};
|
||||||
|
|
||||||
return await ConfigureRegisters(sleepRegisters);
|
return await ConfigureRegisters(sleepRegisters, customDelayMs: 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -967,14 +953,14 @@ class Camera
|
||||||
/// 开始流媒体传输
|
/// 开始流媒体传输
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>配置结果</returns>
|
/// <returns>配置结果</returns>
|
||||||
public async ValueTask<Result<bool>> StartStreaming()
|
public async ValueTask<Result<bool>> WakeUp()
|
||||||
{
|
{
|
||||||
var startRegisters = new UInt16[][]
|
var startRegisters = new UInt16[][]
|
||||||
{
|
{
|
||||||
[0x3008, 0x02] // 开始流
|
[0x3008, 0x02] // 开始流
|
||||||
};
|
};
|
||||||
|
|
||||||
return await ConfigureRegisters(startRegisters);
|
return await ConfigureRegisters(startRegisters, customDelayMs: 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 自动对焦功能
|
#region 自动对焦功能
|
||||||
|
@ -1240,18 +1226,22 @@ class Camera
|
||||||
{
|
{
|
||||||
logger.Info("开始写入OV5640自动对焦固件");
|
logger.Info("开始写入OV5640自动对焦固件");
|
||||||
|
|
||||||
|
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||||
var setStartAddrResult = await ConfigureRegisters([[0x3000, 0x80]]);
|
var setStartAddrResult = await ConfigureRegisters([[0x3000, 0x80]]);
|
||||||
if (!setStartAddrResult.IsSuccessful) return setStartAddrResult;
|
if (!setStartAddrResult.IsSuccessful) return setStartAddrResult;
|
||||||
|
|
||||||
|
// 组装固件写入命令:地址 + 所有固件数据
|
||||||
UInt16 firmwareAddr = 0x8000;
|
UInt16 firmwareAddr = 0x8000;
|
||||||
var firmwareCommands = new List<UInt16[]>();
|
var firmwareCommand = new UInt16[1 + OV5640_AF_FIRMWARE.Length];
|
||||||
|
firmwareCommand[0] = firmwareAddr;
|
||||||
|
|
||||||
|
// 将固件数据复制到命令数组中
|
||||||
for (int i = 0; i < OV5640_AF_FIRMWARE.Length; i++)
|
for (int i = 0; i < OV5640_AF_FIRMWARE.Length; i++)
|
||||||
{
|
{
|
||||||
UInt16 addr = (UInt16)(firmwareAddr + i);
|
firmwareCommand[i + 1] = OV5640_AF_FIRMWARE[i];
|
||||||
firmwareCommands.Add([addr, OV5640_AF_FIRMWARE[i]]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await ConfigureRegisters(firmwareCommands.ToArray());
|
var result = await ConfigureRegisters([firmwareCommand]);
|
||||||
if (!result.IsSuccessful)
|
if (!result.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"固件写入失败: {result.Error}");
|
logger.Error($"固件写入失败: {result.Error}");
|
||||||
|
@ -1364,8 +1354,8 @@ class Camera
|
||||||
logger.Error($"自动对焦超时,状态: 0x{readResult.Value:X2}");
|
logger.Error($"自动对焦超时,状态: 0x{readResult.Value:X2}");
|
||||||
return new(new Exception($"自动对焦超时,状态: 0x{readResult.Value:X2}"));
|
return new(new Exception($"自动对焦超时,状态: 0x{readResult.Value:X2}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(10);
|
await Task.Delay(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤3: 写寄存器 0x3022 为 0x06,暂停对焦过程,使镜头将保持在此对焦位置
|
// 步骤3: 写寄存器 0x3022 为 0x06,暂停对焦过程,使镜头将保持在此对焦位置
|
||||||
|
|
|
@ -37,12 +37,12 @@ static class I2cAddr
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 0x0000_0004: FIFO读出口,仅低8位有效,只读
|
/// 0x0000_0004: FIFO读出口,仅低8位有效,只读
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const UInt32 Read = Base + 0x0000_0003;
|
public const UInt32 Read = Base + 0x0000_0004;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 0x0000_0005: [0] FIFO写入口清空;[8] FIFO读出口清空;
|
/// 0x0000_0005: [0] FIFO写入口清空;[8] FIFO读出口清空;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const UInt32 Clear = Base + 0x0000_0003;
|
public const UInt32 Clear = Base + 0x0000_0005;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -195,15 +195,22 @@ public class I2c
|
||||||
/// 从指定I2C设备读取数据
|
/// 从指定I2C设备读取数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="devAddr">I2C设备地址</param>
|
/// <param name="devAddr">I2C设备地址</param>
|
||||||
/// <param name="length">要读取的数据长度</param>
|
/// <param name="data">要写入的数据(dummy数据)</param>
|
||||||
|
/// <param name="dataReadLength">要读取的数据长度</param>
|
||||||
/// <param name="proto">I2C协议类型</param>
|
/// <param name="proto">I2C协议类型</param>
|
||||||
/// <returns>操作结果,成功返回读取到的数据,否则返回异常信息</returns>
|
/// <returns>操作结果,成功返回读取到的数据,否则返回异常信息</returns>
|
||||||
public async ValueTask<Result<byte[]>> ReadData(UInt32 devAddr, int length, I2cProtocol proto)
|
public async ValueTask<Result<byte[]>> ReadData(UInt32 devAddr, byte[] data, int dataReadLength, I2cProtocol proto)
|
||||||
{
|
{
|
||||||
if (length <= 0 || length > 0x0000_FFFF)
|
if (dataReadLength < 1 || dataReadLength > 0x0000_FFFF)
|
||||||
{
|
{
|
||||||
logger.Error($"Read length {length} is invalid or exceeds maximum allowed 0x0000_FFFF");
|
logger.Error($"Read length {dataReadLength} is invalid or exceeds maximum allowed 0x0000_FFFF");
|
||||||
return new(new ArgumentException($"Read length {length} is invalid or exceeds maximum allowed 0x0000_FFFF"));
|
return new(new ArgumentException($"Read length {dataReadLength} is invalid or exceeds maximum allowed 0x0000_FFFF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.Length > 0x0000_FFFF)
|
||||||
|
{
|
||||||
|
logger.Error($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF");
|
||||||
|
return new(new ArgumentException($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除UDP服务器接收缓冲区
|
// 清除UDP服务器接收缓冲区
|
||||||
|
@ -211,9 +218,36 @@ public class I2c
|
||||||
|
|
||||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||||
|
|
||||||
// 配置本次传输数据量
|
// 配置写FIFO内容,内容为data[]
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.TranConfig, ((uint)(length - 1)));
|
var i2cData = new byte[data.Length * 4];
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in data)
|
||||||
|
{
|
||||||
|
i2cData[i++] = 0x00;
|
||||||
|
i2cData[i++] = 0x00;
|
||||||
|
i2cData[i++] = 0x00;
|
||||||
|
i2cData[i++] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.Write, i2cData);
|
||||||
|
if (!ret.IsSuccessful)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to write data to I2C FIFO: {ret.Error}");
|
||||||
|
return new(ret.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret.Value)
|
||||||
|
{
|
||||||
|
logger.Error("WriteAddr to I2C FIFO returned false");
|
||||||
|
return new(new Exception("Failed to write data to I2C FIFO"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置本次传输数据量:[15:0]为读长度(length-1),[31:16]为dummy长度(data.Length-1)
|
||||||
|
{
|
||||||
|
uint tranConfig = ((uint)(dataReadLength - 1)) | (((uint)(data.Length - 1)) << 16);
|
||||||
|
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.TranConfig, tranConfig);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to configure transfer length: {ret.Error}");
|
logger.Error($"Failed to configure transfer length: {ret.Error}");
|
||||||
|
@ -262,14 +296,14 @@ public class I2c
|
||||||
|
|
||||||
// 读取数据
|
// 读取数据
|
||||||
{
|
{
|
||||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, I2cAddr.Read, length);
|
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, I2cAddr.Read, dataReadLength);
|
||||||
if (!ret.IsSuccessful)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to read data from I2C FIFO: {ret.Error}");
|
logger.Error($"Failed to read data from I2C FIFO: {ret.Error}");
|
||||||
return new(ret.Error);
|
return new(ret.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length != length)
|
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length != dataReadLength)
|
||||||
{
|
{
|
||||||
logger.Error($"ReadAddr returned unexpected data length: {ret.Value.Options.Data?.Length ?? 0}");
|
logger.Error($"ReadAddr returned unexpected data length: {ret.Value.Options.Data?.Length ?? 0}");
|
||||||
return new(new Exception("Failed to read expected amount of data from I2C FIFO"));
|
return new(new Exception("Failed to read expected amount of data from I2C FIFO"));
|
||||||
|
|
|
@ -156,6 +156,8 @@ public class HttpVideoStreamService : BackgroundService
|
||||||
throw new Exception("Please config camera first");
|
throw new Exception("Please config camera first");
|
||||||
}
|
}
|
||||||
_cameraEnable = isEnabled;
|
_cameraEnable = isEnabled;
|
||||||
|
if (_cameraEnable) await _camera.WakeUp();
|
||||||
|
else await _camera.Sleep();
|
||||||
await _camera.EnableHardwareTrans(_cameraEnable);
|
await _camera.EnableHardwareTrans(_cameraEnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,9 +91,9 @@ public class UDPClientPool
|
||||||
var sendLen = socket.SendTo(sendBytes, endPoint);
|
var sendLen = socket.SendTo(sendBytes, endPoint);
|
||||||
socket.Close();
|
socket.Close();
|
||||||
|
|
||||||
// logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||||
// logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||||
// logger.Debug($" Decoded Data: {pkg.ToString()}");
|
logger.Debug($" Decoded Data: {pkg.ToString()}");
|
||||||
|
|
||||||
if (sendLen == sendBytes.Length) { return true; }
|
if (sendLen == sendBytes.Length) { return true; }
|
||||||
else { return false; }
|
else { return false; }
|
||||||
|
@ -164,8 +164,8 @@ public class UDPClientPool
|
||||||
var sendLen = socket.SendTo(sendBytes, endPoint);
|
var sendLen = socket.SendTo(sendBytes, endPoint);
|
||||||
socket.Close();
|
socket.Close();
|
||||||
|
|
||||||
// logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||||
// logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||||
|
|
||||||
if (sendLen == sendBytes.Length) { return true; }
|
if (sendLen == sendBytes.Length) { return true; }
|
||||||
else { return false; }
|
else { return false; }
|
||||||
|
|
8347
src/APIClient.ts
8347
src/APIClient.ts
File diff suppressed because it is too large
Load Diff
|
@ -161,8 +161,13 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative bg-black rounded-lg overflow-hidden"
|
class="relative bg-black rounded-lg overflow-hidden cursor-pointer"
|
||||||
|
:class="[
|
||||||
|
focusAnimationClass,
|
||||||
|
{ 'cursor-not-allowed': !isPlaying || hasVideoError }
|
||||||
|
]"
|
||||||
style="aspect-ratio: 4/3"
|
style="aspect-ratio: 4/3"
|
||||||
|
@click="handleVideoClick"
|
||||||
>
|
>
|
||||||
<!-- 视频播放器 - 使用img标签直接显示MJPEG流 -->
|
<!-- 视频播放器 - 使用img标签直接显示MJPEG流 -->
|
||||||
<div
|
<div
|
||||||
|
@ -178,6 +183,14 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 对焦提示 -->
|
||||||
|
<div
|
||||||
|
v-if="isPlaying && !hasVideoError"
|
||||||
|
class="absolute top-4 right-4 text-white text-sm bg-black bg-opacity-50 px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
{{ isFocusing ? '对焦中...' : '点击画面对焦' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 错误信息显示 -->
|
<!-- 错误信息显示 -->
|
||||||
<div
|
<div
|
||||||
v-if="hasVideoError"
|
v-if="hasVideoError"
|
||||||
|
@ -353,6 +366,10 @@ const isPlaying = ref(false);
|
||||||
const hasVideoError = ref(false);
|
const hasVideoError = ref(false);
|
||||||
const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频');
|
const videoStatus = ref('点击"播放视频流"按钮开始查看实时视频');
|
||||||
|
|
||||||
|
// 对焦相关状态
|
||||||
|
const isFocusing = ref(false);
|
||||||
|
const focusAnimationClass = ref('');
|
||||||
|
|
||||||
// 分辨率相关状态
|
// 分辨率相关状态
|
||||||
const changingResolution = ref(false);
|
const changingResolution = ref(false);
|
||||||
const loadingResolutions = ref(false);
|
const loadingResolutions = ref(false);
|
||||||
|
@ -565,6 +582,67 @@ const tryReconnect = () => {
|
||||||
currentVideoSource.value = `${streamInfo.value.mjpegUrl}?t=${new Date().getTime()}`;
|
currentVideoSource.value = `${streamInfo.value.mjpegUrl}?t=${new Date().getTime()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 执行对焦
|
||||||
|
const performFocus = async () => {
|
||||||
|
if (isFocusing.value || !isPlaying.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isFocusing.value = true;
|
||||||
|
focusAnimationClass.value = 'focus-starting';
|
||||||
|
addLog("info", "正在执行自动对焦...");
|
||||||
|
|
||||||
|
// 调用对焦API
|
||||||
|
const response = await fetch('/api/VideoStream/Focus');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 对焦成功动画
|
||||||
|
focusAnimationClass.value = 'focus-success';
|
||||||
|
addLog("success", "自动对焦执行成功");
|
||||||
|
|
||||||
|
// 2秒后消失
|
||||||
|
setTimeout(() => {
|
||||||
|
focusAnimationClass.value = '';
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
// 对焦失败动画
|
||||||
|
focusAnimationClass.value = 'focus-error';
|
||||||
|
addLog("error", `自动对焦执行失败: ${result.message || '未知错误'}`);
|
||||||
|
|
||||||
|
// 2秒后消失
|
||||||
|
setTimeout(() => {
|
||||||
|
focusAnimationClass.value = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 对焦失败动画
|
||||||
|
focusAnimationClass.value = 'focus-error';
|
||||||
|
addLog("error", `自动对焦执行失败: ${error}`);
|
||||||
|
console.error("自动对焦执行失败:", error);
|
||||||
|
|
||||||
|
// 2秒后消失
|
||||||
|
setTimeout(() => {
|
||||||
|
focusAnimationClass.value = '';
|
||||||
|
}, 2000);
|
||||||
|
} finally {
|
||||||
|
// 1秒后重置对焦状态
|
||||||
|
setTimeout(() => {
|
||||||
|
isFocusing.value = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理视频点击事件
|
||||||
|
const handleVideoClick = (event: MouseEvent) => {
|
||||||
|
// 只在播放状态下才允许对焦
|
||||||
|
if (!isPlaying.value || hasVideoError.value) return;
|
||||||
|
|
||||||
|
// 防止重复点击
|
||||||
|
if (isFocusing.value) return;
|
||||||
|
|
||||||
|
performFocus();
|
||||||
|
};
|
||||||
|
|
||||||
// 启动视频流
|
// 启动视频流
|
||||||
const startStream = async () => {
|
const startStream = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -711,4 +789,69 @@ img {
|
||||||
* {
|
* {
|
||||||
transition: all 500ms ease-in-out;
|
transition: all 500ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 对焦动画样式 */
|
||||||
|
.focus-starting {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
animation: focus-starting-animation 0.5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-success {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
animation: focus-success-animation 2s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-error {
|
||||||
|
border: 3px solid transparent;
|
||||||
|
animation: focus-error-animation 2s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes focus-starting-animation {
|
||||||
|
0% {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: #fbbf24; /* 黄色 */
|
||||||
|
box-shadow: 0 0 20px rgba(251, 191, 36, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes focus-success-animation {
|
||||||
|
0% {
|
||||||
|
border-color: #fbbf24; /* 黄色 */
|
||||||
|
box-shadow: 0 0 20px rgba(251, 191, 36, 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-color: #10b981; /* 绿色 */
|
||||||
|
box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes focus-error-animation {
|
||||||
|
0% {
|
||||||
|
border-color: #fbbf24; /* 黄色 */
|
||||||
|
box-shadow: 0 0 20px rgba(251, 191, 36, 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-color: #ef4444; /* 红色 */
|
||||||
|
box-shadow: 0 0 20px rgba(239, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对焦状态下的鼠标指针 */
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-not-allowed {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue