SF6 — простой формат пакетов для передачи данных

В этой статье, мы расскажем о том, как разработали собственный простой формат пакетов для передачи данных и почему мы это сделали, а также покажем несколько реализаций структуры данных под разработанный формат.

О собственном формате пакетов данных мы задумались после создания ряда программно-аппаратных решений, которые в основном носили демонстрационный характер, но могли бы использоваться и в реальных проектах. Речь идет о двух проектах — спектроанализаторе и уловителе сигналов. В обоих случаях мы использовали внешнюю плату, данные с которой пересылались через COM-порт, и это в принципе работало. Проблема заключается в том, что использованная в реализации идея с разбиением одного крупного значения на байты будет работать только в идеальных условиях. И вот почему: разделенное на байты число не имеет четких признаков своего начала или конца, а потому при приеме может быть неверно интерпретировано принимающей стороной. Но это не единственная проблема — помимо отсутствия маркеров или разметки данных, довольно высока вероятность того, что даже при удачном стечении обстоятельств их приемник будет не в состоянии опознать поврежденные данные.

Со всем этим мы столкнулись почти сразу, но не придав этому значения и посчитав два проекта слишком учебными, решение проблемы мы обошли стороной. Но потом, одному из авторов данной статьи пришлось столкнуться с реальной проблемой — спектроанализатор принимал не те данные… И вот тогда мы задумались о собственном бинарном формате пакетов, достаточно простом, чтобы поместиться в микроконтроллер, и достаточно узнаваемым как человеком, так и компьютером. Формат пакетов был придуман достаточно быстро и получил название SF6 (Send Frame with 6 fields — SF6) и о нем мы расскажем далее.

Наш формат пакетов достаточно прост по своей структуре и является бинарным, но при этом он содержит хорошо узнаваемые данные (в форме байтов). Эти данные представляют собой последовательности байтов, в которых закодированы некоторые связные цепочки символов ASCII. Эти цепочки служат маркерами, за которые цепляется распознающий пакет код, и между этими цепочками нет никаких разделителей: ни новой строки, ни пробелов, ни символов табуляции. Такая структура делает формат менее избыточным, а в совокупности с его фиксированным размером в байтах, позволяет организовать очень простой и быстрый кодер/энкодер пакетов.

Пакет данных в формате SF6 представляет собой последовательность байтов размером в 292 байта и содержит в себе следующие поля: «магическое число», идентификаторы служебных секций, уникальный идентификатор пакета, «особый» идентификатор, заголовок начала секции данных, данные в байтовом виде и заголовок конца секции данных.

«Магическое число» — это сигнатура формата, опознавательный маркер пакета, который одновременно служит признаком начала очередной порции данных, т.е. фрейма данных. В нашем случае «магическое число» является комбинацией байтов, которую можно интерпретировать как строку из ASCII символов следующего вида: «SF6!». За сигнатурой идет первый маркер, который указывает на служебную информацию, и представляет собой последовательной байтов соответствующих ASCII строке вида «SF6_». Сразу за этим маркером идет поле, которое представляет собой уникальный идентификатор пакета, записанный последовательностью данных в обратном порядке. К примеру, идентификатор 0x01abcdef будет представлен последовательностью байтов [0xef, 0xcd, 0xab, 0x01]. Уникальный идентификатор пакета всегда имеет фиксированный размер в байтах,
и он равен 4.

После уникального идентификатора идет второй маркер, который является точно таким же, как и первый, и представляет собой ту же ASCII строку «SF6_». За вторым маркером идет «особый» идентификатор, который имеет фиксированный размер равный 4 байтам. Он также записывается в обратном порядке, и в изначальном варианте представлял собой 32-битный хэш от последовательности байтов внутри секции данных.

После всех служебных пометок идет заголовок секции данных, который представляет собой ASCII последовательность вида «SF6_@BDF» (BDF — Begin of Data Frame). За заголовком секции данных идут сами данные в байтовом виде и данные представлены в виде последовательности байтов размером ровно 256 байт. Размер секции фиксированный и не содержит в себе никаких разделителей между байтами — в этой секции они представлены непрерывным потоком. Завершает пакет заголовок конца секции данных, который является также ASCII строкой вида «SF6_@EDF» (EDF — End of Data Frame).

Структура простая и легко разбирается даже на слабых устройствах с ограниченным функционалом. Отметим также, что идентификатор пакета (id — identifier) и «особый» идентификатор (qn — qualified number) не регламентированы по использованию и могут быть использованы самым разным образом для передачи нужных служебных данных. Это позволяет использовать данные поля по усмотрению программиста и именно он придает им конкретный смысл, несмотря на то, что наличие id и qn обязательно во фрейме данных.

Реализован SF6 в виде структуры данных на D, которая хранит внутреннее содержание пакета в нескольких полях: id, qn, data и packet. К ним прилагаются сеттеры/геттеры позволяющие изменять/считывать содержимое пакета, при этом сеттеры принимают на вход массивы беззнаковых байтов (но, есть перегруженные варианты сеттеров для полей id и qn, принимающие на вход 32-битное значение), а геттеры возвращают такие же массивы.

Идеология работы со структурой данных SF6_Packet проста: c помощью сеттера setPacket в структуру вносятся байты, полученные из некоторого источника, затем запускается процедура decode, которая разбирает пакет на секции, и извлекаются байты данных с помощью геттера getData. Помимо такой работы со структурой возможно не только считывание пакета, но и его формирование, для чего предусмотрено использование сеттеров, устанавливающих отдельные компоненты пакета в нужные значения с последующим его созданием с помощью процедуры encode и извлечением полученного пакета с помощью геттера getPacket.

Структура SF6_Packet реализована в следующем коде на D:

import std.conv;

template addProperty(T, string propertyName, string defaultValue = T.init.to!string)
{
	import std.string : format, toLower;
 
	const char[] addProperty = format(
		`
		private %2$s %1$s = %4$s;
 
		void set%3$s(%2$s %1$s)
		{
			this.%1$s = %1$s;
		}
 
		%2$s get%3$s()
		{
			return %1$s;
		}
		`,
		"_" ~ propertyName.toLower,
		T.stringof,
		propertyName,
		defaultValue
		);
}

/+
 + Simple Frame protocol with 6 fields (SF6)
 + 
 +  Protocol structure:
 + 		magic number: "SF6!"
 + 		system information marker: "SF6_"
 + 		id: 4 byte value
 + 		system information marker: "SF6_"
 + 		qn: 4 byte value
 + 		begin data frame (BDF): "SF6_@BDF"
 + 		data: 256 bytes
 + 		end data frame (EDF): "SF6_@EDF"
 + 
 + 		id is identifier of packet (no reglament)
 + 		qn is another identifier for packet (no reglament)
 +/ 

class SF6_Packet
{
	private
	{
		// magic number is "SF6!"
		enum ubyte[4] MAGIC_NUMBER = [0x53, 0x46, 0x36, 0x21];
		// system fields is "SF6_"
		enum ubyte[4] SF = [0x53, 0x46, 0x36, 0x5f];
		// data begin marker is "SF6_@BDF"
		enum ubyte[8] BDF = [0x53, 0x46, 0x36, 0x5f, 0x40, 0x42, 0x44, 0x46];
		// data end marker is "SF6_@EDF"
		enum ubyte[8] EDF = [0x53, 0x46, 0x36, 0x5f, 0x40, 0x45, 0x44, 0x46];
		// data portion size (in bytes)
		enum DATA_LENGTH = 256;
		// packet size
		enum PACKET_SIZE = MAGIC_NUMBER.length + (2 * SF.length) + (2 * SF.length) + BDF.length + DATA_LENGTH + EDF.length;
	}
	
	mixin(addProperty!(ubyte[4], "ID"));
	mixin(addProperty!(ubyte[4], "QN"));
	mixin(addProperty!(ubyte[256], "Data"));
	mixin(addProperty!(ubyte[], "Packet"));
	
	private
	{
		// encode value
		auto fromUnsignedInteger(uint value)
		{
			 ubyte[4] data;
			 
			 data[0] = (value & 0xff);
			 data[1] = (value & (0xff << 8)) >> 8;
			 data[2] = (value & (0xff << 16)) >> 16;
			 data[3] = (value & (0xff << 24)) >> 24;
			 
			 return data;
		}
	}
	
	this()
	{
		
	}
	
	void setID(uint id)
	{
		_id = fromUnsignedInteger(id);
	}
	
	void setQN(uint qn)
	{
		_qn = fromUnsignedInteger(qn);
	}
	
	auto encode()
	{	
		_packet ~= MAGIC_NUMBER;
		_packet ~= SF;
		_packet ~= _id;
		_packet ~= SF;
		_packet ~= _qn;
		_packet ~= BDF;
		_packet ~= _data;
		_packet ~= EDF;
	}
	
