Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/hid: add Huion Kamvas Pro 19 tests

This tablets gets a lot of things wrong:
- the secondary button is reported through Secondary Tip Switch
- the third button is reported through Invert

We need to add some out of proximity intermediate state when moving
back and forth with the eraser mode as it can only be triggered by
physically returning the pen, meaning that the tolerated transitions
can never happen.

Link: https://lore.kernel.org/r/20240410-bpf_sources-v1-15-a8bf16033ef8@kernel.org
Reviewed-by: Peter Hutterer <peter.hutterer@who-t.net>
Signed-off-by: Benjamin Tissoires <bentiss@kernel.org>

+191
+191
tools/testing/selftests/hid/tests/test_tablet.py
··· 35 35 36 36 PRIMARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS 37 37 SECONDARY_PRESSED = libevdev.EV_KEY.BTN_STYLUS2 38 + THIRD_PRESSED = libevdev.EV_KEY.BTN_STYLUS3 38 39 39 40 40 41 class PenState(Enum): ··· 504 503 buttons = [ 505 504 BtnPressed.PRIMARY_PRESSED, 506 505 BtnPressed.SECONDARY_PRESSED, 506 + BtnPressed.THIRD_PRESSED, 507 507 ] 508 508 if button is not None: 509 509 buttons.remove(button) ··· 787 785 scribble, 788 786 allow_intermediate_states=False, 789 787 button=BtnPressed.SECONDARY_PRESSED, 788 + ) 789 + 790 + @pytest.mark.skip_if_uhdev( 791 + lambda uhdev: "Third Barrel Switch" not in uhdev.fields, 792 + "Device not compatible, missing Third Barrel Switch usage", 793 + ) 794 + @pytest.mark.parametrize("scribble", [True, False], ids=["scribble", "static"]) 795 + @pytest.mark.parametrize( 796 + "state_list", 797 + [ 798 + pytest.param(v, id=k) 799 + for k, v in PenState.legal_transitions_with_button().items() 800 + ], 801 + ) 802 + def test_valid_third_button_pen_states(self, state_list, scribble): 803 + """Rework the transition state machine by adding the secondary button.""" 804 + self._test_states( 805 + state_list, 806 + scribble, 807 + allow_intermediate_states=False, 808 + button=BtnPressed.THIRD_PRESSED, 790 809 ) 791 810 792 811 @pytest.mark.skip_if_uhdev( ··· 1134 1111 return rs 1135 1112 1136 1113 1114 + class Huion_Kamvas_Pro_19_256c_006b(PenDigitizer): 1115 + """ 1116 + Pen that reports secondary barrel switch through secondary TipSwtich 1117 + and 3rd button through Invert 1118 + """ 1119 + 1120 + def __init__( 1121 + self, 1122 + name, 1123 + rdesc_str=None, 1124 + rdesc=None, 1125 + application="Stylus", 1126 + physical=None, 1127 + input_info=(BusType.USB, 0x256C, 0x006B), 1128 + evdev_name_suffix=None, 1129 + ): 1130 + super().__init__( 1131 + name, rdesc_str, rdesc, application, physical, input_info, evdev_name_suffix 1132 + ) 1133 + self.fields.append("Secondary Barrel Switch") 1134 + self.fields.append("Third Barrel Switch") 1135 + self.previous_state = PenState.PEN_IS_OUT_OF_RANGE 1136 + 1137 + def move_to(self, pen, state, button, debug=True): 1138 + # fill in the previous values 1139 + if pen.current_state == PenState.PEN_IS_OUT_OF_RANGE: 1140 + pen.restore() 1141 + 1142 + if debug: 1143 + print(f"\n *** pen is moving to {state} ***") 1144 + 1145 + if state == PenState.PEN_IS_OUT_OF_RANGE: 1146 + pen.backup() 1147 + pen.tipswitch = False 1148 + pen.tippressure = 0 1149 + pen.azimuth = 0 1150 + pen.inrange = False 1151 + pen.width = 0 1152 + pen.height = 0 1153 + pen.invert = False 1154 + pen.eraser = False 1155 + pen.xtilt = 0 1156 + pen.ytilt = 0 1157 + pen.twist = 0 1158 + pen.barrelswitch = False 1159 + pen.secondarytipswitch = False 1160 + elif state == PenState.PEN_IS_IN_RANGE: 1161 + pen.tipswitch = False 1162 + pen.inrange = True 1163 + pen.invert = False 1164 + pen.eraser = False 1165 + pen.barrelswitch = False 1166 + pen.secondarytipswitch = False 1167 + elif state == PenState.PEN_IS_IN_CONTACT: 1168 + pen.tipswitch = True 1169 + pen.inrange = True 1170 + pen.invert = False 1171 + pen.eraser = False 1172 + pen.barrelswitch = False 1173 + pen.secondarytipswitch = False 1174 + elif state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: 1175 + pen.tipswitch = False 1176 + pen.inrange = True 1177 + pen.eraser = False 1178 + assert button is not None 1179 + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED 1180 + pen.secondarytipswitch = button == BtnPressed.SECONDARY_PRESSED 1181 + pen.invert = button == BtnPressed.THIRD_PRESSED 1182 + elif state == PenState.PEN_IS_IN_CONTACT_WITH_BUTTON: 1183 + pen.tipswitch = True 1184 + pen.inrange = True 1185 + pen.eraser = False 1186 + assert button is not None 1187 + pen.barrelswitch = button == BtnPressed.PRIMARY_PRESSED 1188 + pen.secondarytipswitch = button == BtnPressed.SECONDARY_PRESSED 1189 + pen.invert = button == BtnPressed.THIRD_PRESSED 1190 + elif state == PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT: 1191 + pen.tipswitch = False 1192 + pen.inrange = True 1193 + pen.invert = True 1194 + pen.eraser = False 1195 + pen.barrelswitch = False 1196 + pen.secondarytipswitch = False 1197 + elif state == PenState.PEN_IS_ERASING: 1198 + pen.tipswitch = False 1199 + pen.inrange = True 1200 + pen.invert = False 1201 + pen.eraser = True 1202 + pen.barrelswitch = False 1203 + pen.secondarytipswitch = False 1204 + 1205 + pen.current_state = state 1206 + 1207 + def call_input_event(self, report): 1208 + if report[0] == 0x0a: 1209 + # ensures the original second Eraser usage is null 1210 + report[1] &= 0xdf 1211 + 1212 + # ensures the original last bit is equal to bit 6 (In Range) 1213 + if report[1] & 0x40: 1214 + report[1] |= 0x80 1215 + 1216 + super().call_input_event(report) 1217 + 1218 + def send_intermediate_state(self, pen, state, test_button): 1219 + intermediate_pen = copy.copy(pen) 1220 + self.move_to(intermediate_pen, state, test_button, debug=False) 1221 + return super().event(intermediate_pen, test_button) 1222 + 1223 + def event(self, pen, button): 1224 + rs = [] 1225 + 1226 + # it's not possible to go between eraser mode or not without 1227 + # going out-of-prox: the eraser mode is activated by presenting 1228 + # the tail of the pen 1229 + if self.previous_state in ( 1230 + PenState.PEN_IS_IN_RANGE, 1231 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 1232 + PenState.PEN_IS_IN_CONTACT, 1233 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 1234 + ) and pen.current_state in ( 1235 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 1236 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON, 1237 + PenState.PEN_IS_ERASING, 1238 + PenState.PEN_IS_ERASING_WITH_BUTTON, 1239 + ): 1240 + rs.extend( 1241 + self.send_intermediate_state(pen, PenState.PEN_IS_OUT_OF_RANGE, button) 1242 + ) 1243 + 1244 + # same than above except from eraser to normal 1245 + if self.previous_state in ( 1246 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT, 1247 + PenState.PEN_IS_IN_RANGE_WITH_ERASING_INTENT_WITH_BUTTON, 1248 + PenState.PEN_IS_ERASING, 1249 + PenState.PEN_IS_ERASING_WITH_BUTTON, 1250 + ) and pen.current_state in ( 1251 + PenState.PEN_IS_IN_RANGE, 1252 + PenState.PEN_IS_IN_RANGE_WITH_BUTTON, 1253 + PenState.PEN_IS_IN_CONTACT, 1254 + PenState.PEN_IS_IN_CONTACT_WITH_BUTTON, 1255 + ): 1256 + rs.extend( 1257 + self.send_intermediate_state(pen, PenState.PEN_IS_OUT_OF_RANGE, button) 1258 + ) 1259 + 1260 + if self.previous_state == PenState.PEN_IS_OUT_OF_RANGE: 1261 + if pen.current_state == PenState.PEN_IS_IN_RANGE_WITH_BUTTON: 1262 + rs.extend( 1263 + self.send_intermediate_state(pen, PenState.PEN_IS_IN_RANGE, button) 1264 + ) 1265 + 1266 + rs.extend(super().event(pen, button)) 1267 + self.previous_state = pen.current_state 1268 + return rs 1269 + 1270 + 1137 1271 ################################################################################ 1138 1272 # 1139 1273 # Windows 7 compatible devices ··· 1491 1311 "uhid test XPPen Artist 24 28bd 093a", 1492 1312 rdesc="05 0d 09 02 a1 01 85 07 09 20 a1 00 09 42 09 44 09 45 15 00 25 01 75 01 95 03 81 02 95 02 81 03 09 32 95 01 81 02 95 02 81 03 75 10 95 01 35 00 a4 05 01 09 30 65 13 55 0d 46 f0 50 26 ff 7f 81 02 09 31 46 91 2d 26 ff 7f 81 02 b4 09 30 45 00 26 ff 1f 81 42 09 3d 15 81 25 7f 75 08 95 01 81 02 09 3e 15 81 25 7f 81 02 c0 c0", 1493 1313 input_info=(BusType.USB, 0x28BD, 0x093A), 1314 + ) 1315 + 1316 + 1317 + class TestHuion_Kamvas_Pro_19_256c_006b(BaseTest.TestTablet): 1318 + hid_bpfs = [("Huion__Kamvas-Pro-19.bpf.o", True)] 1319 + 1320 + def create_device(self): 1321 + return Huion_Kamvas_Pro_19_256c_006b( 1322 + "uhid test HUION Huion Tablet_GT1902", 1323 + rdesc="05 0d 09 02 a1 01 85 0a 09 20 a1 01 09 42 09 44 09 43 09 3c 09 45 15 00 25 01 75 01 95 06 81 02 09 32 75 01 95 01 81 02 81 03 05 01 09 30 09 31 55 0d 65 33 26 ff 7f 35 00 46 00 08 75 10 95 02 81 02 05 0d 09 30 26 ff 3f 75 10 95 01 81 02 09 3d 09 3e 15 a6 25 5a 75 08 95 02 81 02 c0 c0 05 0d 09 04 a1 01 85 04 09 22 a1 02 05 0d 95 01 75 06 09 51 15 00 25 3f 81 02 09 42 25 01 75 01 95 01 81 02 75 01 95 01 81 03 05 01 75 10 55 0e 65 11 09 30 26 ff 7f 35 00 46 15 0c 81 42 09 31 26 ff 7f 46 cb 06 81 42 05 0d 09 30 26 ff 1f 75 10 95 01 81 02 c0 05 0d 09 22 a1 02 05 0d 95 01 75 06 09 51 15 00 25 3f 81 02 09 42 25 01 75 01 95 01 81 02 75 01 95 01 81 03 05 01 75 10 55 0e 65 11 09 30 26 ff 7f 35 00 46 15 0c 81 42 09 31 26 ff 7f 46 cb 06 81 42 05 0d 09 30 26 ff 1f 75 10 95 01 81 02 c0 05 0d 09 56 55 00 65 00 27 ff ff ff 7f 95 01 75 20 81 02 09 54 25 7f 95 01 75 08 81 02 75 08 95 08 81 03 85 05 09 55 25 0a 75 08 95 01 b1 02 06 00 ff 09 c5 85 06 15 00 26 ff 00 75 08 96 00 01 b1 02 c0", 1324 + input_info=(BusType.USB, 0x256C, 0x006B), 1494 1325 )