Como você resolveria este problema ao analisar os dados?
-
03-07-2019 - |
Pergunta
Esta é uma longa pergunta, então por favor nua comigo.
Estamos implementando um emulador para uma peça de hardware que está a ser desenvolvida ao mesmo tempo. A idéia é dar 3 partes uma solução de software para testar seu software cliente e dar o hardware desenvolvedores um ponto de referência para implementar seu firmware.
As pessoas que escreveram o protocolo para o hardware usado um costume versão do Sun XDR chamado INCA_XDR. É uma ferramenta para serializar e mensagens de-serialize. Ele é escrito em C e queremos evitar qualquer código nativo por isso estamos a analisar os dados de protocolo manualmente.
O protocolo é por natureza bastante complexa e os pacotes de dados pode ter muitas estruturas diferentes, mas sempre tem a mesma estrutura global:
[cabeça] [INTRO] [DATA] [CAUDA]
[HEAD] =
byte sync 0x03
byte length X [MSB] X = length of [HEADER] + [INTRO] + [DATA]
byte length X [LSB] X = length of [HEADER] + [INTRO] + [DATA]
byte check X [MSB] X = crc of [INTRO] [DATA]
byte check X [LSB] X = crc of [INTRO] [DATA]
byte headercheck X X = XOR over [SYNC] [LENGTH] [CHECK]
[INTRO]
byte version 0x03
byte address X X = 0 for point-to-point, 1-254 for specific controller, 255 = broadcast
byte sequence X X = sequence number
byte group X [MSB] X = The category of the message
byte group X [LSB] X = The category of the message
byte type X [MSB] X = The id of the message
byte type X [LSB] X = The id of the message
[DATA] =
The actuall data for the specified message,
this format really differs a lot.
It always starts with a DRCode which is one byte.
It more or less specifies the general structure of
the data, but even within the same structure the data
can mean many different things and have different lenghts.
(I think this is an artifact of the INCA_XDR tool)
[TAIL] =
byte 0x0D
Como você pode ver, há uma grande quantidade de dados gerais, mas isso é porque o protocolo precisa trabalhar com ambos RS232 (ponto-a-multiponto) e TCP / IP (p2p).
name size value
drcode 1 1
name 8 contains a name that can be used as a file name (only alphanumeric characters allowed)
timestamp 14 yyyymmddhhmmss contains timestamp of bitmap library
size 4 size of bitmap library to be loaded
options 1 currently no options
Ou pode ter uma estrutura totalmente diferente:
name size value
drcode 1 2
lastblock 1 0 - 1 1 indicates last block. Firmware can be stored
blocknumber 2 Indicates block of firmware
blocksize 2 N size of block to load
blockdata N data of block of firmware
Às vezes é apenas um DRCode e nenhum dado adicional.
Com base no grupo eo campo Tipo, o emulador precisa para executar determinadas ações. Então, primeiro vamos olhar para aqueles dois campos e com base no que sabemos o que esperar dos dados e tem que analisá-lo corretamente.
Em seguida, os dados de resposta precisa ser gerado, que tem novamente muitas estruturas de dados diferentes. Algumas mensagens simplesmente gerar um ACK ou NACK mensagem, enquanto outros geram uma resposta real com dados.
Nós decidimos quebrar as coisas em pequenos pedaços.
Em primeiro lugar, há o IDataProcessor.
As classes que implementam esta interface são responsáveis para a validação de dados em bruto e a geração de instâncias de classe de mensagem. Eles não são responsáveis ??por commmunication, eles estão simplesmente passou um byte []
-primas meios de validação de dados verificando o cabeçalho para erros de soma de verificação, CRC e comprimento.
A mensagem resultante é passada para uma classe que implementa IMessageProcessor. Mesmo se os dados brutos foi considerada inválida, porque o IDataProcessor não tem noção de mensagens de resposta ou qualquer outra coisa, tudo que faz é validar os dados em bruto.
Para informar o IMessageProcessor sobre erros, foram adicionadas algumas propriedades adicionais para a classe mensagem:
bool nakError = false;
bool tailError = false;
bool crcError = false;
bool headerError = false;
bool lengthError = false;
Eles não estão relacionados com o protocolo e só existe para o IMessageProcessor
O IMessageProcessor é onde o trabalho real é feito. Por causa de todos os diferentes grupos de mensagens e tipos decidi uso F # para implementar a interface IMessageProcessor porque correspondência de padrões parecia uma boa maneira de lotes Evitar de if / else e de castas aninhados. (Eu não tenho nenhuma experiência prévia com F # ou mesmo outras que LINQ e SQL linguagens funcionais)
O IMessageProcessor analisa os dados e decide quais métodos ele deve chamar na IHardwareController. Pode parecer redundante ter IHardwareController, mas queremos ser capaz de trocá-lo com uma implementação diferente e não ser forçado a usar F # também. A implementação atual é um windows WPF, mas pode ser um # janela de cacau ou simplesmente um console por exemplo.
O IHardwareController também é responsável pela gestão do estado, porque os desenvolvedores devem ser capazes de manipular parâmetros de hardware e erros através da interface do usuário.
Assim, uma vez que o IMessageProcessor chamou os métodos corretos sobre IHardwareController, ele tem que gerar a mensagem de resposta. De novo ... os dados dessas mensagens de resposta pode ter muitas estruturas diferentes.
Eventualmente um IDataFactory é usado para converter a mensagem para os dados de protocolo matérias pronto para ser enviado para qualquer classe é responsável pela comunicação. (Encapsulamento adicional dos dados pode ser necessário, por exemplo)
Isto não é nada "dura" sobre como escrever esse código, mas todos os diferentescomandos e estruturas de dados requerem lotes e lotes de código e são poucos coisas que podemos reutilizar. (Pelo menos tanto quanto eu posso ver agora, esperando que alguém pode me provar que estou errado)
Esta é a primeira vez que eu uso F #, então eu estou realmente aprendendo como eu ir. O código abaixo é longe de terminar e, provavelmente, parece uma bagunça gigante. Ele só implementa um punhado de todas as mensagens no protocolo e posso dizer-lhe há muitos e muitos deles. Portanto, este arquivo vai ficar enorme!
Importante saber: a ordem de bytes é invertida sobre o fio (razões históricas)
module Arendee.Hardware.MessageProcessors
open System;
open System.Collections
open Arendee.Hardware.Extenders
open Arendee.Hardware.Interfaces
open System.ComponentModel.Composition
open System.Threading
open System.Text
let VPL_NOERROR = (uint16)0
let VPL_CHECKSUM = (uint16)1
let VPL_FRAMELENGTH = (uint16)2
let VPL_OUTOFSEQUENCE = (uint16)3
let VPL_GROUPNOTSUPPORTED = (uint16)4
let VPL_REQUESTNOTSUPPORTED = (uint16)5
let VPL_EXISTS = (uint16)6
let VPL_INVALID = (uint16)7
let VPL_TYPERROR = (uint16)8
let VPL_NOTLOADING = (uint16)9
let VPL_NOTFOUND = (uint16)10
let VPL_OUTOFMEM = (uint16)11
let VPL_INUSE = (uint16)12
let VPL_SIZE = (uint16)13
let VPL_BUSY = (uint16)14
let SYNC_BYTE = (byte)0xE3
let TAIL_BYTE = (byte)0x0D
let MESSAGE_GROUP_VERSION = 3uy
let MESSAGE_GROUP = 701us
[<Export(typeof<IMessageProcessor>)>]
type public StandardMessageProcessor() = class
let mutable controller : IHardwareController = null
interface IMessageProcessor with
member this.ProcessMessage m : Message =
printfn "%A" controller.Status
controller.Status <- ControllerStatusExtender.DisableBit(controller.Status,ControllerStatus.Nak)
match m with
| m when m.LengthError -> this.nakResponse(m,VPL_FRAMELENGTH)
| m when m.CrcError -> this.nakResponse(m,VPL_CHECKSUM)
| m when m.HeaderError -> this.nakResponse(m,VPL_CHECKSUM)
| m -> this.processValidMessage m
| _ -> null
member public x.HardwareController
with get () = controller
and set y = controller <- y
end
member private this.processValidMessage (m : Message) =
match m.Intro.MessageGroup with
| 701us -> this.processDefaultGroupMessage(m);
| _ -> this.nakResponse(m, VPL_GROUPNOTSUPPORTED);
member private this.processDefaultGroupMessage(m : Message) =
match m.Intro.MessageType with
| (1us) -> this.firmwareVersionListResponse(m) //ListFirmwareVersions 0
| (2us) -> this.StartLoadingFirmwareVersion(m) //StartLoadingFirmwareVersion 1
| (3us) -> this.LoadFirmwareVersionBlock(m) //LoadFirmwareVersionBlock 2
| (4us) -> this.nakResponse(m, VPL_FRAMELENGTH) //RemoveFirmwareVersion 3
| (5us) -> this.nakResponse(m, VPL_FRAMELENGTH) //ActivateFirmwareVersion 3
| (12us) -> this.nakResponse(m,VPL_FRAMELENGTH) //StartLoadingBitmapLibrary 2
| (13us) -> this.nakResponse(m,VPL_FRAMELENGTH) //LoadBitmapLibraryBlock 2
| (21us) -> this.nakResponse(m, VPL_FRAMELENGTH) //ListFonts 0
| (22us) -> this.nakResponse(m, VPL_FRAMELENGTH) //LoadFont 4
| (23us) -> this.nakResponse(m, VPL_FRAMELENGTH) //RemoveFont 3
| (24us) -> this.nakResponse(m, VPL_FRAMELENGTH) //SetDefaultFont 3
| (31us) -> this.nakResponse(m, VPL_FRAMELENGTH) //ListParameterSets 0
| (32us) -> this.nakResponse(m, VPL_FRAMELENGTH) //LoadParameterSets 4
| (33us) -> this.nakResponse(m, VPL_FRAMELENGTH) //RemoveParameterSet 3
| (34us) -> this.nakResponse(m, VPL_FRAMELENGTH) //ActivateParameterSet 3
| (35us) -> this.nakResponse(m, VPL_FRAMELENGTH) //GetParameterSet 3
| (41us) -> this.nakResponse(m, VPL_FRAMELENGTH) //StartSelfTest 0
| (42us) -> this.returnStatus(m) //GetStatus 0
| (43us) -> this.nakResponse(m, VPL_FRAMELENGTH) //GetStatusDetail 0
| (44us) -> this.ResetStatus(m) //ResetStatus 5
| (45us) -> this.nakResponse(m, VPL_FRAMELENGTH) //SetDateTime 6
| (46us) -> this.nakResponse(m, VPL_FRAMELENGTH) //GetDateTime 0
| _ -> this.nakResponse(m, VPL_REQUESTNOTSUPPORTED)
(* The various responses follow *)
//Generate a NAK response
member private this.nakResponse (message : Message , error) =
controller.Status <- controller.Status ||| ControllerStatus.Nak
let intro = new MessageIntro()
intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
intro.Address <- message.Intro.Address
intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
intro.MessageGroup <- MESSAGE_GROUP
intro.MessageType <- 130us
let errorBytes = UShortExtender.ToIntelOrderedByteArray(error)
let data = Array.zero_create(5)
let x = this.getStatusBytes
let y = this.getStatusBytes
data.[0] <- 7uy
data.[1..2] <- this.getStatusBytes
data.[3..4] <- errorBytes
let header = this.buildHeader intro data
let message = new Message()
message.Header <- header
message.Intro <- intro
message.Tail <- TAIL_BYTE
message.Data <- data
message
//Generate an ACK response
member private this.ackResponse (message : Message) =
let intro = new MessageIntro()
intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
intro.Address <- message.Intro.Address
intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
intro.MessageGroup <- MESSAGE_GROUP
intro.MessageType <- 129us
let data = Array.zero_create(3);
data.[0] <- 0x05uy
data.[1..2] <- this.getStatusBytes
let header = this.buildHeader intro data
message.Header <- header
message.Intro <- intro
message.Tail <- TAIL_BYTE
message.Data <- data
message
//Generate a ReturnFirmwareVersionList
member private this.firmwareVersionListResponse (message : Message) =
//Validation
if message.Data.[0] <> 0x00uy then
this.nakResponse(message,VPL_INVALID)
else
let intro = new MessageIntro()
intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
intro.Address <- message.Intro.Address
intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
intro.MessageGroup <- MESSAGE_GROUP
intro.MessageType <- 132us
let firmwareVersions = controller.ReturnFirmwareVersionList();
let firmwareVersionBytes = BitConverter.GetBytes((uint16)firmwareVersions.Count) |> Array.rev
//Create the data
let data = Array.zero_create(3 + (int)firmwareVersions.Count * 27)
data.[0] <- 0x09uy //drcode
data.[1..2] <- firmwareVersionBytes //Number of firmware versions
let mutable index = 0
let loops = firmwareVersions.Count - 1
for i = 0 to loops do
let nameBytes = ASCIIEncoding.ASCII.GetBytes(firmwareVersions.[i].Name) |> Array.rev
let timestampBytes = this.getTimeStampBytes firmwareVersions.[i].Timestamp |> Array.rev
let sizeBytes = BitConverter.GetBytes(firmwareVersions.[i].Size) |> Array.rev
data.[index + 3 .. index + 10] <- nameBytes
data.[index + 11 .. index + 24] <- timestampBytes
data.[index + 25 .. index + 28] <- sizeBytes
data.[index + 29] <- firmwareVersions.[i].Status
index <- index + 27
let header = this.buildHeader intro data
message.Header <- header
message.Intro <- intro
message.Data <- data
message.Tail <- TAIL_BYTE
message
//Generate ReturnStatus
member private this.returnStatus (message : Message) =
//Validation
if message.Data.[0] <> 0x00uy then
this.nakResponse(message,VPL_INVALID)
else
let intro = new MessageIntro()
intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
intro.Address <- message.Intro.Address
intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
intro.MessageGroup <- MESSAGE_GROUP
intro.MessageType <- 131us
let statusDetails = controller.ReturnStatus();
let sizeBytes = BitConverter.GetBytes((uint16)statusDetails.Length) |> Array.rev
let detailBytes = ASCIIEncoding.ASCII.GetBytes(statusDetails) |> Array.rev
let data = Array.zero_create(statusDetails.Length + 5)
data.[0] <- 0x08uy
data.[1..2] <- this.getStatusBytes
data.[3..4] <- sizeBytes //Details size
data.[5..5 + statusDetails.Length - 1] <- detailBytes
let header = this.buildHeader intro data
message.Header <- header
message.Intro <- intro
message.Data <- data
message.Tail <- TAIL_BYTE
message
//Reset some status bytes
member private this.ResetStatus (message : Message) =
if message.Data.[0] <> 0x05uy then
this.nakResponse(message, VPL_INVALID)
else
let flagBytes = message.Data.[1..2] |> Array.rev
let flags = Enum.ToObject(typeof<ControllerStatus>,BitConverter.ToInt16(flagBytes,0)) :?> ControllerStatus
let retVal = controller.ResetStatus flags
if retVal <> 0x00us then
this.nakResponse(message,retVal)
else
this.ackResponse(message)
//StartLoadingFirmwareVersion (Ack/Nak)
member private this.StartLoadingFirmwareVersion (message : Message) =
if (message.Data.[0] <> 0x01uy) then
this.nakResponse(message, VPL_INVALID)
else
//Analyze the data
let name = message.Data.[1..8] |> Array.rev |> ASCIIEncoding.ASCII.GetString
let text = message.Data.[9..22] |> Array.rev |> Seq.map(fun x -> ASCIIEncoding.ASCII.GetBytes(x.ToString()).[0]) |> Seq.to_array |> ASCIIEncoding.ASCII.GetString
let timestamp = DateTime.ParseExact(text,"yyyyMMddHHmmss",Thread.CurrentThread.CurrentCulture)
let size = BitConverter.ToUInt32(message.Data.[23..26] |> Array.rev,0)
let overwrite =
match message.Data.[27] with
| 0x00uy -> false
| _ -> true
//Create a FirmwareVersion instance
let firmware = new FirmwareVersion();
firmware.Name <- name
firmware.Timestamp <- timestamp
firmware.Size <- size
let retVal = controller.StartLoadingFirmwareVersion(firmware,overwrite)
if retVal <> 0x00us then
this.nakResponse(message, retVal) //The controller denied the request
else
this.ackResponse(message);
//LoadFirmwareVersionBlock (ACK/NAK)
member private this.LoadFirmwareVersionBlock (message : Message) =
if message.Data.[0] <> 0x02uy then
this.nakResponse(message, VPL_INVALID)
else
//Analyze the data
let lastBlock =
match message.Data.[1] with
| 0x00uy -> false
| _true -> true
let blockNumber = BitConverter.ToUInt16(message.Data.[2..3] |> Array.rev,0)
let blockSize = BitConverter.ToUInt16(message.Data.[4..5] |> Array.rev,0)
let blockData = message.Data.[6..6 + (int)blockSize - 1] |> Array.rev
let retVal = controller.LoadFirmwareVersionBlock(lastBlock, blockNumber, blockSize, blockData)
if retVal <> 0x00us then
this.nakResponse(message, retVal)
else
this.ackResponse(message)
(* Helper methods *)
//We need to convert the DateTime instance to a byte[] understood by the device "yyyymmddhhmmss"
member private this.getTimeStampBytes (date : DateTime) =
let stringNumberToByte s = Byte.Parse(s.ToString()) //Casting to (byte) would give different results
let yearString = date.Year.ToString("0000")
let monthString = date.Month.ToString("00")
let dayString = date.Day.ToString("00")
let hourString = date.Hour.ToString("00")
let minuteString = date.Minute.ToString("00")
let secondsString = date.Second.ToString("00")
let y1 = stringNumberToByte yearString.[0]
let y2 = stringNumberToByte yearString.[1]
let y3 = stringNumberToByte yearString.[2]
let y4 = stringNumberToByte yearString.[3]
let m1 = stringNumberToByte monthString.[0]
let m2 = stringNumberToByte monthString.[1]
let d1 = stringNumberToByte dayString.[0]
let d2 = stringNumberToByte dayString.[1]
let h1 = stringNumberToByte hourString.[0]
let h2 = stringNumberToByte hourString.[1]
let min1 = stringNumberToByte minuteString.[0]
let min2 = stringNumberToByte minuteString.[1]
let s1 = stringNumberToByte secondsString.[0]
let s2 = stringNumberToByte secondsString.[1]
[| y1 ; y2 ; y3 ; y4 ; m1 ; m2 ; d1 ; d2 ; h1 ; h2 ; min1 ; min2 ; s1; s2 |]
//Sets the high bit of a byte to 1
member private this.setHigh (b : byte) : byte =
let array = new BitArray([| b |])
array.[7] <- true
let mutable converted = [| 0 |]
array.CopyTo(converted, 0);
(byte)converted.[0]
//Build the header of a Message based on Intro + Data
member private this.buildHeader (intro : MessageIntro) (data : byte[]) =
let headerLength = 7;
let introLength = 7;
let length = (uint16)(headerLength + introLength + data.Length)
let crcData = ByteArrayExtender.Concat(intro.GetRawData(),data)
let crcValue = ByteArrayExtender.CalculateCRC16(crcData)
let lengthBytes = UShortExtender.ToIntelOrderedByteArray(length);
let crcValueBytes = UShortExtender.ToIntelOrderedByteArray(crcValue);
let headerChecksum = (byte)(SYNC_BYTE ^^^ lengthBytes.[0] ^^^ lengthBytes.[1] ^^^ crcValueBytes.[0] ^^^ crcValueBytes.[1])
let header = new MessageHeader();
header.Sync <- SYNC_BYTE
header.Length <- length
header.HeaderChecksum <- headerChecksum
header.DataChecksum <- crcValue
header
member private this.getStatusBytes =
let l = controller.Status
let status = (uint16)controller.Status
let statusBytes = BitConverter.GetBytes(status);
statusBytes |> Array.rev
end
(Por favor note que na verdadeira fonte, as classes têm nomes diferentes, mais específico do que "Hardware")
Eu estou esperando por sugestões, maneiras de melhorar o código ou mesmo formas diferentes de lidar com o problema. Por exemplo, seria o uso de uma linguagem dinâmica, como IronPython facilitar as coisas, estou indo ao o caminho errado todos juntos. Qual é a sua experiência com problemas como este, o que você mudaria, evite, etc ....
Update:
Com base na resposta de Brian, eu anotado o seguinte:
type DrCode9Item = {Name : string ; Timestamp : DateTime ; Size : uint32; Status : byte}
type DrCode11Item = {Id : byte ; X : uint16 ; Y : uint16 ; SizeX : uint16 ; SizeY : uint16
Font : string ; Alignment : byte ; Scroll : byte ; Flash : byte}
type DrCode12Item = {Id : byte ; X : uint16 ; Y : uint16 ; SizeX : uint16 ; SizeY : uint16}
type DrCode14Item = {X : byte ; Y : byte}
type DRType =
| DrCode0 of byte
| DrCode1 of byte * string * DateTime * uint32 * byte
| DrCode2 of byte * byte * uint16 * uint16 * array<byte>
| DrCode3 of byte * string
| DrCode4 of byte * string * DateTime * byte * uint16 * array<byte>
| DrCode5 of byte * uint16
| DrCode6 of byte * DateTime
| DrCode7 of byte * uint16 * uint16
| DrCode8 of byte * uint16 * uint16 * uint16 * array<byte>
| DrCode9 of byte * uint16 * array<DrCode9Item>
| DrCode10 of byte * string * DateTime * uint32 * byte * array<byte>
| DrCode11 of byte * array<DrCode11Item>
| DrCode12 of byte * array<DrCode12Item>
| DrCode13 of byte * uint16 * byte * uint16 * uint16 * string * byte * byte
| DrCode14 of byte * array<DrCode14Item>
Eu poderia continuar fazendo isso para todos os tipos de DR (muito poucos), mas eu ainda não entendo como isso iria me ajudar. Eu li sobre ele em Wikibooks e Fundamentos da F #, mas algo não está clicando na minha cabeça ainda.
Update 2
Então, eu entendo que eu poderia fazer o seguinte:
let execute dr =
match dr with
| DrCode0(drCode) -> printfn "Do something"
| DrCode1(drCode, name, timestamp, size, options) -> printfn "Show the size %A" size
| _ -> ()
let date = DateTime.Now
let x = DrCode1(1uy,"blabla", date, 100ul, 0uy)
Mas quando a mensagem entra no IMessageProcessor, a escolha é feita ali mesmo que tipo de mensagem é ea função adequada é então chamado. O acima seria apenas ser código adicional, pelo menos é assim que entendo, então eu realmente deve estar faltando o ponto aqui ... mas eu não vê-lo.
execute x
Solução
Eu acho que F # é um ajuste natural para representar as mensagens neste domínio através sindicatos discriminados; Eu estou imaginando por exemplo.
type Message =
| Message1 of string * DateTime * int * byte //name,timestamp,size,options
| Message2 of bool * short * short * byte[] //last,blocknum,blocksize,data
...
, juntamente com métodos para analisar / Mensagens unparse de / para uma matriz de bytes. Como você diz, este trabalho é simples, apenas tedioso.
Eu sou menos clara sobre o processamento das mensagens, mas no geral com base na sua descrição parece que você tem uma alça sobre ele.
Estou um pouco preocupado com a sua 'flexibilidade ferramenta' - quais são suas limitações? (Por exemplo, .Net, tem de ser mantida por programadores que conhecem tecnologias X, Y, Z, deve atender a certos critérios perf, ...)
Outras dicas
Aqui está o meu 2 centavos (ressalva: Não conheço F #): Você tem um arquivo de entrada finamente especificado, mesmo com uma gramática completa. Você deseja mapear o conteúdo do arquivo de ações. Portanto, sugiro que você analisar o arquivo. F sendo uma linguagem funcional #, pode caber a técnica de análise chamado recursiva Descida de análise . O livro "Expert F #" contém uma discussão de análise descendente recursiva.