pymammotion 0.3.8__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. pymammotion/__init__.py +2 -2
  2. pymammotion/aliyun/cloud_gateway.py +12 -9
  3. pymammotion/aliyun/model/aep_response.py +1 -2
  4. pymammotion/aliyun/model/dev_by_account_response.py +7 -8
  5. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  6. pymammotion/aliyun/model/regions_response.py +3 -3
  7. pymammotion/aliyun/model/session_by_authcode_response.py +1 -2
  8. pymammotion/aliyun/model/stream_subscription_response.py +1 -2
  9. pymammotion/bluetooth/ble.py +5 -5
  10. pymammotion/bluetooth/ble_message.py +9 -13
  11. pymammotion/data/model/device.py +31 -228
  12. pymammotion/data/model/device_config.py +0 -10
  13. pymammotion/data/model/device_info.py +13 -0
  14. pymammotion/data/model/device_limits.py +49 -0
  15. pymammotion/data/model/generate_route_information.py +1 -1
  16. pymammotion/data/model/hash_list.py +6 -2
  17. pymammotion/data/model/plan.py +0 -3
  18. pymammotion/data/model/raw_data.py +215 -0
  19. pymammotion/data/model/region_data.py +10 -11
  20. pymammotion/data/model/report_info.py +1 -1
  21. pymammotion/data/mqtt/event.py +18 -14
  22. pymammotion/data/mqtt/properties.py +1 -1
  23. pymammotion/data/mqtt/status.py +1 -1
  24. pymammotion/data/state_manager.py +83 -23
  25. pymammotion/http/encryption.py +220 -0
  26. pymammotion/http/http.py +92 -39
  27. pymammotion/http/model/http.py +2 -2
  28. pymammotion/mammotion/commands/abstract_message.py +2 -2
  29. pymammotion/mammotion/commands/messages/driver.py +28 -21
  30. pymammotion/mammotion/commands/messages/media.py +10 -14
  31. pymammotion/mammotion/commands/messages/navigation.py +14 -11
  32. pymammotion/mammotion/commands/messages/network.py +15 -12
  33. pymammotion/mammotion/commands/messages/ota.py +9 -14
  34. pymammotion/mammotion/commands/messages/system.py +27 -24
  35. pymammotion/mammotion/commands/messages/video.py +9 -14
  36. pymammotion/mammotion/devices/base.py +7 -14
  37. pymammotion/mammotion/devices/mammotion.py +22 -13
  38. pymammotion/mammotion/devices/mammotion_bluetooth.py +15 -4
  39. pymammotion/mammotion/devices/mammotion_cloud.py +30 -12
  40. pymammotion/mqtt/linkkit/__init__.py +5 -0
  41. pymammotion/mqtt/linkkit/h2client.py +585 -0
  42. pymammotion/mqtt/linkkit/linkkit.py +3020 -0
  43. pymammotion/mqtt/mammotion_mqtt.py +13 -9
  44. pymammotion/proto/__init__.py +2176 -1
  45. pymammotion/proto/luba_mul.proto +1 -0
  46. pymammotion/proto/luba_mul_pb2.py +8 -8
  47. pymammotion/proto/luba_mul_pb2.pyi +1 -0
  48. pymammotion/proto/mctrl_nav_pb2.py +69 -67
  49. pymammotion/proto/mctrl_nav_pb2.pyi +13 -5
  50. pymammotion/proto/mctrl_sys_pb2.py +41 -37
  51. pymammotion/proto/mctrl_sys_pb2.pyi +34 -11
  52. pymammotion/utility/constant/device_constant.py +14 -5
  53. pymammotion/utility/device_config.py +754 -0
  54. pymammotion/utility/device_type.py +64 -16
  55. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/METADATA +9 -9
  56. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/RECORD +58 -62
  57. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/WHEEL +1 -1
  58. pymammotion/aliyun/cloud_service.py +0 -65
  59. pymammotion/proto/basestation.py +0 -59
  60. pymammotion/proto/common.py +0 -12
  61. pymammotion/proto/dev_net.py +0 -381
  62. pymammotion/proto/luba_msg.py +0 -81
  63. pymammotion/proto/luba_mul.py +0 -76
  64. pymammotion/proto/mctrl_driver.py +0 -100
  65. pymammotion/proto/mctrl_nav.py +0 -664
  66. pymammotion/proto/mctrl_ota.py +0 -48
  67. pymammotion/proto/mctrl_pept.py +0 -41
  68. pymammotion/proto/mctrl_sys.py +0 -574
  69. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,220 @@
