pymammotion 0.5.34__py3-none-any.whl → 0.5.53__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.

Potentially problematic release.


This version of pymammotion might be problematic. Click here for more details.

Files changed (63) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/cloud_gateway.py +106 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +169 -21
  4. pymammotion/const.py +3 -0
  5. pymammotion/data/model/device.py +22 -9
  6. pymammotion/data/model/device_config.py +1 -1
  7. pymammotion/data/model/device_info.py +1 -0
  8. pymammotion/data/model/enums.py +5 -3
  9. pymammotion/data/model/events.py +14 -0
  10. pymammotion/data/model/generate_geojson.py +551 -0
  11. pymammotion/data/model/generate_route_information.py +2 -2
  12. pymammotion/data/model/hash_list.py +129 -33
  13. pymammotion/data/model/location.py +4 -4
  14. pymammotion/data/model/region_data.py +4 -4
  15. pymammotion/data/model/report_info.py +7 -0
  16. pymammotion/data/{state_manager.py → mower_state_manager.py} +75 -11
  17. pymammotion/data/mqtt/event.py +47 -22
  18. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  19. pymammotion/data/mqtt/properties.py +32 -29
  20. pymammotion/data/mqtt/status.py +17 -16
  21. pymammotion/event/event.py +5 -2
  22. pymammotion/homeassistant/__init__.py +3 -0
  23. pymammotion/homeassistant/mower_api.py +484 -0
  24. pymammotion/homeassistant/rtk_api.py +54 -0
  25. pymammotion/http/http.py +394 -14
  26. pymammotion/http/model/http.py +82 -2
  27. pymammotion/http/model/response_factory.py +10 -4
  28. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  29. pymammotion/mammotion/commands/messages/navigation.py +39 -6
  30. pymammotion/mammotion/devices/__init__.py +27 -3
  31. pymammotion/mammotion/devices/base.py +16 -138
  32. pymammotion/mammotion/devices/mammotion.py +369 -200
  33. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  34. pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
  35. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  36. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  37. pymammotion/mammotion/devices/managers/managers.py +81 -0
  38. pymammotion/mammotion/devices/mower_device.py +124 -0
  39. pymammotion/mammotion/devices/mower_manager.py +107 -0
  40. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  41. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  42. pymammotion/mammotion/devices/rtk_device.py +50 -0
  43. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  44. pymammotion/mqtt/__init__.py +2 -1
  45. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  46. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  47. pymammotion/mqtt/mqtt_models.py +66 -0
  48. pymammotion/proto/__init__.py +3 -3
  49. pymammotion/proto/mctrl_nav.proto +1 -1
  50. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  51. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  52. pymammotion/proto/mctrl_sys.proto +1 -1
  53. pymammotion/proto/mctrl_sys_pb2.py +1 -1
  54. pymammotion/utility/constant/device_constant.py +1 -1
  55. pymammotion/utility/datatype_converter.py +13 -12
  56. pymammotion/utility/device_type.py +88 -3
  57. pymammotion/utility/map.py +238 -51
  58. pymammotion/utility/mur_mur_hash.py +132 -87
  59. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/METADATA +26 -31
  60. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/RECORD +67 -51
  61. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/WHEEL +1 -1
  62. pymammotion/http/_init_.py +0 -0
  63. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info/licenses}/LICENSE +0 -0
@@ -1,72 +1,259 @@
1
1
  import math
2
2
 
3
3
  import numpy as np
4
+ from numpy.typing import NDArray
4
5
 
5
- from pymammotion.data.model.location import Point
6
+ from pymammotion.data.model.location import LocationPoint
6
7
 
7
8
 
8
9
  class CoordinateConverter:
