Coverage for src / bluetooth_sig / gatt / characteristics / templates / numeric.py: 75%

151 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""Basic integer templates for unsigned and signed integer parsing. 

2 

3Covers Uint8, Sint8, Uint16, Sint16, Uint24, Uint32, Sint32, Uint48 templates. 

4""" 

5 

6from __future__ import annotations 

7 

8from ...constants import ( 

9 SINT8_MAX, 

10 SINT8_MIN, 

11 SINT16_MAX, 

12 SINT16_MIN, 

13 SINT32_MAX, 

14 SINT32_MIN, 

15 UINT8_MAX, 

16 UINT16_MAX, 

17 UINT24_MAX, 

18 UINT32_MAX, 

19 UINT48_MAX, 

20) 

21from ...context import CharacteristicContext 

22from ...exceptions import InsufficientDataError 

23from ..utils.extractors import ( 

24 SINT8, 

25 SINT16, 

26 SINT32, 

27 UINT8, 

28 UINT16, 

29 UINT24, 

30 UINT32, 

31 UINT48, 

32 RawExtractor, 

33) 

34from ..utils.translators import ( 

35 IDENTITY, 

36 ValueTranslator, 

37) 

38from .base import CodingTemplate 

39 

40 

41class Uint8Template(CodingTemplate[int]): 

42 """Template for 8-bit unsigned integer parsing (0-255).""" 

43 

44 @property 

45 def data_size(self) -> int: 

46 """Size: 1 byte.""" 

47 return 1 

48 

49 @property 

50 def extractor(self) -> RawExtractor: 

51 """Get uint8 extractor.""" 

52 return UINT8 

53 

54 @property 

55 def translator(self) -> ValueTranslator[int]: 

56 """Return identity translator for no scaling.""" 

57 return IDENTITY 

58 

59 def decode_value( 

60 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

61 ) -> int: 

62 """Parse 8-bit unsigned integer.""" 

63 if validate and len(data) < offset + 1: 

64 raise InsufficientDataError("uint8", data[offset:], 1) 

65 return self.extractor.extract(data, offset) 

66 

67 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

68 """Encode uint8 value to bytes.""" 

69 if validate and not 0 <= value <= UINT8_MAX: 

70 raise ValueError(f"Value {value} out of range for uint8 (0-{UINT8_MAX})") 

71 return self.extractor.pack(value) 

72 

73 

74class Sint8Template(CodingTemplate[int]): 

75 """Template for 8-bit signed integer parsing (-128 to 127).""" 

76 

77 @property 

78 def data_size(self) -> int: 

79 """Size: 1 byte.""" 

80 return 1 

81 

82 @property 

83 def extractor(self) -> RawExtractor: 

84 """Get sint8 extractor.""" 

85 return SINT8 

86 

87 @property 

88 def translator(self) -> ValueTranslator[int]: 

89 """Return identity translator for no scaling.""" 

90 return IDENTITY 

91 

92 def decode_value( 

93 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

94 ) -> int: 

95 """Parse 8-bit signed integer.""" 

96 if validate and len(data) < offset + 1: 

97 raise InsufficientDataError("sint8", data[offset:], 1) 

98 return self.extractor.extract(data, offset) 

99 

100 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

101 """Encode sint8 value to bytes.""" 

102 if validate and not SINT8_MIN <= value <= SINT8_MAX: 

103 raise ValueError(f"Value {value} out of range for sint8 ({SINT8_MIN} to {SINT8_MAX})") 

104 return self.extractor.pack(value) 

105 

106 

107class Uint16Template(CodingTemplate[int]): 

108 """Template for 16-bit unsigned integer parsing (0-65535).""" 

109 

110 @property 

111 def data_size(self) -> int: 

112 """Size: 2 bytes.""" 

113 return 2 

114 

115 @property 

116 def extractor(self) -> RawExtractor: 

117 """Get uint16 extractor.""" 

118 return UINT16 

119 

120 @property 

121 def translator(self) -> ValueTranslator[int]: 

122 """Return identity translator for no scaling.""" 

123 return IDENTITY 

124 

125 def decode_value( 

126 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

127 ) -> int: 

128 """Parse 16-bit unsigned integer.""" 

129 if validate and len(data) < offset + 2: 

130 raise InsufficientDataError("uint16", data[offset:], 2) 

131 return self.extractor.extract(data, offset) 

132 

133 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

134 """Encode uint16 value to bytes.""" 

135 if validate and not 0 <= value <= UINT16_MAX: 

136 raise ValueError(f"Value {value} out of range for uint16 (0-{UINT16_MAX})") 

137 return self.extractor.pack(value) 

138 

139 

140class Sint16Template(CodingTemplate[int]): 

141 """Template for 16-bit signed integer parsing (-32768 to 32767).""" 

142 

143 @property 

144 def data_size(self) -> int: 

145 """Size: 2 bytes.""" 

146 return 2 

147 

148 @property 

149 def extractor(self) -> RawExtractor: 

150 """Get sint16 extractor.""" 

151 return SINT16 

152 

153 @property 

154 def translator(self) -> ValueTranslator[int]: 

155 """Return identity translator for no scaling.""" 

156 return IDENTITY 

157 

158 def decode_value( 

159 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

160 ) -> int: 

161 """Parse 16-bit signed integer.""" 

162 if validate and len(data) < offset + 2: 

163 raise InsufficientDataError("sint16", data[offset:], 2) 

164 return self.extractor.extract(data, offset) 

165 

166 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

167 """Encode sint16 value to bytes.""" 

168 if validate and not SINT16_MIN <= value <= SINT16_MAX: 

169 raise ValueError(f"Value {value} out of range for sint16 ({SINT16_MIN} to {SINT16_MAX})") 

170 return self.extractor.pack(value) 

171 

172 

173class Uint24Template(CodingTemplate[int]): 

174 """Template for 24-bit unsigned integer parsing (0-16777215).""" 

175 

176 @property 

177 def data_size(self) -> int: 

178 """Size: 3 bytes.""" 

179 return 3 

180 

181 @property 

182 def extractor(self) -> RawExtractor: 

183 """Get uint24 extractor.""" 

184 return UINT24 

185 

186 @property 

187 def translator(self) -> ValueTranslator[int]: 

188 """Return identity translator for no scaling.""" 

189 return IDENTITY 

190 

191 def decode_value( 

192 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

193 ) -> int: 

194 """Parse 24-bit unsigned integer.""" 

195 if validate and len(data) < offset + 3: 

196 raise InsufficientDataError("uint24", data[offset:], 3) 

197 return self.extractor.extract(data, offset) 

198 

199 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

200 """Encode uint24 value to bytes.""" 

201 if validate and not 0 <= value <= UINT24_MAX: 

202 raise ValueError(f"Value {value} out of range for uint24 (0-{UINT24_MAX})") 

203 return self.extractor.pack(value) 

204 

205 

206class Uint32Template(CodingTemplate[int]): 

207 """Template for 32-bit unsigned integer parsing.""" 

208 

209 @property 

210 def data_size(self) -> int: 

211 """Size: 4 bytes.""" 

212 return 4 

213 

214 @property 

215 def extractor(self) -> RawExtractor: 

216 """Get uint32 extractor.""" 

217 return UINT32 

218 

219 @property 

220 def translator(self) -> ValueTranslator[int]: 

221 """Return identity translator for no scaling.""" 

222 return IDENTITY 

223 

224 def decode_value( 

225 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

226 ) -> int: 

227 """Parse 32-bit unsigned integer.""" 

228 if validate and len(data) < offset + 4: 

229 raise InsufficientDataError("uint32", data[offset:], 4) 

230 return self.extractor.extract(data, offset) 

231 

232 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

233 """Encode uint32 value to bytes.""" 

234 if validate and not 0 <= value <= UINT32_MAX: 

235 raise ValueError(f"Value {value} out of range for uint32 (0-{UINT32_MAX})") 

236 return self.extractor.pack(value) 

237 

238 

239class Sint32Template(CodingTemplate[int]): 

240 """Template for 32-bit signed integer parsing.""" 

241 

242 @property 

243 def data_size(self) -> int: 

244 """Size: 4 bytes.""" 

245 return 4 

246 

247 @property 

248 def extractor(self) -> RawExtractor: 

249 """Get sint32 extractor.""" 

250 return SINT32 

251 

252 @property 

253 def translator(self) -> ValueTranslator[int]: 

254 """Return identity translator for no scaling.""" 

255 return IDENTITY 

256 

257 def decode_value( 

258 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

259 ) -> int: 

260 """Parse 32-bit signed integer.""" 

261 if validate and len(data) < offset + 4: 

262 raise InsufficientDataError("sint32", data[offset:], 4) 

263 return self.extractor.extract(data, offset) 

264 

265 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

266 """Encode sint32 value to bytes.""" 

267 if validate and not SINT32_MIN <= value <= SINT32_MAX: 

268 raise ValueError(f"Value {value} out of range for sint32 ({SINT32_MIN} to {SINT32_MAX})") 

269 return self.extractor.pack(value) 

270 

271 

272class Uint48Template(CodingTemplate[int]): 

273 """Template for 48-bit unsigned integer parsing (0-281474976710655).""" 

274 

275 @property 

276 def data_size(self) -> int: 

277 """Size: 6 bytes.""" 

278 return 6 

279 

280 @property 

281 def extractor(self) -> RawExtractor: 

282 """Get uint48 extractor.""" 

283 return UINT48 

284 

285 @property 

286 def translator(self) -> ValueTranslator[int]: 

287 """Return identity translator for no scaling.""" 

288 return IDENTITY 

289 

290 def decode_value( 

291 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

292 ) -> int: 

293 """Parse 48-bit unsigned integer.""" 

294 if validate and len(data) < offset + 6: 

295 raise InsufficientDataError("uint48", data[offset:], 6) 

296 return self.extractor.extract(data, offset) 

297 

298 def encode_value(self, value: int, *, validate: bool = True) -> bytearray: 

299 """Encode uint48 value to bytes.""" 

300 if validate and not 0 <= value <= UINT48_MAX: 

301 raise ValueError(f"Value {value} out of range for uint48 (0-{UINT48_MAX})") 

302 return self.extractor.pack(value)