Files
geutebruck/GeViSetEditor/GeViSetEditor.Core/Parsers/SetFileParser.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

560 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq;
using GeViSetEditor.Core.Models;
namespace GeViSetEditor.Core.Parsers
{
/// <summary>
/// Parser for GeViSet .set configuration files (binary format)
/// Format: Proprietary binary with sections, rules, and action mappings
/// </summary>
public class SetFileParser
{
private byte[] _data;
private int _position;
public GeViSetConfiguration Parse(string filePath)
{
_data = File.ReadAllBytes(filePath);
_position = 0;
var config = new GeViSetConfiguration();
// Read header
if (_data[_position] == 0x00) _position++; // Skip initial marker
config.Header = ReadPascalString() ?? "GeViSoft Parameters";
Console.WriteLine($"Parsing {filePath} ({_data.Length:N0} bytes)");
Console.WriteLine($"Header: {config.Header}\n");
// Parse all sections until end of file
int sectionCount = 0;
while (_position < _data.Length - 20)
{
var section = TryParseSection();
if (section != null)
{
config.Sections.Add(section);
sectionCount++;
Console.WriteLine($"Section {sectionCount}: {section.Name} ({section.Items.Count} items, {section.Rules.Count} rules) - pos: {_position}");
}
else
{
_position++; // Skip unknown byte
}
}
Console.WriteLine($"\nTotal: {sectionCount} sections parsed");
// Second pass: Find all "Rules" markers and extract action mappings directly
Console.WriteLine($"\nSearching for Rules sections...");
ExtractRulesDirectly(config);
return config;
}
private void ExtractRulesDirectly(GeViSetConfiguration config)
{
// Search for pattern: 05 52 75 6C 65 73 = "05 Rules"
byte[] rulesPattern = new byte[] { 0x05, 0x52, 0x75, 0x6C, 0x65, 0x73 };
for (int i = 0; i < _data.Length - rulesPattern.Length; i++)
{
bool match = true;
for (int j = 0; j < rulesPattern.Length; j++)
{
if (_data[i + j] != rulesPattern[j])
{
match = false;
break;
}
}
if (match)
{
Console.WriteLine($" Found Rules pattern at offset {i} (0x{i:X})");
_position = i + rulesPattern.Length;
// Create or find a section for these rules
var rulesSection = config.Sections.FirstOrDefault(s => s.Name == "ActionMappings");
if (rulesSection == null)
{
rulesSection = new Section { Name = "ActionMappings" };
config.Sections.Add(rulesSection);
}
// Try to parse rules from this location
ParseRulesSection(rulesSection);
Console.WriteLine($" Extracted {rulesSection.Rules.Count} rules");
}
}
}
private Section TryParseSection()
{
int startPos = _position;
// Try to read section name
string sectionName = ReadPascalString();
if (string.IsNullOrEmpty(sectionName) || sectionName.Length > 50)
{
_position = startPos;
return null;
}
var section = new Section { Name = sectionName };
// Parse section contents
int maxIterations = 1000; // Safety limit
while (_position < _data.Length - 10 && maxIterations-- > 0)
{
// Check for "Rules" subsection (can be Pascal string OR length-prefixed)
int savePos = _position;
// Try Pascal string first (0x07 marker)
string possibleRules = ReadPascalString();
if (possibleRules == "Rules")
{
Console.WriteLine($" Found Rules subsection (Pascal) at offset {savePos} in section '{sectionName}'");
ParseRulesSection(section);
break; // Rules typically end a section
}
// Rewind and try length-prefixed format (no 0x07 marker)
_position = savePos;
possibleRules = ReadLengthPrefixedString();
if (possibleRules == "Rules")
{
Console.WriteLine($" Found Rules subsection (LenPrefix) at offset {savePos} in section '{sectionName}'");
ParseRulesSection(section);
break; // Rules typically end a section
}
// Neither worked, rewind
_position = savePos;
// Try to read a config item
var item = TryReadConfigItem();
if (item != null)
{
section.Items.Add(item);
}
else
{
// Check if we've hit the next section or end
if (IsLikelySectionStart())
break;
_position++;
}
}
return section;
}
private void ParseRulesSection(Section section)
{
int startRuleCount = section.Rules.Count;
int startPos = _position;
// Skip count/metadata bytes
while (_position < _data.Length && _data[_position] <= 0x04)
{
_position++;
}
Console.WriteLine($" ParseRulesSection: start={startPos}, after skip={_position}, bytes: {BitConverter.ToString(_data, _position, Math.Min(20, _data.Length - _position))}");
// Parse rules - keep trying until we stop finding action strings
int maxRules = 100;
int attempts = 0;
int consecutiveFails = 0;
while (maxRules-- > 0 && _position < _data.Length - 20)
{
int beforePos = _position;
var rule = TryParseRule();
attempts++;
if (rule != null)
{
section.Rules.Add(rule);
Console.WriteLine($" Found rule at pos {beforePos}, now at {_position}");
consecutiveFails = 0;
}
else
{
_position++;
consecutiveFails++;
// Stop if we've had too many consecutive failures
if (consecutiveFails > 100)
{
Console.WriteLine($" Too many consecutive failures at {_position}, stopping");
break;
}
}
if (attempts > 200) break; // Safety limit
}
Console.WriteLine($" ParseRulesSection complete: extracted {section.Rules.Count - startRuleCount} rules");
}
private ActionRule TryParseRule()
{
int startPos = _position;
// Look for rule marker: 01 <id>
if (_position + 2 >= _data.Length || _data[_position] != 0x01)
{
_position = startPos;
return null;
}
var rule = new ActionRule
{
RuleId = _data[_position + 1]
};
_position += 2;
// Skip size bytes (05 00 00 00 or 0a 00 00 00 patterns)
if (_position + 4 < _data.Length && (_data[_position] == 0x05 || _data[_position] == 0x0a))
{
_position += 4;
}
// Read trigger properties (fields starting with .)
for (int i = 0; i < 30; i++) // Max 30 properties
{
if (_position >= _data.Length) break;
// Check for property field (01 <len> .<name>)
if (_data[_position] == 0x01 && _position + 2 < _data.Length)
{
int len = _data[_position + 1];
if (len > 0 && len < 50 && _position + 2 + len < _data.Length)
{
string propName = Encoding.UTF8.GetString(_data, _position + 2, len);
_position += 2 + len;
// Read boolean value
if (_position < _data.Length)
{
bool value = _data[_position++] != 0;
if (propName.StartsWith("."))
{
rule.TriggerProperties[propName] = value;
}
}
}
else
{
break;
}
}
else
{
break;
}
}
// Read main action string (07 01 40 <len_2bytes> <data>)
string mainAction = ReadActionString();
if (!string.IsNullOrEmpty(mainAction))
{
rule.MainAction = mainAction;
}
// Skip metadata (04 02 40... patterns)
SkipMetadata();
// Look for nested Rules section (action variations)
int checkPos = _position;
string nestedRules = ReadPascalString();
if (nestedRules == "Rules")
{
ParseActionVariations(rule);
}
else
{
// Try length-prefixed format
_position = checkPos;
nestedRules = ReadLengthPrefixedString();
if (nestedRules == "Rules")
{
ParseActionVariations(rule);
}
else
{
_position = checkPos; // Rewind if not "Rules"
}
}
// Only return rule if it has meaningful data
if (!string.IsNullOrEmpty(rule.MainAction) || rule.TriggerProperties.Count > 0)
{
return rule;
}
_position = startPos;
return null;
}
private void ParseActionVariations(ActionRule rule)
{
// Skip count bytes
while (_position < _data.Length && _data[_position] <= 0x04)
{
_position++;
}
// Parse each variation
int maxVariations = 30;
while (maxVariations-- > 0 && _position < _data.Length - 20)
{
var variation = TryParseActionVariation();
if (variation != null)
{
rule.ActionVariations.Add(variation);
}
else
{
if (IsLikelySectionStart())
break;
// Look ahead for next variation
bool foundNext = false;
for (int i = 1; i < 20 && _position + i < _data.Length; i++)
{
if (_data[_position + i] == 0x01 && _position + i + 1 < _data.Length)
{
_position += i;
foundNext = true;
break;
}
}
if (!foundNext)
break;
}
}
}
private ActionVariation TryParseActionVariation()
{
int startPos = _position;
// Look for variation marker: 01 <id>
if (_position + 2 >= _data.Length || _data[_position] != 0x01)
{
_position = startPos;
return null;
}
var variation = new ActionVariation
{
VariationId = _data[_position + 1]
};
_position += 2;
// Skip size (05 00 00 00)
if (_position + 4 < _data.Length && _data[_position] == 0x05)
{
_position += 4;
}
// Read action string
variation.ActionString = ReadActionString() ?? "";
// Skip metadata
SkipMetadata();
// Read action type (GscAction, GCoreAction, etc.)
string actionType = ReadPascalString();
if (actionType != null && actionType.Contains("Action"))
{
variation.ActionType = actionType;
// Read full command (another action string)
variation.FullCommand = ReadActionString() ?? "";
// Read server type
variation.ServerType = ReadPascalString() ?? "";
// Try to read server name
int savePos = _position;
string serverName = ReadPascalString();
if (!string.IsNullOrEmpty(serverName) && serverName.Length < 100 && !serverName.StartsWith("."))
{
variation.ServerName = serverName;
}
else
{
_position = savePos; // Rewind if not a valid server name
}
}
// Only return if we got meaningful data
if (!string.IsNullOrEmpty(variation.ActionString))
{
return variation;
}
_position = startPos;
return null;
}
private ConfigItem TryReadConfigItem()
{
int startPos = _position;
string name = ReadPascalString();
if (string.IsNullOrEmpty(name) || name.Length > 100)
{
_position = startPos;
return null;
}
// Try to read value
object value = ReadValue();
if (value == null)
{
_position = startPos;
return null;
}
return new ConfigItem
{
Name = name,
Value = value,
Type = value switch
{
bool => ConfigValueType.Boolean,
int => ConfigValueType.Integer,
string => ConfigValueType.String,
_ => ConfigValueType.Binary
}
};
}
// Helper methods for reading binary data
private string ReadPascalString()
{
if (_position + 2 > _data.Length || _data[_position] != 0x07)
return null;
byte length = _data[_position + 1];
if (_position + 2 + length > _data.Length || length == 0)
return null;
string result = Encoding.UTF8.GetString(_data, _position + 2, length);
_position += 2 + length;
return result;
}
private string ReadLengthPrefixedString()
{
// Simpler format: just <length_byte> <string_data> (no 0x07 marker)
if (_position + 1 > _data.Length)
return null;
byte length = _data[_position];
if (length == 0 || length > 50 || _position + 1 + length > _data.Length)
return null;
string result = Encoding.UTF8.GetString(_data, _position + 1, length);
_position += 1 + length;
return result;
}
private string ReadActionString()
{
// Action strings: 07 01 40 <len_2bytes> <data>
if (_position + 5 > _data.Length ||
_data[_position] != 0x07 ||
_data[_position + 1] != 0x01 ||
_data[_position + 2] != 0x40)
return null;
int length = BitConverter.ToUInt16(_data, _position + 3);
if (_position + 5 + length > _data.Length || length > 500)
return null;
string result = Encoding.UTF8.GetString(_data, _position + 5, length);
_position += 5 + length;
return result;
}
private object ReadValue()
{
if (_position >= _data.Length)
return null;
byte type = _data[_position];
switch (type)
{
case 0x01: // Boolean
if (_position + 2 <= _data.Length)
{
_position++;
return _data[_position++] != 0;
}
break;
case 0x04: // Integer
if (_position + 5 <= _data.Length)
{
_position++;
int value = BitConverter.ToInt32(_data, _position);
_position += 4;
return value;
}
break;
case 0x07: // String
return ReadPascalString();
}
return null;
}
private void SkipMetadata()
{
// Skip common metadata patterns (04 02 40... and 04 0a...)
while (_position + 5 < _data.Length && _data[_position] == 0x04)
{
_position += 5;
}
}
private string PeekString(int length)
{
if (_position + length > _data.Length)
return "";
return Encoding.UTF8.GetString(_data, _position, length);
}
private bool IsLikelySectionStart()
{
if (_position + 10 >= _data.Length)
return true;
// Check for multiple nulls (section boundary)
int nullCount = 0;
for (int i = 0; i < Math.Min(10, _data.Length - _position); i++)
{
if (_data[_position + i] == 0x00)
nullCount++;
}
return nullCount >= 4;
}
}
}