9
- def __init__(self, latitude_rad: float, longitude_rad: float) -> None:
10
- # Initialize constants
11
- self.WGS84A = 6378137.0
12
- self.f_ = 3.3528106647474805e-21
13
- self.b_ = (1.0 - self.f_) * self.WGS84A
14
- self.e2_ = (2.0 - self.f_) * self.f_
15
- self.e2m_ = (1.0 - self.f_) * (1.0 - self.f_)
16
- self.ep2_ = ((self.WGS84A**2) - (self.b_**2)) / (self.b_**2)
17
-
18
- # Initialize rotation matrix
19
- self.R_ = np.zeros((3, 3))
20
-
21
- # Variables to store initial LLA
22
- self.x0_ = 0.0
23
- self.y0_ = 0.0
24
- self.z0_ = 0.0
25
-
26
- # Call set_init_lla with provided lat/lon
27
- self.set_init_lla(latitude_rad, longitude_rad)
28
-
29
- def set_init_lla(self, lat_rad, lon_rad) -> None:
10
+ """Converts between ENU (East-North-Up) and LLA (Latitude-Longitude-Altitude) coordinate systems.
11
+
12
+ Uses WGS84 ellipsoid model for Earth coordinate transformations.
13
+ """
14
+
15
+ # WGS84 ellipsoid constants
16
+ WGS84A: float = 6378137.0 # Semi-major axis (equatorial radius) in meters
17
+ FLATTENING: float = 0.0033528106647474805 # WGS84 flattening factor
18
+
19
+ def __init__(self, latitude_rad: float, longitude_rad: float, yaw_rad: float = 0.0) -> None:
20
+ """Initialize the coordinate converter.
21
+
22
+ Args:
23
+ latitude_rad: Reference latitude in radians
24
+ longitude_rad: Reference longitude in radians
25
+ yaw_rad: Reference yaw angle in radians (default: 0.0)
26
+
27
+ """
28
+ # WGS84 ellipsoid parameters
29
+ self.semi_major_axis: float = self.WGS84A
30
+ self.flattening: float = self.FLATTENING
31
+ self.semi_minor_axis: float = (1.0 - self.flattening) * self.WGS84A
32
+
33
+ # Eccentricity parameters
34
+ self.first_eccentricity_squared: float = (2.0 - self.flattening) * self.flattening
35
+ self.eccentricity_ratio_squared: float = (1.0 - self.flattening) * (1.0 - self.flattening)
36
+ self.second_eccentricity_squared: float = (self.semi_major_axis**2 - self.semi_minor_axis**2) / (
37
+ self.semi_minor_axis**2
38
+ )
39
+
40
+ # Rotation matrix for coordinate transformation
41
+ self.rotation_matrix: NDArray[np.float64] = np.zeros((3, 3), dtype=np.float64)
42
+
43
+ # ECEF origin coordinates
44
+ self.x0: float = 0.0
45
+ self.y0: float = 0.0
46
+ self.z0: float = 0.0
47
+
48
+ # Yaw angle
49
+ self.yaw: float = yaw_rad
50
+
51
+ # Initialize with provided reference point
52
+ self.set_init_lla(latitude_rad, longitude_rad, yaw_rad)
53
+
54
+ def set_init_lla(self, latitude_rad: float, longitude_rad: float, yaw_rad: float = 0.0) -> None:
55
+ """Set the initial LLA reference point and yaw angle.
56
+
57
+ Args:
58
+ latitude_rad: Reference latitude in radians
59
+ longitude_rad: Reference longitude in radians
60
+ yaw_rad: Reference yaw angle in radians (default: 0.0)
61
+
62
+ """
63
+ self.yaw = yaw_rad
64
+
65
+ sin_lat = math.sin(latitude_rad)
66
+ cos_lat = math.cos(latitude_rad)
67
+ sin_lon = math.sin(longitude_rad)
68
+ cos_lon = math.cos(longitude_rad)
69
+
70
+ # Radius of curvature in the prime vertical
71
+ N = self.semi_major_axis / math.sqrt(1.0 - self.first_eccentricity_squared * (sin_lat**2))
72
+
73
+ # Calculate ECEF origin coordinates (altitude = 0)
74
+ horizontal_distance = N * cos_lat
75
+ self.x0 = horizontal_distance * cos_lon
76
+ self.y0 = horizontal_distance * sin_lon
77
+ self.z0 = self.eccentricity_ratio_squared * N * sin_lat
78
+
79
+ # Build rotation matrix (ECEF to ENU)
80
+ self.rotation_matrix[0, 0] = -sin_lon
81
+ self.rotation_matrix[0, 1] = cos_lon
82
+ self.rotation_matrix[0, 2] = 0.0
83
+
84
+ self.rotation_matrix[1, 0] = -cos_lon * sin_lat
85
+ self.rotation_matrix[1, 1] = -sin_lon * sin_lat
86
+ self.rotation_matrix[1, 2] = cos_lat
87
+
88
+ self.rotation_matrix[2, 0] = cos_lon * cos_lat
89
+ self.rotation_matrix[2, 1] = sin_lon * cos_lat
90
+ self.rotation_matrix[2, 2] = sin_lat
91
+
92
+ def enu_to_lla(self, east: float, north: float) -> LocationPoint:
93
+ """Convert ENU (East-North-Up) coordinates to LLA (Latitude-Longitude-Altitude).
94
+
95
+ Args:
96
+ east: East coordinate in meters
97
+ north: North coordinate in meters
98
+
99
+ Returns:
100
+ Point with latitude and longitude in degrees
101
+
102
+ """
103
+ # Transform ENU to ECEF (Earth-Centered, Earth-Fixed) coordinates
104
+ # using rotation matrix and origin offset
105
+ ecef_x = self.rotation_matrix[0, 0] * north + self.rotation_matrix[1, 0] * east + self.x0
106
+ ecef_y = self.rotation_matrix[0, 1] * north + self.rotation_matrix[1, 1] * east + self.y0
107
+ ecef_z = self.rotation_matrix[0, 2] * north + self.rotation_matrix[1, 2] * east + self.z0
108
+
109
+ # Calculate horizontal distance from Earth's axis
110
+ horizontal_distance = math.hypot(ecef_x, ecef_y)
111
+
112
+ # Initial latitude estimate using simplified formula
113
+ initial_latitude = math.atan2(self.semi_major_axis * ecef_z, self.semi_minor_axis * horizontal_distance)
114
+
115
+ sin_initial_lat = math.sin(initial_latitude)
116
+ cos_initial_lat = math.cos(initial_latitude)
117
+
118
+ # Calculate longitude (straightforward conversion from ECEF)
119
+ longitude_deg = math.degrees(math.atan2(ecef_y, ecef_x))
120
+
121
+ # Calculate precise latitude using iterative correction
122
+ # This accounts for Earth's ellipsoidal shape
123
+ latitude_rad = math.atan2(
124
+ ecef_z + self.second_eccentricity_squared * self.semi_minor_axis * (sin_initial_lat**3),
125
+ horizontal_distance - self.first_eccentricity_squared * self.semi_major_axis * (cos_initial_lat**3),
126
+ )
127
+ latitude_deg = math.degrees(latitude_rad)
128
+
129
+ return LocationPoint(latitude=latitude_deg, longitude=longitude_deg)
130
+
131
+ def lla_to_enu(self, longitude_deg: float, latitude_deg: float) -> list[float]:
132
+ """Convert LLA (Latitude-Longitude-Altitude) to ENU (East-North-Up) coordinates.
133
+
134
+ Args:
135
+ longitude_deg: Longitude in degrees
136
+ latitude_deg: Latitude in degrees
137
+
138
+ Returns:
139
+ List of [east, north] coordinates in meters
140
+
141
+ """
142
+ # Convert to radians
143
+ lat_rad = math.radians(latitude_deg)
144
+ lon_rad = math.radians(longitude_deg)
145
+
30
146
  sin_lat = math.sin(lat_rad)