	auto decode()
	{
		if (_packet.length == PACKET_SIZE)
		{
			auto magicNumber = _packet[0..MAGIC_NUMBER.length];
			auto edfNumber = _packet[$-EDF.length..$];
			
			if ((magicNumber == MAGIC_NUMBER) && (edfNumber == EDF))
			{
				auto idPosition = MAGIC_NUMBER.length + SF.length;
				auto qnPosition = idPosition + SF.length + 4;
				auto dataPosition = qnPosition + 4 + BDF.length;
				
				_id = _packet[idPosition..idPosition+SF.length];
				_qn = _packet[qnPosition..qnPosition+4];
				_data = _packet[dataPosition..dataPosition+DATA_LENGTH];
			}
		}
	}
	
	bool isValid()
	{
		return ((_packet[0..4] == MAGIC_NUMBER) &&
				(_packet[4..8] == SF) &&
				(_packet[12..16] == SF) &&
				(_packet[20..28] == BDF) &&
				(_packet[284..292] == EDF));
	}
}

Кроме этого, мы создали реализацию передатчика SF6 пакетов для Arduino (использовали Arduino MKR Zero). В этом случае плата получает данные от встроенного АЦП разрядностью 12 бит, разбивает полученные значения на два байта, упаковывает их в SF6 пакет и передает пакет по UART, который не задействован для общения платы с компьютером. Реализация выглядит так:

// send data via UART
void sendData(byte *data, unsigned int length)
{
	for (unsigned int i = 0; i < length; i++)
	{
		Serial1.write(data[i]);
	}
}

// encode unsigned long (is analog to D's uint) as array of bytes
void encodeUnsignedLong(byte *data, unsigned long value)
{
	data[0] = (value & 0xff);
	data[1] = (value & (0xff << 8)) >> 8;
	data[2] = (value & (0xff << 16)) >> 16;
	data[3] = (value & (0xff << 24)) >> 24;
}

// send SF6 data packet: data - array of bytes, id - identifier of packet, hash - data hash);
void send_SF6Packet(byte *data, unsigned int id, unsigned int hash)
{
	byte MAGIC_NUMBER[4] = {0x53, 0x46, 0x36, 0x21};
	// system fields is "SF6_"
	byte SF[4] = {0x53, 0x46, 0x36, 0x5f};
	// data begin marker is "SF6_@BDF"
	byte BDF[8] = {0x53, 0x46, 0x36, 0x5f, 0x40, 0x42, 0x44, 0x46};
	// data end marker is "SF6_@EDF"
	byte EDF[8] = {0x53, 0x46, 0x36, 0x5f, 0x40, 0x45, 0x44, 0x46};
	 
	byte ID[4], HASH[4];
	encodeUnsignedLong(ID, id);
	encodeUnsignedLong(HASH, hash);
	 
	sendData(MAGIC_NUMBER, 4);
	sendData(SF, 4);
	sendData(ID, 4);
	sendData(SF, 4);
	sendData(HASH, 4);
	sendData(BDF, 8);
	sendData(data, 256);
	sendData(EDF, 8);
}

// simple hashing (R.Pike, B.Kernigan "Practice of programming")
unsigned int simpleHash(byte *data, unsigned int length)
{
	unsigned int MULTIPLIER = 31;
	
	unsigned int h;

	for (unsigned int i = 0; i < length; i++)
	{
		h = h * MULTIPLIER + data[i];	
	}

	return h % length;
}

int id = 0;

void setup() {
	Serial1.begin(230400);
}

void loop() {
   
    byte data[256];
    analogReadResolution(12);
    for (unsigned long i = 0; i < 256; i += 2)
    {
        unsigned int d = analogRead(A1);
		
		data[i] = (byte) (d & 0x00ff);
		data[i+1] = (byte) ((d & 0xff00) >> 8);
    }
   
    unsigned int hash = simpleHash(data, 256);
   
    send_SF6Packet(data, id, hash);
    id++;
}

Для испытания концепции формата помимо платы Arduino MKR Zero была использована плата Sipeed Maix Bit на базе процессора архитектуры RISC-V (RV64IMAFDC) с прошитым в нее MaixPy (интерпретатор Python 3 на базе Micropython). Под эту плату был
написан код, который реализует прием пакетов SF6 от Arduino MKR Zero, разбор пакетов, выполнение быстрого преобразования Фурье для поступивших данных и вывод результата на миниатюрный LCD-экран размером 320×240. Код для платы Maix Bit:

