Skip to content

Instantly share code, notes, and snippets.

@semiversus
Created May 21, 2019 11:14
Show Gist options
  • Save semiversus/840e671532c47fd4c99bdf8894d8dccd to your computer and use it in GitHub Desktop.
Save semiversus/840e671532c47fd4c99bdf8894d8dccd to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# HQ Protocol\n",
"\n",
"## Description\n",
"\n",
"The HighQ Protocol is used to communicate with the laser or with one of its\n",
"components. A communication is initiated by a dedicated master and is then\n",
"answered by the addressed slave.\n",
"\n",
"## Communication parameters\n",
"* 4800 Baud\n",
"* 8 Bit\n",
"* No Parity\n",
"* 1 Stop Bit\n",
"\n",
"Commonly used is the RS422 interface. Other interfaces are also possible.\n",
"\n",
"## Packet definition\n",
"\n",
"\n",
"| Index | Size | Name | Description\n",
"|---|---|---|---|\n",
"|   | 1 | SYN | Synchronization Byte (0x16) |\n",
"| 0 | 1 | STX | Start Byte (0x02) |\n",
"| 1 | 1 | LEN | Message Data Length N + 7 (for other fields except SYN) |\n",
"| 2 | 1 | SRC | ID of the Source Device |\n",
"| 3 | 1 | DST | ID of the Destination Device |\n",
"| 4 | 1 | CMD | Command Byte |\n",
"| 5 | N | DATA | Data Bytes, Maximum is 32 bytes |\n",
"| 5+N | 2 | CRC | Checksum IBM CRC16 (high byte first) |\n",
"\n",
"Each field of the packet explained\n",
"\n",
"### SYN\n",
"The synchronization byte is send before the actual packet and is not\n",
"part of the packet itself. So it doesn't count for the packet length\n",
"(LEN) and the CRC. Sync bytes are usually defined as optional and if they\n",
"are used there could also be multiple ones, BUT in the HighQ protocol has\n",
"to be exactly one sync byte before the actual packet.\n",
"\n",
"### STX\n",
"The start byte is the first byte in the packet and signalizes the\n",
"start of the packet.\n",
"\n",
"### LEN\n",
"The length byte specifies the length of the packet. Counted are\n",
"the following bytes: STX, LEN, SRC, DST, CMD, N DATA bytes and\n",
"2 bytes CRC. So this length is the amount of data bytes plus 7.\n",
"There is a restriction to maximum 32 data bytes, so the maximum\n",
"value for the LEN byte is 39.\n",
"\n",
"### SRC\n",
"Source ID. Master has the source id 0, if it is a request message. The\n",
"answer message has the source id of the addressed slave.\n",
"\n",
"### DST\n",
"Destination ID. Addressing the slave in a request message. The answer\n",
"message has usually a destination ID 0 (addressing the master).\n",
"If the request has a destination ID of 255, every slave is addressed.\n",
"This could be used, if only one slave is connected to a master and the\n",
"ID is not known.\n",
"\n",
"### CMD\n",
"Specifies the requested command the slave should process. The answer\n",
"message from the slave uses the same value. Some command values are\n",
"predefined and listed below.\n",
"\n",
"### DATA\n",
"Contains the message data for this particular command. In the request\n",
"message the master sends data to the slave and vice versa for the answer.\n",
"Some requests or answers will have no data. The data is usually represented\n",
"as big endian (high bytes first).\n",
"\n",
"### CRC\n",
"The CRC is calculated from STX to the end of DATA (or to CMD if no DATA is\n",
"available). The used CRC polynom is IBM-CRC-16 (x^16 + x^15 + x^2 + 1),\n",
"start value is 0.\n",
"\n",
"## Request Sequence\n",
"A request is initated by the PC using the source id `0`. Use the command byte to send\n",
"the requested command. May add additional data in the data field. The destionation field\n",
"contains the id of the selected slave device.\n",
"\n",
"Example of a request:\n",
"\n",
" PC -> Slave: 16 02 07 00 02 50 E8 79\n",
" Slave -> PC: 16 02 07 02 00 50 48 D9\n",
"\n",
"In this example the PC send a request to slave id 2 requesting the command 0x50. The slave answers with a packet containing the same command byte and no additional data.\n",
"\n",
"## Usage of hq_protocol c module\n",
"\n",
"### include hq_protocol.h\n",
"\n",
" #include \"hq_protocol.h\"\n",
"\n",
"### instantiate a `hq_protocol_stack_t` instance\n",
"\n",
" hq_protocol_stack_t stack;\n",
"\n",
"### initialize the stack and define two functions\n",
"\n",
"`uint8_t send_byte(hq_protocol_stack_t *stack, uint8_t byte)`\n",
"\n",
"This function should send the given byte over the serial interface. `stack` is\n",
"used for differentiation if more than one connection is handled.\n",
"\n",
"Return value should be 0, is sending was OK. If a problem occured return 1\n",
"\n",
"`void process_packet(hq_protocol_stack_t *stack, hq_protcol_packet_t *packet)`\n",
"\n",
"When a packet was received by the stack this function will be called. See\n",
"the definition of hq_protocol_packet_t for its members.\n",
"\n",
"### initialize stack and set function pointers\n",
"\n",
" void init(void) {\n",
" hq_protocol_init(&stack);\n",
" stack.send_byte = send_byte;\n",
" stack.process_packet = process_packet;\n",
" }\n",
"\n",
"### send a request by using `hq_protocol_send_packet_assembled`\n",
"\n",
"e.g. sending a string \"Hello\" to ID 1 via cmd is 0x20 (source ID will be 0):\n",
"\n",
" hq_protocol_send_packet_assembled(&stack, 0, 1, 0x20, 5, \"Hello\");\n",
"\n",
"Running this function `send_byte(...)` will be called and will send the bytes\n",
"via serial interface.\n",
"\n",
"### call `hq_protocol_process_rx_byte` on each received byte\n",
"\n",
"See definition of hq_protocol_process_rx_byte for the return values. When a\n",
"packet was received the function `process_packet` will be called.\n",
"\n",
"## Interactive Python examples\n",
"### Calculating CRC"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"def calc_crc(data: bytes, crc: int=0) -> int:\n",
" \"\"\"Calulates 16 bit crc IBM (polynom x^16+x^15+x^2+1)\n",
"\n",
" :param data: byte string\n",
" :param crc: initial value of crc\n",
" \"\"\" \n",
" for byte in data: # for each byte in array\n",
" for i in range(8): # for each bit in byte\n",
" if ((byte^crc)&0x01):\n",
" crc=((crc>>1) ^ 0xA001)\n",
" else:\n",
" crc=(crc >> 1)\n",
" byte=byte>>1\n",
" return crc"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Calculating the crc of a given packet. Be aware that the SYNC byte is **not** included in this calculation!"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"59513"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"base_packet_bstr = b'\\x02\\x07\\x00\\x02\\x50'\n",
"calc_crc(base_packet_bstr)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So the complete request to send is in this case:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'16 02 07 00 02 50 e8 79'"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import struct\n",
"\n",
"def bytes_to_str(bstr: bytes) -> str:\n",
" \"\"\" Helper function to pretty print a bytes array as string with hex values.\"\"\"\n",
" return \" \".join([\"{:02x}\".format(b) for b in bstr])\n",
"\n",
"bytes_to_str(b'\\x16' + base_packet_bstr + struct.pack('>H', calc_crc(base_packet_bstr)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Creating a request packet"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"def build_packet(destination_id: int, command: int, data: bytes, source_id: int = 0):\n",
" \"\"\" Bulding a HighQ protocol packet \"\"\"\n",
" packet = b'\\x02' + struct.pack('>BBBB', len(data)+7, source_id, destination_id, command) + data\n",
" return b'\\x16' + packet + struct.pack('>H', calc_crc(packet))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Example usage with following packet:\n",
"\n",
"* Destination ID is 7\n",
"* Command is 0x20\n",
"* Data to be send is a 16 bit integer with value 1000"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'16 02 09 00 07 20 03 e8 59 23'"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"packet_bstr = build_packet(destination_id=7, command=0x20, data=struct.pack('>H', 1000))\n",
"bytes_to_str(packet_bstr)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Interactive creation of packet"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "a7453b05530647f5ad59db467ae0aabd",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"interactive(children=(IntSlider(value=0, description='source_id', max=255), IntSlider(value=7, description='de…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from ipywidgets import interact\n",
"\n",
"@interact(source_id=(0, 255), destination_id=(0, 255), command=(0, 255), data_str='00 00')\n",
"def build_interactive_packet(source_id=0, destination_id=7, command=0x20, data_str=''):\n",
" try:\n",
" data = bytes.fromhex(data_str)\n",
" except ValueError: # this can be raised during editing\n",
" data =b''\n",
" packet = build_packet(destination_id, command, data, source_id=source_id)\n",
" return bytes_to_str(packet)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Parsing a packet"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Packet(source_id=7, destination_id=0, command=32, data=b'\\x00\\x00')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from collections import namedtuple\n",
"\n",
"Packet = namedtuple('Packet', 'source_id destination_id command data')\n",
"\n",
"def parse_packet(received_bytes: bytes) -> Packet:\n",
" \"\"\" This is a rather naive approach for parsing a packet but it shows how it works \"\"\"\n",
" start_index = received_bytes.find(b'\\x16\\x02')\n",
" if start_index < 0:\n",
" return 'No SYNC and START byte found'\n",
" \n",
" if len(received_bytes) < start_index + 8:\n",
" return 'Not enough bytes received'\n",
" \n",
" length = received_bytes[start_index + 2]\n",
" \n",
" if length < 7 or len(received_bytes) < start_index + length + 1:\n",
" return 'Length byte invalid or too few data bytes received'\n",
" \n",
" source_id = received_bytes[start_index + 3]\n",
" destination_id = received_bytes[start_index + 4]\n",
" command = received_bytes[start_index + 5]\n",
" data = received_bytes[start_index + 6: start_index + length - 1]\n",
" crc = struct.unpack('>H', received_bytes[start_index + length - 1: start_index + length + 1])[0]\n",
" \n",
" if crc != calc_crc(received_bytes[start_index + 1: start_index + length - 1]):\n",
" return 'Wrong CRC'\n",
" \n",
" return Packet(source_id, destination_id, command, data)\n",
" \n",
"\n",
"parse_packet(b'\\x16\\x02\\x09\\x07\\x00\\x20\\x00\\x00\\x53\\x97')\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Interactive parsing of bytes"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "662398117b1e4cb1b490b4c107b0fdae",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"interactive(children=(Text(value='16 02 09 07 00 20 00 00 53 97', description='received_str'), Output()), _dom…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"@interact(received_str='16 02 09 07 00 20 00 00 53 97')\n",
"def interactive_parse_packet(received_str):\n",
" try:\n",
" received_bytes = bytes.fromhex(received_str)\n",
" except ValueError: # this can be raised during editing\n",
" received_bytes =b''\n",
" \n",
" result = parse_packet(received_bytes)\n",
" \n",
" if isinstance(result, Packet):\n",
" print('Packet receceived with source id %d, command id 0x%02X and data %r' % (result.source_id, result.command, result.data))\n",
" else:\n",
" print(result)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.7"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
"0386a03f26c34d26b2bd972bdddbc6f3": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "SliderStyleModel",
"state": {
"description_width": ""
}
},
"232467ff20634ceaaa0dc8e82a1530eb": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "IntSliderModel",
"state": {
"description": "source_id",
"layout": "IPY_MODEL_c28764a892cf43d39ff017783f837a2a",
"max": 255,
"style": "IPY_MODEL_aeacab40a0d74353952bd94406f9add5"
}
},
"3077e6e75b834ab792acc8e1eb410145": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"58d29003048242009dc4187d67fe8538": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "DescriptionStyleModel",
"state": {
"description_width": ""
}
},
"5deaffd8abea4af8a639dddbdce63c52": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "SliderStyleModel",
"state": {
"description_width": ""
}
},
"5fe366df26c545a0b33331ec7b345a53": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "TextModel",
"state": {
"description": "data_str",
"layout": "IPY_MODEL_756884c4e96f4738809c8e09bdca9f76",
"style": "IPY_MODEL_58d29003048242009dc4187d67fe8538",
"value": "00 00"
}
},
"609a3233ca2e42a1b0d5eea002d324bb": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_70c4aa189c7343a5a733759eea193bd1",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": "Packet receceived with source id 7, command id 0x20 and data b'\\x00\\x00'\n"
}
]
}
},
"6222cb606e2f4f6d8eb6751118eaba68": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"662398117b1e4cb1b490b4c107b0fdae": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "VBoxModel",
"state": {
"_dom_classes": [
"widget-interact"
],
"children": [
"IPY_MODEL_723d33ebffe2423eb56a430a2e152a5a",
"IPY_MODEL_609a3233ca2e42a1b0d5eea002d324bb"
],
"layout": "IPY_MODEL_3077e6e75b834ab792acc8e1eb410145"
}
},
"70c4aa189c7343a5a733759eea193bd1": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"723d33ebffe2423eb56a430a2e152a5a": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "TextModel",
"state": {
"description": "received_str",
"layout": "IPY_MODEL_779f7d40266b4033ad1d71b468f7a664",
"style": "IPY_MODEL_f868fd1ec32e4627912953bf70a7f805",
"value": "16 02 09 07 00 20 00 00 53 97"
}
},
"73b9a5d946034212951a6976016598f6": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"756884c4e96f4738809c8e09bdca9f76": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"779f7d40266b4033ad1d71b468f7a664": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"a643197fd00645689ec0f58cbf4cef23": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "IntSliderModel",
"state": {
"description": "command",
"layout": "IPY_MODEL_cd7dba87d8674e7d826ea9333aba3a26",
"max": 255,
"style": "IPY_MODEL_0386a03f26c34d26b2bd972bdddbc6f3",
"value": 32
}
},
"a7453b05530647f5ad59db467ae0aabd": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "VBoxModel",
"state": {
"_dom_classes": [
"widget-interact"
],
"children": [
"IPY_MODEL_232467ff20634ceaaa0dc8e82a1530eb",
"IPY_MODEL_ce1e89ce9e2143c4b9158ab4565951de",
"IPY_MODEL_a643197fd00645689ec0f58cbf4cef23",
"IPY_MODEL_5fe366df26c545a0b33331ec7b345a53",
"IPY_MODEL_c1a7986ca552444989cb01c41ec0d564"
],
"layout": "IPY_MODEL_6222cb606e2f4f6d8eb6751118eaba68"
}
},
"aeacab40a0d74353952bd94406f9add5": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "SliderStyleModel",
"state": {
"description_width": ""
}
},
"c1a7986ca552444989cb01c41ec0d564": {
"model_module": "@jupyter-widgets/output",
"model_module_version": "1.0.0",
"model_name": "OutputModel",
"state": {
"layout": "IPY_MODEL_d695f1d0cc8647a688c7fc0bd7344422",
"outputs": [
{
"data": {
"text/plain": "'16 02 09 00 07 20 00 00 e7 23'"
},
"metadata": {},
"output_type": "display_data"
}
]
}
},
"c28764a892cf43d39ff017783f837a2a": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"cd7dba87d8674e7d826ea9333aba3a26": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"ce1e89ce9e2143c4b9158ab4565951de": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "IntSliderModel",
"state": {
"description": "destination_id",
"layout": "IPY_MODEL_73b9a5d946034212951a6976016598f6",
"max": 255,
"style": "IPY_MODEL_5deaffd8abea4af8a639dddbdce63c52",
"value": 7
}
},
"d695f1d0cc8647a688c7fc0bd7344422": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "1.1.0",
"model_name": "LayoutModel",
"state": {}
},
"f868fd1ec32e4627912953bf70a7f805": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "1.4.0",
"model_name": "DescriptionStyleModel",
"state": {
"description_width": ""
}
}
},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment