Files
geutebruck/GeViSetEditor/GeViSetEditor.Core/Parsers/CompleteBinaryParser.cs
Administrator 14893e62a5 feat: Geutebruck GeViScope/GeViSoft Action Mapping System - MVP
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>
2025-12-31 18:10:54 +01:00

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;
}
}
}