class SF6_Packet:
    # signature
    MAGIC_NUMBER = bytes([0x53, 0x46, 0x36, 0x21])
    # system info marker
    SF = bytes([0x53, 0x46, 0x36, 0x5f])
    # begin data frame
    BDF = bytes([0x53, 0x46, 0x36, 0x5f, 0x40, 0x42, 0x44, 0x46])
    # end data frame
    EDF = bytes([0x53, 0x46, 0x36, 0x5f, 0x40, 0x45, 0x44, 0x46])
    # data frame size (in bytes)
    DATA_LENGTH = 256;
    # packet length (in bytes)
    PACKET_LENGTH = 292;

    def fromUnsignedInt(self, value):
        data = []
        data.append(value & 0xff)
        data.append((value & (0xff << 8)) >> 8)
        data.append((value & (0xff << 16)) >> 16)
        data.append((value & (0xff << 24)) >> 24)
        return bytes(data)

    def __init__(self):
        self._id  = []
        self._qn = []
        self._data = []
        self._packet = []
    def encode(self):
        self._packet.extend(self.MAGIC_NUMBER)
        self._packet.extend(self.SF)
        self._packet.extend(self._id)
        self._packet.extend(self.SF)
        self._packet.extend(self._qn)
        self._packet.extend(self.BDF)
        self._packet.extend(self._data)
        self._packet.extend(self.EDF)
    def decode(self):
        if (len(self._packet) == self.PACKET_LENGTH):
            magicNumber = self._packet[:len(self.MAGIC_NUMBER)]
            edfNumber = self._packet[len(self._packet) - len(self.EDF):]
            if ((magicNumber == self.MAGIC_NUMBER) and (edfNumber == self.EDF)):
                idPosition = len(self.MAGIC_NUMBER) + len(self.SF)
                qnPosition = idPosition + len(self.SF) + 4;
                dataPosition = qnPosition + 4 + len(self.BDF)
                self._id = self._packet[idPosition:idPosition+len(self.SF)];
                self._qn = self._packet[qnPosition:qnPosition+4];
                self._data = self._packet[dataPosition:dataPosition+self.DATA_LENGTH];

from fpioa_manager import fm, board_info
from machine import UART
from Maix import FFT

import image
import lcd

# Pin 15 - TX
fm.register (board_info.PIN15, fm.fpioa.UART1_TX)
# Pin 17 - RX
fm.register (board_info.PIN17, fm.fpioa.UART1_RX)
# UART: 230 400 baud
uart = UART(UART.UART1, 230400, timeout=1000)
# init packet class
sf6 = SF6_Packet()
data = []
lcd.init()
img = image.Image()
sample_points = 1024
FFT_points = 512
lcd_width = 320
lcd_height = 240
hist_num = FFT_points

if hist_num > 320:
    hist_num = 320
hist_width = int(320 / hist_num) #changeable
x_shift = 0

while True:
    while len(data) < sample_points:
        d = uart.read(292)
        if not (d is None):
            sf6._packet = d
            sf6.decode()
            p = sf6._data
            data.extend(p)
    FFT_res = FFT.run(bytearray(data), FFT_points)
    FFT_amp = FFT.amplitude(FFT_res)
    img = img.clear()
    x_shift = 0
    for i in range(hist_num):
            if FFT_amp[i] > 240:
                hist_height = 240
            else:
                hist_height = FFT_amp[i]
            img = img.draw_rectangle((x_shift,240-hist_height,hist_width,hist_height),[255,255,255],2,True)
            x_shift = x_shift + hist_width
    lcd.display(img)
    data = []

Выглядит это впечатляюще:

Но и на этом наша команда не остановилась: мы создали приемник пакетов SF6 и для компьютера, воспользовавшись D и его библиотекой из реестра dub под названием serialport. Для этого мы написали простую демонстрацию, в которой использована старая версия SF6, отличающаяся только наименованием служебного поля qn (в старой версии оно называется hash), и которая выглядит следующим образом:

#!/usr/bin/env dub
/+ dub.sdl:
	name "packets"
	dependency "serialport" version="~>2.2.3"
+/

import serialport;

import std.algorithm;
import std.format;
import std.range;
import std.stdio;
import std.string;

import std.conv;


template addProperty(T, string propertyName, string defaultValue = T.init.to!string)
{
	import std.string : format, toLower;
 
	const char[] addProperty = format(
		`
		private %2$s %1$s = %4$s;
 
		void set%3$s(%2$s %1$s)
		{
			this.%1$s = %1$s;
		}
 
		%2$s get%3$s()
		{
			return %1$s;
		}
		`,
		"_" ~ propertyName.toLower,
		T.stringof,
		propertyName,
		defaultValue
		);
}

class SF6_Packet
{
	private
	{
		// magic number is "SF6!"
		enum ubyte[4] MAGIC_NUMBER = [0x53, 0x46, 0x36, 0x21];
		// system fields is "SF6_"
		enum ubyte[4] SF = [0x53, 0x46, 0x36, 0x5f];
		// data begin marker is "SF6_@BDF"
		enum ubyte[8] BDF = [0x53, 0x46, 0x36, 0x5f, 0x40, 0x42, 0x44, 0x46];
		// data end marker is "SF6_@EDF"
		enum ubyte[8] EDF = [0x53, 0x46, 0x36, 0x5f, 0x40, 0x45, 0x44, 0x46];
		// data portion size (in bytes)
		enum DATA_LENGTH = 256;
		// packet size
		enum PACKET_SIZE = MAGIC_NUMBER.length + (2 * SF.length) + (2 * SF.length) + BDF.length + DATA_LENGTH + EDF.length;
	}
	
	private
	{
		mixin(addProperty!(ubyte[4], "ID"));
		mixin(addProperty!(ubyte[4], "Hash"));
		mixin(addProperty!(ubyte[256], "Data"));
		mixin(addProperty!(ubyte[], "Packet"));
		
		// encode value
		auto fromUnsignedInteger(uint value)
		{
			 ubyte[4] data;
			 
			 data[0] = (value & 0xff);
			 data[1] = (value & (0xff << 8)) >> 8;
			 data[2] = (value & (0xff << 16)) >> 16;
			 data[3] = (value & (0xff << 24)) >> 24;
			 
			 return data;
		}
	}
	
	this()
	{
		
	}
	
	void setID(uint id)
	{
		_id = fromUnsignedInteger(id);
	}
	
	void setHash(uint hash)
	{
		_hash = fromUnsignedInteger(hash);
	}
	
	auto encode()
	{	
		_packet ~= MAGIC_NUMBER;
		_packet ~= SF;
		_packet ~= _id;
		_packet ~= SF;
		_packet ~= _hash;
		_packet ~= BDF;
		_packet ~= _data;
		_packet ~= EDF;
	}
	
	auto decode()
	{
		if (_packet.length == PACKET_SIZE)
		{
			auto magicNumber = _packet[0..MAGIC_NUMBER.length];
			auto edfNumber = _packet[$-EDF.length..$];
			
			if ((magicNumber == MAGIC_NUMBER) && (edfNumber == EDF))
			{
				auto idPosition = MAGIC_NUMBER.length + SF.length;
				auto hashPosition = idPosition + SF.length + 4;
				auto dataPosition = hashPosition + 4 + BDF.length;
				
				_id = _packet[idPosition..idPosition+SF.length];
				_hash = _packet[hashPosition..hashPosition+4];
				_data = _packet[dataPosition..dataPosition+256];
			}
		}
	}
	
	bool isValid()
	{
		return ((_packet[0..4] == MAGIC_NUMBER) &&
				(_packet[4..8] == SF) &&
				(_packet[12..16] == SF) &&
				(_packet[20..28] == BDF) &&
				(_packet[284..292] == EDF));
	}
}

void main()
{
    enum PORT_SPEED = 230_400;
   
	SerialPortBlk device = new SerialPortBlk("/dev/ttyACM0", PORT_SPEED);
	device.config = SPConfig(PORT_SPEED, DataBits.data8, Parity.none, StopBits.one);
	
	ubyte[292] buffer;
	device.read(buffer);
   
	auto p = new SF6_Packet;
	p.setPacket(buffer);
	p.decode;
	p.getID.writeln;
	p.getHash.writeln;
	p.getData.writeln;
	p.getPacket.writeln;
	p.isValid.writeln;
	
	float[] data;
	auto tmp = p.getData;
	
	for (int i = 0; i < 256; i += 2)
	{
		uint value;
		value |= tmp[i];
		value |= (tmp[i+1] << 8);
		data ~= value;
		data ~= (value / 1024.0) * 3.3;
	}
	
	data.writeln;
}

Запускается демонстрация командой:
dub run --single packets2.d

Реализация пакетов SF6 оказалась очень простой и доступной, что позволило нам использовать её для своих проектов. Описанная выше проблема со спектроанализатором была решена в тот же день, когда была написана первая версия формата SF6, и потребовала незначительной модификации кода с добавлением файла sf6.d. Это событие нас очень порадовало и поэтому мы решили рассказать о собственном формате пакетов, который пусть и является экспериментальным, но все же может быть использован и вами, уважаемые читатели…

Добавить комментарий