31
147
  cos_lat = math.cos(lat_rad)
32
148
  sin_lon = math.sin(lon_rad)
33
149
  cos_lon = math.cos(lon_rad)
34
150
 
35
- sqrt = self.WGS84A / math.sqrt(1.0 - (self.e2_ * (sin_lat**2)))
36
- d3 = sqrt * cos_lat
151
+ # Calculate radius of curvature
152
+ N = self.semi_major_axis / math.sqrt(1.0 - self.first_eccentricity_squared * (sin_lat**2))
37
153
 
38
- self.x0_ = d3 * cos_lon
39
- self.y0_ = d3 * sin_lon
40
- self.z0_ = self.e2m_ * sqrt * sin_lat
154
+ # Convert to ECEF (altitude = 0)
155
+ horizontal = N * cos_lat
156
+ ecef_x = horizontal * cos_lon
157
+ ecef_y = horizontal * sin_lon
158
+ ecef_z = self.eccentricity_ratio_squared * N * sin_lat
41
159
 
42
- self.R_[0][0] = -sin_lon
43
- self.R_[0][1] = cos_lon
44
- self.R_[0][2] = 0.0
160
+ # Translate to origin
161
+ dx = ecef_x - self.x0
162
+ dy = ecef_y - self.y0
163
+ dz = ecef_z - self.z0
45
164
 