1
+ import base64
2
+ import logging
3
+ import secrets
4
+ import string
5
+
6
+ from cryptography.hazmat.backends import default_backend
7
+ from cryptography.hazmat.primitives import padding, serialization
8
+ from cryptography.hazmat.primitives.asymmetric import padding as rsa_padding
9
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ class EncryptionUtils:
15
+ PRIVATE_KEY = """MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOFizbd1fC5XNKJ89u0XNvPZNR/L
16
+ 0h547iSWjOCuvvMu76ZSaS3/Tu2C1C+XmlnmBWTyY4ON+xECiNUXm/aWQ3P0g+wf60zjPbNzgL2Q
17
+ 7njXJG6wka4KkbdQxUdS0TTpL256LnV1LsG855bsbJIJiQPbfUq6HbB5xH7sXdrmFu1DAgMBAAEC
18
+ gYEAoT2TGE1ncquWjyxBZup1uMvKkp25C23OSMSfslmxZ75LWjyY3HxK1eYDsKyPkwLZFxfFE6du
19
+ VwPuKiyCuk1ToPfnb4niTGzXPyC2PbO4SFrWL8n1YZ80M0bfTGI9dMCZvpmZJ41WYUsBaf2374lt
20
+ oEiDEHJp7MeXk/970xiKP1ECQQD65rLHk840q+FZS6kZVexJucPZj/YAII6klU1E20ctioe8Pi5m
21
+ WSPqclH27/t4FqdvP7tFqaavyXg+CEQpxmxLAkEA5fddDuzcjWgF9pl9fP7/baFMYjUS9z1Vc3gx
22
+ CnvAgCnv71wjDQhvsUc6sAiidsBGFDyud06RyyLcOlQchMb36QJBAIui/Xjpn+fciQxjeXcqRNk7
23
+ U+6vml+zvu+GUHyz9Uc5RBXWHYjEr6J5gXiHU1MgeIsH0zgQFT7cR9luTFFbp0UCQFIntfogCocG
24
+ E6NOoHMoUi5jQnuPRHBJXB69YJ/DKDlhQhN8EhWU3voxXTkITKop9J9EMnvy+MjecljwNaQFxQkC
25
+ QB9lz67iDe9Gj8NxSElVZxUm9EfbL1RPqTZPx/lADR06CPB8pP3Bl5/5/5RGzc+UTZ+wX5GWKvC7
26
+ zUJaROxQB+E=""".replace(" ", "")
27
+
28
+ PUBLIC_KEY_PROD = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLbeSgOvnwLTWbhaBQWNnnHMtSDAi
29
+ Gz0PEDbrtd1tLYoO0hukW5PSa6eHykch0Hc6etiqEx1xziS+vNf+iOXds70I4htaYit6yRToZlQ
30
+ Mim3DQxaZX68nIHIZogur0zGv9U8j01v5l/rHRxyDdlVx3+JkBg6Cqx4U1PXEnAJriqcyg0B8Gm
31
+ V8Lnmfng+aJLRyq5MkhstYCRv9AsmWu8NpZDJ1ffbkaS02Z9/wpubXTiFP6DG3V2mDw2VvzEcHi
32
+ cchw49oXmTi92yui+kBgSYlNygssOAyU6H071AfmRUeH3+TsV5u5rg+bCiKyHemVmcKdd3hhZB+
33
+ HjA8o3On6rg5wIDAQAB""".replace(" ", "")
34
+
35
+ PUBLIC_KEY_TEST = """MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1nAzH31arNBmYKvTlvKgkxI1MIr4HpfLbmM
36
+ XPIhd8D/cXB0dYY1ppUq4a/ezq41YShN88e0elyZgqdnFrkhiLpnKWa7jXtVRgXi9eS18PLO8ns
37
+ eHude9URaj7relK1AZ0xovKsbLKHd01PpmngLXZfnKA06J2ru/zH+cnpXdy8QIDAQAB""".replace(" ", "")
38
+
39
+ def __init__(self) -> None:
40
+ self.AES_PASW = self.get_aes_key() # Get from previous implementation
41
+ self.IV = self.get_iv() # Get from previous implementation
42
+ self._public_key = self.load_public_key()
43
+ self._private_key = self.load_private_key()
44
+
45
+ @staticmethod
46
+ def load_private_key():
47
+ """Load the private key from base64 encoded string"""
48
+ try:
49
+ private_key_bytes = base64.b64decode(EncryptionUtils.PRIVATE_KEY)
50
+ return serialization.load_der_private_key(private_key_bytes, password=None, backend=default_backend())
51
+ except Exception as e:
52
+ raise Exception(f"Failed to load private key: {e!s}")
53
+
54
+ @staticmethod
55
+ def load_public_key(is_production: bool = True):
56
+ """Load the public key from base64 encoded string
57
+
58
+ Args:
59
+ is_production (bool): If True, uses production key, else uses test key
60
+
61
+ """
62
+ try:
63
+ key_string = EncryptionUtils.PUBLIC_KEY_PROD if is_production else EncryptionUtils.PUBLIC_KEY_TEST
64
+ public_key_bytes = base64.b64decode(key_string)
65
+ return serialization.load_der_public_key(public_key_bytes, backend=default_backend())
66
+ except Exception as e:
67
+ raise Exception(f"Failed to load public key: {e!s}")
68
+
69
+ @staticmethod
70
+ def encrypt(plaintext: str, key: str, iv: str) -> str:
71
+ """Encrypt text using AES/CBC/PKCS5Padding
72
+
73
+ Args:
74
+ plaintext (str): Text to encrypt
75
+ key (str): Encryption key
76
+ iv (str): Initialization vector
77
+
78
+ Returns:
79
+ str: Base64 encoded encrypted string
80
+
81
+ Raises:
82
+ Exception: If encryption fails
83
+
84
+ """
85
+ try:
86
+ # Convert strings to bytes
87
+ plaintext_bytes = plaintext.encode("utf-8")
88
+ key_bytes = key.encode("utf-8")
89
+ iv_bytes = iv.encode("utf-8")
90
+
91
+ # Create padder
92
+ padder = padding.PKCS7(128).padder()
93
+ padded_data = padder.update(plaintext_bytes) + padder.finalize()
94
+
95
+ # Create cipher
96
+ cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv_bytes), backend=default_backend())
97
+
98
+ # Encrypt
99
+ encryptor = cipher.encryptor()
100
+ encrypted_bytes = encryptor.update(padded_data) + encryptor.finalize()
101
+
102
+ # Encode to base64
103
+ return base64.b64encode(encrypted_bytes).decode("utf-8")
104
+
105
+ except Exception as e:
106
+ raise Exception(f"Encryption failed: {e!s}")
107
+
108
+ def encryption_by_aes(self, text: str) -> str:
109
+ """Encrypt text using AES with class-level key and IV
110
+
111
+ Args:
112
+ text (str): Text to encrypt
113
+
114
+ Returns:
115
+ str: Encrypted text or None if encryption fails
116
+
117
+ """
118
+ try:
119
+ # Perform encryption
120
+ encrypted = self.encrypt(text, self.AES_PASW, self.IV)
121
+
122
+ return encrypted
123
+
124
+ except Exception as e:
125
+ _LOGGER.error(f"Encryption failed: {e!s}")
126
+ return None
127
+
128
+ def encrypt_by_public_key(self) -> str | None:
129
+ """Encrypt data using RSA public key.
130
+
131
+ Args:
132
+
133
+ Returns:
134
+ Optional[str]: Base64 encoded encrypted data or None if encryption fails
135
+
136
+ """
137
+
138
+ data = f"{self.AES_PASW},{self.IV}"
139
+
140
+ if not self._public_key:
141
+ _LOGGER.error("Public key not initialized")
142
+ return None
143
+
144
+ try:
145
+ # Convert input string to bytes
146
+ data_bytes = data.encode("utf-8")
147
+
148
+ # Encrypt the data padding.PKCS7(128).padder()
149
+ encrypted_bytes = self._public_key.encrypt(data_bytes, rsa_padding.PKCS1v15())
150
+
151
+ # Convert to base64 string
152
+ encrypted_str = base64.b64encode(encrypted_bytes).decode("utf-8")
153
+ _LOGGER.debug("Data encrypted successfully")
154
+
155
+ return encrypted_str
156
+
157
+ except Exception as err:
158
+ _LOGGER.error("Encryption failed: %s", str(err))
159
+ return None
160
+
161
+ @staticmethod
162
+ def get_random_string(length: int) -> str:
163
+ """Generate a random string of specified length using alphanumeric characters.
164
+
165
+ Args:
166
+ length (int): The desired length of the random string
167
+
168
+ Returns:
169
+ str: A random alphanumeric string of specified length
170
+
171
+ Raises:
172
+ ValueError: If length is less than 1
173
+
174
+ """
175
+ if length < 1:
176
+ raise ValueError("Length must be positive")
177
+
178
+ charset = string.ascii_letters + string.digits
179
+ return "".join(secrets.choice(charset) for _ in range(length))
180
+
181
+ @staticmethod
182
+ def get_random_int(length: int) -> str:
183
+ """Generate a random string of specified length containing only digits.
184
+
185
+ Args:
186
+ length (int): The desired length of the random number string
187
+
188
+ Returns:
189
+ str: A string of random digits of specified length
190
+
191
+ Raises:
192
+ ValueError: If length is less than 1
193
+
194
+ """
195
+ if length < 1:
196
+ raise ValueError("Length must be positive")
197
+
198
+ return "".join(secrets.choice(string.digits) for _ in range(length))
199
+
200
+ @staticmethod
201
+ def get_aes_key() -> str:
202
+ """Generate a random AES key of 16 characters using alphanumeric characters.
203
+ Matches Java implementation behavior.
204
+
205
+ Returns:
206
+ str: A 16-character random string for AES key
207
+
208
+ """
209
+ return EncryptionUtils.get_random_string(16)
210
+
211
+ @staticmethod
212
+ def get_iv() -> str:
213
+ """Generate a random initialization vector of 16 digits.
214
+ Matches Java implementation behavior.
215
+
216
+ Returns:
217
+ str: A 16-digit random string for initialization vector
218
+
219
+ """
220
+ return EncryptionUtils.get_random_int(16)
pymammotion/http/http.py CHANGED
@@ -4,23 +4,26 @@ from typing import cast
4
4
  from aiohttp import ClientSession
5
5
 
6
6
  from pymammotion.aliyun.model.stream_subscription_response import StreamSubscriptionResponse
7
- from pymammotion.const import (
8
- MAMMOTION_API_DOMAIN,
9
- MAMMOTION_CLIENT_ID,
10
- MAMMOTION_CLIENT_SECRET,
11
- MAMMOTION_DOMAIN,
12
- )
7
+ from pymammotion.const import MAMMOTION_API_DOMAIN, MAMMOTION_CLIENT_ID, MAMMOTION_CLIENT_SECRET, MAMMOTION_DOMAIN
8
+ from pymammotion.http.encryption import EncryptionUtils
13
9
  from pymammotion.http.model.http import ErrorInfo, LoginResponseData, Response
14
10
 
15
11
 
16
12
  class MammotionHTTP:
17
- def __init__(self, response: Response) -> None:
13
+ def __init__(self) -> None:
14
+ self.code = None
15
+ self.msg = None
16
+ self.response: Response | None = None
17
+ self.login_info: LoginResponseData | None = None
18
18
  self._headers = {"User-Agent": "okhttp/3.14.9", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"}
19
- self.login_info = LoginResponseData.from_dict(response.data) if response.data else None
20
- self._headers["Authorization"] = f"Bearer {self.login_info.access_token}" if response.data else None
21
- self.response = response
22
- self.msg = response.msg
23
- self.code = response.code
19
+ self.encryption_utils = EncryptionUtils()
20
+
21
+ @staticmethod
22
+ def generate_headers(token: str) -> dict:
23
+ return {"Authorization": f"Bearer {token}"}
24
+
25
+ async def login_by_email(self, email: str, password: str) -> Response[LoginResponseData]:
26
+ return await self.login(email, password)
24
27
 
25
28
  async def get_all_error_codes(self) -> dict[str, ErrorInfo]:
26
29
  async with ClientSession(MAMMOTION_API_DOMAIN) as session:
@@ -36,15 +39,55 @@ class MammotionHTTP:
36
39
  codes[error_info.code] = error_info
37
40
  return codes
38
41
 
39
- async def oauth_check(self) -> None:
42
+ async def oauth_check(self) -> Response:
40
43
  """Check if token is valid.
