Repository: newAM/monitorcontrol

Issue:
Severity of VCPIOError errors
Due to high level of errors severity it is impossible to use monitorcontrol (VCPIOError exception thrown) with monitors which do not follow the DDC/CI standard e.g. Philips 223V monitors returns corrupted data i.e. incorrect checksum and/or wrong length, despite that it is possible to control it. 

Example output:
`Reading 0x60...`
`Send: 51 82 01 60 dc                                  | Q...`
`Recv: 6e                                              | n`
`Invalid response, corrupted data - xor is 0x5e, length 0x08`
`6e 88 88 02 00 60 00 00 03 00 01                | n.........`
`Send: 51 82 01 60 dc                                  | Q...`
`Recv: 6e                                              | n`
`Invalid response, corrupted data - xor is 0x5e, length 0x08`
`6e 88 88 02 00 60 00 00 03 00 01                | n.........`
`Send: 51 82 01 60 dc                                  | Q...`
`Recv: 6e                                              | n`
`Invalid response, corrupted data - xor is 0x5e, length 0x08`
`6e 88 88 02 00 60 00 00 03 00 01                | n.........`

If possible, it could be better to handle errors without throwing an exception and try to continue execution.
monitorcontrol doesn't switch inputs
monitorcontrol does nothing when attempting to switch inputs

<!-- Please provide this information if relevant -->
* Monitor manufacturer and model number: Dell S3221QS
* Input source (HDMI, VGA, display port, ect.): HDMI1 HDMI2
* Output device (video card, discrete graphics, ect.): Nvidia gt1030
* Operating system: Linux - Ubuntu 22.04
* Python version: 3.10.4
* monitorcontrol version (`monitorcontrol --version`): 3.0.0

# Steps to Reproduce
Works:   
  `ddccontrol -r 0x60 -w 18 dev:/dev/i2c-4`  
  `ddccontrol -r 0x60 -w 17 dev:/dev/i2c-4`

Doesn't:  
  `monitorcontrol  --set-input-source HDMI1`  
  `monitorcontrol  --set-input-source HDMI2`
<!-- Add information on how to reproduce this bug -->


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc9a019..1e08c78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
 as of version 2.1.1.
 
+## [3.0.1] - 2022-05-11
+### Fixed
+- Fixed checksum calculation on Linux.
+
 ## [3.0.0] - 2022-04-09
 ### Added
 - Added support for python 3.10.
@@ -70,7 +74,8 @@ as of version 2.1.1.
 - Added a changelog.
 
 
-[Unreleased]: https://github.com/newAM/monitorcontrol/compare/3.0.0...HEAD
+[Unreleased]: https://github.com/newAM/monitorcontrol/compare/3.0.1...HEAD
+[3.0.1]: https://github.com/newAM/monitorcontrol/compare/3.0.0...3.0.1
 [3.0.0]: https://github.com/newAM/monitorcontrol/compare/2.5.1...3.0.0
 [2.5.1]: https://github.com/newAM/monitorcontrol/compare/2.5.0...2.5.1
 [2.5.0]: https://github.com/newAM/monitorcontrol/compare/2.4.2...2.5.0
diff --git a/flake.lock b/flake.lock
index 266d8f7..f130d94 100644
--- a/flake.lock
+++ b/flake.lock
@@ -17,11 +17,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1650831523,
-        "narHash": "sha256-6pDZ08SAXsUx5rOP391x+TG39ENP/XA8VMa1tQvgEjc=",
+        "lastModified": 1652231724,
+        "narHash": "sha256-MjalcXFZgcgchp4QqnF05JTkFBBGad5hbksA1EKoP98=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "87d34a6b8982e901b8e50096b8e79ebc0e66cda0",
+        "rev": "41ff747f882914c1f8c233207ce280ac9d0c867f",
         "type": "github"
       },
       "original": {
diff --git a/monitorcontrol/vcp/vcp_linux.py b/monitorcontrol/vcp/vcp_linux.py
index 28b31c3..4ee2cdb 100644
--- a/monitorcontrol/vcp/vcp_linux.py
+++ b/monitorcontrol/vcp/vcp_linux.py
@@ -38,7 +38,7 @@ class LinuxVCP(VCP):
 
     # addresses
     DDCCI_ADDR = 0x37  # DDC-CI command address on the I2C bus
-    HOST_ADDRESS = 0x50  # virtual I2C slave address of the host
+    HOST_ADDRESS = 0x51  # virtual I2C slave address of the host
     I2C_SLAVE = 0x0703  # I2C bus slave address
 
     GET_VCP_RESULT_CODES = {
@@ -110,9 +110,12 @@ class LinuxVCP(VCP):
         # add headers and footers
         data.insert(0, (len(data) | self.PROTOCOL_FLAG))
         data.insert(0, self.HOST_ADDRESS)
-        data.append(self.get_checksum(data))
+        data.append(
+            self.get_checksum(bytearray([self.DDCCI_ADDR << 1]) + data)
+        )
 
         # write data
+        self.logger.debug("data=" + " ".join([f"{x:02X}" for x in data]))
         self.write_bytes(data)
 
         # store time of last set VCP
@@ -141,24 +144,26 @@ class LinuxVCP(VCP):
         # add headers and footers
         data.insert(0, (len(data) | self.PROTOCOL_FLAG))
         data.insert(0, self.HOST_ADDRESS)
-        data.append(self.get_checksum(data))
-        self.logger.debug(f"data={data}")
+        data.append(
+            self.get_checksum(bytearray([self.DDCCI_ADDR << 1]) + data)
+        )
 
         # write data
+        self.logger.debug("data=" + " ".join([f"{x:02X}" for x in data]))
         self.write_bytes(data)
 
         time.sleep(self.GET_VCP_TIMEOUT)
 
         # read the data
         header = self.read_bytes(self.GET_VCP_HEADER_LENGTH)
-        self.logger.debug(f"header={header}")
-        source, length = struct.unpack("BB", header)
+        self.logger.debug("header=" + " ".join([f"{x:02X}" for x in header]))
+        source, length = struct.unpack("=BB", header)
         length &= ~self.PROTOCOL_FLAG  # clear protocol flag
         payload = self.read_bytes(length + 1)
-        self.logger.debug(f"payload={payload}")
+        self.logger.debug("payload=" + " ".join([f"{x:02X}" for x in payload]))
 
         # check checksum
-        payload, checksum = struct.unpack(f"{length}sB", payload)
+        payload, checksum = struct.unpack(f"={length}sB", payload)
         calculated_checksum = self.get_checksum(header + payload)
         checksum_xor = checksum ^ calculated_checksum
         if checksum_xor:
@@ -245,11 +250,15 @@ class LinuxVCP(VCP):
 
             # read the data
             header = self.read_bytes(self.GET_VCP_HEADER_LENGTH)
-            self.logger.debug(f"response header={header}")
+            self.logger.debug(
+                "header=" + " ".join([f"{x:02X}" for x in header])
+            )
             source, length = struct.unpack("BB", header)
             length &= ~self.PROTOCOL_FLAG  # clear protocol flag
             payload = self.read_bytes(length + 1)
-            self.logger.debug(f"payload={payload}")
+            self.logger.debug(
+                "payload=" + " ".join([f"{x:02X}" for x in payload])
+            )
 
             # check if length is valid
             if length < 3 or length > 35:
@@ -298,19 +307,19 @@ class LinuxVCP(VCP):
 
         return caps_str
 
-    def get_checksum(self, data: List, prime: bool = False) -> int:
+    @staticmethod
+    def get_checksum(data: bytearray) -> int:
         """
         Computes the checksum for a set of data, with the option to
         use the virtual host address (per the DDC-CI specification).
 
         Args:
-            data: data array to transmit
-            prime: compute checksum using the 0x50 virtual host address
+            data: Data array to transmit.
 
         Returns:
-            checksum for the data
+            Checksum for the data.
         """
-        checksum = self.HOST_ADDRESS
+        checksum = 0x00
         for data_byte in data:
             checksum ^= data_byte
         return checksum
diff --git a/poetry.lock b/poetry.lock
index f9aadf7..d2a2db8 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -82,7 +82,7 @@ unicode_backport = ["unicodedata2"]
 
 [[package]]
 name = "click"
-version = "8.1.2"
+version = "8.1.3"
 description = "Composable command line interface toolkit"
 category = "dev"
 optional = false
@@ -226,7 +226,7 @@ python-versions = "*"
 
 [[package]]
 name = "jinja2"
-version = "3.1.1"
+version = "3.1.2"
 description = "A very fast and expressive template engine."
 category = "dev"
 optional = false
@@ -351,7 +351,7 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "pyparsing"
-version = "3.0.8"
+version = "3.0.9"
 description = "pyparsing module - Classes and methods to define and execute parsing grammars"
 category = "dev"
 optional = false
@@ -672,8 +672,8 @@ charset-normalizer = [
     {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
 ]
 click = [
-    {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
-    {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
+    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
 ]
 colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -762,8 +762,8 @@ iniconfig = [
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 ]
 jinja2 = [
-    {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"},
-    {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"},
+    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
 ]
 markupsafe = [
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
@@ -852,8 +852,8 @@ pygments = [
     {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
 ]
 pyparsing = [
-    {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"},
-    {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"},
+    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
 ]
 pytest = [
     {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
diff --git a/pyproject.toml b/pyproject.toml
index de37709..4c548b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ target-version = ["py36", "py37", "py38"]
 [tool.poetry]
 name = "monitorcontrol"
 description = "Monitor controls using MCCS over DDC-CI."
-version = "3.0.0"
+version = "3.0.1"
 authors = ["Alex Martens <alex@thinglab.org>"]
 license = "MIT"
 readme = "README.rst"


Test patch:
diff --git a/tests/test_linux_vcp.py b/tests/test_linux_vcp.py
new file mode 100644
index 0000000..0cdfa8f
--- /dev/null
+++ b/tests/test_linux_vcp.py
@@ -0,0 +1,20 @@
+import pytest
+from monitorcontrol.vcp.vcp_linux import LinuxVCP
+
+
+@pytest.mark.parametrize(
+    "data, checksum",
+    [
+        (bytearray([0x6E, 0x51, 0x82, 0x01, 0x10]), 0xAC),
+        (bytearray([0xF0, 0xF1, 0x81, 0xB1]), 0x31),
+        (bytearray([0x6E, 0xF1, 0x81, 0xB1]), 0xAF),
+    ],
+)
+def test_get_checksum(data: bytearray, checksum: int):
+    computed = LinuxVCP.get_checksum(data)
+    xor = checksum ^ computed
+    assert computed == checksum, (
+        f"computed=0x{computed:02X} 0b{computed:08b} "
+        f"checksum=0x{checksum:02X} 0b{checksum:08b} "
+        f"xor=0x{xor:02X} 0b{xor:08b}"
+    )