46
- self.R_[1][0] = -cos_lon * sin_lat
47
- self.R_[1][1] = -sin_lon * sin_lat
48
- self.R_[1][2] = cos_lat
165
+ # Rotate to ENU frame
166
+ east = self.rotation_matrix[0, 0] * dx + self.rotation_matrix[0, 1] * dy + self.rotation_matrix[0, 2] * dz
167
+ north = self.rotation_matrix[1, 0] * dx + self.rotation_matrix[1, 1] * dy + self.rotation_matrix[1, 2] * dz
49
168
 
50
- self.R_[2][0] = cos_lon * cos_lat
51
- self.R_[2][1] = sin_lon * cos_lat
52
- self.R_[2][2] = sin_lat
169
+ # Apply yaw rotation (inverse)
170
+ rotated_east = math.cos(-self.yaw) * east - math.sin(-self.yaw) * north
171
+ rotated_north = math.sin(-self.yaw) * east + math.cos(-self.yaw) * north
53
172
 
54
- def enu_to_lla(self, e, n) -> Point:
55
- d3 = self.R_[0][0] * n + self.R_[1][0] * e + self.x0_
56
- d4 = self.R_[0][1] * n + self.R_[1][1] * e + self.y0_
57
- d5 = self.R_[0][2] * n + self.R_[1][2] * e + self.z0_
173
+ return [round(rotated_east, 3), round(rotated_north, 3)]
58
174
 
59
- hypot = math.hypot(d3, d4)
60
- atan2_lat = math.atan2(self.WGS84A * d5, self.b_ * hypot)
175
+ def get_transform_yaw_with_yaw(self, yaw_degrees: float) -> float:
176
+ """Transform a yaw angle from global coordinates to local coordinates.
61
177
 
62
- sin_lat = math.sin(atan2_lat)
63
- cos_lat = math.cos(atan2_lat)
178
+ This applies the inverse of the reference yaw rotation to convert a global
179
+ heading angle into the local coordinate frame.
64
180
 
65
- lon = math.atan2(d4, d3) * 180.0 / math.pi
66
- lat = (
67
- math.atan2(d5 + self.ep2_ * self.b_ * (sin_lat**3), hypot - self.e2_ * self.WGS84A * (cos_lat**3))
68
- * 180.0
69
- / math.pi
70
- )
181
+ Args:
182
+ yaw_degrees: Input yaw angle in degrees
183
+
184
+ Returns:
185
+ Transformed yaw angle in degrees
186
+
187
+ """
188
+ # Convert input angle to radians
189
+ yaw_rad = math.radians(yaw_degrees)
190
+
191
+ # Apply inverse rotation: -self.yaw
192
+ inverse_yaw = -self.yaw
193
+
194
+ # Using angle addition formula: atan2(sin(a+b), cos(a+b))
195
+ # where a = inverse_yaw, b = yaw_rad
196
+ sin_sum = math.sin(inverse_yaw) * math.cos(yaw_rad) + math.cos(inverse_yaw) * math.sin(yaw_rad)
197
+ cos_sum = math.cos(inverse_yaw) * math.cos(yaw_rad) - math.sin(inverse_yaw) * math.sin(yaw_rad)
198
+
199
+ # Calculate resulting angle and convert back to degrees
200
+ result_rad = math.atan2(sin_sum, cos_sum)
201
+ result_degrees = math.degrees(result_rad)
202
+
203
+ return result_degrees
204
+
205
+ def get_angle_yaw(self) -> float:
206
+ """Get the current yaw angle in degrees.
207
+
208
+ Returns:
209
+ Yaw angle in degrees
210
+
211
+ """
212
+ return math.degrees(self.yaw)
213
+
214
+ def get_yaw(self) -> float:
215
+ """Get the current yaw angle in radians.
216
+
217
+ Returns:
218
+ Yaw angle in radians
219
+
220
+ """
221
+ return self.yaw
222
+
223
+ def set_yaw(self, yaw_rad: float) -> None:
224
+ """Set the yaw angle.
225
+
226
+ Args:
227
+ yaw_rad: Yaw angle in radians
228
+
229
+ """
230
+ self.yaw = yaw_rad
231
+
232
+ def set_yaw_degrees(self, yaw_degrees: float) -> None:
233
+ """Set the yaw angle from degrees.
234
+
235
+ Args:
236
+ yaw_degrees: Yaw angle in degrees
237
+
238
+ """
239
+ self.yaw = math.radians(yaw_degrees)
240
+
241
+
242
+ # Usage example
243
+ if __name__ == "__main__":
244
+ # Initialize converter with reference point
245
+ converter = CoordinateConverter(
246
+ latitude_rad=math.radians(40.0), longitude_rad=math.radians(-105.0), yaw_rad=math.radians(45.0)
247
+ )
248
+
249
+ # Convert ENU to LLA
250
+ point = converter.enu_to_lla(east=100.0, north=200.0)
251
+ print(f"Latitude: {point.latitude}, Longitude: {point.longitude}")
252
+
253
+ # Convert LLA to ENU
254
+ enu = converter.lla_to_enu(longitude_deg=-105.0, latitude_deg=40.0)
255
+ print(f"East: {enu[0]}, North: {enu[1]}")
71
256
 
