This MVP release provides a complete full-stack solution for managing action mappings in Geutebruck's GeViScope and GeViSoft video surveillance systems. ## Features ### Flutter Web Application (Port 8081) - Modern, responsive UI for managing action mappings - Action picker dialog with full parameter configuration - Support for both GSC (GeViScope) and G-Core server actions - Consistent UI for input and output actions with edit/delete capabilities - Real-time action mapping creation, editing, and deletion - Server categorization (GSC: prefix for GeViScope, G-Core: prefix for G-Core servers) ### FastAPI REST Backend (Port 8000) - RESTful API for action mapping CRUD operations - Action template service with comprehensive action catalog (247 actions) - Server management (G-Core and GeViScope servers) - Configuration tree reading and writing - JWT authentication with role-based access control - PostgreSQL database integration ### C# SDK Bridge (gRPC, Port 50051) - Native integration with GeViSoft SDK (GeViProcAPINET_4_0.dll) - Action mapping creation with correct binary format - Support for GSC and G-Core action types - Proper Camera parameter inclusion in action strings (fixes CrossSwitch bug) - Action ID lookup table with server-specific action IDs - Configuration reading/writing via SetupClient ## Bug Fixes - **CrossSwitch Bug**: GSC and G-Core actions now correctly display camera/PTZ head parameters in GeViSet - Action strings now include Camera parameter: `@ PanLeft (Comment: "", Camera: 101028)` - Proper filter flags and VideoInput=0 for action mappings - Correct action ID assignment (4198 for GSC, 9294 for G-Core PanLeft) ## Technical Stack - **Frontend**: Flutter Web, Dart, Dio HTTP client - **Backend**: Python FastAPI, PostgreSQL, Redis - **SDK Bridge**: C# .NET 8.0, gRPC, GeViSoft SDK - **Authentication**: JWT tokens - **Configuration**: GeViSoft .set files (binary format) ## Credentials - GeViSoft/GeViScope: username=sysadmin, password=masterkey - Default admin: username=admin, password=admin123 ## Deployment All services run on localhost: - Flutter Web: http://localhost:8081 - FastAPI: http://localhost:8000 - SDK Bridge gRPC: localhost:50051 - GeViServer: localhost (default port) Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
403 lines
12 KiB
C#
403 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using GeViSetEditor.Core.Models;
|
|
|
|
namespace GeViSetEditor.Core.Parsers
|
|
{
|
|
/// <summary>
|
|
/// Complete binary parser that understands the entire .set file structure
|
|
/// Parses ALL sections, items, and rules for full JSON serialization
|
|
/// </summary>
|
|
public class CompleteBinaryParser
|
|
{
|
|
private byte[] _data;
|
|
private int _position;
|
|
private Dictionary<int, string> _debugLog = new();
|
|
|
|
public SetFileComplete Parse(byte[] data)
|
|
{
|
|
_data = data;
|
|
_position = 0;
|
|
|
|
var setFile = new SetFileComplete
|
|
{
|
|
FileSize = data.Length,
|
|
RawData = data // Keep original for verification
|
|
};
|
|
|
|
Console.WriteLine($"=== Complete Binary Parser ===");
|
|
Console.WriteLine($"File size: {data.Length:N0} bytes\n");
|
|
|
|
try
|
|
{
|
|
// Parse header
|
|
ParseHeader(setFile);
|
|
|
|
// Parse all sections
|
|
ParseAllSections(setFile);
|
|
|
|
Console.WriteLine($"\n=== Parsing Complete ===");
|
|
Console.WriteLine($"Sections parsed: {setFile.Sections.Count}");
|
|
Console.WriteLine($"Total items: {setFile.Sections.Sum(s => s.Items.Count)}");
|
|
Console.WriteLine($"Total rules: {setFile.Sections.Sum(s => s.Rules.Count)}");
|
|
|
|
return setFile;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"\nERROR at position {_position} (0x{_position:X}): {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void ParseHeader(SetFileComplete setFile)
|
|
{
|
|
Console.WriteLine("Parsing header...");
|
|
|
|
// Optional null byte
|
|
if (_position < _data.Length && _data[_position] == 0x00)
|
|
{
|
|
setFile.HeaderNullByte = true;
|
|
_position++;
|
|
}
|
|
|
|
// Header is length-prefixed string (NOT Pascal string with 0x07)
|
|
if (_position + 2 > _data.Length)
|
|
throw new Exception("File too short for header");
|
|
|
|
byte headerLen = _data[_position++];
|
|
if (headerLen == 0 || headerLen > 50 || _position + headerLen > _data.Length)
|
|
throw new Exception($"Invalid header length: {headerLen}");
|
|
|
|
setFile.Header = Encoding.UTF8.GetString(_data, _position, headerLen);
|
|
_position += headerLen;
|
|
|
|
Console.WriteLine($" Header: '{setFile.Header}'");
|
|
Console.WriteLine($" Position after header: {_position} (0x{_position:X})\n");
|
|
}
|
|
|
|
private void ParseAllSections(SetFileComplete setFile)
|
|
{
|
|
Console.WriteLine("Parsing sections...\n");
|
|
|
|
int sectionCount = 0;
|
|
int maxIterations = 10000; // Safety limit
|
|
|
|
while (_position < _data.Length - 10 && maxIterations-- > 0)
|
|
{
|
|
int sectionStart = _position;
|
|
|
|
var section = TryParseSection();
|
|
if (section != null)
|
|
{
|
|
sectionCount++;
|
|
setFile.Sections.Add(section);
|
|
|
|
if (sectionCount <= 20 || section.Items.Count > 0 || section.Rules.Count > 0)
|
|
{
|
|
Console.WriteLine($"Section #{sectionCount}: '{section.Name}' " +
|
|
$"(offset {sectionStart}, {section.Items.Count} items, {section.Rules.Count} rules)");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Skip unknown byte
|
|
_position++;
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"\nTotal sections parsed: {sectionCount}");
|
|
}
|
|
|
|
private SectionComplete TryParseSection()
|
|
{
|
|
int startPos = _position;
|
|
|
|
try
|
|
{
|
|
// Try to read section name (Pascal string: 0x07 <len> <data>)
|
|
string sectionName = TryReadPascalString();
|
|
if (string.IsNullOrEmpty(sectionName) || sectionName.Length > 100)
|
|
{
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
|
|
var section = new SectionComplete
|
|
{
|
|
Name = sectionName,
|
|
FileOffset = startPos
|
|
};
|
|
|
|
// Parse section contents
|
|
ParseSectionContents(section);
|
|
|
|
section.FileEndOffset = _position;
|
|
return section;
|
|
}
|
|
catch
|
|
{
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void ParseSectionContents(SectionComplete section)
|
|
{
|
|
int maxIterations = 1000;
|
|
bool foundRules = false;
|
|
|
|
while (_position < _data.Length - 10 && maxIterations-- > 0)
|
|
{
|
|
int itemStart = _position;
|
|
|
|
// Check for "Rules" marker (length-prefixed, no 0x07)
|
|
if (CheckForRulesMarker())
|
|
{
|
|
foundRules = true;
|
|
ParseRulesSubsection(section);
|
|
break; // Rules typically end a section
|
|
}
|
|
|
|
// Try to parse a config item
|
|
var item = TryParseConfigItem();
|
|
if (item != null)
|
|
{
|
|
section.Items.Add(item);
|
|
continue;
|
|
}
|
|
|
|
// Check if we've hit next section
|
|
if (IsLikelyNextSection())
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Move forward
|
|
_position++;
|
|
}
|
|
}
|
|
|
|
private bool CheckForRulesMarker()
|
|
{
|
|
// Pattern: 05 52 75 6C 65 73 = "Rules" (length 5 + data)
|
|
if (_position + 6 > _data.Length)
|
|
return false;
|
|
|
|
return _data[_position] == 0x05 &&
|
|
_data[_position + 1] == 0x52 && // 'R'
|
|
_data[_position + 2] == 0x75 && // 'u'
|
|
_data[_position + 3] == 0x6C && // 'l'
|
|
_data[_position + 4] == 0x65 && // 'e'
|
|
_data[_position + 5] == 0x73; // 's'
|
|
}
|
|
|
|
private void ParseRulesSubsection(SectionComplete section)
|
|
{
|
|
// Skip "Rules" marker
|
|
_position += 6;
|
|
|
|
// Skip metadata bytes (counts, flags, etc.)
|
|
while (_position < _data.Length && _data[_position] <= 0x04)
|
|
{
|
|
_position++;
|
|
}
|
|
|
|
// Parse action rules
|
|
int ruleCount = 0;
|
|
int maxRules = 100;
|
|
int consecutiveFails = 0;
|
|
|
|
while (maxRules-- > 0 && _position < _data.Length - 20)
|
|
{
|
|
var rule = TryParseActionRule();
|
|
if (rule != null)
|
|
{
|
|
rule.RuleId = ruleCount++;
|
|
section.Rules.Add(rule);
|
|
consecutiveFails = 0;
|
|
}
|
|
else
|
|
{
|
|
_position++;
|
|
consecutiveFails++;
|
|
if (consecutiveFails > 100) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private ActionRuleComplete TryParseActionRule()
|
|
{
|
|
int startPos = _position;
|
|
|
|
try
|
|
{
|
|
var rule = new ActionRuleComplete
|
|
{
|
|
FileOffset = startPos
|
|
};
|
|
|
|
// Try to read action string (07 01 40 <len_2bytes> <data>)
|
|
string actionString = TryReadActionString();
|
|
if (!string.IsNullOrEmpty(actionString))
|
|
{
|
|
rule.Actions.Add(actionString);
|
|
return rule;
|
|
}
|
|
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
catch
|
|
{
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private ConfigItemComplete TryParseConfigItem()
|
|
{
|
|
int startPos = _position;
|
|
|
|
try
|
|
{
|
|
// Read key (Pascal string)
|
|
string key = TryReadPascalString();
|
|
if (string.IsNullOrEmpty(key) || key.Length > 200)
|
|
{
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
|
|
// Read value (typed)
|
|
var (value, type) = TryReadTypedValue();
|
|
if (value == null)
|
|
{
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
|
|
return new ConfigItemComplete
|
|
{
|
|
Key = key,
|
|
Value = value,
|
|
ValueType = type,
|
|
FileOffset = startPos
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
_position = startPos;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string TryReadPascalString()
|
|
{
|
|
if (_position + 2 > _data.Length || _data[_position] != 0x07)
|
|
return null;
|
|
|
|
byte length = _data[_position + 1];
|
|
if (length == 0 || length > 200 || _position + 2 + length > _data.Length)
|
|
return null;
|
|
|
|
string result = Encoding.UTF8.GetString(_data, _position + 2, length);
|
|
_position += 2 + length;
|
|
return result;
|
|
}
|
|
|
|
private string TryReadActionString()
|
|
{
|
|
// Pattern: 07 01 40 <len_2bytes_LE> <data>
|
|
if (_position + 5 > _data.Length)
|
|
return null;
|
|
|
|
if (_data[_position] != 0x07 ||
|
|
_data[_position + 1] != 0x01 ||
|
|
_data[_position + 2] != 0x40)
|
|
return null;
|
|
|
|
int length = BitConverter.ToUInt16(_data, _position + 3);
|
|
if (length == 0 || length > 500 || _position + 5 + length > _data.Length)
|
|
return null;
|
|
|
|
string result = Encoding.UTF8.GetString(_data, _position + 5, length);
|
|
_position += 5 + length;
|
|
|
|
// Validate it looks like an action
|
|
if (string.IsNullOrWhiteSpace(result) || result.Length < 3)
|
|
return null;
|
|
|
|
return result;
|
|
}
|
|
|
|
private (object value, string type) TryReadTypedValue()
|
|
{
|
|
if (_position >= _data.Length)
|
|
return (null, null);
|
|
|
|
byte typeMarker = _data[_position];
|
|
|
|
switch (typeMarker)
|
|
{
|
|
case 0x01: // Boolean
|
|
if (_position + 2 <= _data.Length)
|
|
{
|
|
_position++;
|
|
bool value = _data[_position++] != 0;
|
|
return (value, "boolean");
|
|
}
|
|
break;
|
|
|
|
case 0x04: // Integer (4 bytes, little-endian)
|
|
if (_position + 5 <= _data.Length)
|
|
{
|
|
_position++;
|
|
int value = BitConverter.ToInt32(_data, _position);
|
|
_position += 4;
|
|
return (value, "integer");
|
|
}
|
|
break;
|
|
|
|
case 0x07: // String (Pascal string)
|
|
string value_str = TryReadPascalString();
|
|
if (value_str != null)
|
|
{
|
|
return (value_str, "string");
|
|
}
|
|
break;
|
|
}
|
|
|
|
return (null, null);
|
|
}
|
|
|
|
private bool IsLikelyNextSection()
|
|
{
|
|
// Check if next bytes look like a new section (Pascal string)
|
|
if (_position + 3 >= _data.Length)
|
|
return true;
|
|
|
|
if (_data[_position] != 0x07)
|
|
return false;
|
|
|
|
byte length = _data[_position + 1];
|
|
if (length < 3 || length > 50)
|
|
return false;
|
|
|
|
// Check if it's printable ASCII (likely a section name)
|
|
for (int i = 0; i < Math.Min(5, (int)length); i++)
|
|
{
|
|
if (_position + 2 + i >= _data.Length)
|
|
return false;
|
|
|
|
byte b = _data[_position + 2 + i];
|
|
if (b < 32 || b > 126)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|