41
44
 
42
45
  Returns 401 if token is invalid. We then need to re-authenticate, can try to refresh token first
43
46
  """
44
47
  async with ClientSession(MAMMOTION_API_DOMAIN) as session:
45
- async with session.post("/user-server/v1/user/oauth/check") as resp:
48
+ async with session.post("/user-server/v1/user/oauth/check", headers=self._headers) as resp:
49
+ data = await resp.json()
50
+ return Response.from_dict(data)
51
+
52
+ async def pair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
53
+ async with ClientSession(MAMMOTION_API_DOMAIN) as session:
54
+ async with session.post(
55
+ "/device-server/v1/iot/device/pairing",
56
+ headers=self._headers,
57
+ json={"mowerName": mower_name, "rtkName": rtk_name},
58
+ ) as resp:
59
+ data = await resp.json()
60
+ if data.get("status") == 200:
61
+ print(data)
62
+ return Response.from_dict(data)
63
+ else:
64
+ print(data)
65
+
66
+ async def unpair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
67
+ async with ClientSession(MAMMOTION_API_DOMAIN) as session:
68
+ async with session.post(
69
+ "/device-server/v1/iot/device/unpairing",
70
+ headers=self._headers,
71
+ json={"mowerName": mower_name, "rtkName": rtk_name},
72
+ ) as resp:
73
+ data = await resp.json()
74
+ if data.get("status") == 200:
75
+ print(data)
76
+ return Response.from_dict(data)
77
+ else:
78
+ print(data)
79
+
80
+ async def net_rtk_enable(self, device_id: str) -> Response:
81
+ async with ClientSession(MAMMOTION_API_DOMAIN) as session:
82
+ async with session.post(
83
+ "/device-server/v1/iot/net-rtk/enable", headers=self._headers, json={"deviceId": device_id}
84
+ ) as resp:
46
85
  data = await resp.json()
47
- response = Response.from_dict(data)
86
+ if data.get("status") == 200:
87
+ print(data)
88
+ return Response.from_dict(data)
89
+ else:
90
+ print(data)
48
91
 
49
92
  async def get_stream_subscription(self, iot_id: str) -> Response[StreamSubscriptionResponse]:
50
93
  """Get agora.io data for view camera stream"""
@@ -63,27 +106,37 @@ class MammotionHTTP:
63
106
  # Assuming the data format matches the expected structure
64
107
  return Response[StreamSubscriptionResponse].from_dict(data)
65
108
 
66
- @classmethod
67
- async def login(cls, session: ClientSession, username: str, password: str) -> Response[LoginResponseData]:
68
- async with session.post(
69
- "/oauth/token",
70
- headers={"User-Agent": "okhttp/3.14.9", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"},
71
- params=dict(
72
- username=username,
73
- password=password,
74
- client_id=MAMMOTION_CLIENT_ID,
75
- client_secret=MAMMOTION_CLIENT_SECRET,
76
- grant_type="password",
77
- ),
78
- ) as resp:
79
- data = await resp.json()
80
- response = Response[LoginResponseData].from_dict(data)
81
- # TODO catch errors from mismatch user / password elsewhere
82
- # Assuming the data format matches the expected structure
83
- return response
84
-
85
-
86
- async def connect_http(username: str, password: str) -> MammotionHTTP:
87
- async with ClientSession(MAMMOTION_DOMAIN) as session:
88
- login_response = await MammotionHTTP.login(session, username, password)
89
- return MammotionHTTP(login_response)
109
+ async def login(self, username: str, password: str) -> Response[LoginResponseData]:
110
+ async with ClientSession(MAMMOTION_DOMAIN) as session:
111
+ async with session.post(
112
+ "/oauth/token",
113
+ headers={
114
+ "User-Agent": "okhttp/3.14.9",
115
+ "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332",
116
+ "Encrypt-Key": self.encryption_utils.encrypt_by_public_key(),
117
+ "Decrypt-Type": "3",
118
+ "Ec-Version": "v1",
119
+ },
120
+ params=dict(
121
+ username=self.encryption_utils.encryption_by_aes(username),
122
+ password=self.encryption_utils.encryption_by_aes(password),
123
+ client_id=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_ID),
124
+ client_secret=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_SECRET),
125
+ grant_type=self.encryption_utils.encryption_by_aes("password"),
126
+ ),
127
+ ) as resp:
128
+ if resp.status != 200:
129
+ print(resp.json())
130
+ return Response.from_dict({"status": resp.status, "msg": "Login failed"})
131
+ data = await resp.json()
132
+ login_response = Response[LoginResponseData].from_dict(data)
133
+ self.login_info = LoginResponseData.from_dict(login_response.data)
134
+ self._headers["Authorization"] = (
135
+ f"Bearer {self.login_info.access_token}" if login_response.data else None
136
+ )
137
+ self.response = login_response
138
+ self.msg = login_response.msg
139
+ self.code = login_response.code
140
+ # TODO catch errors from mismatch user / password elsewhere
141
+ # Assuming the data format matches the expected structure
142
+ return login_response
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import Generic, Literal, Optional, TypeVar
2
+ from typing import Generic, Literal, TypeVar
3
3
 
4
4
  from mashumaro import DataClassDictMixin
5
5
  from mashumaro.config import BaseConfig
@@ -83,7 +83,7 @@ class LoginResponseUserInformation(DataClassORJSONMixin):
83
83
  userId: str
84
84
  userAccount: str
85
85
  authType: str
86
- email: Optional[str] = None
86
+ email: str | None = None
87
87
 
88
88
  class Config(BaseConfig):
89
89
  omit_none = True
@@ -1,6 +1,6 @@
1
1
  from abc import abstractmethod
2
2
 
3
- from pymammotion.proto.luba_msg import MsgCmdType, MsgDevice
3
+ from pymammotion.proto import MsgCmdType, MsgDevice
4
4
  from pymammotion.utility.device_type import DeviceType
5
5
 
6
6
 
@@ -16,7 +16,7 @@ class AbstractMessage:
16
16
  """Changes the rcver name if it's not a luba1."""
17
17
  if (
18
18
  not DeviceType.is_luba1(self.get_device_name(), self.get_device_product_key())
19
- and msg_type == MsgCmdType.MSG_CMD_TYPE_NAV
19
+ and msg_type == MsgCmdType.NAV
20
20
  ):
21
21
  return MsgDevice.DEV_NAVIGATION
22
22
  return msg_device
@@ -1,11 +1,22 @@
1
1
  # === sendOrderMsg_Driver ===
2
- import time
3
2
  from abc import ABC
4
3
  from logging import getLogger
4
+ import time
5
5
 
6
6
  from pymammotion.mammotion.commands.abstract_message import AbstractMessage
7
- from pymammotion.proto import mctrl_driver
8
- from pymammotion.proto.luba_msg import LubaMsg, MsgAttr, MsgCmdType, MsgDevice
7
+ from pymammotion.proto import (
8
+ DrvKnifeHeight,
9
+ DrvMotionCtrl,
10
+ DrvMowCtrlByHand,
11
+ DrvSrSpeed,
12
+ LubaMsg,
13
+ MctlDriver,
14
+ MsgAttr,
15
+ MsgCmdType,
16
+ MsgDevice,
17
+ RtkCfgReqT,
18
+ RtkSysMaskQueryT,
19
+ )
9
20
 
