Skip to content

Commit

Permalink
feat(api): G-Code Diffing (#8135)
Browse files Browse the repository at this point in the history
* refactor(api): Refactor supported text modes

* supported_text_modes: Provide lookup function inside of Enum so text mode can be looked up by "nice" value
* Add tests for it

* refactor(api): Refactor supported_text_modes

* Won't be useful because response is needed. But if the response was added it is a single line difference from default and seems kinda useless
* Add InvalidTextModeError

* Refactor(api): Expand test coverage to 100%

* Refactor(api): Fix linting

* Refactor(api): Remove enum value

* feat(api): G-Code Parsing

* feat(api): Adding pipfile

* feat(api): Move package to dev-packages
  • Loading branch information
DerekMaggio authored Jul 20, 2021
1 parent d351611 commit 33e3ee3
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 0 deletions.
1 change: 1 addition & 0 deletions api/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ flake8 = "~=3.9.0"
flake8-annotations = "~=2.6.2"
flake8-docstrings = "~=1.6.0"
flake8-noqa = "~=1.1.0"
diff-match-patch = "*"
decoy = "~=1.6.2"

[packages]
Expand Down
8 changes: 8 additions & 0 deletions api/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions api/src/opentrons/hardware_control/g_code_parsing/g_code_differ.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

from typing import Tuple
from diff_match_patch import diff_match_patch as dmp # type: ignore
from opentrons.hardware_control.g_code_parsing.g_code_program.g_code_program import \
GCodeProgram
from opentrons.hardware_control.g_code_parsing.g_code_program.supported_text_modes \
import SupportedTextModes


class GCodeDiffer:
INSERTION_TYPE = 'Insertion'
EQUALITY_TYPE = 'Equality'
DELETION_TYPE = 'Deletion'

DIFF_TYPE_LOOKUP = {
-1: DELETION_TYPE,
0: EQUALITY_TYPE,
1: INSERTION_TYPE
}

@classmethod
def from_g_code_program(
cls,
program_1: GCodeProgram,
program_2: GCodeProgram
) -> GCodeDiffer:
string_1 = program_1.get_text_explanation(SupportedTextModes.CONCISE.value)
string_2 = program_2.get_text_explanation(SupportedTextModes.CONCISE.value)
return cls(string_1, string_2)

def __init__(self, string_1: str, string_2: str):
self._string_1 = string_1
self._string_2 = string_2

def get_diff(self, timeout_secs=10):
differ = dmp()
differ.Diff_Timeout = timeout_secs
return differ.diff_main(
text1=self._string_1,
text2=self._string_2
)

def get_html_diff(self):
return dmp().diff_prettyHtml(self.get_diff())

@classmethod
def get_diff_type(cls, diff_tuple: Tuple[int, str]):
return cls.DIFF_TYPE_LOOKUP[diff_tuple[0]]

@classmethod
def get_diff_content(cls, diff_tuple: Tuple[int, str]):
return diff_tuple[1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import pytest
from textwrap import dedent
from opentrons.hardware_control.g_code_parsing.g_code_differ import GCodeDiffer
from opentrons.hardware_control.g_code_parsing.g_code import GCode
from opentrons.hardware_control.g_code_parsing.g_code_program.g_code_program import \
GCodeProgram
from opentrons.hardware_control.g_code_parsing.g_code_program.supported_text_modes \
import SupportedTextModes

# Naive Strings
HELLO = 'Hello'
HELLO_WORLD = 'Hello World'

# G-Codes Only
G_CODE_1 = dedent(
"""
G28.2 ABC
G0 F5.005
"""
).strip()
G_CODE_2 = dedent(
"""
G28.2 ABC
M400
G0 F5.005
"""
).strip()


# Concise G-Code Text Explanations
@pytest.fixture
def first_g_code_explanation() -> str:
g_code_line_1 = GCode.from_raw_code(
'G28.2 ABCXYZ',
GCode.SMOOTHIE_IDENT,
'ok\r\nok\r\n'
)[0]
g_code_line_2 = GCode.from_raw_code(
'G38.2 F420Y-40.0',
GCode.SMOOTHIE_IDENT,
'G38.2 F420Y-40.0\r\n\r\n[PRB:296.825,292.663,218.000:1]\nok\r\nok\r\n'
)[0]
g_code_line_3 = GCode.from_raw_code(
'M203.1 A125 B40 C40 X600 Y400 Z125\r\n\r\n',
GCode.SMOOTHIE_IDENT,
''
)[0]
mode = SupportedTextModes.get_text_mode(SupportedTextModes.CONCISE.value)
return '\n'.join(
[
mode.builder(line)
for line in [g_code_line_1, g_code_line_2, g_code_line_3]
]
)


@pytest.fixture
def second_g_code_explanation() -> str:
g_code_line_1 = GCode.from_raw_code(
'G28.2 ABCXYZ',
GCode.SMOOTHIE_IDENT,
'ok\r\nok\r\n'
)[0]
g_code_line_2 = GCode.from_raw_code(
'M203.1 A125 B40 C40 X600 Y400 Z125\r\n\r\n',
GCode.SMOOTHIE_IDENT,
''
)[0]

mode = SupportedTextModes.get_text_mode(SupportedTextModes.CONCISE.value)
return '\n'.join(
[
mode.builder(line)
for line in [g_code_line_1, g_code_line_2]
]
)


@pytest.fixture
def first_g_code_program() -> GCodeProgram:
raw_codes = [
['G28.2 ABCXYZ', 'ok\r\nok\r\n'],
['G0 X113.38 Y11.24', 'ok\r\nok\r\n'],
['G4 P555', 'ok\r\nok\r\n'],
[
'M114.2',
'M114.2\r\n\r\nok MCS: A:218.0 B:0.0 C:0.0 X:418.0 Y:-3.0 Z:218.0'
],
]

g_code_list = [
GCode.from_raw_code(code, 'smoothie', response)
for code, response in raw_codes
]

return GCodeProgram([code[0] for code in g_code_list])


@pytest.fixture
def second_g_code_program() -> GCodeProgram:
raw_codes = [
['G28.2 ABCXYZ', 'ok\r\nok\r\n'],
['G0 X113.01 Y11.24', 'ok\r\nok\r\n'],
['G4 P555', 'ok\r\nok\r\n'],
]

g_code_list = [
GCode.from_raw_code(code, 'smoothie', response)
for code, response in raw_codes
]

return GCodeProgram([code[0] for code in g_code_list])


def test_naive_insertion():
diff = GCodeDiffer(HELLO, HELLO_WORLD).get_diff()
hello = diff[0]
world = diff[1]
assert GCodeDiffer.get_diff_type(hello) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(hello) == 'Hello'
assert GCodeDiffer.get_diff_type(world) == GCodeDiffer.INSERTION_TYPE
assert GCodeDiffer.get_diff_content(world) == ' World'


def test_naive_deletion():
diff = GCodeDiffer(HELLO_WORLD, HELLO).get_diff()
hello = diff[0]
world = diff[1]
assert GCodeDiffer.get_diff_type(hello) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(hello) == 'Hello'
assert GCodeDiffer.get_diff_type(world) == GCodeDiffer.DELETION_TYPE
assert GCodeDiffer.get_diff_content(world) == ' World'


def test_g_code_insertion():
diff = GCodeDiffer(G_CODE_1, G_CODE_2).get_diff()
line_1 = diff[0]
line_2 = diff[1]
line_3 = diff[2]

assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_1) == 'G28.2 ABC\n'
assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.INSERTION_TYPE
assert GCodeDiffer.get_diff_content(line_2) == 'M400\n'
assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_3) == 'G0 F5.005'


def test_g_code_deletion():
diff = GCodeDiffer(G_CODE_2, G_CODE_1).get_diff()
line_1 = diff[0]
line_2 = diff[1]
line_3 = diff[2]

assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_1) == 'G28.2 ABC\n'
assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.DELETION_TYPE
assert GCodeDiffer.get_diff_content(line_2) == 'M400\n'
assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_3) == 'G0 F5.005'


def test_g_code_explanation_insertion(
first_g_code_explanation,
second_g_code_explanation
):
diff = GCodeDiffer(second_g_code_explanation, first_g_code_explanation).get_diff()
line_1 = diff[0]
line_2 = diff[1]
line_3 = diff[2]

assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_1) == \
'G28.2 A B C X Y Z -> Homing the following axes: X, Y, Z, A, B, C ->\n'

assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.INSERTION_TYPE
assert GCodeDiffer.get_diff_content(line_2) == \
'G38.2 F420.0 Y-40.0 -> Probing -40.0 on the Y axis, at a speed of 420.0 ' \
'-> Probed to : X Axis: 296.825 Y Axis: 292.663 Z Axis: 218.000\n'
assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_3) == \
'M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed ' \
'for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 ' \
'A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 ->'


def test_g_code_explanation_deletion(
first_g_code_explanation,
second_g_code_explanation
):
diff = GCodeDiffer(first_g_code_explanation, second_g_code_explanation).get_diff()
line_1 = diff[0]
line_2 = diff[1]
line_3 = diff[2]

assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_1) == \
'G28.2 A B C X Y Z -> Homing the following axes: X, Y, Z, A, B, C ->\n'

assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.DELETION_TYPE
assert GCodeDiffer.get_diff_content(line_2) == \
'G38.2 F420.0 Y-40.0 -> Probing -40.0 on the Y axis, at a speed of 420.0 ' \
'-> Probed to : X Axis: 296.825 Y Axis: 292.663 Z Axis: 218.000\n'
assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(line_3) == \
'M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed ' \
'for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 ' \
'A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 ->'


def test_g_code_program_diff(first_g_code_program, second_g_code_program):
diff = GCodeDiffer.from_g_code_program(
first_g_code_program, second_g_code_program
).get_diff()
line_1 = diff[0]
remove_38 = diff[1]
add_01 = diff[2]
beginning_partial_response = diff[3]
remove_response_38 = diff[4]
add_response_01 = diff[5]
remaining_partial_response = diff[6]
remove_last_line = diff[7]

assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(
line_1
) == 'G28.2 A B C X Y Z -> Homing the following axes: X, Y, Z, A, B, C ->\nG0 X113.'

assert GCodeDiffer.get_diff_type(remove_38) == GCodeDiffer.DELETION_TYPE
assert GCodeDiffer.get_diff_content(remove_38) == '38'

assert GCodeDiffer.get_diff_type(add_01) == GCodeDiffer.INSERTION_TYPE
assert GCodeDiffer.get_diff_content(add_01) == '01'

assert GCodeDiffer.get_diff_type(beginning_partial_response) == \
GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(
beginning_partial_response
) == ' Y11.24 -> Moving the robot as follows: The gantry to 113.'

assert GCodeDiffer.get_diff_type(remove_response_38) == GCodeDiffer.DELETION_TYPE
assert GCodeDiffer.get_diff_content(remove_response_38) == '38'

assert GCodeDiffer.get_diff_type(add_response_01) == GCodeDiffer.INSERTION_TYPE
assert GCodeDiffer.get_diff_content(add_response_01) == '01'

assert GCodeDiffer.get_diff_type(remaining_partial_response) == \
GCodeDiffer.EQUALITY_TYPE
assert GCodeDiffer.get_diff_content(
remaining_partial_response
) == ' on the X-Axis The gantry to 11.24 on the Y-Axis ->\nG4 P555.0 -> Pausing ' \
'movement for 555.0ms ->'

assert GCodeDiffer.get_diff_type(remove_last_line) == GCodeDiffer.DELETION_TYPE
assert GCodeDiffer.get_diff_content(remove_last_line) == \
'\nM114.2 -> Getting current position for all axes -> The current ' \
'position of the robot is: A Axis: 218.0 B Axis: 0.0 C Axis: 0.0 ' \
'X Axis: 418.0 Y Axis: -3.0 Z Axis: 218.0'


def test_html_diff(
first_g_code_explanation,
second_g_code_explanation
):
expected_html = '<span>G28.2 A B C X Y Z -&gt; Homing the following axes: ' \
'X, Y, Z, A, B, C -&gt;&para;<br></span><del style="background:' \
'#ffe6e6;">G38.2 F420.0 Y-40.0 -&gt; Probing -40.0 on the Y ' \
'axis, at a speed of 420.0 -&gt; Probed to : X Axis: 296.825 Y ' \
'Axis: 292.663 Z Axis: 218.000&para;<br></del><span>M203.1 ' \
'A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -&gt; Setting the max ' \
'speed for the following axes: X-Axis: 600.0 Y-Axis: 400.0 ' \
'Z-Axis: 125.0 A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 -&gt;</span>'
diff = GCodeDiffer(first_g_code_explanation, second_g_code_explanation)
html = diff.get_html_diff()
assert html == expected_html

0 comments on commit 33e3ee3

Please sign in to comment.