72
- return Point(latitude=lat, longitude=lon)
257
+ # Transform yaw angle
258
+ transformed_yaw = converter.get_transform_yaw_with_yaw(90.0)
259
+ print(f"Transformed yaw: {transformed_yaw}°")
@@ -2,113 +2,158 @@ import struct
2
2
 
3
3
 
4
4
  class MurMurHashUtil:
5
+ MASK_32 = 0xFFFFFFFF
6
+ MULTIPLIER = 1540483477
7
+
5
8
  @staticmethod
6
- def get_unsigned_int(i):
7
- return i & 0xFFFFFFFF
9
+ def get_unsigned_int(i: int) -> int:
10
+ """Convert signed int to unsigned (32-bit)"""
11
+ return i & MurMurHashUtil.MASK_32
8
12
 
9
13
  @staticmethod
10
- def hash(byte_arr: bytes):
11
- # Create a bytearray view with little endian order
12
- position = 0
13
- remaining_bytes = len(byte_arr)
14
+ def hash(data: bytes) -> int:
15
+ """MurmurHash2 64-bit implementation"""
16
+ pos = 0
17
+ data_len = len(data)
14
18
 
15
- # Initial values
16
- remaining = remaining_bytes ^ 97
19
+ remaining = data_len ^ 97
17
20
  j = 0
18
21
 
19
22
  # Process 8 bytes at a time