10
21
  logger = getLogger(__name__)
11
22
 
@@ -13,10 +24,10 @@ logger = getLogger(__name__)
13
24
  class MessageDriver(AbstractMessage, ABC):
14
25
  def send_order_msg_driver(self, driver) -> bytes:
15
26
  return LubaMsg(
16
- msgtype=MsgCmdType.MSG_CMD_TYPE_EMBED_DRIVER,
27
+ msgtype=MsgCmdType.EMBED_DRIVER,
17
28
  sender=MsgDevice.DEV_MOBILEAPP,
18
- rcver=self.get_msg_device(MsgCmdType.MSG_CMD_TYPE_EMBED_DRIVER, MsgDevice.DEV_MAINCTL),
19
- msgattr=MsgAttr.MSG_ATTR_REQ,
29
+ rcver=self.get_msg_device(MsgCmdType.EMBED_DRIVER, MsgDevice.DEV_MAINCTL),
30
+ msgattr=MsgAttr.REQ,
20
31
  timestamp=round(time.time() * 1000),
21
32
  seqs=1,
22
33
  version=1,
@@ -26,25 +37,23 @@ class MessageDriver(AbstractMessage, ABC):
26
37
 
27
38
  def set_blade_height(self, height: int):
28
39
  logger.debug(f"Send knife height height={height}")
29
- build = mctrl_driver.MctlDriver(todev_knife_height_set=mctrl_driver.DrvKnifeHeight(knife_height=height))
40
+ build = MctlDriver(todev_knife_height_set=DrvKnifeHeight(knife_height=height))
30
41
  logger.debug(f"Send command--Knife motor height setting height={height}")
31
42
  return self.send_order_msg_driver(build)
32
43
 
33
44
  def set_speed(self, speed: float):
34
45
  logger.debug(f"{self.get_device_name()} set speed, {speed}")
35
- build = mctrl_driver.MctlDriver(bidire_speed_read_set=mctrl_driver.DrvSrSpeed(speed=speed, rw=1))
46
+ build = MctlDriver(bidire_speed_read_set=DrvSrSpeed(speed=speed, rw=1))
36
47
  logger.debug(f"Send command--Speed setting speed={speed}")
37
48
  return self.send_order_msg_driver(build)
38
49
 
39
50
  def syn_nav_star_point_data(self, sat_system: int):
40
- build = mctrl_driver.MctlDriver(rtk_sys_mask_query=mctrl_driver.RtkSysMaskQueryT(sat_system=sat_system))
51
+ build = MctlDriver(rtk_sys_mask_query=RtkSysMaskQueryT(sat_system=sat_system))
41
52
  logger.debug(f"Send command--Navigation satellite frequency point synchronization={sat_system}")
42
53
  return self.send_order_msg_driver(build)
43
54
 
44
55
  def set_nav_star_point(self, cmd_req: str):
45
- build = mctrl_driver.MctlDriver(
46
- rtk_cfg_req=mctrl_driver.RtkCfgReqT(cmd_req=cmd_req, cmd_length=len(cmd_req) - 1)
47
- )
56
+ build = MctlDriver(rtk_cfg_req=RtkCfgReqT(cmd_req=cmd_req, cmd_length=len(cmd_req) - 1))
48
57
  logger.debug(f"Send command--Navigation satellite frequency point setting={cmd_req}")
49
58
  logger.debug(
50
59
  f"Navigation satellite setting, Send command--Navigation satellite frequency point setting={cmd_req}"
@@ -52,7 +61,7 @@ class MessageDriver(AbstractMessage, ABC):
52
61
  return self.send_order_msg_driver(build)
53
62
 
54
63
  def get_speed(self):
55
- build = mctrl_driver.MctlDriver(bidire_speed_read_set=mctrl_driver.DrvSrSpeed(rw=0))
64
+ build = MctlDriver(bidire_speed_read_set=DrvSrSpeed(rw=0))
56
65
  logger.debug("Send command--Get speed value")
57
66
  return self.send_order_msg_driver(build)
58
67
 
@@ -63,12 +72,12 @@ class MessageDriver(AbstractMessage, ABC):
63
72
  cut_knife_height: int,
64
73
  max_run_speed: float,
65
74
  ):
66
- build = mctrl_driver.MctlDriver(
67
- mow_ctrl_by_hand=mctrl_driver.DrvMowCtrlByHand(
75
+ build = MctlDriver(
76
+ mow_ctrl_by_hand=DrvMowCtrlByHand(
68
77
  main_ctrl=main_ctrl,
69
78
  cut_knife_ctrl=cut_knife_ctrl,
70
79
  cut_knife_height=cut_knife_height,
71
- max_run__speed=max_run_speed,
80
+ max_run_speed=max_run_speed,
72
81
  )
73
82
  )
74
83
  logger.debug(
@@ -78,13 +87,11 @@ class MessageDriver(AbstractMessage, ABC):
78
87
 
79
88
  return self.send_order_msg_driver(build)
80
89
 
81
- def send_movement(self, linear_speed: int, angular_speed: int):
90
+ def send_movement(self, linear_speed: int, angular_speed: int) -> bytes:
82
91
  logger.debug(f"Control command print, linearSpeed={
83
92
  linear_speed} // angularSpeed={angular_speed}")
84
93
  return self.send_order_msg_driver(
85
- mctrl_driver.MctlDriver(
86
- todev_devmotion_ctrl=mctrl_driver.DrvMotionCtrl(
87
- set_linear_speed=linear_speed, set_angular_speed=angular_speed
88
- )
94
+ MctlDriver(
95
+ todev_devmotion_ctrl=DrvMotionCtrl(set_linear_speed=linear_speed, set_angular_speed=angular_speed)
89
96
  )
90
97
  )
@@ -2,18 +2,16 @@
2
2
  from abc import ABC
3
3
 
4
4
  from pymammotion.mammotion.commands.abstract_message import AbstractMessage
5
- from pymammotion.proto import luba_msg_pb2, luba_mul_pb2
6
- from pymammotion.proto.luba_msg import MsgCmdType, MsgDevice
7
- from pymammotion.proto.luba_mul import MUL_LANGUAGE
5
+ from pymammotion.proto import LubaMsg, MsgAttr, MsgCmdType, MsgDevice, MulLanguage
8
6
 
9
7
 
10
8
  class MessageMedia(AbstractMessage, ABC):
11
9
  def send_order_msg_media(self, mul):
12
- luba_msg = luba_msg_pb2.LubaMsg(
13
- msgtype=luba_msg_pb2.MSG_CMD_TYPE_MUL,
14
- sender=luba_msg_pb2.DEV_MOBILEAPP,
15
- rcver=self.get_msg_device(MsgCmdType.MSG_CMD_TYPE_MUL, MsgDevice.SOC_MODULE_MULTIMEDIA),
16
- msgattr=luba_msg_pb2.MSG_ATTR_REQ,
10
+ luba_msg = LubaMsg.LubaMsg(
11
+ msgtype=MsgCmdType.MUL,
12
+ sender=MsgDevice.DEV_MOBILEAPP,
13
+ rcver=self.get_msg_device(MsgCmdType.MUL, MsgDevice.SOC_MODULE_MULTIMEDIA),
14
+ msgattr=MsgAttr.REQ,
17
15
  seqs=1,
18
16
  version=1,
19
17
  subtype=1,
@@ -23,12 +21,10 @@ class MessageMedia(AbstractMessage, ABC):
23
21
  return luba_msg.SerializeToString()
24
22
 
25
23
  def set_car_volume(self, volume: int):
26
- return self.send_order_msg_media(luba_mul_pb2.SocMul(set_audio=luba_mul_pb2.MulSetAudio(at_switch=volume)))
24
+ return self.send_order_msg_media(LubaMsg.SocMul(set_audio=LubaMsg.MulSetAudio(at_switch=volume)))
27
25
 
28
- def set_car_voice_language(self, language_type: MUL_LANGUAGE | str | None):
29
- return self.send_order_msg_media(
30
- luba_mul_pb2.SocMul(set_audio=luba_mul_pb2.MulSetAudio(au_language=language_type))
31
- )
26
+ def set_car_voice_language(self, language_type: MulLanguage | str | None):
27
+ return self.send_order_msg_media(LubaMsg.SocMul(set_audio=LubaMsg.MulSetAudio(au_language=language_type)))
32
28
 
33
29
  def set_car_wiper(self, round_num: int):
34
- return self.send_order_msg_media(luba_mul_pb2.SocMul(set_wiper=luba_mul_pb2.MulSetWiper(round=round_num)))
30
+ return self.send_order_msg_media(LubaMsg.SocMul(set_wiper=LubaMsg.MulSetWiper(round=round_num)))
@@ -1,16 +1,19 @@
1
1
  # === sendOrderMsg_Nav ===
2
+ from abc import ABC
2
3
  import logging
3
4
  import time
4
- from abc import ABC
5
5
 
6
6
  from pymammotion.data.model import GenerateRouteInformation
7
7
  from pymammotion.data.model.plan import Plan
8
8
  from pymammotion.data.model.region_data import RegionData
9
9
  from pymammotion.mammotion.commands.abstract_message import AbstractMessage
10
- from pymammotion.proto.luba_msg import LubaMsg, MsgAttr, MsgCmdType, MsgDevice
11
- from pymammotion.proto.mctrl_nav import (
10
+ from pymammotion.proto import (
12
11
  AppRequestCoverPathsT,
12
+ LubaMsg,
13
13
  MctlNav,
14
+ MsgAttr,
15
+ MsgCmdType,
16
+ MsgDevice,
14
17
  NavGetCommData,
15
18
  NavGetHashList,
16
19
  NavMapNameMsg,
@@ -32,10 +35,10 @@ logger = logging.getLogger(__name__)
32
35
  class MessageNavigation(AbstractMessage, ABC):
33
36
  def send_order_msg_nav(self, build) -> bytes:
34
37
  luba_msg = LubaMsg(
35
- msgtype=MsgCmdType.MSG_CMD_TYPE_NAV,
38
+ msgtype=MsgCmdType.NAV,
36
39
  sender=MsgDevice.DEV_MOBILEAPP,
37
- rcver=self.get_msg_device(MsgCmdType.MSG_CMD_TYPE_NAV, MsgDevice.DEV_MAINCTL),
38
- msgattr=MsgAttr.MSG_ATTR_REQ,
40
+ rcver=self.get_msg_device(MsgCmdType.NAV, MsgDevice.DEV_MAINCTL),
41
+ msgattr=MsgAttr.REQ,
39
42
  seqs=1,
40
43
  version=1,
41
44
  subtype=1,
@@ -268,7 +271,7 @@ class MessageNavigation(AbstractMessage, ABC):
268
271
  logger.debug("Send command--Get area name list")
269
272
  return self.send_order_msg_nav(mctl_nav)
270
273
 
271
- def set_area_name(self, device_id, hash_id: int, name: str) -> bytes:
274
+ def set_area_name(self, device_id: str, hash_id: int, name: str) -> bytes:
272
275
  # Build the NavMapNameMsg with the specified parameters
273
276
  mctl_nav = MctlNav(
274
277
  toapp_map_name_msg=NavMapNameMsg(hash=hash_id, name=name, result=0, device_id=device_id, rw=1)
@@ -327,9 +330,9 @@ class MessageNavigation(AbstractMessage, ABC):
327
330
  logger.debug(f"Send command--Send tool command id={param_id},values={values}")
328
331
  return self.send_order_msg_nav(build)
329
332
 
330
- def end_draw_border(self, type: int) -> bytes:
333
+ def end_draw_border(self, type: int) -> bytes | None:
331
334
  if type == -1:
332
- return
335
+ return None
333
336
  build = MctlNav(todev_get_commondata=NavGetCommData(pver=1, action=1, type=type))
334
337
  logger.debug(f"Send command--End drawing boundary, obstacle, channel command type={type}")
335
338
  return self.send_order_msg_nav(build)
@@ -339,9 +342,9 @@ class MessageNavigation(AbstractMessage, ABC):
339
342
  logger.debug("Send command--Cancel current recording (boundary, obstacle)")
340
343
  return self.send_order_msg_nav(build)
341
344
 
342
- def delete_map_elements(self, type: int, hash_num: int) -> bytes:
345
+ def delete_map_elements(self, type: int, hash_num: int) -> bytes | None:
343
346
  if type == -1:
344
- return
347
+ return None
345
348
  build = MctlNav(todev_get_commondata=NavGetCommData(pver=1, action=6, type=type, hash=hash_num))
346
349
  logger.debug(f"Send command--Delete boundary or obstacle or channel command type={type},hash={hash}")
347
350
  return self.send_order_msg_nav(build)