using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace PagerParser; public enum AlertLevel { Code1 = 1, Code3 = 3 } [Flags] public enum EmergencyService { Ambulance = 0x01, Fire = 0x02, Police = 0x04, Rescue = 0x08, SES = 0x10 } [Index(nameof(Message))] public class PagerMessage { [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int PagerMessageId { get; set; } public DateTime Timestamp { get; set; } public string Message { get; set; } public virtual ParsedPagerMessage? ParsedMessage { get; set; } public override int GetHashCode() => (Timestamp, Message).GetHashCode(); } public enum MapType { Melways = 1, SpacialVisionCentral = 2, SpacialVisionNorthEast = 3, SpacialVisionNorthWest = 4, SpacialVisionSouthEast = 5, SpacialVisionSouthWest = 6 } [Index(nameof(FirecomJobNo))] [Index(nameof(PageDestination))] public class ParsedPagerMessage { [JsonIgnore] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ParsedPagerMessageId { get; set; } public int FirecomJobNo { get; set; } public string AssignmentArea { get; set; } public string JobType { get; set; } public AlertLevel AlertLevel { get; set; } public string Description { get; set; } public MapType? MapType { get; set; } public int? MapNo { get; set; } public string? MapGrid { get; set; } public int? GridReference { get; set; } public EmergencyService AttendingServices { get; set; } public string? Note { get; set; } public int? FireGroundChannel { get; set; } public List PagedServices { get; set; } = new(); public string PageDestination { get; set; } public virtual GpsPosition? GpsPosition { get; set; } [JsonIgnore] [ForeignKey(nameof(PagerMessage))] public virtual PagerMessage OriginalMessage { get; set; } } public interface IPagerMessageParserService { public ParsedPagerMessage Parse(string message); public ParsedPagerMessage? TryParse(string message); } // We run the pager message parser as a service with a single instance // as this allows us to pre-compile the parsing regular expression // which will help when potentially processing 1000's of pager messages. // // Running a single instance of the service isn't an issue, as this // service is only used by the fetch service for parsing new messages. public class PagerMessageParserService : IPagerMessageParserService { public static readonly Dictionary MapTypeMap = new() { { "M", MapType.Melways }, { "SVC", MapType.SpacialVisionCentral }, { "SVNE", MapType.SpacialVisionNorthEast }, { "SVNW", MapType.SpacialVisionNorthWest }, { "SVSE", MapType.SpacialVisionSouthEast }, { "SVSW", MapType.SpacialVisionSouthWest }, }; private const string Pattern = @"^@@ALERT\s+([A-Z]*[0-9]+)\s+([A-Z&]+)C([13])\s+(\*\s+)?(.*)\s+(M|SVC|SVNE|SVNW|SVSE|SVSW)\s+(\d+)\s+([A-Z]\d+)\s+\((\d+)\)\s+(\*\s+[^*]+\*\s+)?([AFPRS]+)\s+(([A-Z0-9]+\s)+)F([0-9]+)\s+\[([A-Z0-9]+)[_]?\]$"; private Regex pageMessageRegex = new Regex(Pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); private Regex firegroundRegex = new Regex("^FGD([0-9]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public ParsedPagerMessage Parse(string message) { ParsedPagerMessage m = new(); var match = pageMessageRegex.Match(message); if(!match.Success) throw new ArgumentException(); // Parse non-optional message components m.AssignmentArea = match.Groups[1].Value; m.JobType = match.Groups[2].Value; m.Description = match.Groups[5].Value; m.FirecomJobNo = Int32.Parse(match.Groups[14].Value); m.PageDestination = match.Groups[15].Value; // Parse optional message components if(match.Groups[7].Success) m.MapNo = Int32.Parse(match.Groups[7].Value); if(match.Groups[8].Success) m.MapGrid = match.Groups[8].Value; if(match.Groups[9].Success) m.GridReference = Int32.Parse(match.Groups[9].Value); if(match.Groups[10].Success) m.Note = match.Groups[10].Value; // Parse map type using map type dictionary if(match.Groups[6].Success) { MapTypeMap.TryGetValue(match.Groups[6].Value, out var mapType); m.MapType = mapType; } var pagedServicesField = match.Groups[12].Value.Split(" "); // Attempt to extract the fire-ground channel as it can be mixed // into the list of paged services. m.FireGroundChannel = pagedServicesField .Select(x => firegroundRegex.Match(x)) .Where(m => m.Success) .Select(m => (int?) int.Parse(m.Groups[1].Value)) .DefaultIfEmpty(null) .First(); // Parse the paged services, ignoring the fireground channel m.PagedServices = pagedServicesField .Where(x => !firegroundRegex.IsMatch(x)) .Where(x => !string.IsNullOrEmpty(x)) .ToList(); switch(match.Groups[3].Value) { case "1": m.AlertLevel = AlertLevel.Code1; break; case "3": m.AlertLevel = AlertLevel.Code3; break; default: throw new ArgumentException(); } // Handle each character to construct a bitmap of attending services foreach(char c in match.Groups[11].Value) { switch(c) { case 'A': m.AttendingServices |= EmergencyService.Ambulance; break; case 'F': m.AttendingServices |= EmergencyService.Fire; break; case 'P': m.AttendingServices |= EmergencyService.Police; break; case 'R': m.AttendingServices |= EmergencyService.Rescue; break; case 'S': m.AttendingServices |= EmergencyService.SES; break; } } return m; } public ParsedPagerMessage? TryParse(string message) { try { return Parse(message); } catch { return null; } } }