20
- while remaining_bytes >= 8:
21
- multiplier = 1540483477
22
-
23
- # First 4 bytes
24
- unsigned_int = struct.unpack_from("<I", byte_arr, position)[0]
25
- position += 4
26
- unsigned_int = (MurMurHashUtil.get_unsigned_int(unsigned_int) * multiplier) & 0xFFFFFFFF
27
- remaining = (
28
- ((remaining * multiplier) & 0xFFFFFFFF)
29
- ^ (((unsigned_int ^ (unsigned_int >> 24)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF
30
- ) & 0xFFFFFFFF
31
-
32
- # Next 4 bytes
33
- unsigned_int2 = struct.unpack_from("<I", byte_arr, position)[0]
34
- position += 4
35
- unsigned_int2 = (MurMurHashUtil.get_unsigned_int(unsigned_int2) * multiplier) & 0xFFFFFFFF
36
- j = (
37
- ((j * multiplier) & 0xFFFFFFFF)
38
- ^ ((((unsigned_int2 ^ (unsigned_int2 >> 24)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF)
39
- ) & 0xFFFFFFFF
40
-
41
- remaining_bytes -= 8
23
+ while (data_len - pos) >= 8:
24
+ val1 = struct.unpack_from("<i", data, pos)[0]
25
+ pos += 4
26
+
27
+ unsigned_int_1 = MurMurHashUtil.get_unsigned_int(val1)
28
+ temp1 = (unsigned_int_1 * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
29
+ temp2 = (temp1 ^ (temp1 >> 24)) & MurMurHashUtil.MASK_32
30
+ temp3 = (temp2 * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
31
+
32
+ remaining = ((remaining * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32) ^ temp3
33
+ remaining = remaining & MurMurHashUtil.MASK_32
34
+
35
+ val2 = struct.unpack_from("<i", data, pos)[0]
36
+ pos += 4
37
+
38
+ unsigned_int_2 = MurMurHashUtil.get_unsigned_int(val2)
39
+ temp1 = (unsigned_int_2 * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
40
+ temp2 = (temp1 ^ (temp1 >> 24)) & MurMurHashUtil.MASK_32
41
+ temp3 = (temp2 * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
42
+
43
+ j = ((j * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32) ^ temp3
44
+ j = j & MurMurHashUtil.MASK_32
42
45
 
43
46
  # Process remaining 4 bytes if available
44
- if remaining_bytes >= 4:
45
- multiplier = 1540483477
46
- unsigned_int3 = struct.unpack_from("<I", byte_arr, position)[0]
47
- position += 4
48
- unsigned_int3 = (MurMurHashUtil.get_unsigned_int(unsigned_int3) * multiplier) & 0xFFFFFFFF
49
- remaining = (
50
- ((remaining * multiplier) & 0xFFFFFFFF)
51
- ^ ((((unsigned_int3 ^ (unsigned_int3 >> 24)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF)
52
- ) & 0xFFFFFFFF
53
- remaining_bytes -= 4
54
-
55
- # Process final 1-3 bytes if available
56
- if remaining_bytes == 1:
57
- j = (
58
- ((j ^ (MurMurHashUtil.get_unsigned_int(byte_arr[position]) & 0xFF)) & 0xFFFFFFFF) * 1540483477
59
- ) & 0xFFFFFFFF
60
- elif remaining_bytes == 2:
61
- j = (j ^ ((MurMurHashUtil.get_unsigned_int(byte_arr[position + 1]) & 0xFF) << 8)) & 0xFFFFFFFF
62
- j = (
63
- ((j ^ (MurMurHashUtil.get_unsigned_int(byte_arr[position]) & 0xFF)) & 0xFFFFFFFF) * 1540483477
64
- ) & 0xFFFFFFFF
65
- elif remaining_bytes == 3:
66
- j = (j ^ ((MurMurHashUtil.get_unsigned_int(byte_arr[position + 2]) & 0xFF) << 16)) & 0xFFFFFFFF
67
- j = (j ^ ((MurMurHashUtil.get_unsigned_int(byte_arr[position + 1]) & 0xFF) << 8)) & 0xFFFFFFFF
68
- j = (
69
- ((j ^ (MurMurHashUtil.get_unsigned_int(byte_arr[position]) & 0xFF)) & 0xFFFFFFFF) * 1540483477
70
- ) & 0xFFFFFFFF
71
-
72
- # Final mixing
73
- multiplier = 1540483477
74
- j5 = (((remaining ^ (j >> 18)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF
75
- j6 = (((j ^ (j5 >> 22)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF
76
- j7 = (((j5 ^ (j6 >> 17)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF
47
+ if (data_len - pos) >= 4:
48
+ val = struct.unpack_from("<i", data, pos)[0]
49
+ pos += 4
50
+
51
+ unsigned_int_3 = MurMurHashUtil.get_unsigned_int(val)
52
+ temp1 = (unsigned_int_3 * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
53
+ temp2 = (temp1 ^ (temp1 >> 24)) & MurMurHashUtil.MASK_32
54
+ temp3 = (temp2 * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
55
+
56
+ remaining = ((remaining * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32) ^ temp3
57
+ remaining = remaining & MurMurHashUtil.MASK_32
58
+
59
+ # Process tail bytes (1-3 bytes)
60
+ bytes_remaining = data_len - pos
61
+
62
+ if bytes_remaining == 1:
63
+ byte_val = data[pos] if data[pos] < 128 else data[pos] - 256
64
+ j = (j ^ (MurMurHashUtil.get_unsigned_int(byte_val) & 255)) & MurMurHashUtil.MASK_32
65
+ j = (j * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
66
+
67
+ elif bytes_remaining == 2:
68
+ byte_val1 = data[pos + 1] if data[pos + 1] < 128 else data[pos + 1] - 256
69
+ j = (j ^ ((MurMurHashUtil.get_unsigned_int(byte_val1) & 255) << 8)) & MurMurHashUtil.MASK_32
70
+
71
+ byte_val0 = data[pos] if data[pos] < 128 else data[pos] - 256
72
+ j = (j ^ (MurMurHashUtil.get_unsigned_int(byte_val0) & 255)) & MurMurHashUtil.MASK_32
73
+ j = (j * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
74
+
75
+ elif bytes_remaining == 3:
76
+ byte_val2 = data[pos + 2] if data[pos + 2] < 128 else data[pos + 2] - 256
77
+ j = (j ^ ((MurMurHashUtil.get_unsigned_int(byte_val2) & 255) << 16)) & MurMurHashUtil.MASK_32
78
+
79
+ byte_val1 = data[pos + 1] if data[pos + 1] < 128 else data[pos + 1] - 256
80
+ j = (j ^ ((MurMurHashUtil.get_unsigned_int(byte_val1) & 255) << 8)) & MurMurHashUtil.MASK_32
81
+
82
+ byte_val0 = data[pos] if data[pos] < 128 else data[pos] - 256
83
+ j = (j ^ (MurMurHashUtil.get_unsigned_int(byte_val0) & 255)) & MurMurHashUtil.MASK_32
84
+ j = (j * MurMurHashUtil.MULTIPLIER) & MurMurHashUtil.MASK_32
85
+
86
+ # Final avalanche
87
+ j4 = MurMurHashUtil.MULTIPLIER
88
+
89
+ j5 = remaining ^ (j >> 18)
90
+ j5 = (j5 & MurMurHashUtil.MASK_32) * j4
91
+ j5 = j5 & MurMurHashUtil.MASK_32
92
+
93
+ j6 = j ^ (j5 >> 22)
94
+ j6 = (j6 & MurMurHashUtil.MASK_32) * j4
95
+ j6 = j6 & MurMurHashUtil.MASK_32
96
+
97
+ j7 = j5 ^ (j6 >> 17)
98
+ j7 = (j7 & MurMurHashUtil.MASK_32) * j4
99
+ j7 = j7 & MurMurHashUtil.MASK_32
100
+
101
+ # Combine high and low parts
77
102
  j8 = j7 << 32
78
103
 
79
- return j8 | ((((j6 ^ (j7 >> 19)) & 0xFFFFFFFF) * multiplier) & 0xFFFFFFFF)
104
+ low = j6 ^ (j7 >> 19)
105
+ low = (low & MurMurHashUtil.MASK_32) * j4
106
+ low = low & MurMurHashUtil.MASK_32
80
107
 
81
- @staticmethod
82
- def hash_unsigned(s: str | bytes) -> None:
83
- if isinstance(s, str):
84
- return MurMurHashUtil.read_unsigned_long(MurMurHashUtil.hash(s.encode()))
85
- elif isinstance(s, bytes) or isinstance(s, bytearray):
86
- return MurMurHashUtil.read_unsigned_long(MurMurHashUtil.hash(s))
87
- return None
108
+ result = j8 | low
109
+
110
+ # Convert to signed 64-bit
111
+ if result > 0x7FFFFFFFFFFFFFFF:
112
+ result = result - 0x10000000000000000
113
+
114
+ return result
88
115
 
89
116
  @staticmethod
90
- def long2bytes(value: int) -> bytes:
91
- # Convert long to 8 bytes in little-endian order
92
- result = bytearray(8)
93
- for i in range(8):
94
- result[7 - i] = (value >> (i * 8)) & 0xFF
95
- return bytes(result)
117
+ def hash_string(s: str) -> int:
118
+ """Hash a string using UTF-8 encoding"""
119
+ return MurMurHashUtil.hash(s.encode("utf-8"))
96
120
 
97
121
  @staticmethod
98
122
  def read_unsigned_long(value: int) -> int:
123
+ """Convert to unsigned by masking with Long.MAX_VALUE"""
99
124
  return value & 0x7FFFFFFFFFFFFFFF
100
125
 
101
126
  @staticmethod
102
- def hash_unsigned_list(long_list: list[int]):
103
- byte_arr = b""
104
- for i in range(len(long_list)):
105
- if i == 0:
106
- byte_arr = MurMurHashUtil.long2bytes(long_list[i])
107
- else:
108
- byte_arr += MurMurHashUtil.long2bytes(long_list[i])
127
+ def hash_unsigned(data: str | bytes) -> int:
128
+ """Get unsigned hash value
129
+ Can accept bytes or string
130
+ """
131
+ if isinstance(data, str):
132
+ hash_val = MurMurHashUtil.hash_string(data)
133
+ else:
134
+ hash_val = MurMurHashUtil.hash(data)
109
135
 
110
- return MurMurHashUtil.read_unsigned_long(MurMurHashUtil.hash(byte_arr))
136
+ return MurMurHashUtil.read_unsigned_long(hash_val)
111
137
 
112
138
  @staticmethod
113
- def hash_string(s: str):
114
- return MurMurHashUtil.hash(s.encode())
139
+ def long_to_bytes(value: int) -> bytes:
140
+ """Convert long to bytes exactly as Java does:
141
+ 1. Pack as big-endian (ByteBuffer default)
142
+ 2. Reverse all bytes
143
+ """
144
+ if value < 0:
145
+ value = value & 0xFFFFFFFFFFFFFFFF
146
+
147
+ big_endian = struct.pack(">Q", value)
148
+ return big_endian[::-1]
149
+
150
+ @staticmethod
151
+ def hash_unsigned_list(values: list[int]) -> int:
152
+ """Hash a list of long values"""
153
+ data = b""
154
+
155
+ for val in values:
156
+ data += MurMurHashUtil.long_to_bytes(val)
157
+
158
+ hash_val = MurMurHashUtil.hash(data)
159
+ return MurMurHashUtil.read_unsigned_long(hash_val)
@@ -1,34 +1,30 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pymammotion
3
- Version: 0.5.34
4
- Summary:
5
- License: GPL-3.0
6
- Author: Michael Arthur
7
- Author-email: michael@jumblesoft.co.nz
8
- Requires-Python: >=3.10
9
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Classifier: Programming Language :: Python :: 3.13
14
- Requires-Dist: aiohttp (>=3.9.1)
15
- Requires-Dist: alibabacloud-apigateway-util (>=0.0.2,<0.0.3)
16
- Requires-Dist: alibabacloud-iot-api-gateway (>=0.0.4,<0.0.5)
17
- Requires-Dist: alicloud-gateway-iot (>=1.0.0,<2.0.0)
18
- Requires-Dist: async-timeout (>=4.0.3,<5.0.0)
19
- Requires-Dist: betterproto2 (>=0.8.0,<0.9.0)
20
- Requires-Dist: bleak (>=0.21.0)
21
- Requires-Dist: bleak-retry-connector (>=3.5.0)
22
- Requires-Dist: crcmod (>=1.7,<2.0)
23
- Requires-Dist: cryptography (>=43.0.1)
24
- Requires-Dist: grpclib (>=0.4.8,<0.5.0)
25
- Requires-Dist: jsonic (>=1.0.0,<2.0.0)
26
- Requires-Dist: mashumaro (>=3.13,<4.0)
27
- Requires-Dist: numpy (>=1.26.0)
28
- Requires-Dist: orjson (>=3.9.15,<4.0.0)
29
- Requires-Dist: paho-mqtt (>=2.1.0,<3.0.0)
30
- Requires-Dist: protobuf (>=4.23.1)
31
- Requires-Dist: py-jsonic (>=0.0.2,<0.0.3)
3
+ Version: 0.5.53
4
+ Author: jLynx
5
+ Author-email: Michael Arthur <michael@jumblesoft.co.nz>
6
+ License-Expression: GPL-3.0
7
+ License-File: LICENSE
8
+ Requires-Python: <4.0,>=3.12
9
+ Requires-Dist: aiohttp>=3.9.1
10
+ Requires-Dist: alibabacloud-apigateway-util<0.0.3,>=0.0.2
11
+ Requires-Dist: alibabacloud-iot-api-gateway<0.0.5,>=0.0.4
12
+ Requires-Dist: alicloud-gateway-iot<2,>=1.0.0
13
+ Requires-Dist: async-timeout<5,>=4.0.3
14
+ Requires-Dist: betterproto2>=0.9.1
15
+ Requires-Dist: bleak-retry-connector>=3.5.0
16
+ Requires-Dist: bleak>=0.21.0
17
+ Requires-Dist: crcmod~=1.7
18
+ Requires-Dist: cryptography>=43.0.1
19
+ Requires-Dist: jsonic<2,>=1.0.0
20
+ Requires-Dist: mashumaro~=3.13
21
+ Requires-Dist: numpy>=1.26.0
22
+ Requires-Dist: orjson<4,>=3.9.15
23
+ Requires-Dist: paho-mqtt<3,>=2.1.0
24
+ Requires-Dist: protobuf>=4.23.1
25
+ Requires-Dist: py-jsonic<0.0.3,>=0.0.2
26
+ Requires-Dist: pyjwt>=2.10.1
27
+ Requires-Dist: shapely>=2.1.2
32
28
  Description-Content-Type: text/markdown
33
29
 
34
30
  # PyMammotion - Python API for Mammotion Mowers [![Discord](https://img.shields.io/discord/1247286396297678879)](https://discord.gg/vpZdWhJX8x)
@@ -97,4 +93,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
97
93
 
98
94
  The trademarks "Mammotion," "Luba," and "Yuka" referenced herein are registered trademarks of their respective owners. The author of this software repository is not affiliated with, endorsed by, or connected to these trademark owners in any way.
99
95
 
100
-