mirror of
https://github.com/System-End/hackpad.git
synced 2026-04-19 22:15:14 +00:00
Format Kalpad PR
This commit is contained in:
parent
dd12330934
commit
18851c9513
85 changed files with 188221 additions and 0 deletions
149907
hackpads/Kalpad/CAD/kalpad.step
Normal file
149907
hackpads/Kalpad/CAD/kalpad.step
Normal file
File diff suppressed because it is too large
Load diff
15034
hackpads/Kalpad/PCB/Kalpad.kicad_pcb
Normal file
15034
hackpads/Kalpad/PCB/Kalpad.kicad_pcb
Normal file
File diff suppressed because it is too large
Load diff
83
hackpads/Kalpad/PCB/Kalpad.kicad_prl
Normal file
83
hackpads/Kalpad/PCB/Kalpad.kicad_prl
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"board": {
|
||||
"active_layer": 0,
|
||||
"active_layer_preset": "All Layers",
|
||||
"auto_track_width": true,
|
||||
"hidden_netclasses": [],
|
||||
"hidden_nets": [],
|
||||
"high_contrast_mode": 0,
|
||||
"net_color_mode": 1,
|
||||
"opacity": {
|
||||
"images": 0.6,
|
||||
"pads": 1.0,
|
||||
"tracks": 1.0,
|
||||
"vias": 1.0,
|
||||
"zones": 0.6
|
||||
},
|
||||
"selection_filter": {
|
||||
"dimensions": true,
|
||||
"footprints": true,
|
||||
"graphics": true,
|
||||
"keepouts": true,
|
||||
"lockedItems": false,
|
||||
"otherItems": true,
|
||||
"pads": true,
|
||||
"text": true,
|
||||
"tracks": true,
|
||||
"vias": true,
|
||||
"zones": true
|
||||
},
|
||||
"visible_items": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
39,
|
||||
40
|
||||
],
|
||||
"visible_layers": "fffffff_ffffffff",
|
||||
"zone_display_mode": 0
|
||||
},
|
||||
"git": {
|
||||
"repo_password": "",
|
||||
"repo_type": "",
|
||||
"repo_username": "",
|
||||
"ssh_key": ""
|
||||
},
|
||||
"meta": {
|
||||
"filename": "Kalpad.kicad_prl",
|
||||
"version": 3
|
||||
},
|
||||
"project": {
|
||||
"files": []
|
||||
}
|
||||
}
|
||||
592
hackpads/Kalpad/PCB/Kalpad.kicad_pro
Normal file
592
hackpads/Kalpad/PCB/Kalpad.kicad_pro
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
"board_outline_line_width": 0.05,
|
||||
"copper_line_width": 0.2,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.05,
|
||||
"dimension_precision": 4,
|
||||
"dimension_units": 3,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": false,
|
||||
"text_position": 0,
|
||||
"units_format": 1
|
||||
},
|
||||
"fab_line_width": 0.1,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.1,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 0.762,
|
||||
"height": 1.524,
|
||||
"width": 1.524
|
||||
},
|
||||
"silk_line_width": 0.1,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.1,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"min_clearance": 0.5
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "ignore",
|
||||
"hole_clearance": "error",
|
||||
"hole_near_hole": "error",
|
||||
"holes_co_located": "warning",
|
||||
"invalid_outline": "error",
|
||||
"isolated_copper": "warning",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"lib_footprint_issues": "warning",
|
||||
"lib_footprint_mismatch": "warning",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"net_conflict": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
"padstack": "warning",
|
||||
"pth_inside_courtyard": "ignore",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "warning",
|
||||
"silk_over_copper": "warning",
|
||||
"silk_overlap": "warning",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "error",
|
||||
"text_height": "warning",
|
||||
"text_thickness": "warning",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rules": {
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.0,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.5,
|
||||
"min_hole_clearance": 0.25,
|
||||
"min_hole_to_hole": 0.25,
|
||||
"min_microvia_diameter": 0.2,
|
||||
"min_microvia_drill": 0.1,
|
||||
"min_resolved_spokes": 2,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_text_height": 0.8,
|
||||
"min_text_thickness": 0.08,
|
||||
"min_through_hole_diameter": 0.3,
|
||||
"min_track_width": 0.0,
|
||||
"min_via_annular_width": 0.1,
|
||||
"min_via_diameter": 0.5,
|
||||
"solder_mask_to_copper_clearance": 0.005,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpadsmd": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_ontrackend": false,
|
||||
"td_onviapad": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [],
|
||||
"tuning_pattern_settings": {
|
||||
"diff_pair_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 1.0
|
||||
},
|
||||
"diff_pair_skew_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
},
|
||||
"single_track_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
}
|
||||
},
|
||||
"via_dimensions": [],
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": ""
|
||||
},
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"conflicting_netclasses": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"global_label_dangling": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"lib_symbol_issues": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"similar_labels": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"unannotated": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "Kalpad.kicad_pro",
|
||||
"version": 1
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.2,
|
||||
"via_diameter": 0.6,
|
||||
"via_drill": 0.3,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 3
|
||||
},
|
||||
"net_colors": null,
|
||||
"netclass_assignments": null,
|
||||
"netclass_patterns": []
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"pos_files": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"svg": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"bom_export_filename": "",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"name": "Grouped By Value",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"ngspice": {
|
||||
"fix_include_paths": true,
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"model_mode": 4,
|
||||
"workbook_filename": ""
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"spice_current_sheet_as_root": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"spice_model_current_sheet_as_root": true,
|
||||
"spice_save_all_currents": false,
|
||||
"spice_save_all_dissipations": false,
|
||||
"spice_save_all_voltages": false,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"626270de-5fab-4eac-819f-8b0143e96062",
|
||||
"Root"
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
||||
4546
hackpads/Kalpad/PCB/Kalpad.kicad_sch
Normal file
4546
hackpads/Kalpad/PCB/Kalpad.kicad_sch
Normal file
File diff suppressed because it is too large
Load diff
BIN
hackpads/Kalpad/assets/model.png
Normal file
BIN
hackpads/Kalpad/assets/model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 592 KiB |
BIN
hackpads/Kalpad/assets/pcb-3d.png
Normal file
BIN
hackpads/Kalpad/assets/pcb-3d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 391 KiB |
BIN
hackpads/Kalpad/assets/pcb.png
Normal file
BIN
hackpads/Kalpad/assets/pcb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
BIN
hackpads/Kalpad/assets/schematic.png
Normal file
BIN
hackpads/Kalpad/assets/schematic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
0
hackpads/Kalpad/firmware/boot.py
Normal file
0
hackpads/Kalpad/firmware/boot.py
Normal file
0
hackpads/Kalpad/firmware/kmk/__init__.py
Normal file
0
hackpads/Kalpad/firmware/kmk/__init__.py
Normal file
107
hackpads/Kalpad/firmware/kmk/bootcfg.py
Normal file
107
hackpads/Kalpad/firmware/kmk/bootcfg.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
try:
|
||||
from typing import Optional
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import digitalio
|
||||
import microcontroller
|
||||
import usb_hid
|
||||
|
||||
|
||||
def bootcfg(
|
||||
sense: Optional[microcontroller.Pin, digitalio.DigitalInOut] = None,
|
||||
source: Optional[microcontroller.Pin, digitalio.DigitalInOut] = None,
|
||||
autoreload: bool = True,
|
||||
boot_device: int = 0,
|
||||
cdc_console: bool = True,
|
||||
cdc_data: bool = False,
|
||||
consumer_control: bool = True,
|
||||
keyboard: bool = True,
|
||||
midi: bool = True,
|
||||
mouse: bool = True,
|
||||
nkro: bool = False,
|
||||
pan: bool = False,
|
||||
storage: bool = True,
|
||||
usb_id: Optional[tuple[str, str]] = None,
|
||||
**kwargs,
|
||||
) -> bool:
|
||||
|
||||
if len(kwargs):
|
||||
print('unknown option', kwargs)
|
||||
|
||||
if isinstance(sense, microcontroller.Pin):
|
||||
sense = digitalio.DigitalInOut(sense)
|
||||
sense.direction = digitalio.Direction.INPUT
|
||||
sense.pull = digitalio.Pull.UP
|
||||
|
||||
if isinstance(source, microcontroller.Pin):
|
||||
source = digitalio.DigitalInOut(source)
|
||||
source.direction = digitalio.Direction.OUTPUT
|
||||
source.value = False
|
||||
|
||||
if not autoreload:
|
||||
import supervisor
|
||||
|
||||
supervisor.runtime.autoreload = False
|
||||
|
||||
# configure HID devices
|
||||
devices = []
|
||||
if keyboard:
|
||||
if nkro:
|
||||
from kmk.hid_reports import nkro_keyboard
|
||||
|
||||
devices.append(nkro_keyboard.NKRO_KEYBOARD)
|
||||
else:
|
||||
devices.append(usb_hid.Device.KEYBOARD)
|
||||
if mouse:
|
||||
if pan:
|
||||
from kmk.hid_reports import pointer
|
||||
|
||||
devices.append(pointer.POINTER)
|
||||
else:
|
||||
devices.append(usb_hid.Device.MOUSE)
|
||||
if consumer_control:
|
||||
devices.append(usb_hid.Device.CONSUMER_CONTROL)
|
||||
if devices:
|
||||
usb_hid.enable(devices, boot_device)
|
||||
else:
|
||||
usb_hid.disable()
|
||||
|
||||
# configure midi over usb
|
||||
if not midi:
|
||||
import usb_midi
|
||||
|
||||
usb_midi.disable()
|
||||
|
||||
# configure usb vendor and product id
|
||||
if usb_id is not None:
|
||||
import supervisor
|
||||
|
||||
if hasattr(supervisor, 'set_usb_identification'):
|
||||
supervisor.set_usb_identification(*usb_id)
|
||||
|
||||
# configure data serial
|
||||
if cdc_data:
|
||||
import usb_cdc
|
||||
|
||||
usb_cdc.enable(data=True)
|
||||
|
||||
# sense not provided or pulled low -> Skip boot configuration that may
|
||||
# disable debug or rescue facilities.
|
||||
if sense is None or not sense.value:
|
||||
return False
|
||||
|
||||
# Entries for serial console (REPL) and storage are intentionally evaluated
|
||||
# last to ensure the board is debuggable, mountable and rescueable, in case
|
||||
# any of the previous code throws an exception.
|
||||
if not cdc_console:
|
||||
import usb_cdc
|
||||
|
||||
usb_cdc.enable(console=False)
|
||||
|
||||
if not storage:
|
||||
import storage
|
||||
|
||||
storage.disable_usb_drive()
|
||||
|
||||
return True
|
||||
54
hackpads/Kalpad/firmware/kmk/extensions/__init__.py
Normal file
54
hackpads/Kalpad/firmware/kmk/extensions/__init__.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
class InvalidExtensionEnvironment(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Extension:
|
||||
_enabled = True
|
||||
|
||||
def enable(self, keyboard):
|
||||
self._enabled = True
|
||||
|
||||
self.on_runtime_enable(keyboard)
|
||||
|
||||
def disable(self, keyboard):
|
||||
self._enabled = False
|
||||
|
||||
self.on_runtime_disable(keyboard)
|
||||
|
||||
# The below methods should be implemented by subclasses
|
||||
|
||||
def on_runtime_enable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def on_runtime_disable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be injected as an extra matrix update
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be replace matrix update if supplied
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def deinit(self, keyboard):
|
||||
pass
|
||||
279
hackpads/Kalpad/firmware/kmk/extensions/display/__init__.py
Normal file
279
hackpads/Kalpad/firmware/kmk/extensions/display/__init__.py
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
from supervisor import ticks_ms
|
||||
|
||||
import displayio
|
||||
import terminalio
|
||||
from adafruit_display_text import label
|
||||
|
||||
from kmk.extensions import Extension
|
||||
from kmk.keys import make_key
|
||||
from kmk.kmktime import PeriodicTimer, ticks_diff
|
||||
from kmk.modules.split import Split, SplitSide
|
||||
from kmk.utils import clamp
|
||||
|
||||
|
||||
class TextEntry:
|
||||
def __init__(
|
||||
self,
|
||||
text='',
|
||||
x=0,
|
||||
y=0,
|
||||
x_anchor='L',
|
||||
y_anchor='T',
|
||||
direction='LTR',
|
||||
line_spacing=0.75,
|
||||
inverted=False,
|
||||
layer=None,
|
||||
side=None,
|
||||
):
|
||||
self.text = text
|
||||
self.direction = direction
|
||||
self.line_spacing = line_spacing
|
||||
self.inverted = inverted
|
||||
self.layer = layer
|
||||
self.color = 0xFFFFFF
|
||||
self.background_color = 0x000000
|
||||
self.x_anchor = 0.0
|
||||
self.y_anchor = 0.0
|
||||
if x_anchor == 'L':
|
||||
self.x_anchor = 0.0
|
||||
x = x + 1
|
||||
if x_anchor == 'M':
|
||||
self.x_anchor = 0.5
|
||||
if x_anchor == 'R':
|
||||
self.x_anchor = 1.0
|
||||
if y_anchor == 'T':
|
||||
self.y_anchor = 0.0
|
||||
if y_anchor == 'M':
|
||||
self.y_anchor = 0.5
|
||||
if y_anchor == 'B':
|
||||
self.y_anchor = 1.0
|
||||
self.anchor_point = (self.x_anchor, self.y_anchor)
|
||||
self.anchored_position = (x, y)
|
||||
if inverted:
|
||||
self.color = 0x000000
|
||||
self.background_color = 0xFFFFFF
|
||||
self.side = side
|
||||
if side == 'L':
|
||||
self.side = SplitSide.LEFT
|
||||
if side == 'R':
|
||||
self.side = SplitSide.RIGHT
|
||||
|
||||
|
||||
class ImageEntry:
|
||||
def __init__(self, x=0, y=0, image='', layer=None, side=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.image = displayio.OnDiskBitmap(image)
|
||||
self.layer = layer
|
||||
self.side = side
|
||||
if side == 'L':
|
||||
self.side = SplitSide.LEFT
|
||||
if side == 'R':
|
||||
self.side = SplitSide.RIGHT
|
||||
|
||||
|
||||
class DisplayBase:
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def during_bootup(self, width, height, rotation):
|
||||
raise NotImplementedError
|
||||
|
||||
def deinit(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def sleep(self):
|
||||
self.display.sleep()
|
||||
|
||||
def wake(self):
|
||||
self.display.wake()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self.display.brightness
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, new_brightness):
|
||||
self.display.brightness = new_brightness
|
||||
|
||||
# display.show() is deprecated, so use root_group instead
|
||||
@property
|
||||
def root_group(self):
|
||||
return self.display.root_group
|
||||
|
||||
@root_group.setter
|
||||
def root_group(self, group):
|
||||
self.display.root_group = group
|
||||
|
||||
|
||||
class Display(Extension):
|
||||
def __init__(
|
||||
self,
|
||||
display=None,
|
||||
entries=[],
|
||||
width=128,
|
||||
height=32,
|
||||
flip: bool = False,
|
||||
flip_left: bool = False,
|
||||
flip_right: bool = False,
|
||||
brightness=0.8,
|
||||
brightness_step=0.1,
|
||||
dim_time=20,
|
||||
dim_target=0.1,
|
||||
off_time=60,
|
||||
powersave_dim_time=10,
|
||||
powersave_dim_target=0.1,
|
||||
powersave_off_time=30,
|
||||
):
|
||||
self.display = display
|
||||
self.flip = flip
|
||||
self.flip_left = flip_left
|
||||
self.flip_right = flip_right
|
||||
self.entries = entries
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.prev_layer = None
|
||||
self.brightness = brightness
|
||||
self.brightness_step = brightness_step
|
||||
self.timer_start = ticks_ms()
|
||||
self.powersave = False
|
||||
self.dim_time_ms = dim_time * 1000
|
||||
self.dim_target = dim_target
|
||||
self.off_time_ms = off_time * 1000
|
||||
self.powersavedim_time_ms = powersave_dim_time * 1000
|
||||
self.powersave_dim_target = powersave_dim_target
|
||||
self.powersave_off_time_ms = powersave_off_time * 1000
|
||||
self.dim_period = PeriodicTimer(50)
|
||||
self.split_side = None
|
||||
|
||||
make_key(names=('DIS_BRI',), on_press=self.display_brightness_increase)
|
||||
make_key(names=('DIS_BRD',), on_press=self.display_brightness_decrease)
|
||||
|
||||
def render(self, layer):
|
||||
splash = displayio.Group()
|
||||
|
||||
for entry in self.entries:
|
||||
if entry.layer != layer and entry.layer is not None:
|
||||
continue
|
||||
if isinstance(entry, TextEntry):
|
||||
splash.append(
|
||||
label.Label(
|
||||
terminalio.FONT,
|
||||
text=entry.text,
|
||||
color=entry.color,
|
||||
background_color=entry.background_color,
|
||||
anchor_point=entry.anchor_point,
|
||||
anchored_position=entry.anchored_position,
|
||||
label_direction=entry.direction,
|
||||
line_spacing=entry.line_spacing,
|
||||
padding_left=1,
|
||||
)
|
||||
)
|
||||
elif isinstance(entry, ImageEntry):
|
||||
splash.append(
|
||||
displayio.TileGrid(
|
||||
entry.image,
|
||||
pixel_shader=entry.image.pixel_shader,
|
||||
x=entry.x,
|
||||
y=entry.y,
|
||||
)
|
||||
)
|
||||
self.display.root_group = splash
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
for module in keyboard.modules:
|
||||
if isinstance(module, Split):
|
||||
self.split_side = module.split_side
|
||||
|
||||
if self.split_side == SplitSide.LEFT:
|
||||
self.flip = self.flip_left
|
||||
elif self.split_side == SplitSide.RIGHT:
|
||||
self.flip = self.flip_right
|
||||
|
||||
for idx, entry in enumerate(self.entries):
|
||||
if entry.side != self.split_side and entry.side is not None:
|
||||
del self.entries[idx]
|
||||
|
||||
self.display.during_bootup(self.width, self.height, 180 if self.flip else 0)
|
||||
self.display.brightness = self.brightness
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
if self.dim_period.tick():
|
||||
self.dim()
|
||||
if sandbox.active_layers[0] != self.prev_layer:
|
||||
self.prev_layer = sandbox.active_layers[0]
|
||||
self.render(sandbox.active_layers[0])
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
if sandbox.matrix_update or sandbox.secondary_matrix_update:
|
||||
self.timer_start = ticks_ms()
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
self.powersave = True
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
self.powersave = False
|
||||
|
||||
def deinit(self, sandbox):
|
||||
displayio.release_displays()
|
||||
self.display.deinit()
|
||||
|
||||
def display_brightness_increase(self, *args):
|
||||
self.display.brightness = clamp(
|
||||
self.display.brightness + self.brightness_step, 0, 1
|
||||
)
|
||||
self.brightness = self.display.brightness # Save current brightness
|
||||
|
||||
def display_brightness_decrease(self, *args):
|
||||
self.display.brightness = clamp(
|
||||
self.display.brightness - self.brightness_step, 0, 1
|
||||
)
|
||||
self.brightness = self.display.brightness # Save current brightness
|
||||
|
||||
def dim(self):
|
||||
if self.powersave:
|
||||
if (
|
||||
self.powersave_off_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start)
|
||||
> self.powersave_off_time_ms
|
||||
):
|
||||
self.display.sleep()
|
||||
|
||||
elif (
|
||||
self.powersave_dim_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start)
|
||||
> self.powersave_dim_time_ms
|
||||
):
|
||||
self.display.brightness = self.powersave_dim_target
|
||||
|
||||
else:
|
||||
self.display.brightness = self.brightness
|
||||
self.display.wake()
|
||||
|
||||
elif (
|
||||
self.off_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start) > self.off_time_ms
|
||||
):
|
||||
self.display.sleep()
|
||||
|
||||
elif (
|
||||
self.dim_time_ms
|
||||
and ticks_diff(ticks_ms(), self.timer_start) > self.dim_time_ms
|
||||
):
|
||||
self.display.brightness = self.dim_target
|
||||
|
||||
else:
|
||||
self.display.brightness = self.brightness
|
||||
self.display.wake()
|
||||
24
hackpads/Kalpad/firmware/kmk/extensions/display/builtin.py
Normal file
24
hackpads/Kalpad/firmware/kmk/extensions/display/builtin.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from . import DisplayBase
|
||||
|
||||
|
||||
# Intended for displays with drivers built into CircuitPython
|
||||
# that can be used directly without manual initialization
|
||||
class BuiltInDisplay(DisplayBase):
|
||||
def __init__(self, display=None, sleep_command=None, wake_command=None):
|
||||
self.display = display
|
||||
self.sleep_command = sleep_command
|
||||
self.wake_command = wake_command
|
||||
self.is_awake = True
|
||||
|
||||
def during_bootup(self, width, height, rotation):
|
||||
self.display.rotation = rotation
|
||||
return self.display
|
||||
|
||||
def deinit(self):
|
||||
return
|
||||
|
||||
def sleep(self):
|
||||
self.display.bus.send(self.sleep_command, b'')
|
||||
|
||||
def wake(self):
|
||||
self.display.bus.send(self.wake_command, b'')
|
||||
49
hackpads/Kalpad/firmware/kmk/extensions/display/sh1106.py
Normal file
49
hackpads/Kalpad/firmware/kmk/extensions/display/sh1106.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import busio
|
||||
|
||||
import adafruit_displayio_sh1106 # Display-specific library
|
||||
import displayio
|
||||
|
||||
from . import DisplayBase
|
||||
|
||||
# Required to initialize this display
|
||||
displayio.release_displays()
|
||||
|
||||
|
||||
class SH1106(DisplayBase):
|
||||
def __init__(
|
||||
self,
|
||||
spi=None,
|
||||
sck=None,
|
||||
mosi=None,
|
||||
command=None,
|
||||
chip_select=None,
|
||||
reset=None,
|
||||
baudrate=1000000,
|
||||
):
|
||||
self.command = command
|
||||
self.chip_select = chip_select
|
||||
self.reset = reset
|
||||
self.baudrate = baudrate
|
||||
# spi initialization
|
||||
self.spi = spi
|
||||
if self.spi is None:
|
||||
self.spi = busio.SPI(sck, mosi)
|
||||
|
||||
def during_bootup(self, width, height, rotation):
|
||||
self.display = adafruit_displayio_sh1106.SH1106(
|
||||
displayio.FourWire(
|
||||
self.spi,
|
||||
command=self.command,
|
||||
chip_select=self.chip_select,
|
||||
reset=self.reset,
|
||||
baudrate=self.baudrate,
|
||||
),
|
||||
width=width,
|
||||
height=height,
|
||||
rotation=rotation,
|
||||
)
|
||||
|
||||
return self.display
|
||||
|
||||
def deinit(self):
|
||||
self.spi.deinit()
|
||||
31
hackpads/Kalpad/firmware/kmk/extensions/display/ssd1306.py
Normal file
31
hackpads/Kalpad/firmware/kmk/extensions/display/ssd1306.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import busio
|
||||
|
||||
import adafruit_displayio_ssd1306 # Display-specific library
|
||||
import displayio
|
||||
|
||||
from . import DisplayBase
|
||||
|
||||
# Required to initialize this display
|
||||
displayio.release_displays()
|
||||
|
||||
|
||||
class SSD1306(DisplayBase):
|
||||
def __init__(self, i2c=None, sda=None, scl=None, device_address=0x3C):
|
||||
self.device_address = device_address
|
||||
# i2c initialization
|
||||
self.i2c = i2c
|
||||
if self.i2c is None:
|
||||
self.i2c = busio.I2C(scl, sda)
|
||||
|
||||
def during_bootup(self, width, height, rotation):
|
||||
self.display = adafruit_displayio_ssd1306.SSD1306(
|
||||
displayio.I2CDisplay(self.i2c, device_address=self.device_address),
|
||||
width=width,
|
||||
height=height,
|
||||
rotation=rotation,
|
||||
)
|
||||
|
||||
return self.display
|
||||
|
||||
def deinit(self):
|
||||
self.i2c.deinit()
|
||||
63
hackpads/Kalpad/firmware/kmk/extensions/international.py
Normal file
63
hackpads/Kalpad/firmware/kmk/extensions/international.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
'''Adds international keys'''
|
||||
|
||||
from kmk.extensions import Extension
|
||||
from kmk.keys import KeyboardKey, make_key
|
||||
|
||||
|
||||
class International(Extension):
|
||||
'''Adds international keys'''
|
||||
|
||||
def __init__(self):
|
||||
# International
|
||||
codes = (
|
||||
(50, ('NONUS_HASH', 'NUHS')),
|
||||
(100, ('NONUS_BSLASH', 'NUBS')),
|
||||
(101, ('APP', 'APPLICATION', 'SEL', 'WINMENU')),
|
||||
(135, ('INT1', 'RO')),
|
||||
(136, ('INT2', 'KANA')),
|
||||
(137, ('INT3', 'JYEN')),
|
||||
(138, ('INT4', 'HENK')),
|
||||
(139, ('INT5', 'MHEN')),
|
||||
(140, ('INT6',)),
|
||||
(141, ('INT7',)),
|
||||
(142, ('INT8',)),
|
||||
(143, ('INT9',)),
|
||||
(144, ('LANG1', 'HAEN')),
|
||||
(145, ('LANG2', 'HAEJ')),
|
||||
(146, ('LANG3',)),
|
||||
(147, ('LANG4',)),
|
||||
(148, ('LANG5',)),
|
||||
(149, ('LANG6',)),
|
||||
(150, ('LANG7',)),
|
||||
(151, ('LANG8',)),
|
||||
(152, ('LANG9',)),
|
||||
)
|
||||
for code, names in codes:
|
||||
make_key(names=names, constructor=KeyboardKey, code=code)
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
return
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# What's this?
|
||||
# This is a keycode conversion script. With this, KMK will work as a JIS keyboard.
|
||||
|
||||
# Usage
|
||||
# ```python
|
||||
# import kmk.extensions.keymap_extras.keymap_jp
|
||||
# ```
|
||||
|
||||
from kmk.keys import KC
|
||||
|
||||
KC.CIRC = KC.EQL # ^
|
||||
KC.AT = KC.LBRC # @
|
||||
KC.LBRC = KC.RBRC # [
|
||||
KC.EISU = KC.CAPS # Eisū (英数)
|
||||
KC.COLN = KC.QUOT # :
|
||||
KC.LCBR = KC.LSFT(KC.RBRC) # {
|
||||
KC.RBRC = KC.NUHS # ]
|
||||
KC.BSLS = KC.INT1 # (backslash)
|
||||
KC.PLUS = KC.LSFT(KC.SCLN)
|
||||
KC.TILD = KC.LSFT(KC.EQL) # ~
|
||||
KC.GRV = KC.LSFT(KC.AT) # `
|
||||
KC.DQUO = KC.LSFT(KC.N2) # "
|
||||
KC.AMPR = KC.LSFT(KC.N6) # &
|
||||
KC.ASTR = KC.LSFT(KC.QUOT) # *
|
||||
KC.QUOT = KC.LSFT(KC.N7) # '
|
||||
KC.LPRN = KC.LSFT(KC.N8) # (
|
||||
KC.RPRN = KC.LSFT(KC.N9) # )
|
||||
KC.EQL = KC.LSFT(KC.MINS) # =
|
||||
KC.PIPE = KC.LSFT(KC.INT3) # |
|
||||
KC.RCBR = KC.LSFT(KC.NUHS) # }
|
||||
KC.LABK = KC.LSFT(KC.COMM) # <
|
||||
KC.RABK = KC.LSFT(KC.DOT) # >
|
||||
KC.QUES = KC.LSFT(KC.SLSH) # ?
|
||||
KC.UNDS = KC.LSFT(KC.INT1) # _
|
||||
256
hackpads/Kalpad/firmware/kmk/extensions/led.py
Normal file
256
hackpads/Kalpad/firmware/kmk/extensions/led.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
import pwmio
|
||||
from math import e, exp, pi, sin
|
||||
|
||||
from kmk.extensions import Extension, InvalidExtensionEnvironment
|
||||
from kmk.keys import Key, make_argumented_key, make_key
|
||||
from kmk.utils import Debug, clamp
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class LEDKey(Key):
|
||||
def __init__(self, *leds, brightness=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.leds = leds
|
||||
self.brightness = None
|
||||
|
||||
|
||||
def led_set_key(brightness, *leds):
|
||||
return LEDKey(*leds, brightness=brightness)
|
||||
|
||||
|
||||
class AnimationModes:
|
||||
OFF = 0
|
||||
STATIC = 1
|
||||
STATIC_STANDBY = 2
|
||||
BREATHING = 3
|
||||
USER = 4
|
||||
|
||||
|
||||
class LED(Extension):
|
||||
def __init__(
|
||||
self,
|
||||
led_pin,
|
||||
brightness=50,
|
||||
brightness_step=5,
|
||||
brightness_limit=100,
|
||||
breathe_center=1.5,
|
||||
animation_mode=AnimationModes.STATIC,
|
||||
animation_speed=1,
|
||||
user_animation=None,
|
||||
val=100,
|
||||
):
|
||||
try:
|
||||
pins_iter = iter(led_pin)
|
||||
except TypeError:
|
||||
pins_iter = [led_pin]
|
||||
|
||||
try:
|
||||
self._leds = [pwmio.PWMOut(pin) for pin in pins_iter]
|
||||
except Exception as e:
|
||||
if debug.enabled:
|
||||
debug(e)
|
||||
raise InvalidExtensionEnvironment(
|
||||
'Unable to create pwmio.PWMOut() instance with provided led_pin'
|
||||
)
|
||||
|
||||
self._brightness = brightness
|
||||
self._pos = 0
|
||||
self._effect_init = False
|
||||
self._enabled = True
|
||||
|
||||
self.brightness_step = brightness_step
|
||||
self.brightness_limit = brightness_limit
|
||||
self.animation_mode = animation_mode
|
||||
self.animation_speed = animation_speed
|
||||
self.breathe_center = breathe_center
|
||||
self.val = val
|
||||
|
||||
if user_animation is not None:
|
||||
self.user_animation = user_animation
|
||||
|
||||
make_argumented_key(
|
||||
names=('LED_TOG',),
|
||||
constructor=LEDKey,
|
||||
on_press=self._key_led_tog,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('LED_INC',),
|
||||
constructor=LEDKey,
|
||||
on_press=self._key_led_inc,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('LED_DEC',),
|
||||
constructor=LEDKey,
|
||||
on_press=self._key_led_dec,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('LED_SET',),
|
||||
constructor=led_set_key,
|
||||
on_press=self._key_led_set,
|
||||
)
|
||||
make_key(names=('LED_ANI',), on_press=self._key_led_ani)
|
||||
make_key(names=('LED_AND',), on_press=self._key_led_and)
|
||||
make_key(
|
||||
names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static
|
||||
)
|
||||
make_key(
|
||||
names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'LED({self._to_dict()})'
|
||||
|
||||
def _to_dict(self):
|
||||
return {
|
||||
'_brightness': self._brightness,
|
||||
'_pos': self._pos,
|
||||
'brightness_step': self.brightness_step,
|
||||
'brightness_limit': self.brightness_limit,
|
||||
'animation_mode': self.animation_mode,
|
||||
'animation_speed': self.animation_speed,
|
||||
'breathe_center': self.breathe_center,
|
||||
'val': self.val,
|
||||
}
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
self.animate()
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def _init_effect(self):
|
||||
self._pos = 0
|
||||
self._effect_init = False
|
||||
return self
|
||||
|
||||
def set_brightness(self, percent, leds=None):
|
||||
leds = leds or range(0, len(self._leds))
|
||||
for i in leds:
|
||||
self._leds[i].duty_cycle = int(percent / 100 * 65535)
|
||||
|
||||
def step_brightness(self, step, leds=None):
|
||||
leds = leds or range(0, len(self._leds))
|
||||
for i in leds:
|
||||
brightness = int(self._leds[i].duty_cycle / 65535 * 100) + step
|
||||
self.set_brightness(clamp(brightness), [i])
|
||||
|
||||
def increase_brightness(self, step=None, leds=None):
|
||||
if step is None:
|
||||
step = self.brightness_step
|
||||
self.step_brightness(step, leds)
|
||||
|
||||
def decrease_brightness(self, step=None, leds=None):
|
||||
if step is None:
|
||||
step = self.brightness_step
|
||||
self.step_brightness(-step, leds)
|
||||
|
||||
def off(self):
|
||||
self.set_brightness(0)
|
||||
|
||||
def increase_ani(self):
|
||||
'''
|
||||
Increases animation speed by 1 amount stopping at 10
|
||||
:param step:
|
||||
'''
|
||||
if (self.animation_speed + 1) >= 10:
|
||||
self.animation_speed = 10
|
||||
else:
|
||||
self.val += 1
|
||||
|
||||
def decrease_ani(self):
|
||||
'''
|
||||
Decreases animation speed by 1 amount stopping at 0
|
||||
:param step:
|
||||
'''
|
||||
if (self.val - 1) <= 0:
|
||||
self.val = 0
|
||||
else:
|
||||
self.val -= 1
|
||||
|
||||
def effect_breathing(self):
|
||||
# http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
|
||||
# https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
|
||||
sined = sin((self._pos / 255.0) * pi)
|
||||
multip_1 = exp(sined) - self.breathe_center / e
|
||||
multip_2 = self.brightness_limit / (e - 1 / e)
|
||||
|
||||
self._brightness = int(multip_1 * multip_2)
|
||||
self._pos = (self._pos + self.animation_speed) % 256
|
||||
self.set_brightness(self._brightness)
|
||||
|
||||
def effect_static(self):
|
||||
self.set_brightness(self._brightness)
|
||||
# Set animation mode to standby to prevent cycles from being wasted
|
||||
self.animation_mode = AnimationModes.STATIC_STANDBY
|
||||
|
||||
def animate(self):
|
||||
'''
|
||||
Activates a "step" in the animation based on the active mode
|
||||
:return: Returns the new state in animation
|
||||
'''
|
||||
if self._effect_init:
|
||||
self._init_effect()
|
||||
if self._enabled:
|
||||
if self.animation_mode == AnimationModes.BREATHING:
|
||||
return self.effect_breathing()
|
||||
elif self.animation_mode == AnimationModes.STATIC:
|
||||
return self.effect_static()
|
||||
elif self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||||
pass
|
||||
elif self.animation_mode == AnimationModes.USER:
|
||||
return self.user_animation(self)
|
||||
else:
|
||||
self.off()
|
||||
|
||||
def _key_led_tog(self, *args, **kwargs):
|
||||
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||||
self.animation_mode = AnimationModes.STATIC
|
||||
|
||||
if self._enabled:
|
||||
self.off()
|
||||
self._enabled = not self._enabled
|
||||
|
||||
def _key_led_inc(self, key, *args, **kwargs):
|
||||
self.increase_brightness(leds=key.leds)
|
||||
|
||||
def _key_led_dec(self, key, *args, **kwargs):
|
||||
self.decrease_brightness(leds=key.leds)
|
||||
|
||||
def _key_led_set(self, key, *args, **kwargs):
|
||||
self.set_brightness(percent=key.brightness, leds=key.leds)
|
||||
|
||||
def _key_led_ani(self, *args, **kwargs):
|
||||
self.increase_ani()
|
||||
|
||||
def _key_led_and(self, *args, **kwargs):
|
||||
self.decrease_ani()
|
||||
|
||||
def _key_led_mode_static(self, *args, **kwargs):
|
||||
self._effect_init = True
|
||||
self.animation_mode = AnimationModes.STATIC
|
||||
|
||||
def _key_led_mode_breathe(self, *args, **kwargs):
|
||||
self._effect_init = True
|
||||
self.animation_mode = AnimationModes.BREATHING
|
||||
69
hackpads/Kalpad/firmware/kmk/extensions/lock_status.py
Normal file
69
hackpads/Kalpad/firmware/kmk/extensions/lock_status.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import usb_hid
|
||||
from micropython import const
|
||||
|
||||
from kmk.extensions import Extension
|
||||
|
||||
_NUMLOCK = const(0x01)
|
||||
_CAPSLOCK = const(0x02)
|
||||
_SCROLLLOCK = const(0x04)
|
||||
_COMPOSE = const(0x08)
|
||||
_KANA = const(0x10)
|
||||
|
||||
|
||||
class LockStatus(Extension):
|
||||
def __init__(self):
|
||||
self.report = 0
|
||||
self.hid = None
|
||||
self._report_updated = False
|
||||
|
||||
def __repr__(self):
|
||||
return f'LockStatus(report={self.report})'
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
for device in usb_hid.devices:
|
||||
if device.usage == usb_hid.Device.KEYBOARD.usage:
|
||||
self.hid = device
|
||||
if self.hid is None:
|
||||
raise RuntimeError
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
report = self.hid.get_last_received_report()
|
||||
if report is None:
|
||||
self._report_updated = False
|
||||
else:
|
||||
self.report = report[0]
|
||||
self._report_updated = True
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
return
|
||||
|
||||
@property
|
||||
def report_updated(self):
|
||||
return self._report_updated
|
||||
|
||||
def get_num_lock(self):
|
||||
return bool(self.report & _NUMLOCK)
|
||||
|
||||
def get_caps_lock(self):
|
||||
return bool(self.report & _CAPSLOCK)
|
||||
|
||||
def get_scroll_lock(self):
|
||||
return bool(self.report & _SCROLLLOCK)
|
||||
|
||||
def get_compose(self):
|
||||
return bool(self.report & _COMPOSE)
|
||||
|
||||
def get_kana(self):
|
||||
return bool(self.report & _KANA)
|
||||
61
hackpads/Kalpad/firmware/kmk/extensions/media_keys.py
Normal file
61
hackpads/Kalpad/firmware/kmk/extensions/media_keys.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from kmk.extensions import Extension
|
||||
from kmk.keys import ConsumerKey, make_key
|
||||
|
||||
|
||||
class MediaKeys(Extension):
|
||||
def __init__(self):
|
||||
# Consumer ("media") keys. Most known keys aren't supported here. A much
|
||||
# longer list used to exist in this file, but the codes were almost certainly
|
||||
# incorrect, conflicting with each other, or otherwise 'weird'. We'll add them
|
||||
# back in piecemeal as needed. PRs welcome.
|
||||
#
|
||||
# A super useful reference for these is http://www.freebsddiary.org/APC/usb_hid_usages.php
|
||||
# Note that currently we only have the PC codes. Recent MacOS versions seem to
|
||||
# support PC media keys, so I don't know how much value we would get out of
|
||||
# adding the old Apple-specific consumer codes, but again, PRs welcome if the
|
||||
# lack of them impacts you.
|
||||
|
||||
codes = (
|
||||
(0xE2, ('AUDIO_MUTE', 'MUTE')),
|
||||
(0xE9, ('AUDIO_VOL_UP', 'VOLU')),
|
||||
(0xEA, ('AUDIO_VOL_DOWN', 'VOLD')),
|
||||
(0x6F, ('BRIGHTNESS_UP', 'BRIU')),
|
||||
(0x70, ('BRIGHTNESS_DOWN', 'BRID')),
|
||||
(0xB5, ('MEDIA_NEXT_TRACK', 'MNXT')),
|
||||
(0xB6, ('MEDIA_PREV_TRACK', 'MPRV')),
|
||||
(0xB7, ('MEDIA_STOP', 'MSTP')),
|
||||
(0xCD, ('MEDIA_PLAY_PAUSE', 'MPLY')),
|
||||
(0xB8, ('MEDIA_EJECT', 'EJCT')),
|
||||
(0xB3, ('MEDIA_FAST_FORWARD', 'MFFD')),
|
||||
(0xB4, ('MEDIA_REWIND', 'MRWD')),
|
||||
)
|
||||
|
||||
for code, names in codes:
|
||||
make_key(names=names, constructor=ConsumerKey, code=code)
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
return
|
||||
200
hackpads/Kalpad/firmware/kmk/extensions/peg_rgb_matrix.py
Normal file
200
hackpads/Kalpad/firmware/kmk/extensions/peg_rgb_matrix.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import neopixel
|
||||
|
||||
from storage import getmount
|
||||
|
||||
from kmk.extensions import Extension
|
||||
from kmk.keys import make_key
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class Color:
|
||||
OFF = [0, 0, 0]
|
||||
BLACK = OFF
|
||||
WHITE = [249, 249, 249]
|
||||
RED = [255, 0, 0]
|
||||
AZURE = [153, 245, 255]
|
||||
BLUE = [0, 0, 255]
|
||||
CYAN = [0, 255, 255]
|
||||
GREEN = [0, 255, 0]
|
||||
YELLOW = [255, 247, 0]
|
||||
MAGENTA = [255, 0, 255]
|
||||
ORANGE = [255, 77, 0]
|
||||
PURPLE = [255, 0, 242]
|
||||
TEAL = [0, 128, 128]
|
||||
PINK = [255, 0, 255]
|
||||
|
||||
|
||||
class Rgb_matrix_data:
|
||||
def __init__(self, keys=[], underglow=[]):
|
||||
if len(keys) == 0:
|
||||
if debug.enabled:
|
||||
debug('No colors passed for your keys')
|
||||
return
|
||||
if len(underglow) == 0:
|
||||
if debug.enabled:
|
||||
debug('No colors passed for your underglow')
|
||||
return
|
||||
self.data = keys + underglow
|
||||
|
||||
@staticmethod
|
||||
def generate_led_map(
|
||||
number_of_keys, number_of_underglow, key_color, underglow_color
|
||||
):
|
||||
keys = [key_color] * number_of_keys
|
||||
underglow = [underglow_color] * number_of_underglow
|
||||
if debug.enabled:
|
||||
debug('Rgb_matrix_data(keys=', keys, ', nunderglow=', underglow, ')')
|
||||
|
||||
|
||||
class Rgb_matrix(Extension):
|
||||
def __init__(
|
||||
self,
|
||||
rgb_order=(1, 0, 2), # GRB WS2812
|
||||
disable_auto_write=False,
|
||||
ledDisplay=[],
|
||||
split=False,
|
||||
rightSide=False,
|
||||
):
|
||||
name = str(getmount('/').label)
|
||||
self.rgb_order = rgb_order
|
||||
self.disable_auto_write = disable_auto_write
|
||||
self.split = split
|
||||
self.rightSide = rightSide
|
||||
self.brightness_step = 0.1
|
||||
self.brightness = 0
|
||||
|
||||
if name.endswith('L'):
|
||||
self.rightSide = False
|
||||
elif name.endswith('R'):
|
||||
self.rightSide = True
|
||||
if type(ledDisplay) is Rgb_matrix_data:
|
||||
self.ledDisplay = ledDisplay.data
|
||||
else:
|
||||
self.ledDisplay = ledDisplay
|
||||
|
||||
make_key(names=('RGB_TOG',), on_press=self._rgb_tog)
|
||||
make_key(names=('RGB_BRI',), on_press=self._rgb_bri)
|
||||
make_key(names=('RGB_BRD',), on_press=self._rgb_brd)
|
||||
|
||||
def _rgb_tog(self, *args, **kwargs):
|
||||
if self.enable:
|
||||
self.off()
|
||||
else:
|
||||
self.on()
|
||||
self.enable = not self.enable
|
||||
|
||||
def _rgb_bri(self, *args, **kwargs):
|
||||
self.increase_brightness()
|
||||
|
||||
def _rgb_brd(self, *args, **kwargs):
|
||||
self.decrease_brightness()
|
||||
|
||||
def on(self):
|
||||
if self.neopixel:
|
||||
self.setBasedOffDisplay()
|
||||
self.neopixel.show()
|
||||
|
||||
def off(self):
|
||||
if self.neopixel:
|
||||
self.set_rgb_fill((0, 0, 0))
|
||||
|
||||
def set_rgb_fill(self, rgb):
|
||||
if self.neopixel:
|
||||
self.neopixel.fill(rgb)
|
||||
if self.disable_auto_write:
|
||||
self.neopixel.show()
|
||||
|
||||
def set_brightness(self, brightness=None):
|
||||
if brightness is None:
|
||||
brightness = self.brightness
|
||||
|
||||
if self.neopixel:
|
||||
self.neopixel.brightness = brightness
|
||||
if self.disable_auto_write:
|
||||
self.neopixel.show()
|
||||
|
||||
def increase_brightness(self, step=None):
|
||||
if step is None:
|
||||
step = self.brightness_step
|
||||
|
||||
self.brightness = (
|
||||
self.brightness + step if self.brightness + step <= 1.0 else 1.0
|
||||
)
|
||||
|
||||
self.set_brightness(self.brightness)
|
||||
|
||||
def decrease_brightness(self, step=None):
|
||||
if step is None:
|
||||
step = self.brightness_step
|
||||
|
||||
self.brightness = (
|
||||
self.brightness - step if self.brightness - step >= 0.0 else 0.0
|
||||
)
|
||||
self.set_brightness(self.brightness)
|
||||
|
||||
def setBasedOffDisplay(self):
|
||||
if self.split:
|
||||
for i, val in enumerate(self.ledDisplay):
|
||||
if self.rightSide:
|
||||
if self.keyPos[i] >= (self.num_pixels / 2):
|
||||
self.neopixel[int(self.keyPos[i] - (self.num_pixels / 2))] = (
|
||||
val[0],
|
||||
val[1],
|
||||
val[2],
|
||||
)
|
||||
else:
|
||||
if self.keyPos[i] <= (self.num_pixels / 2):
|
||||
self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
|
||||
else:
|
||||
for i, val in enumerate(self.ledDisplay):
|
||||
self.neopixel[self.keyPos[i]] = (val[0], val[1], val[2])
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, board):
|
||||
self.neopixel = neopixel.NeoPixel(
|
||||
board.rgb_pixel_pin,
|
||||
board.num_pixels,
|
||||
brightness=board.brightness_limit,
|
||||
pixel_order=self.rgb_order,
|
||||
auto_write=not self.disable_auto_write,
|
||||
)
|
||||
self.num_pixels = board.num_pixels
|
||||
self.keyPos = board.led_key_pos
|
||||
self.brightness = board.brightness_limit
|
||||
self.on()
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
if self.neopixel:
|
||||
self.neopixel.brightness = (
|
||||
self.neopixel.brightness / 2
|
||||
if self.neopixel.brightness / 2 > 0
|
||||
else 0.1
|
||||
)
|
||||
if self.disable_auto_write:
|
||||
self.neopixel.show()
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
if self.neopixel:
|
||||
self.neopixel.brightness = self.brightness
|
||||
if self.disable_auto_write:
|
||||
self.neopixel.show()
|
||||
551
hackpads/Kalpad/firmware/kmk/extensions/rgb.py
Normal file
551
hackpads/Kalpad/firmware/kmk/extensions/rgb.py
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
from adafruit_pixelbuf import PixelBuf
|
||||
from math import e, exp, pi, sin
|
||||
|
||||
from kmk.extensions import Extension
|
||||
from kmk.keys import make_key
|
||||
from kmk.scheduler import create_task
|
||||
from kmk.utils import Debug, clamp
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
rgb_config = {}
|
||||
|
||||
|
||||
def hsv_to_rgb(hue, sat, val):
|
||||
'''
|
||||
Converts HSV values, and returns a tuple of RGB values
|
||||
:param hue:
|
||||
:param sat:
|
||||
:param val:
|
||||
:return: (r, g, b)
|
||||
'''
|
||||
if sat == 0:
|
||||
return (val, val, val)
|
||||
|
||||
hue = 6 * (hue & 0xFF)
|
||||
frac = hue & 0xFF
|
||||
sxt = hue >> 8
|
||||
|
||||
base = (0xFF - sat) * val
|
||||
color = (val * sat * frac) >> 8
|
||||
val <<= 8
|
||||
|
||||
if sxt == 0:
|
||||
r = val
|
||||
g = base + color
|
||||
b = base
|
||||
elif sxt == 1:
|
||||
r = val - color
|
||||
g = val
|
||||
b = base
|
||||
elif sxt == 2:
|
||||
r = base
|
||||
g = val
|
||||
b = base + color
|
||||
elif sxt == 3:
|
||||
r = base
|
||||
g = val - color
|
||||
b = val
|
||||
elif sxt == 4:
|
||||
r = base + color
|
||||
g = base
|
||||
b = val
|
||||
elif sxt == 5:
|
||||
r = val
|
||||
g = base
|
||||
b = val - color
|
||||
|
||||
return (r >> 8), (g >> 8), (b >> 8)
|
||||
|
||||
|
||||
def hsv_to_rgbw(hue, sat, val):
|
||||
'''
|
||||
Converts HSV values, and returns a tuple of RGBW values
|
||||
:param hue:
|
||||
:param sat:
|
||||
:param val:
|
||||
:return: (r, g, b, w)
|
||||
'''
|
||||
rgb = hsv_to_rgb(hue, sat, val)
|
||||
return rgb[0], rgb[1], rgb[2], min(rgb)
|
||||
|
||||
|
||||
class AnimationModes:
|
||||
OFF = 0
|
||||
STATIC = 1
|
||||
STATIC_STANDBY = 2
|
||||
BREATHING = 3
|
||||
RAINBOW = 4
|
||||
BREATHING_RAINBOW = 5
|
||||
KNIGHT = 6
|
||||
SWIRL = 7
|
||||
USER = 8
|
||||
|
||||
|
||||
class RGB(Extension):
|
||||
pos = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pixel_pin,
|
||||
num_pixels=0,
|
||||
rgb_order=(1, 0, 2), # GRB WS2812
|
||||
val_limit=255,
|
||||
hue_default=0,
|
||||
sat_default=255,
|
||||
val_default=255,
|
||||
hue_step=4,
|
||||
sat_step=13,
|
||||
val_step=13,
|
||||
animation_speed=1,
|
||||
breathe_center=1, # 1.0-2.7
|
||||
knight_effect_length=3,
|
||||
animation_mode=AnimationModes.STATIC,
|
||||
effect_init=False,
|
||||
reverse_animation=False,
|
||||
user_animation=None,
|
||||
pixels=None,
|
||||
refresh_rate=60,
|
||||
):
|
||||
self.pixel_pin = pixel_pin
|
||||
self.num_pixels = num_pixels
|
||||
self.rgb_order = rgb_order
|
||||
self.hue_step = hue_step
|
||||
self.sat_step = sat_step
|
||||
self.val_step = val_step
|
||||
self.hue = hue_default
|
||||
self.hue_default = hue_default
|
||||
self.sat = sat_default
|
||||
self.sat_default = sat_default
|
||||
self.val = val_default
|
||||
self.val_default = val_default
|
||||
self.breathe_center = breathe_center
|
||||
self.knight_effect_length = knight_effect_length
|
||||
self.val_limit = val_limit
|
||||
self.animation_mode = animation_mode
|
||||
self.animation_speed = animation_speed
|
||||
self.effect_init = effect_init
|
||||
self.reverse_animation = reverse_animation
|
||||
self.user_animation = user_animation
|
||||
self.pixels = pixels
|
||||
self.refresh_rate = refresh_rate
|
||||
|
||||
self.rgbw = bool(len(rgb_order) == 4)
|
||||
|
||||
self._substep = 0
|
||||
|
||||
make_key(names=('RGB_TOG',), on_press=self._rgb_tog)
|
||||
make_key(names=('RGB_HUI',), on_press=self._rgb_hui)
|
||||
make_key(names=('RGB_HUD',), on_press=self._rgb_hud)
|
||||
make_key(names=('RGB_SAI',), on_press=self._rgb_sai)
|
||||
make_key(names=('RGB_SAD',), on_press=self._rgb_sad)
|
||||
make_key(names=('RGB_VAI',), on_press=self._rgb_vai)
|
||||
make_key(names=('RGB_VAD',), on_press=self._rgb_vad)
|
||||
make_key(names=('RGB_ANI',), on_press=self._rgb_ani)
|
||||
make_key(names=('RGB_AND',), on_press=self._rgb_and)
|
||||
make_key(names=('RGB_MODE_PLAIN', 'RGB_M_P'), on_press=self._rgb_mode_static)
|
||||
make_key(names=('RGB_MODE_BREATHE', 'RGB_M_B'), on_press=self._rgb_mode_breathe)
|
||||
make_key(names=('RGB_MODE_RAINBOW', 'RGB_M_R'), on_press=self._rgb_mode_rainbow)
|
||||
make_key(
|
||||
names=('RGB_MODE_BREATHE_RAINBOW', 'RGB_M_BR'),
|
||||
on_press=self._rgb_mode_breathe_rainbow,
|
||||
)
|
||||
make_key(names=('RGB_MODE_SWIRL', 'RGB_M_S'), on_press=self._rgb_mode_swirl)
|
||||
make_key(names=('RGB_MODE_KNIGHT', 'RGB_M_K'), on_press=self._rgb_mode_knight)
|
||||
make_key(names=('RGB_RESET', 'RGB_RST'), on_press=self._rgb_reset)
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
if self.pixels is None:
|
||||
import neopixel
|
||||
|
||||
self.pixels = neopixel.NeoPixel(
|
||||
self.pixel_pin,
|
||||
self.num_pixels,
|
||||
pixel_order=self.rgb_order,
|
||||
)
|
||||
|
||||
# PixelBuffer are already iterable, can't do the usual `try: iter(...)`
|
||||
if issubclass(self.pixels.__class__, PixelBuf):
|
||||
self.pixels = (self.pixels,)
|
||||
|
||||
# Turn off auto_write on the backend. We handle the propagation of auto_write
|
||||
# behaviour.
|
||||
for pixel in self.pixels:
|
||||
pixel.auto_write = False
|
||||
|
||||
if self.num_pixels == 0:
|
||||
for pixels in self.pixels:
|
||||
self.num_pixels += len(pixels)
|
||||
|
||||
if debug.enabled:
|
||||
for n, pixels in enumerate(self.pixels):
|
||||
debug(f'pixels[{n}] = {pixels.__class__}[{len(pixels)}]')
|
||||
|
||||
self._task = create_task(self.animate, period_ms=(1000 // self.refresh_rate))
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
pass
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
self._do_update()
|
||||
|
||||
def deinit(self, sandbox):
|
||||
for pixel in self.pixels:
|
||||
pixel.deinit()
|
||||
|
||||
def set_hsv(self, hue, sat, val, index):
|
||||
'''
|
||||
Takes HSV values and displays it on a single LED/Neopixel
|
||||
:param hue:
|
||||
:param sat:
|
||||
:param val:
|
||||
:param index: Index of LED/Pixel
|
||||
'''
|
||||
|
||||
val = clamp(val, 0, self.val_limit)
|
||||
|
||||
if self.rgbw:
|
||||
self.set_rgb(hsv_to_rgbw(hue, sat, val), index)
|
||||
else:
|
||||
self.set_rgb(hsv_to_rgb(hue, sat, val), index)
|
||||
|
||||
def set_hsv_fill(self, hue, sat, val):
|
||||
'''
|
||||
Takes HSV values and displays it on all LEDs/Neopixels
|
||||
:param hue:
|
||||
:param sat:
|
||||
:param val:
|
||||
'''
|
||||
|
||||
val = clamp(val, 0, self.val_limit)
|
||||
|
||||
if self.rgbw:
|
||||
self.set_rgb_fill(hsv_to_rgbw(hue, sat, val))
|
||||
else:
|
||||
self.set_rgb_fill(hsv_to_rgb(hue, sat, val))
|
||||
|
||||
def set_rgb(self, rgb, index):
|
||||
'''
|
||||
Takes an RGB or RGBW and displays it on a single LED/Neopixel
|
||||
:param rgb: RGB or RGBW
|
||||
:param index: Index of LED/Pixel
|
||||
'''
|
||||
if 0 <= index <= self.num_pixels - 1:
|
||||
for pixels in self.pixels:
|
||||
if index <= (len(pixels) - 1):
|
||||
pixels[index] = rgb
|
||||
break
|
||||
index -= len(pixels)
|
||||
|
||||
def set_rgb_fill(self, rgb):
|
||||
'''
|
||||
Takes an RGB or RGBW and displays it on all LEDs/Neopixels
|
||||
:param rgb: RGB or RGBW
|
||||
'''
|
||||
for pixels in self.pixels:
|
||||
pixels.fill(rgb)
|
||||
|
||||
def increase_hue(self, step=None):
|
||||
'''
|
||||
Increases hue by step amount rolling at 256 and returning to 0
|
||||
:param step:
|
||||
'''
|
||||
if step is None:
|
||||
step = self.hue_step
|
||||
|
||||
self.hue = (self.hue + step) % 256
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def decrease_hue(self, step=None):
|
||||
'''
|
||||
Decreases hue by step amount rolling at 0 and returning to 256
|
||||
:param step:
|
||||
'''
|
||||
if step is None:
|
||||
step = self.hue_step
|
||||
|
||||
if (self.hue - step) <= 0:
|
||||
self.hue = (self.hue + 256 - step) % 256
|
||||
else:
|
||||
self.hue = (self.hue - step) % 256
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def increase_sat(self, step=None):
|
||||
'''
|
||||
Increases saturation by step amount stopping at 255
|
||||
:param step:
|
||||
'''
|
||||
if step is None:
|
||||
step = self.sat_step
|
||||
|
||||
self.sat = clamp(self.sat + step, 0, 255)
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def decrease_sat(self, step=None):
|
||||
'''
|
||||
Decreases saturation by step amount stopping at 0
|
||||
:param step:
|
||||
'''
|
||||
if step is None:
|
||||
step = self.sat_step
|
||||
|
||||
self.sat = clamp(self.sat - step, 0, 255)
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def increase_val(self, step=None):
|
||||
'''
|
||||
Increases value by step amount stopping at 100
|
||||
:param step:
|
||||
'''
|
||||
if step is None:
|
||||
step = self.val_step
|
||||
|
||||
self.val = clamp(self.val + step, 0, 255)
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def decrease_val(self, step=None):
|
||||
'''
|
||||
Decreases value by step amount stopping at 0
|
||||
:param step:
|
||||
'''
|
||||
if step is None:
|
||||
step = self.val_step
|
||||
|
||||
self.val = clamp(self.val - step, 0, 255)
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def increase_ani(self):
|
||||
'''
|
||||
Increases animation speed by 1 amount stopping at 10
|
||||
:param step:
|
||||
'''
|
||||
self.animation_speed = clamp(self.animation_speed + 1, 0, 10)
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def decrease_ani(self):
|
||||
'''
|
||||
Decreases animation speed by 1 amount stopping at 0
|
||||
:param step:
|
||||
'''
|
||||
self.animation_speed = clamp(self.animation_speed - 1, 0, 10)
|
||||
|
||||
if self._check_update():
|
||||
self._do_update()
|
||||
|
||||
def off(self):
|
||||
'''
|
||||
Turns off all LEDs/Neopixels without changing stored values
|
||||
'''
|
||||
self.set_hsv_fill(0, 0, 0)
|
||||
|
||||
self.show()
|
||||
|
||||
def show(self):
|
||||
'''
|
||||
Turns on all LEDs/Neopixels without changing stored values
|
||||
'''
|
||||
for pixels in self.pixels:
|
||||
pixels.show()
|
||||
|
||||
def animate(self):
|
||||
'''
|
||||
Activates a "step" in the animation based on the active mode
|
||||
:return: Returns the new state in animation
|
||||
'''
|
||||
if self.effect_init:
|
||||
self._init_effect()
|
||||
|
||||
if self.animation_mode is AnimationModes.STATIC_STANDBY:
|
||||
return
|
||||
|
||||
if not self.enable:
|
||||
return
|
||||
|
||||
self._animation_step()
|
||||
|
||||
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||||
return
|
||||
elif self.animation_mode == AnimationModes.BREATHING:
|
||||
self.effect_breathing()
|
||||
elif self.animation_mode == AnimationModes.BREATHING_RAINBOW:
|
||||
self.effect_breathing_rainbow()
|
||||
elif self.animation_mode == AnimationModes.KNIGHT:
|
||||
self.effect_knight()
|
||||
elif self.animation_mode == AnimationModes.RAINBOW:
|
||||
self.effect_rainbow()
|
||||
elif self.animation_mode == AnimationModes.STATIC:
|
||||
self.effect_static()
|
||||
elif self.animation_mode == AnimationModes.SWIRL:
|
||||
self.effect_swirl()
|
||||
elif self.animation_mode == AnimationModes.USER:
|
||||
self.user_animation(self)
|
||||
else:
|
||||
self.off()
|
||||
|
||||
self.show()
|
||||
|
||||
def _animation_step(self):
|
||||
self._substep += self.animation_speed / 4
|
||||
self._step = int(self._substep)
|
||||
self._substep -= self._step
|
||||
|
||||
def _init_effect(self):
|
||||
self.pos = 0
|
||||
self.reverse_animation = False
|
||||
self.effect_init = False
|
||||
|
||||
def _check_update(self):
|
||||
return bool(self.animation_mode == AnimationModes.STATIC_STANDBY)
|
||||
|
||||
def _do_update(self):
|
||||
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||||
self.animation_mode = AnimationModes.STATIC
|
||||
|
||||
def effect_static(self):
|
||||
self.set_hsv_fill(self.hue, self.sat, self.val)
|
||||
self.animation_mode = AnimationModes.STATIC_STANDBY
|
||||
|
||||
def effect_breathing(self):
|
||||
# http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
|
||||
# https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
|
||||
sined = sin((self.pos / 255.0) * pi)
|
||||
multip_1 = exp(sined) - self.breathe_center / e
|
||||
multip_2 = clamp(self.val, 0, self.val_limit) / (e - 1 / e)
|
||||
|
||||
val = int(multip_1 * multip_2)
|
||||
self.pos = (self.pos + self._step) % 256
|
||||
self.set_hsv_fill(self.hue, self.sat, val)
|
||||
|
||||
def effect_breathing_rainbow(self):
|
||||
self.increase_hue(self._step)
|
||||
self.effect_breathing()
|
||||
|
||||
def effect_rainbow(self):
|
||||
self.increase_hue(self._step)
|
||||
self.set_hsv_fill(self.hue, self.sat, self.val)
|
||||
|
||||
def effect_swirl(self):
|
||||
self.increase_hue(self._step)
|
||||
for i in range(0, self.num_pixels):
|
||||
self.set_hsv(
|
||||
(self.hue - (i * self.num_pixels)) % 256, self.sat, self.val, i
|
||||
)
|
||||
|
||||
def effect_knight(self):
|
||||
# Determine which LEDs should be lit up
|
||||
self.off() # Fill all off
|
||||
pos = int(self.pos)
|
||||
|
||||
# Set all pixels on in range of animation length offset by position
|
||||
for i in range(pos, (pos + self.knight_effect_length)):
|
||||
self.set_hsv(self.hue, self.sat, self.val, i)
|
||||
|
||||
# Reverse animation when a boundary is hit
|
||||
if pos >= self.num_pixels:
|
||||
self.reverse_animation = True
|
||||
elif 1 - pos > self.knight_effect_length:
|
||||
self.reverse_animation = False
|
||||
|
||||
if self.reverse_animation:
|
||||
self.pos -= self._step / 2
|
||||
else:
|
||||
self.pos += self._step / 2
|
||||
|
||||
def _rgb_tog(self, *args, **kwargs):
|
||||
if self.animation_mode == AnimationModes.STATIC:
|
||||
self.animation_mode = AnimationModes.STATIC_STANDBY
|
||||
self._do_update()
|
||||
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||||
self.animation_mode = AnimationModes.STATIC
|
||||
self._do_update()
|
||||
if self.enable:
|
||||
self.off()
|
||||
self.enable = not self.enable
|
||||
|
||||
def _rgb_hui(self, *args, **kwargs):
|
||||
self.increase_hue()
|
||||
|
||||
def _rgb_hud(self, *args, **kwargs):
|
||||
self.decrease_hue()
|
||||
|
||||
def _rgb_sai(self, *args, **kwargs):
|
||||
self.increase_sat()
|
||||
|
||||
def _rgb_sad(self, *args, **kwargs):
|
||||
self.decrease_sat()
|
||||
|
||||
def _rgb_vai(self, *args, **kwargs):
|
||||
self.increase_val()
|
||||
|
||||
def _rgb_vad(self, *args, **kwargs):
|
||||
self.decrease_val()
|
||||
|
||||
def _rgb_ani(self, *args, **kwargs):
|
||||
self.increase_ani()
|
||||
|
||||
def _rgb_and(self, *args, **kwargs):
|
||||
self.decrease_ani()
|
||||
|
||||
def _rgb_mode_static(self, *args, **kwargs):
|
||||
self.effect_init = True
|
||||
self.animation_mode = AnimationModes.STATIC
|
||||
|
||||
def _rgb_mode_breathe(self, *args, **kwargs):
|
||||
self.effect_init = True
|
||||
self.animation_mode = AnimationModes.BREATHING
|
||||
|
||||
def _rgb_mode_breathe_rainbow(self, *args, **kwargs):
|
||||
self.effect_init = True
|
||||
self.animation_mode = AnimationModes.BREATHING_RAINBOW
|
||||
|
||||
def _rgb_mode_rainbow(self, *args, **kwargs):
|
||||
self.effect_init = True
|
||||
self.animation_mode = AnimationModes.RAINBOW
|
||||
|
||||
def _rgb_mode_swirl(self, *args, **kwargs):
|
||||
self.effect_init = True
|
||||
self.animation_mode = AnimationModes.SWIRL
|
||||
|
||||
def _rgb_mode_knight(self, *args, **kwargs):
|
||||
self.effect_init = True
|
||||
self.animation_mode = AnimationModes.KNIGHT
|
||||
|
||||
def _rgb_reset(self, *args, **kwargs):
|
||||
self.hue = self.hue_default
|
||||
self.sat = self.sat_default
|
||||
self.val = self.val_default
|
||||
if self.animation_mode == AnimationModes.STATIC_STANDBY:
|
||||
self.animation_mode = AnimationModes.STATIC
|
||||
self._do_update()
|
||||
149
hackpads/Kalpad/firmware/kmk/extensions/statusled.py
Normal file
149
hackpads/Kalpad/firmware/kmk/extensions/statusled.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Use this extension for showing layer status with three leds
|
||||
|
||||
import pwmio
|
||||
import time
|
||||
|
||||
from kmk.extensions import Extension, InvalidExtensionEnvironment
|
||||
from kmk.keys import make_key
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class statusLED(Extension):
|
||||
def __init__(
|
||||
self,
|
||||
led_pins,
|
||||
brightness=30,
|
||||
brightness_step=5,
|
||||
brightness_limit=100,
|
||||
):
|
||||
self._leds = []
|
||||
for led in led_pins:
|
||||
try:
|
||||
self._leds.append(pwmio.PWMOut(led))
|
||||
except Exception as e:
|
||||
if debug.enabled:
|
||||
debug(e)
|
||||
raise InvalidExtensionEnvironment(
|
||||
'Unable to create pulseio.PWMOut() instance with provided led_pin'
|
||||
)
|
||||
self._led_count = len(self._leds)
|
||||
|
||||
self.brightness = brightness
|
||||
self._layer_last = -1
|
||||
|
||||
self.brightness_step = brightness_step
|
||||
self.brightness_limit = brightness_limit
|
||||
|
||||
make_key(names=('SLED_INC',), on_press=self._key_led_inc)
|
||||
make_key(names=('SLED_DEC',), on_press=self._key_led_dec)
|
||||
|
||||
def _layer_indicator(self, layer_active, *args, **kwargs):
|
||||
'''
|
||||
Indicates layer with leds
|
||||
|
||||
For the time being just a simple consecutive single led
|
||||
indicator. And when there are more layers than leds it
|
||||
wraps around to the first led again.
|
||||
(Also works for a single led, which just lights when any
|
||||
layer is active)
|
||||
'''
|
||||
|
||||
if self._layer_last != layer_active:
|
||||
led_last = 0 if self._layer_last == 0 else 1 + (self._layer_last - 1) % 3
|
||||
if layer_active > 0:
|
||||
led_active = 0 if layer_active == 0 else 1 + (layer_active - 1) % 3
|
||||
self.set_brightness(self.brightness, led_active)
|
||||
self.set_brightness(0, led_last)
|
||||
else:
|
||||
self.set_brightness(0, led_last)
|
||||
self._layer_last = layer_active
|
||||
|
||||
def __repr__(self):
|
||||
return f'SLED({self._to_dict()})'
|
||||
|
||||
def _to_dict(self):
|
||||
return {
|
||||
'brightness': self.brightness,
|
||||
'brightness_step': self.brightness_step,
|
||||
'brightness_limit': self.brightness_limit,
|
||||
}
|
||||
|
||||
def on_runtime_enable(self, sandbox):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, sandbox):
|
||||
return
|
||||
|
||||
def during_bootup(self, sandbox):
|
||||
'''Light up every single led once for 200 ms'''
|
||||
for i in range(self._led_count + 2):
|
||||
if i < self._led_count:
|
||||
self._leds[i].duty_cycle = int(self.brightness / 100 * 65535)
|
||||
i_off = i - 2
|
||||
if i_off >= 0 and i_off < self._led_count:
|
||||
self._leds[i_off].duty_cycle = int(0)
|
||||
time.sleep(0.1)
|
||||
for led in self._leds:
|
||||
led.duty_cycle = int(0)
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, sandbox):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, sandbox):
|
||||
self._layer_indicator(sandbox.active_layers[0])
|
||||
return
|
||||
|
||||
def before_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def after_hid_send(self, sandbox):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, sandbox):
|
||||
self.set_brightness(0)
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, sandbox):
|
||||
self.set_brightness(self.brightness)
|
||||
self._leds[2].duty_cycle = int(50 / 100 * 65535)
|
||||
time.sleep(0.2)
|
||||
self._leds[2].duty_cycle = int(0)
|
||||
return
|
||||
|
||||
def set_brightness(self, percent, layer_id=-1):
|
||||
if layer_id < 0:
|
||||
for led in self._leds:
|
||||
led.duty_cycle = int(percent / 100 * 65535)
|
||||
else:
|
||||
self._leds[layer_id - 1].duty_cycle = int(percent / 100 * 65535)
|
||||
|
||||
def increase_brightness(self, step=None):
|
||||
if not step:
|
||||
self.brightness += self.brightness_step
|
||||
else:
|
||||
self.brightness += step
|
||||
|
||||
if self.brightness > 100:
|
||||
self.brightness = 100
|
||||
|
||||
self.set_brightness(self.brightness, self._layer_last)
|
||||
|
||||
def decrease_brightness(self, step=None):
|
||||
if not step:
|
||||
self.brightness -= self.brightness_step
|
||||
else:
|
||||
self.brightness -= step
|
||||
|
||||
if self.brightness < 0:
|
||||
self.brightness = 0
|
||||
|
||||
self.set_brightness(self.brightness, self._layer_last)
|
||||
|
||||
def _key_led_inc(self, *args, **kwargs):
|
||||
self.increase_brightness()
|
||||
|
||||
def _key_led_dec(self, *args, **kwargs):
|
||||
self.decrease_brightness()
|
||||
44
hackpads/Kalpad/firmware/kmk/extensions/stringy_keymaps.py
Normal file
44
hackpads/Kalpad/firmware/kmk/extensions/stringy_keymaps.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from kmk.extensions import Extension
|
||||
from kmk.keys import KC
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class StringyKeymaps(Extension):
|
||||
def on_runtime_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
for _, layer in enumerate(keyboard.keymap):
|
||||
for key_idx, key in enumerate(layer):
|
||||
if isinstance(key, str):
|
||||
replacement = KC.get(key)
|
||||
if replacement is None:
|
||||
replacement = KC.NO
|
||||
if debug.enabled:
|
||||
debug('Failed replacing ', key, '. Using KC.NO')
|
||||
elif debug.enabled:
|
||||
debug('Replacing ', key, ' with ', replacement)
|
||||
layer[key_idx] = replacement
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
0
hackpads/Kalpad/firmware/kmk/handlers/__init__.py
Normal file
0
hackpads/Kalpad/firmware/kmk/handlers/__init__.py
Normal file
121
hackpads/Kalpad/firmware/kmk/handlers/stock.py
Normal file
121
hackpads/Kalpad/firmware/kmk/handlers/stock.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
from time import sleep
|
||||
|
||||
|
||||
def passthrough(key, keyboard, *args, **kwargs):
|
||||
return keyboard
|
||||
|
||||
|
||||
def reset(*args, **kwargs):
|
||||
import microcontroller
|
||||
|
||||
microcontroller.reset()
|
||||
|
||||
|
||||
def reload(*args, **kwargs):
|
||||
import supervisor
|
||||
|
||||
supervisor.reload()
|
||||
|
||||
|
||||
def bootloader(*args, **kwargs):
|
||||
import microcontroller
|
||||
|
||||
microcontroller.on_next_reset(microcontroller.RunMode.BOOTLOADER)
|
||||
microcontroller.reset()
|
||||
|
||||
|
||||
def gesc_pressed(key, keyboard, KC, *args, **kwargs):
|
||||
GESC_TRIGGERS = {KC.LSHIFT, KC.RSHIFT, KC.LGUI, KC.RGUI}
|
||||
|
||||
if GESC_TRIGGERS.intersection(keyboard.keys_pressed):
|
||||
# First, release GUI if already pressed
|
||||
keyboard._send_hid()
|
||||
# if Shift is held, KC_GRAVE will become KC_TILDE on OS level
|
||||
keyboard.keys_pressed.add(KC.GRAVE)
|
||||
keyboard.hid_pending = True
|
||||
return keyboard
|
||||
|
||||
# else return KC_ESC
|
||||
keyboard.keys_pressed.add(KC.ESCAPE)
|
||||
keyboard.hid_pending = True
|
||||
|
||||
return keyboard
|
||||
|
||||
|
||||
def gesc_released(key, keyboard, KC, *args, **kwargs):
|
||||
keyboard.keys_pressed.discard(KC.ESCAPE)
|
||||
keyboard.keys_pressed.discard(KC.GRAVE)
|
||||
keyboard.hid_pending = True
|
||||
return keyboard
|
||||
|
||||
|
||||
def bkdl_pressed(key, keyboard, KC, *args, **kwargs):
|
||||
BKDL_TRIGGERS = {KC.LGUI, KC.RGUI}
|
||||
|
||||
if BKDL_TRIGGERS.intersection(keyboard.keys_pressed):
|
||||
keyboard._send_hid()
|
||||
keyboard.keys_pressed.add(KC.DEL)
|
||||
keyboard.hid_pending = True
|
||||
return keyboard
|
||||
|
||||
# else return KC_ESC
|
||||
keyboard.keys_pressed.add(KC.BKSP)
|
||||
keyboard.hid_pending = True
|
||||
|
||||
return keyboard
|
||||
|
||||
|
||||
def bkdl_released(key, keyboard, KC, *args, **kwargs):
|
||||
keyboard.keys_pressed.discard(KC.BKSP)
|
||||
keyboard.keys_pressed.discard(KC.DEL)
|
||||
keyboard.hid_pending = True
|
||||
return keyboard
|
||||
|
||||
|
||||
def sleep_pressed(key, keyboard, KC, *args, **kwargs):
|
||||
sleep(key.meta.ms / 1000)
|
||||
return keyboard
|
||||
|
||||
|
||||
def uc_mode_pressed(key, keyboard, *args, **kwargs):
|
||||
keyboard.unicode_mode = key.meta.mode
|
||||
|
||||
return keyboard
|
||||
|
||||
|
||||
def hid_switch(key, keyboard, *args, **kwargs):
|
||||
keyboard.hid_type, keyboard.secondary_hid_type = (
|
||||
keyboard.secondary_hid_type,
|
||||
keyboard.hid_type,
|
||||
)
|
||||
keyboard._init_hid()
|
||||
return keyboard
|
||||
|
||||
|
||||
def ble_refresh(key, keyboard, *args, **kwargs):
|
||||
from kmk.hid import HIDModes
|
||||
|
||||
if keyboard.hid_type != HIDModes.BLE:
|
||||
return keyboard
|
||||
|
||||
keyboard._hid_helper.stop_advertising()
|
||||
keyboard._hid_helper.start_advertising()
|
||||
return keyboard
|
||||
|
||||
|
||||
def ble_disconnect(key, keyboard, *args, **kwargs):
|
||||
from kmk.hid import HIDModes
|
||||
|
||||
if keyboard.hid_type != HIDModes.BLE:
|
||||
return keyboard
|
||||
|
||||
keyboard._hid_helper.clear_bonds()
|
||||
return keyboard
|
||||
|
||||
|
||||
def any_pressed(key, keyboard, *args, **kwargs):
|
||||
from random import randint
|
||||
|
||||
key.code = randint(4, 56)
|
||||
keyboard.keys_pressed.add(key)
|
||||
keyboard.hid_pending = True
|
||||
311
hackpads/Kalpad/firmware/kmk/hid.py
Normal file
311
hackpads/Kalpad/firmware/kmk/hid.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
import supervisor
|
||||
import usb_hid
|
||||
from micropython import const
|
||||
|
||||
from struct import pack, pack_into
|
||||
|
||||
from kmk.keys import Axis, ConsumerKey, KeyboardKey, ModifierKey, MouseKey
|
||||
from kmk.scheduler import cancel_task, create_task
|
||||
from kmk.utils import Debug, clamp
|
||||
|
||||
try:
|
||||
from adafruit_ble import BLERadio
|
||||
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
|
||||
from adafruit_ble.services.standard.hid import HIDService
|
||||
from storage import getmount
|
||||
|
||||
_BLE_APPEARANCE_HID_KEYBOARD = const(961)
|
||||
except ImportError:
|
||||
# BLE not supported on this platform
|
||||
pass
|
||||
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class HIDModes:
|
||||
NOOP = 0 # currently unused; for testing?
|
||||
USB = 1
|
||||
BLE = 2
|
||||
|
||||
|
||||
_USAGE_PAGE_CONSUMER = const(0x0C)
|
||||
_USAGE_PAGE_KEYBOARD = const(0x01)
|
||||
_USAGE_PAGE_MOUSE = const(0x01)
|
||||
_USAGE_PAGE_SYSCONTROL = const(0x01)
|
||||
|
||||
_USAGE_CONSUMER = const(0x01)
|
||||
_USAGE_KEYBOARD = const(0x06)
|
||||
_USAGE_MOUSE = const(0x02)
|
||||
_USAGE_SYSCONTROL = const(0x80)
|
||||
|
||||
_REPORT_SIZE_CONSUMER = const(2)
|
||||
_REPORT_SIZE_KEYBOARD = const(8)
|
||||
_REPORT_SIZE_KEYBOARD_NKRO = const(16)
|
||||
_REPORT_SIZE_MOUSE = const(4)
|
||||
_REPORT_SIZE_MOUSE_HSCROLL = const(5)
|
||||
_REPORT_SIZE_SYSCONTROL = const(8)
|
||||
|
||||
|
||||
def find_device(devices, usage_page, usage):
|
||||
for device in devices:
|
||||
if (
|
||||
device.usage_page == usage_page
|
||||
and device.usage == usage
|
||||
and hasattr(device, 'send_report')
|
||||
):
|
||||
return device
|
||||
|
||||
|
||||
class Report:
|
||||
def __init__(self, size):
|
||||
self.buffer = bytearray(size)
|
||||
self.pending = False
|
||||
|
||||
def clear(self):
|
||||
for k, v in enumerate(self.buffer):
|
||||
if v:
|
||||
self.buffer[k] = 0x00
|
||||
self.pending = True
|
||||
|
||||
def get_action_map(self):
|
||||
return {}
|
||||
|
||||
|
||||
class KeyboardReport(Report):
|
||||
def __init__(self, size=_REPORT_SIZE_KEYBOARD):
|
||||
self.buffer = bytearray(size)
|
||||
self.prev_buffer = bytearray(size)
|
||||
|
||||
@property
|
||||
def pending(self):
|
||||
return self.buffer != self.prev_buffer
|
||||
|
||||
@pending.setter
|
||||
def pending(self, v):
|
||||
if v is False:
|
||||
self.prev_buffer[:] = self.buffer[:]
|
||||
|
||||
def clear(self):
|
||||
for idx in range(len(self.buffer)):
|
||||
self.buffer[idx] = 0x00
|
||||
|
||||
def add_key(self, key):
|
||||
# Find the first empty slot in the key report, and fill it; drop key if
|
||||
# report is full.
|
||||
idx = self.buffer.find(b'\x00', 2)
|
||||
|
||||
if 0 < idx < _REPORT_SIZE_KEYBOARD:
|
||||
self.buffer[idx] = key.code
|
||||
|
||||
def remove_key(self, key):
|
||||
idx = self.buffer.find(pack('B', key.code), 2)
|
||||
if 0 < idx:
|
||||
self.buffer[idx] = 0x00
|
||||
|
||||
def add_modifier(self, modifier):
|
||||
self.buffer[0] |= modifier.code
|
||||
|
||||
def remove_modifier(self, modifier):
|
||||
self.buffer[0] &= ~modifier.code
|
||||
|
||||
def get_action_map(self):
|
||||
return {KeyboardKey: self.add_key, ModifierKey: self.add_modifier}
|
||||
|
||||
|
||||
class NKROKeyboardReport(KeyboardReport):
|
||||
def __init__(self):
|
||||
super().__init__(_REPORT_SIZE_KEYBOARD_NKRO)
|
||||
|
||||
def add_key(self, key):
|
||||
self.buffer[(key.code >> 3) + 1] |= 1 << (key.code & 0x07)
|
||||
|
||||
def remove_key(self, key):
|
||||
self.buffer[(key.code >> 3) + 1] &= ~(1 << (key.code & 0x07))
|
||||
|
||||
|
||||
class ConsumerControlReport(Report):
|
||||
def __init__(self):
|
||||
super().__init__(_REPORT_SIZE_CONSUMER)
|
||||
|
||||
def add_cc(self, cc):
|
||||
pack_into('<H', self.buffer, 0, cc.code)
|
||||
self.pending = True
|
||||
|
||||
def remove_cc(self):
|
||||
if self.buffer != b'\x00\x00':
|
||||
self.buffer = b'\x00\x00'
|
||||
self.pending = True
|
||||
|
||||
def get_action_map(self):
|
||||
return {ConsumerKey: self.add_cc}
|
||||
|
||||
|
||||
class PointingDeviceReport(Report):
|
||||
def __init__(self, size=_REPORT_SIZE_MOUSE):
|
||||
super().__init__(size)
|
||||
|
||||
def add_button(self, key):
|
||||
self.buffer[0] |= key.code
|
||||
self.pending = True
|
||||
|
||||
def remove_button(self, key):
|
||||
self.buffer[0] &= ~key.code
|
||||
self.pending = True
|
||||
|
||||
def move_axis(self, axis):
|
||||
delta = clamp(axis.delta, -127, 127)
|
||||
axis.delta -= delta
|
||||
try:
|
||||
self.buffer[axis.code + 1] = 0xFF & delta
|
||||
self.pending = True
|
||||
except IndexError:
|
||||
if debug.enabled:
|
||||
debug(axis, ' not supported')
|
||||
|
||||
def get_action_map(self):
|
||||
return {Axis: self.move_axis, MouseKey: self.add_button}
|
||||
|
||||
|
||||
class HSPointingDeviceReport(PointingDeviceReport):
|
||||
def __init__(self):
|
||||
super().__init__(_REPORT_SIZE_MOUSE_HSCROLL)
|
||||
|
||||
|
||||
class AbstractHID:
|
||||
def __init__(self):
|
||||
self.report_map = {}
|
||||
self.device_map = {}
|
||||
self._setup_task = create_task(self.setup, period_ms=100)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def create_report(self, keys):
|
||||
for report in self.device_map.keys():
|
||||
report.clear()
|
||||
|
||||
for key in keys:
|
||||
if action := self.report_map.get(type(key)):
|
||||
action(key)
|
||||
|
||||
def send(self):
|
||||
for report in self.device_map.keys():
|
||||
if report.pending:
|
||||
self.device_map[report].send_report(report.buffer)
|
||||
report.pending = False
|
||||
|
||||
def setup(self):
|
||||
if not self.connected:
|
||||
return
|
||||
|
||||
try:
|
||||
self.setup_keyboard_hid()
|
||||
self.setup_consumer_control()
|
||||
self.setup_mouse_hid()
|
||||
|
||||
cancel_task(self._setup_task)
|
||||
self._setup_task = None
|
||||
if debug.enabled:
|
||||
self.show_debug()
|
||||
|
||||
except OSError as e:
|
||||
if debug.enabled:
|
||||
debug(type(e), ':', e)
|
||||
|
||||
def setup_keyboard_hid(self):
|
||||
if device := find_device(self.devices, _USAGE_PAGE_KEYBOARD, _USAGE_KEYBOARD):
|
||||
# bodgy NKRO autodetect
|
||||
try:
|
||||
report = KeyboardReport()
|
||||
device.send_report(report.buffer)
|
||||
except ValueError:
|
||||
report = NKROKeyboardReport()
|
||||
|
||||
self.report_map.update(report.get_action_map())
|
||||
self.device_map[report] = device
|
||||
|
||||
def setup_consumer_control(self):
|
||||
if device := find_device(self.devices, _USAGE_PAGE_CONSUMER, _USAGE_CONSUMER):
|
||||
report = ConsumerControlReport()
|
||||
self.report_map.update(report.get_action_map())
|
||||
self.device_map[report] = device
|
||||
|
||||
def setup_mouse_hid(self):
|
||||
if device := find_device(self.devices, _USAGE_PAGE_MOUSE, _USAGE_MOUSE):
|
||||
# bodgy pointing device panning autodetect
|
||||
try:
|
||||
report = PointingDeviceReport()
|
||||
device.send_report(report.buffer)
|
||||
except ValueError:
|
||||
report = HSPointingDeviceReport()
|
||||
|
||||
self.report_map.update(report.get_action_map())
|
||||
self.device_map[report] = device
|
||||
|
||||
def show_debug(self):
|
||||
for report in self.device_map.keys():
|
||||
debug('use ', report.__class__.__name__)
|
||||
|
||||
|
||||
class USBHID(AbstractHID):
|
||||
@property
|
||||
def connected(self):
|
||||
return supervisor.runtime.usb_connected
|
||||
|
||||
@property
|
||||
def devices(self):
|
||||
return usb_hid.devices
|
||||
|
||||
|
||||
class BLEHID(AbstractHID):
|
||||
def __init__(self, ble_name=None):
|
||||
super().__init__()
|
||||
|
||||
self.ble = BLERadio()
|
||||
self.ble.name = ble_name if ble_name else getmount('/').label
|
||||
self.ble_connected = False
|
||||
|
||||
self.hid = HIDService()
|
||||
self.hid.protocol_mode = 0 # Boot protocol
|
||||
|
||||
create_task(self.ble_monitor, period_ms=1000)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self.ble.connected
|
||||
|
||||
@property
|
||||
def devices(self):
|
||||
return self.hid.devices
|
||||
|
||||
def ble_monitor(self):
|
||||
if self.ble_connected != self.connected:
|
||||
self.ble_connected = self.connected
|
||||
if self._connected:
|
||||
if debug.enabled:
|
||||
debug('BLE connected')
|
||||
else:
|
||||
# Security-wise this is not right. While you're away someone turns
|
||||
# on your keyboard and they can pair with it nice and clean and then
|
||||
# listen to keystrokes.
|
||||
# On the other hand we don't have LESC so it's like shouting your
|
||||
# keystrokes in the air
|
||||
self.start_advertising()
|
||||
if debug.enabled:
|
||||
debug('BLE disconnected')
|
||||
|
||||
def clear_bonds(self):
|
||||
import _bleio
|
||||
|
||||
_bleio.adapter.erase_bonding()
|
||||
|
||||
def start_advertising(self):
|
||||
if not self.ble.advertising:
|
||||
advertisement = ProvideServicesAdvertisement(self.hid)
|
||||
advertisement.appearance = _BLE_APPEARANCE_HID_KEYBOARD
|
||||
|
||||
self.ble.start_advertising(advertisement)
|
||||
|
||||
def stop_advertising(self):
|
||||
self.ble.stop_advertising()
|
||||
50
hackpads/Kalpad/firmware/kmk/hid_reports/nkro_keyboard.py
Normal file
50
hackpads/Kalpad/firmware/kmk/hid_reports/nkro_keyboard.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import usb_hid
|
||||
|
||||
# fmt:off
|
||||
report_descriptor = bytes(
|
||||
(
|
||||
0x05, 0x01, # Usage Page (Generic Desktop Ctrls),
|
||||
0x09, 0x06, # Usage (Keyboard),
|
||||
0xA1, 0x01, # Collection (Application),
|
||||
0x85, 0x01, # Report ID (1)
|
||||
# modifiers
|
||||
0x05, 0x07, # Usage Page (Key Codes),
|
||||
0x19, 0xE0, # Usage Minimum (224),
|
||||
0x29, 0xE7, # Usage Maximum (231),
|
||||
0x15, 0x00, # Logical Minimum (0),
|
||||
0x25, 0x01, # Logical Maximum (1),
|
||||
0x75, 0x01, # Report Size (1),
|
||||
0x95, 0x08, # Report Count (8),
|
||||
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
# LEDs
|
||||
0x05, 0x08, # Usage Page (LEDs),
|
||||
0x19, 0x01, # Usage Minimum (1),
|
||||
0x29, 0x05, # Usage Maximum (5),
|
||||
0x95, 0x05, # Report Count (5),
|
||||
0x75, 0x01, # Report Size (1),
|
||||
0x91, 0x02, # Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non- olatile)
|
||||
0x95, 0x01, # Report Count (1),
|
||||
0x75, 0x03, # Report Size (3),
|
||||
0x91, 0x01, # Output (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,N n-volatile)
|
||||
# keys
|
||||
0x05, 0x07, # Usage Page (Kbrd/Keypad),
|
||||
0x19, 0x00, # Usage Minimum (0),
|
||||
0x29, 0x77, # Usage Maximum (119),
|
||||
0x15, 0x00, # Logical Minimum (0),
|
||||
0x25, 0x01, # Logical Maximum(1),
|
||||
0x95, 0x78, # Report Count (120),
|
||||
0x75, 0x01, # Report Size (1),
|
||||
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
0xC0, # End Collection
|
||||
)
|
||||
)
|
||||
# fmt:on
|
||||
|
||||
NKRO_KEYBOARD = usb_hid.Device(
|
||||
report_descriptor=report_descriptor,
|
||||
usage_page=0x01,
|
||||
usage=0x06,
|
||||
report_ids=(0x01,),
|
||||
in_report_lengths=(16,),
|
||||
out_report_lengths=(1,),
|
||||
)
|
||||
48
hackpads/Kalpad/firmware/kmk/hid_reports/pointer.py
Normal file
48
hackpads/Kalpad/firmware/kmk/hid_reports/pointer.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import usb_hid
|
||||
|
||||
# fmt:off
|
||||
report_descriptor = bytes(
|
||||
(
|
||||
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
|
||||
0x09, 0x02, # Usage (Mouse)
|
||||
0xA1, 0x01, # Collection (Application)
|
||||
0x09, 0x01, # Usage (Pointer)
|
||||
0xA1, 0x00, # Collection (Physical)
|
||||
0x85, 0x02, # 10, 11 Report ID (2)
|
||||
0x05, 0x09, # Usage Page (Button)
|
||||
0x19, 0x01, # Usage Minimum (0x01)
|
||||
0x29, 0x05, # Usage Maximum (0x05)
|
||||
0x15, 0x00, # Logical Minimum (0)
|
||||
0x25, 0x01, # Logical Maximum (1)
|
||||
0x95, 0x05, # Report Count (5)
|
||||
0x75, 0x01, # Report Size (1)
|
||||
0x81, 0x02, # Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
0x95, 0x01, # Report Count (1)
|
||||
0x75, 0x03, # Report Size (3)
|
||||
0x81, 0x01, # Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
|
||||
0x05, 0x01, # Usage Page (Generic Desktop Ctrls)
|
||||
0x09, 0x30, # Usage (X)
|
||||
0x09, 0x31, # Usage (Y)
|
||||
0x09, 0x38, # Usage (Wheel)
|
||||
0x05, 0x0C, # Usage Page (Consumer Devices)
|
||||
0x0A, 0x38, 0x02, # Usage (AC Pan)
|
||||
0x15, 0x81, # Logical Minimum (-127)
|
||||
0x25, 0x7F, # Logical Maximum (127)
|
||||
0x95, 0x04, # Report Count (4)
|
||||
0x75, 0x08, # Report Size (8)
|
||||
0x81, 0x06, # Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position)
|
||||
0xC0, # End Collection
|
||||
0xC0, # End Collection
|
||||
)
|
||||
)
|
||||
# fmt:on
|
||||
|
||||
|
||||
POINTER = usb_hid.Device(
|
||||
report_descriptor=report_descriptor,
|
||||
usage_page=0x01,
|
||||
usage=0x02,
|
||||
report_ids=(0x02,),
|
||||
in_report_lengths=(5,),
|
||||
out_report_lengths=(0,),
|
||||
)
|
||||
575
hackpads/Kalpad/firmware/kmk/keys.py
Normal file
575
hackpads/Kalpad/firmware/kmk/keys.py
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
try:
|
||||
from typing import Callable, Optional, Tuple
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import kmk.handlers.stock as handlers
|
||||
from kmk.utils import Debug
|
||||
|
||||
# Type aliases / forward declaration; can't use the proper types because of circular imports.
|
||||
Keyboard = object
|
||||
Key = object
|
||||
|
||||
|
||||
ALL_ALPHAS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
ALL_NUMBERS = '1234567890'
|
||||
# since KC.1 isn't valid Python, alias to KC.N1
|
||||
ALL_NUMBER_ALIASES = tuple(f'N{x}' for x in ALL_NUMBERS)
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class Axis:
|
||||
def __init__(self, code: int) -> None:
|
||||
self.code = code
|
||||
self.delta = 0
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'Axis(code={self.code}, delta={self.delta})'
|
||||
|
||||
def move(self, keyboard: Keyboard, delta: int):
|
||||
self.delta += delta
|
||||
if self.delta:
|
||||
keyboard.keys_pressed.add(self)
|
||||
keyboard.hid_pending = True
|
||||
else:
|
||||
keyboard.keys_pressed.discard(self)
|
||||
|
||||
|
||||
class AX:
|
||||
P = Axis(3)
|
||||
W = Axis(2)
|
||||
X = Axis(0)
|
||||
Y = Axis(1)
|
||||
|
||||
|
||||
def maybe_make_key(
|
||||
names: Tuple[str, ...],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> Callable[[str], Key]:
|
||||
def closure(candidate):
|
||||
if candidate in names:
|
||||
return make_key(names=names, *args, **kwargs)
|
||||
|
||||
return closure
|
||||
|
||||
|
||||
def maybe_make_argumented_key(
|
||||
names: Tuple[str, ...],
|
||||
constructor: [Key, Callable[[...], Key]],
|
||||
**kwargs,
|
||||
) -> Callable[[str], Key]:
|
||||
def closure(candidate):
|
||||
if candidate in names:
|
||||
return make_argumented_key(
|
||||
names=names,
|
||||
constructor=constructor,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return closure
|
||||
|
||||
|
||||
def maybe_make_no_key(candidate: str) -> Optional[Key]:
|
||||
# NO and TRNS are functionally identical in how they (don't) mutate
|
||||
# the state, but are tracked semantically separately, so create
|
||||
# two keys with the exact same functionality
|
||||
keys = (
|
||||
('NO', 'XXXXXXX'),
|
||||
('TRANSPARENT', 'TRNS'),
|
||||
)
|
||||
|
||||
for names in keys:
|
||||
if candidate in names:
|
||||
return make_key(
|
||||
names=names,
|
||||
on_press=handlers.passthrough,
|
||||
on_release=handlers.passthrough,
|
||||
)
|
||||
|
||||
|
||||
def maybe_make_alpha_key(candidate: str) -> Optional[Key]:
|
||||
if len(candidate) != 1:
|
||||
return
|
||||
|
||||
candidate_upper = candidate.upper()
|
||||
if candidate_upper in ALL_ALPHAS:
|
||||
return make_key(
|
||||
names=(candidate_upper, candidate.lower()),
|
||||
constructor=KeyboardKey,
|
||||
code=4 + ALL_ALPHAS.index(candidate_upper),
|
||||
)
|
||||
|
||||
|
||||
def maybe_make_numeric_key(candidate: str) -> Optional[Key]:
|
||||
if candidate in ALL_NUMBERS or candidate in ALL_NUMBER_ALIASES:
|
||||
try:
|
||||
offset = ALL_NUMBERS.index(candidate)
|
||||
except ValueError:
|
||||
offset = ALL_NUMBER_ALIASES.index(candidate)
|
||||
|
||||
return make_key(
|
||||
names=(ALL_NUMBERS[offset], ALL_NUMBER_ALIASES[offset]),
|
||||
constructor=KeyboardKey,
|
||||
code=30 + offset,
|
||||
)
|
||||
|
||||
|
||||
def maybe_make_mod_key(candidate: str) -> Optional[Key]:
|
||||
# MEH = LCTL | LALT | LSFT
|
||||
# HYPR = LCTL | LALT | LSFT | LGUI
|
||||
mods = (
|
||||
(0x01, ('LEFT_CONTROL', 'LCTRL', 'LCTL')),
|
||||
(0x02, ('LEFT_SHIFT', 'LSHIFT', 'LSFT')),
|
||||
(0x04, ('LEFT_ALT', 'LALT', 'LOPT')),
|
||||
(0x08, ('LEFT_SUPER', 'LGUI', 'LCMD', 'LWIN')),
|
||||
(0x10, ('RIGHT_CONTROL', 'RCTRL', 'RCTL')),
|
||||
(0x20, ('RIGHT_SHIFT', 'RSHIFT', 'RSFT')),
|
||||
(0x40, ('RIGHT_ALT', 'RALT', 'ROPT')),
|
||||
(0x80, ('RIGHT_SUPER', 'RGUI', 'RCMD', 'RWIN')),
|
||||
(0x07, ('MEH',)),
|
||||
(0x0F, ('HYPER', 'HYPR')),
|
||||
)
|
||||
|
||||
for code, names in mods:
|
||||
if candidate in names:
|
||||
return make_key(names=names, constructor=ModifierKey, code=code)
|
||||
|
||||
|
||||
def maybe_make_more_ascii(candidate: str) -> Optional[Key]:
|
||||
codes = (
|
||||
(40, ('ENTER', 'ENT', '\n')),
|
||||
(41, ('ESCAPE', 'ESC')),
|
||||
(42, ('BACKSPACE', 'BSPACE', 'BSPC', 'BKSP')),
|
||||
(43, ('TAB', '\t')),
|
||||
(44, ('SPACE', 'SPC', ' ')),
|
||||
(45, ('MINUS', 'MINS', '-')),
|
||||
(46, ('EQUAL', 'EQL', '=')),
|
||||
(47, ('LBRACKET', 'LBRC', '[')),
|
||||
(48, ('RBRACKET', 'RBRC', ']')),
|
||||
(49, ('BACKSLASH', 'BSLASH', 'BSLS', '\\')),
|
||||
(51, ('SEMICOLON', 'SCOLON', 'SCLN', ';')),
|
||||
(52, ('QUOTE', 'QUOT', "'")),
|
||||
(53, ('GRAVE', 'GRV', 'ZKHK', '`')),
|
||||
(54, ('COMMA', 'COMM', ',')),
|
||||
(55, ('DOT', '.')),
|
||||
(56, ('SLASH', 'SLSH', '/')),
|
||||
)
|
||||
|
||||
for code, names in codes:
|
||||
if candidate in names:
|
||||
return make_key(names=names, constructor=KeyboardKey, code=code)
|
||||
|
||||
|
||||
def maybe_make_fn_key(candidate: str) -> Optional[Key]:
|
||||
codes = (
|
||||
(58, ('F1',)),
|
||||
(59, ('F2',)),
|
||||
(60, ('F3',)),
|
||||
(61, ('F4',)),
|
||||
(62, ('F5',)),
|
||||
(63, ('F6',)),
|
||||
(64, ('F7',)),
|
||||
(65, ('F8',)),
|
||||
(66, ('F9',)),
|
||||
(67, ('F10',)),
|
||||
(68, ('F11',)),
|
||||
(69, ('F12',)),
|
||||
(104, ('F13',)),
|
||||
(105, ('F14',)),
|
||||
(106, ('F15',)),
|
||||
(107, ('F16',)),
|
||||
(108, ('F17',)),
|
||||
(109, ('F18',)),
|
||||
(110, ('F19',)),
|
||||
(111, ('F20',)),
|
||||
(112, ('F21',)),
|
||||
(113, ('F22',)),
|
||||
(114, ('F23',)),
|
||||
(115, ('F24',)),
|
||||
)
|
||||
|
||||
for code, names in codes:
|
||||
if candidate in names:
|
||||
return make_key(names=names, constructor=KeyboardKey, code=code)
|
||||
|
||||
|
||||
def maybe_make_navlock_key(candidate: str) -> Optional[Key]:
|
||||
codes = (
|
||||
(57, ('CAPS_LOCK', 'CAPSLOCK', 'CLCK', 'CAPS')),
|
||||
# FIXME: Investigate whether this key actually works, and
|
||||
# uncomment when/if it does.
|
||||
# (130, ('LOCKING_CAPS', 'LCAP')),
|
||||
(70, ('PRINT_SCREEN', 'PSCREEN', 'PSCR')),
|
||||
(71, ('SCROLL_LOCK', 'SCROLLLOCK', 'SLCK')),
|
||||
# FIXME: Investigate whether this key actually works, and
|
||||
# uncomment when/if it does.
|
||||
# (132, ('LOCKING_SCROLL', 'LSCRL')),
|
||||
(72, ('PAUSE', 'PAUS', 'BRK')),
|
||||
(73, ('INSERT', 'INS')),
|
||||
(74, ('HOME',)),
|
||||
(75, ('PGUP',)),
|
||||
(76, ('DELETE', 'DEL')),
|
||||
(77, ('END',)),
|
||||
(78, ('PGDOWN', 'PGDN')),
|
||||
(79, ('RIGHT', 'RGHT')),
|
||||
(80, ('LEFT',)),
|
||||
(81, ('DOWN',)),
|
||||
(82, ('UP',)),
|
||||
)
|
||||
|
||||
for code, names in codes:
|
||||
if candidate in names:
|
||||
return make_key(names=names, constructor=KeyboardKey, code=code)
|
||||
|
||||
|
||||
def maybe_make_numpad_key(candidate: str) -> Optional[Key]:
|
||||
codes = (
|
||||
(83, ('NUM_LOCK', 'NUMLOCK', 'NLCK')),
|
||||
(84, ('KP_SLASH', 'NUMPAD_SLASH', 'PSLS')),
|
||||
(85, ('KP_ASTERISK', 'NUMPAD_ASTERISK', 'PAST')),
|
||||
(86, ('KP_MINUS', 'NUMPAD_MINUS', 'PMNS')),
|
||||
(87, ('KP_PLUS', 'NUMPAD_PLUS', 'PPLS')),
|
||||
(88, ('KP_ENTER', 'NUMPAD_ENTER', 'PENT')),
|
||||
(89, ('KP_1', 'P1', 'NUMPAD_1')),
|
||||
(90, ('KP_2', 'P2', 'NUMPAD_2')),
|
||||
(91, ('KP_3', 'P3', 'NUMPAD_3')),
|
||||
(92, ('KP_4', 'P4', 'NUMPAD_4')),
|
||||
(93, ('KP_5', 'P5', 'NUMPAD_5')),
|
||||
(94, ('KP_6', 'P6', 'NUMPAD_6')),
|
||||
(95, ('KP_7', 'P7', 'NUMPAD_7')),
|
||||
(96, ('KP_8', 'P8', 'NUMPAD_8')),
|
||||
(97, ('KP_9', 'P9', 'NUMPAD_9')),
|
||||
(98, ('KP_0', 'P0', 'NUMPAD_0')),
|
||||
(99, ('KP_DOT', 'PDOT', 'NUMPAD_DOT')),
|
||||
(103, ('KP_EQUAL', 'PEQL', 'NUMPAD_EQUAL')),
|
||||
(133, ('KP_COMMA', 'PCMM', 'NUMPAD_COMMA')),
|
||||
(134, ('KP_EQUAL_AS400', 'NUMPAD_EQUAL_AS400')),
|
||||
)
|
||||
|
||||
for code, names in codes:
|
||||
if candidate in names:
|
||||
return make_key(names=names, constructor=KeyboardKey, code=code)
|
||||
|
||||
|
||||
def maybe_make_shifted_key(candidate: str) -> Optional[Key]:
|
||||
codes = (
|
||||
('1', ('EXCLAIM', 'EXLM', '!')),
|
||||
('2', ('AT', '@')),
|
||||
('3', ('HASH', 'POUND', '#')),
|
||||
('4', ('DOLLAR', 'DLR', '$')),
|
||||
('5', ('PERCENT', 'PERC', '%')),
|
||||
('6', ('CIRCUMFLEX', 'CIRC', '^')),
|
||||
('7', ('AMPERSAND', 'AMPR', '&')),
|
||||
('8', ('ASTERISK', 'ASTR', '*')),
|
||||
('9', ('LEFT_PAREN', 'LPRN', '(')),
|
||||
('0', ('RIGHT_PAREN', 'RPRN', ')')),
|
||||
('-', ('UNDERSCORE', 'UNDS', '_')),
|
||||
('=', ('PLUS', '+')),
|
||||
('[', ('LEFT_CURLY_BRACE', 'LCBR', '{')),
|
||||
(']', ('RIGHT_CURLY_BRACE', 'RCBR', '}')),
|
||||
('\\', ('PIPE', '|')),
|
||||
(';', ('COLON', 'COLN', ':')),
|
||||
("'", ('DOUBLE_QUOTE', 'DQUO', 'DQT', '"')),
|
||||
('`', ('TILDE', 'TILD', '~')),
|
||||
(',', ('LEFT_ANGLE_BRACKET', 'LABK', '<')),
|
||||
('.', ('RIGHT_ANGLE_BRACKET', 'RABK', '>')),
|
||||
('/', ('QUESTION', 'QUES', '?')),
|
||||
)
|
||||
|
||||
for unshifted, names in codes:
|
||||
if candidate in names:
|
||||
return make_key(
|
||||
names=names,
|
||||
constructor=ModifiedKey,
|
||||
code=KC[unshifted],
|
||||
modifier=KC.LSFT,
|
||||
)
|
||||
|
||||
|
||||
def maybe_make_firmware_key(candidate: str) -> Optional[Key]:
|
||||
keys = (
|
||||
((('BLE_REFRESH',), handlers.ble_refresh)),
|
||||
((('BLE_DISCONNECT',), handlers.ble_disconnect)),
|
||||
((('BOOTLOADER',), handlers.bootloader)),
|
||||
((('HID_SWITCH', 'HID'), handlers.hid_switch)),
|
||||
((('RELOAD', 'RLD'), handlers.reload)),
|
||||
((('RESET',), handlers.reset)),
|
||||
((('ANY',), handlers.any_pressed)),
|
||||
)
|
||||
|
||||
for names, handler in keys:
|
||||
if candidate in names:
|
||||
return make_key(names=names, on_press=handler)
|
||||
|
||||
|
||||
KEY_GENERATORS = (
|
||||
maybe_make_no_key,
|
||||
maybe_make_alpha_key,
|
||||
maybe_make_numeric_key,
|
||||
maybe_make_firmware_key,
|
||||
maybe_make_key(
|
||||
('BKDL',),
|
||||
on_press=handlers.bkdl_pressed,
|
||||
on_release=handlers.bkdl_released,
|
||||
),
|
||||
maybe_make_key(
|
||||
('GESC', 'GRAVE_ESC'),
|
||||
on_press=handlers.gesc_pressed,
|
||||
on_release=handlers.gesc_released,
|
||||
),
|
||||
maybe_make_mod_key,
|
||||
# More ASCII standard keys
|
||||
maybe_make_more_ascii,
|
||||
# Function Keys
|
||||
maybe_make_fn_key,
|
||||
# Lock Keys, Navigation, etc.
|
||||
maybe_make_navlock_key,
|
||||
# Numpad
|
||||
# FIXME: Investigate whether this key actually works, and
|
||||
# uncomment when/if it does.
|
||||
# maybe_make_key(131, ('LOCKING_NUM', 'LNUM')),
|
||||
maybe_make_numpad_key,
|
||||
# Making life better for folks on tiny keyboards especially: exposes
|
||||
# the 'shifted' keys as raw keys. Under the hood we're still
|
||||
# sending Shift+(whatever key is normally pressed) to get these, so
|
||||
# for example `KC_AT` will hold shift and press 2.
|
||||
maybe_make_shifted_key,
|
||||
)
|
||||
|
||||
|
||||
class KeyAttrDict:
|
||||
# Instead of relying on the uncontrollable availability of a big chunk of
|
||||
# contiguous memory for key caching, we can manually fragment the cache into
|
||||
# reasonably small partitions. The partition size is chosen from the magic
|
||||
# values of CPs hash allocation sizes.
|
||||
# (https://github.com/adafruit/circuitpython/blob/main/py/map.c, 2023-02)
|
||||
__partition_size = 37
|
||||
__cache = [{}]
|
||||
|
||||
def __iter__(self):
|
||||
for partition in self.__cache:
|
||||
for name in partition:
|
||||
yield name
|
||||
|
||||
def __setitem__(self, name: str, key: Key):
|
||||
# Overwrite existing reference.
|
||||
for partition in self.__cache:
|
||||
if name in partition:
|
||||
partition[name] = key
|
||||
return key
|
||||
|
||||
# Insert new reference.
|
||||
if len(self.__cache[-1]) >= self.__partition_size:
|
||||
self.__cache.append({})
|
||||
self.__cache[-1][name] = key
|
||||
return key
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
return self.__getitem__(name)
|
||||
|
||||
def get(self, name: str, default: Optional[Key] = None):
|
||||
try:
|
||||
return self.__getitem__(name)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
def clear(self):
|
||||
self.__cache.clear()
|
||||
self.__cache.append({})
|
||||
|
||||
def __getitem__(self, name: str):
|
||||
for partition in self.__cache:
|
||||
if name in partition:
|
||||
return partition[name]
|
||||
|
||||
for func in KEY_GENERATORS:
|
||||
maybe_key = func(name)
|
||||
if maybe_key:
|
||||
break
|
||||
|
||||
if not maybe_key:
|
||||
if debug.enabled:
|
||||
debug('Invalid key: ', name)
|
||||
return KC.NO
|
||||
|
||||
return maybe_key
|
||||
|
||||
|
||||
# Global state, will be filled in throughout this file, and
|
||||
# anywhere the user creates custom keys
|
||||
KC = KeyAttrDict()
|
||||
|
||||
|
||||
class Key:
|
||||
'''Generic Key class with assignable handlers.'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_press: Callable[[object, Key, Keyboard, ...], None] = handlers.passthrough,
|
||||
on_release: Callable[[object, Key, Keyboard, ...], None] = handlers.passthrough,
|
||||
):
|
||||
self._on_press = on_press
|
||||
self._on_release = on_release
|
||||
|
||||
def __repr__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def on_press(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
|
||||
self._on_press(self, keyboard, KC, coord_int)
|
||||
|
||||
def on_release(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
|
||||
self._on_release(self, keyboard, KC, coord_int)
|
||||
|
||||
|
||||
class _DefaultKey(Key):
|
||||
'''Meta class implementing handlers for Keys with HID codes.'''
|
||||
|
||||
def __init__(self, code: Optional[int] = None):
|
||||
self.code = code
|
||||
|
||||
def __repr__(self):
|
||||
return super().__repr__() + '(code=' + str(self.code) + ')'
|
||||
|
||||
def on_press(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
|
||||
if keyboard.implicit_modifier is not None:
|
||||
keyboard.keys_pressed.discard(keyboard.implicit_modifier)
|
||||
keyboard.implicit_modifier = None
|
||||
if self in keyboard.keys_pressed:
|
||||
keyboard.keys_pressed.discard(self)
|
||||
keyboard.hid_pending = True
|
||||
keyboard._send_hid()
|
||||
keyboard.keys_pressed.add(self)
|
||||
keyboard.hid_pending = True
|
||||
|
||||
def on_release(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
|
||||
keyboard.keys_pressed.discard(self)
|
||||
keyboard.hid_pending = True
|
||||
|
||||
|
||||
class KeyboardKey(_DefaultKey):
|
||||
pass
|
||||
|
||||
|
||||
class ModifierKey(_DefaultKey):
|
||||
def __call__(self, key: Key) -> Key:
|
||||
# don't duplicate when applying the same modifier twice
|
||||
if (
|
||||
isinstance(key, ModifiedKey)
|
||||
and key.modifier.code & self.code == key.modifier.code
|
||||
):
|
||||
return key
|
||||
elif isinstance(key, ModifierKey) and key.code & self.code == key.code:
|
||||
return key
|
||||
|
||||
return ModifiedKey(key, self)
|
||||
|
||||
|
||||
class ModifiedKey(Key):
|
||||
def __init__(self, code: [Key, int], modifier: [ModifierKey]):
|
||||
# generate from code by maybe_make_shifted_key
|
||||
if isinstance(code, int):
|
||||
key = KeyboardKey(code=code)
|
||||
else:
|
||||
key = code
|
||||
|
||||
# stack modifier keys
|
||||
if isinstance(key, ModifierKey):
|
||||
modifier = ModifierKey(key.code | modifier.code)
|
||||
key = None
|
||||
# stack modified keys
|
||||
elif isinstance(key, ModifiedKey):
|
||||
modifier = ModifierKey(key.modifier.code | modifier.code)
|
||||
key = key.key
|
||||
# clone modifier so it doesn't override explicit mods
|
||||
else:
|
||||
modifier = ModifierKey(modifier.code)
|
||||
|
||||
self.key = key
|
||||
self.modifier = modifier
|
||||
|
||||
def on_press(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
|
||||
if self.key in keyboard.keys_pressed:
|
||||
self.key.on_release(keyboard, coord_int)
|
||||
keyboard._send_hid()
|
||||
keyboard.keys_pressed.add(self.modifier)
|
||||
if self.key is not None:
|
||||
self.key.on_press(keyboard, coord_int)
|
||||
if keyboard.implicit_modifier is not None:
|
||||
keyboard.keys_pressed.discard(keyboard.implicit_modifier)
|
||||
keyboard.implicit_modifier = self.modifier
|
||||
keyboard.hid_pending = True
|
||||
|
||||
def on_release(self, keyboard: Keyboard, coord_int: Optional[int] = None) -> None:
|
||||
keyboard.keys_pressed.discard(self.modifier)
|
||||
if self.key is not None:
|
||||
self.key.on_release(keyboard, coord_int)
|
||||
if keyboard.implicit_modifier == self.modifier:
|
||||
keyboard.implicit_modifier = None
|
||||
keyboard.hid_pending = True
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
super().__repr__()
|
||||
+ '(key='
|
||||
+ str(self.key)
|
||||
+ ', modifier='
|
||||
+ str(self.modifier)
|
||||
+ ')'
|
||||
)
|
||||
|
||||
|
||||
class ConsumerKey(_DefaultKey):
|
||||
pass
|
||||
|
||||
|
||||
class MouseKey(_DefaultKey):
|
||||
pass
|
||||
|
||||
|
||||
def make_key(
|
||||
names: Tuple[str, ...],
|
||||
constructor: Key = Key,
|
||||
**kwargs,
|
||||
) -> Key:
|
||||
'''
|
||||
Create a new key, aliased by `names` in the KC lookup table.
|
||||
|
||||
Names are globally unique. If a later key is created with
|
||||
the same name as an existing entry in `KC`, it will overwrite
|
||||
the existing entry.
|
||||
|
||||
Names are case sensitive.
|
||||
|
||||
All **kwargs are passed to the Key constructor
|
||||
'''
|
||||
|
||||
key = constructor(**kwargs)
|
||||
|
||||
for name in names:
|
||||
KC[name] = key
|
||||
|
||||
return key
|
||||
|
||||
|
||||
# Argumented keys are implicitly internal, so auto-gen of code
|
||||
# is almost certainly the best plan here
|
||||
def make_argumented_key(
|
||||
names: Tuple[str, ...],
|
||||
constructor: [Key, Callable[[...], Key]],
|
||||
**_kwargs,
|
||||
) -> Key:
|
||||
|
||||
def argumented_key(*args, **kwargs) -> Key:
|
||||
# This is a very ugly workaround for missing syntax in mpy-cross 8.x
|
||||
# and, once EOL, can be replaced by:
|
||||
# return constructor(*args, **_kwargs, **kwargs)
|
||||
k = _kwargs.copy()
|
||||
k.update(**kwargs)
|
||||
return constructor(*args, **k)
|
||||
|
||||
for name in names:
|
||||
KC[name] = argumented_key
|
||||
|
||||
return argumented_key
|
||||
528
hackpads/Kalpad/firmware/kmk/kmk_keyboard.py
Normal file
528
hackpads/Kalpad/firmware/kmk/kmk_keyboard.py
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
try:
|
||||
from typing import Callable, Optional
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from collections import namedtuple
|
||||
from keypad import Event as KeyEvent
|
||||
|
||||
from kmk.hid import BLEHID, USBHID, AbstractHID, HIDModes
|
||||
from kmk.keys import KC, Axis, Key
|
||||
from kmk.modules import Module
|
||||
from kmk.scanners.keypad import MatrixScanner
|
||||
from kmk.scheduler import Task, cancel_task, create_task, get_due_task
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug('kmk.keyboard')
|
||||
|
||||
KeyBufferFrame = namedtuple(
|
||||
'KeyBufferFrame', ('key', 'is_pressed', 'int_coord', 'index')
|
||||
)
|
||||
|
||||
|
||||
def debug_error(module, message: str, error: Exception):
|
||||
if debug.enabled:
|
||||
debug(
|
||||
message, ': ', error.__class__.__name__, ': ', error, name=module.__module__
|
||||
)
|
||||
|
||||
|
||||
class Sandbox:
|
||||
matrix_update = None
|
||||
secondary_matrix_update = None
|
||||
active_layers = None
|
||||
|
||||
|
||||
class KMKKeyboard:
|
||||
def __init__(self) -> None:
|
||||
#####
|
||||
# User-configurable
|
||||
self.keymap = []
|
||||
self.coord_mapping = None
|
||||
|
||||
self.row_pins = None
|
||||
self.col_pins = None
|
||||
self.diode_orientation = None
|
||||
self.matrix = None
|
||||
|
||||
self.modules = []
|
||||
self.extensions = []
|
||||
self.sandbox = Sandbox()
|
||||
|
||||
#####
|
||||
# Internal State
|
||||
self.keys_pressed = set()
|
||||
self._coordkeys_pressed = {}
|
||||
self.implicit_modifier = None
|
||||
self.hid_type = HIDModes.USB
|
||||
self.secondary_hid_type = None
|
||||
self._hid_helper = None
|
||||
self._hid_send_enabled = False
|
||||
self.hid_pending = False
|
||||
self.matrix_update = None
|
||||
self.secondary_matrix_update = None
|
||||
self.matrix_update_queue = []
|
||||
self._trigger_powersave_enable = False
|
||||
self._trigger_powersave_disable = False
|
||||
self._go_args = None
|
||||
self._resume_buffer = []
|
||||
self._resume_buffer_x = []
|
||||
|
||||
# this should almost always be PREpended to, replaces
|
||||
# former use of reversed_active_layers which had pointless
|
||||
# overhead (the underlying list was never used anyway)
|
||||
self.active_layers = [0]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
def _send_hid(self) -> None:
|
||||
if not self._hid_send_enabled:
|
||||
return
|
||||
|
||||
if debug.enabled:
|
||||
if self.keys_pressed:
|
||||
debug('keys_pressed=', self.keys_pressed)
|
||||
|
||||
self._hid_helper.create_report(self.keys_pressed)
|
||||
try:
|
||||
self._hid_helper.send()
|
||||
except Exception as err:
|
||||
debug_error(self._hid_helper, 'send', err)
|
||||
|
||||
self.hid_pending = False
|
||||
|
||||
for key in self.keys_pressed:
|
||||
if isinstance(key, Axis):
|
||||
key.move(self, 0)
|
||||
|
||||
def _handle_matrix_report(self, kevent: KeyEvent) -> None:
|
||||
if kevent is not None:
|
||||
self._on_matrix_changed(kevent)
|
||||
|
||||
def _find_key_in_map(self, int_coord: int) -> Key:
|
||||
try:
|
||||
idx = self.coord_mapping.index(int_coord)
|
||||
except ValueError:
|
||||
if debug.enabled:
|
||||
debug('no such int_coord: ', int_coord)
|
||||
return None
|
||||
|
||||
key = None
|
||||
for layer in self.active_layers:
|
||||
try:
|
||||
key = self.keymap[layer][idx]
|
||||
except IndexError:
|
||||
if debug.enabled:
|
||||
debug('keymap IndexError: idx=', idx, ' layer=', layer)
|
||||
|
||||
if key and key != KC.TRNS:
|
||||
break
|
||||
|
||||
return key
|
||||
|
||||
def _on_matrix_changed(self, kevent: KeyEvent) -> None:
|
||||
int_coord = kevent.key_number
|
||||
is_pressed = kevent.pressed
|
||||
key = None
|
||||
|
||||
if not is_pressed:
|
||||
key = self._coordkeys_pressed.pop(int_coord, None)
|
||||
|
||||
if key is None:
|
||||
key = self._find_key_in_map(int_coord)
|
||||
|
||||
if key is None:
|
||||
return
|
||||
|
||||
if debug.enabled:
|
||||
debug(kevent, ': ', key)
|
||||
|
||||
self.pre_process_key(key, is_pressed, int_coord)
|
||||
|
||||
def _process_resume_buffer(self):
|
||||
'''
|
||||
Resume the processing of buffered, delayed, deferred, etc. key events
|
||||
emitted by modules.
|
||||
|
||||
We use a copy of the `_resume_buffer` as a working buffer. The working
|
||||
buffer holds all key events in the correct order for processing. If
|
||||
during processing new events are pushed to the `_resume_buffer`, they
|
||||
are prepended to the working buffer (which may not be emptied), in
|
||||
order to preserve key event order.
|
||||
We also double-buffer `_resume_buffer` with `_resume_buffer_x`, only
|
||||
copying the reference to hopefully safe some time on allocations.
|
||||
'''
|
||||
|
||||
buffer, self._resume_buffer = self._resume_buffer, self._resume_buffer_x
|
||||
|
||||
while buffer:
|
||||
ksf = buffer.pop(0)
|
||||
key = ksf.key
|
||||
|
||||
# Handle any unaccounted-for layer shifts by looking up the key resolution again.
|
||||
if ksf.int_coord is not None:
|
||||
if ksf.is_pressed:
|
||||
key = self._find_key_in_map(ksf.int_coord)
|
||||
else:
|
||||
key = self._coordkeys_pressed.pop(ksf.int_coord, key)
|
||||
|
||||
# Resume the processing of the key event and update the HID report
|
||||
# when applicable.
|
||||
self.pre_process_key(key, ksf.is_pressed, ksf.int_coord, ksf.index)
|
||||
|
||||
if self.hid_pending:
|
||||
self._send_hid()
|
||||
self.hid_pending = False
|
||||
|
||||
# Any newly buffered key events must be prepended to the working
|
||||
# buffer.
|
||||
if self._resume_buffer:
|
||||
self._resume_buffer.extend(buffer)
|
||||
buffer.clear()
|
||||
buffer, self._resume_buffer = self._resume_buffer, buffer
|
||||
|
||||
self._resume_buffer_x = buffer
|
||||
|
||||
def pre_process_key(
|
||||
self,
|
||||
key: Key,
|
||||
is_pressed: bool,
|
||||
int_coord: Optional[int] = None,
|
||||
index: int = 0,
|
||||
) -> None:
|
||||
for module in self.modules[index:]:
|
||||
try:
|
||||
key = module.process_key(self, key, is_pressed, int_coord)
|
||||
if key is None:
|
||||
break
|
||||
except Exception as err:
|
||||
debug_error(module, 'process_key', err)
|
||||
|
||||
if int_coord is not None:
|
||||
if is_pressed:
|
||||
self._coordkeys_pressed[int_coord] = key
|
||||
|
||||
if debug.enabled:
|
||||
debug('coordkeys_pressed=', self._coordkeys_pressed)
|
||||
|
||||
if key:
|
||||
self.process_key(key, is_pressed, int_coord)
|
||||
|
||||
def process_key(
|
||||
self, key: Key, is_pressed: bool, int_coord: Optional[int] = None
|
||||
) -> None:
|
||||
if is_pressed:
|
||||
key.on_press(self, int_coord)
|
||||
else:
|
||||
key.on_release(self, int_coord)
|
||||
|
||||
def resume_process_key(
|
||||
self,
|
||||
module: Module,
|
||||
key: Key,
|
||||
is_pressed: bool,
|
||||
int_coord: Optional[int] = None,
|
||||
reprocess: Optional[bool] = False,
|
||||
) -> None:
|
||||
index = self.modules.index(module) + (0 if reprocess else 1)
|
||||
ksf = KeyBufferFrame(
|
||||
key=key, is_pressed=is_pressed, int_coord=int_coord, index=index
|
||||
)
|
||||
self._resume_buffer.append(ksf)
|
||||
|
||||
def remove_key(self, keycode: Key) -> None:
|
||||
self.process_key(keycode, False)
|
||||
|
||||
def add_key(self, keycode: Key) -> None:
|
||||
self.process_key(keycode, True)
|
||||
|
||||
def tap_key(self, keycode: Key) -> None:
|
||||
self.add_key(keycode)
|
||||
# On the next cycle, we'll remove the key.
|
||||
self.set_timeout(0, lambda: self.remove_key(keycode))
|
||||
|
||||
def set_timeout(self, after_ticks: int, callback: Callable[[None], None]) -> [Task]:
|
||||
return create_task(callback, after_ms=after_ticks)
|
||||
|
||||
def cancel_timeout(self, timeout_key: int) -> None:
|
||||
cancel_task(timeout_key)
|
||||
|
||||
def _process_timeouts(self) -> None:
|
||||
for task in get_due_task():
|
||||
task()
|
||||
|
||||
def _init_coord_mapping(self) -> None:
|
||||
'''
|
||||
Attempt to sanely guess a coord_mapping if one is not provided. No-op
|
||||
if `kmk.extensions.split.Split` is used, it provides equivalent
|
||||
functionality in `on_bootup`
|
||||
|
||||
To save RAM on boards that don't use Split, we don't import Split
|
||||
and do an isinstance check, but instead do string detection
|
||||
'''
|
||||
if any(x.__class__.__module__ == 'kmk.modules.split' for x in self.modules):
|
||||
return
|
||||
|
||||
if not self.coord_mapping:
|
||||
cm = []
|
||||
for m in self.matrix:
|
||||
cm.extend(m.coord_mapping)
|
||||
self.coord_mapping = tuple(cm)
|
||||
|
||||
def _init_hid(self) -> None:
|
||||
if self.hid_type == HIDModes.NOOP:
|
||||
self._hid_helper = AbstractHID
|
||||
elif self.hid_type == HIDModes.USB:
|
||||
self._hid_helper = USBHID
|
||||
elif self.hid_type == HIDModes.BLE:
|
||||
self._hid_helper = BLEHID
|
||||
else:
|
||||
self._hid_helper = AbstractHID
|
||||
self._hid_helper = self._hid_helper(**self._go_args)
|
||||
self._hid_send_enabled = True
|
||||
|
||||
if debug.enabled:
|
||||
debug('hid=', self._hid_helper)
|
||||
|
||||
def _deinit_hid(self) -> None:
|
||||
try:
|
||||
self._hid_helper.create_report({})
|
||||
self._hid_helper.send()
|
||||
except Exception as e:
|
||||
debug_error(self, '_deinit_hid', e)
|
||||
|
||||
def _init_matrix(self) -> None:
|
||||
if self.matrix is None:
|
||||
self.matrix = MatrixScanner(
|
||||
column_pins=self.col_pins,
|
||||
row_pins=self.row_pins,
|
||||
columns_to_anodes=self.diode_orientation,
|
||||
)
|
||||
|
||||
try:
|
||||
self.matrix = tuple(iter(self.matrix))
|
||||
offset = 0
|
||||
for matrix in self.matrix:
|
||||
matrix.offset = offset
|
||||
offset += matrix.key_count
|
||||
except TypeError:
|
||||
self.matrix = (self.matrix,)
|
||||
|
||||
if debug.enabled:
|
||||
debug('matrix=', [_.__class__.__name__ for _ in self.matrix])
|
||||
|
||||
def during_bootup(self) -> None:
|
||||
# Modules and extensions that fail `during_bootup` get removed from
|
||||
# their respective lists. This serves as a self-check mechanism; any
|
||||
# modules or extensions that initialize peripherals or data structures
|
||||
# should do that in `during_bootup`.
|
||||
for idx, module in enumerate(self.modules):
|
||||
try:
|
||||
module.during_bootup(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'during_bootup', err)
|
||||
self.modules[idx] = None
|
||||
|
||||
self.modules[:] = [_ for _ in self.modules if _]
|
||||
|
||||
if debug.enabled:
|
||||
debug('modules=', [_.__class__.__name__ for _ in self.modules])
|
||||
|
||||
for idx, ext in enumerate(self.extensions):
|
||||
try:
|
||||
ext.during_bootup(self)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'during_bootup', err)
|
||||
self.extensions[idx] = None
|
||||
|
||||
self.extensions[:] = [_ for _ in self.extensions if _]
|
||||
|
||||
if debug.enabled:
|
||||
debug('extensions=', [_.__class__.__name__ for _ in self.extensions])
|
||||
|
||||
def before_matrix_scan(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.before_matrix_scan(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'before_matrix_scan', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.before_matrix_scan(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'before_matrix_scan', err)
|
||||
|
||||
def after_matrix_scan(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.after_matrix_scan(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'after_matrix_scan', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.after_matrix_scan(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'after_matrix_scan', err)
|
||||
|
||||
def before_hid_send(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.before_hid_send(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'before_hid_send', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.before_hid_send(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'before_hid_send', err)
|
||||
|
||||
def after_hid_send(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.after_hid_send(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'after_hid_send', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.after_hid_send(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'after_hid_send', err)
|
||||
|
||||
def powersave_enable(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.on_powersave_enable(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'powersave_enable', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.on_powersave_enable(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'powersave_enable', err)
|
||||
|
||||
def powersave_disable(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.on_powersave_disable(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'powersave_disable', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.on_powersave_disable(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'powersave_disable', err)
|
||||
|
||||
def deinit(self) -> None:
|
||||
for module in self.modules:
|
||||
try:
|
||||
module.deinit(self)
|
||||
except Exception as err:
|
||||
debug_error(module, 'deinit', err)
|
||||
|
||||
for ext in self.extensions:
|
||||
try:
|
||||
ext.deinit(self.sandbox)
|
||||
except Exception as err:
|
||||
debug_error(ext, 'deinit', err)
|
||||
|
||||
def go(self, hid_type=HIDModes.USB, secondary_hid_type=None, **kwargs) -> None:
|
||||
try:
|
||||
self._init(
|
||||
hid_type=hid_type,
|
||||
secondary_hid_type=secondary_hid_type,
|
||||
**kwargs,
|
||||
)
|
||||
while True:
|
||||
self._main_loop()
|
||||
except Exception as err:
|
||||
import traceback
|
||||
|
||||
traceback.print_exception(err)
|
||||
finally:
|
||||
debug('cleaning up...')
|
||||
self._deinit_hid()
|
||||
self.deinit()
|
||||
debug('...done')
|
||||
|
||||
if not debug.enabled:
|
||||
import supervisor
|
||||
|
||||
supervisor.reload()
|
||||
|
||||
def _init(
|
||||
self,
|
||||
hid_type: HIDModes = HIDModes.USB,
|
||||
secondary_hid_type: Optional[HIDModes] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self._go_args = kwargs
|
||||
self.hid_type = hid_type
|
||||
self.secondary_hid_type = secondary_hid_type
|
||||
|
||||
if debug.enabled:
|
||||
debug('Initialising ', self)
|
||||
|
||||
self._init_hid()
|
||||
self._init_matrix()
|
||||
self._init_coord_mapping()
|
||||
self.during_bootup()
|
||||
|
||||
if debug.enabled:
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
debug('mem_info used:', gc.mem_alloc(), ' free:', gc.mem_free())
|
||||
|
||||
def _main_loop(self) -> None:
|
||||
self.sandbox.active_layers = self.active_layers.copy()
|
||||
|
||||
self.before_matrix_scan()
|
||||
|
||||
self._process_resume_buffer()
|
||||
|
||||
for matrix in self.matrix:
|
||||
update = matrix.scan_for_changes()
|
||||
if update:
|
||||
self.matrix_update = update
|
||||
break
|
||||
self.sandbox.matrix_update = self.matrix_update
|
||||
self.sandbox.secondary_matrix_update = self.secondary_matrix_update
|
||||
|
||||
self.after_matrix_scan()
|
||||
|
||||
if self.secondary_matrix_update:
|
||||
self.matrix_update_queue.append(self.secondary_matrix_update)
|
||||
self.secondary_matrix_update = None
|
||||
|
||||
if self.matrix_update:
|
||||
self.matrix_update_queue.append(self.matrix_update)
|
||||
self.matrix_update = None
|
||||
|
||||
# only handle one key per cycle.
|
||||
if self.matrix_update_queue:
|
||||
self._handle_matrix_report(self.matrix_update_queue.pop(0))
|
||||
|
||||
self.before_hid_send()
|
||||
|
||||
if self.hid_pending:
|
||||
self._send_hid()
|
||||
|
||||
self._process_timeouts()
|
||||
|
||||
if self.hid_pending:
|
||||
self._send_hid()
|
||||
|
||||
self.after_hid_send()
|
||||
|
||||
if self._trigger_powersave_enable:
|
||||
self.powersave_enable()
|
||||
|
||||
if self._trigger_powersave_disable:
|
||||
self.powersave_disable()
|
||||
34
hackpads/Kalpad/firmware/kmk/kmktime.py
Normal file
34
hackpads/Kalpad/firmware/kmk/kmktime.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from micropython import const
|
||||
from supervisor import ticks_ms
|
||||
|
||||
_TICKS_PERIOD = const(1 << 29)
|
||||
_TICKS_MAX = const(_TICKS_PERIOD - 1)
|
||||
_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
|
||||
|
||||
|
||||
def ticks_diff(new: int, start: int) -> int:
|
||||
diff = (new - start) & _TICKS_MAX
|
||||
diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
|
||||
return diff
|
||||
|
||||
|
||||
def ticks_add(ticks: int, delta: int) -> int:
|
||||
return (ticks + delta) % _TICKS_PERIOD
|
||||
|
||||
|
||||
def check_deadline(new: int, start: int, ms: int) -> int:
|
||||
return ticks_diff(new, start) < ms
|
||||
|
||||
|
||||
class PeriodicTimer:
|
||||
def __init__(self, period: int):
|
||||
self.period = period
|
||||
self.last_tick = ticks_ms()
|
||||
|
||||
def tick(self) -> bool:
|
||||
now = ticks_ms()
|
||||
if ticks_diff(now, self.last_tick) >= self.period:
|
||||
self.last_tick = now
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
46
hackpads/Kalpad/firmware/kmk/modules/__init__.py
Normal file
46
hackpads/Kalpad/firmware/kmk/modules/__init__.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
class InvalidExtensionEnvironment(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Module:
|
||||
'''
|
||||
Modules differ from extensions in that they not only can read the state, but
|
||||
are allowed to modify the state. The will be loaded on boot, and are not
|
||||
allowed to be unloaded as they are required to continue functioning in a
|
||||
consistant manner.
|
||||
'''
|
||||
|
||||
# The below methods should be implemented by subclasses
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be injected as an extra matrix update
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be replace matrix update if supplied
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
raise NotImplementedError
|
||||
|
||||
def deinit(self, keyboard):
|
||||
pass
|
||||
230
hackpads/Kalpad/firmware/kmk/modules/adns9800.py
Normal file
230
hackpads/Kalpad/firmware/kmk/modules/adns9800.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
import busio
|
||||
import digitalio
|
||||
import microcontroller
|
||||
|
||||
import time
|
||||
|
||||
from kmk.keys import AX
|
||||
from kmk.modules import Module
|
||||
from kmk.modules.adns9800_firmware import firmware
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class REG:
|
||||
Product_ID = 0x0
|
||||
Revision_ID = 0x1
|
||||
MOTION = 0x2
|
||||
DELTA_X_L = 0x3
|
||||
DELTA_X_H = 0x4
|
||||
DELTA_Y_L = 0x5
|
||||
DELTA_Y_H = 0x6
|
||||
SQUAL = 0x7
|
||||
PIXEL_SUM = 0x8
|
||||
Maximum_Pixel = 0x9
|
||||
Minimum_Pixel = 0xA
|
||||
Shutter_Lower = 0xB
|
||||
Shutter_Upper = 0xC
|
||||
Frame_Period_Lower = 0xD
|
||||
Frame_Period_Upper = 0xE
|
||||
Configuration_I = 0xF
|
||||
Configuration_II = 0x10
|
||||
Frame_Capture = 0x12
|
||||
SROM_Enable = 0x13
|
||||
Run_Downshift = 0x14
|
||||
Rest1_Rate = 0x15
|
||||
Rest1_Downshift = 0x16
|
||||
Rest2_Rate = 0x17
|
||||
Rest2_Downshift = 0x18
|
||||
Rest3_Rate = 0x19
|
||||
Frame_Period_Max_Bound_Lower = 0x1A
|
||||
Frame_Period_Max_Bound_Upper = 0x1B
|
||||
Frame_Period_Min_Bound_Lower = 0x1C
|
||||
Frame_Period_Min_Bound_Upper = 0x1D
|
||||
Shutter_Max_Bound_Lower = 0x1E
|
||||
Shutter_Max_Bound_Upper = 0x1F
|
||||
LASER_CTRL0 = 0x20
|
||||
Observation = 0x24
|
||||
Data_Out_Lower = 0x25
|
||||
Data_Out_Upper = 0x26
|
||||
SROM_ID = 0x2A
|
||||
Lift_Detection_Thr = 0x2E
|
||||
Configuration_V = 0x2F
|
||||
Configuration_IV = 0x39
|
||||
Power_Up_Reset = 0x3A
|
||||
Shutdown = 0x3B
|
||||
Inverse_Product_ID = 0x3F
|
||||
Snap_Angle = 0x42
|
||||
Motion_Burst = 0x50
|
||||
SROM_Load_Burst = 0x62
|
||||
Pixel_Burst = 0x64
|
||||
|
||||
|
||||
class ADNS9800(Module):
|
||||
tswr = tsww = 120
|
||||
tsrw = tsrr = 20
|
||||
tsrad = 100
|
||||
tbexit = 1
|
||||
baud = 2000000
|
||||
cpol = 1
|
||||
cpha = 1
|
||||
DIR_WRITE = 0x80
|
||||
DIR_READ = 0x7F
|
||||
|
||||
def __init__(self, cs, sclk, miso, mosi, invert_x=False, invert_y=False):
|
||||
self.cs = digitalio.DigitalInOut(cs)
|
||||
self.cs.direction = digitalio.Direction.OUTPUT
|
||||
self.spi = busio.SPI(clock=sclk, MOSI=mosi, MISO=miso)
|
||||
self.invert_x = invert_x
|
||||
self.invert_y = invert_y
|
||||
|
||||
def adns_start(self):
|
||||
self.cs.value = False
|
||||
|
||||
def adns_stop(self):
|
||||
self.cs.value = True
|
||||
|
||||
def adns_write(self, reg, data):
|
||||
while not self.spi.try_lock():
|
||||
pass
|
||||
try:
|
||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
|
||||
self.adns_start()
|
||||
self.spi.write(bytes([reg | self.DIR_WRITE, data]))
|
||||
finally:
|
||||
self.spi.unlock()
|
||||
self.adns_stop()
|
||||
|
||||
def adns_read(self, reg):
|
||||
result = bytearray(1)
|
||||
while not self.spi.try_lock():
|
||||
pass
|
||||
try:
|
||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
|
||||
self.adns_start()
|
||||
self.spi.write(bytes([reg & self.DIR_READ]))
|
||||
microcontroller.delay_us(self.tsrad)
|
||||
self.spi.readinto(result)
|
||||
finally:
|
||||
self.spi.unlock()
|
||||
self.adns_stop()
|
||||
|
||||
return result[0]
|
||||
|
||||
def adns_upload_srom(self):
|
||||
while not self.spi.try_lock():
|
||||
pass
|
||||
try:
|
||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
|
||||
self.adns_start()
|
||||
self.spi.write(bytes([REG.SROM_Load_Burst | self.DIR_WRITE]))
|
||||
for b in firmware:
|
||||
self.spi.write(bytes([b]))
|
||||
finally:
|
||||
self.spi.unlock()
|
||||
self.adns_stop()
|
||||
|
||||
def delta_to_int(self, high, low):
|
||||
comp = (high << 8) | low
|
||||
if comp & 0x8000:
|
||||
return (-1) * (0xFFFF + 1 - comp)
|
||||
return comp
|
||||
|
||||
def adns_read_motion(self):
|
||||
result = bytearray(14)
|
||||
while not self.spi.try_lock():
|
||||
pass
|
||||
try:
|
||||
self.spi.configure(baudrate=self.baud, polarity=self.cpol, phase=self.cpha)
|
||||
self.adns_start()
|
||||
self.spi.write(bytes([REG.Motion_Burst & self.DIR_READ]))
|
||||
microcontroller.delay_us(self.tsrad)
|
||||
self.spi.readinto(result)
|
||||
finally:
|
||||
self.spi.unlock()
|
||||
self.adns_stop()
|
||||
microcontroller.delay_us(self.tbexit)
|
||||
self.adns_write(REG.MOTION, 0x0)
|
||||
return result
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
|
||||
self.adns_write(REG.Power_Up_Reset, 0x5A)
|
||||
time.sleep(0.1)
|
||||
self.adns_read(REG.MOTION)
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
self.adns_read(REG.DELTA_X_L)
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
self.adns_read(REG.DELTA_X_H)
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
self.adns_read(REG.DELTA_Y_L)
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
self.adns_read(REG.DELTA_Y_H)
|
||||
microcontroller.delay_us(self.tsrw)
|
||||
|
||||
self.adns_write(REG.Configuration_IV, 0x2)
|
||||
microcontroller.delay_us(self.tsww)
|
||||
self.adns_write(REG.SROM_Enable, 0x1D)
|
||||
microcontroller.delay_us(1000)
|
||||
self.adns_write(REG.SROM_Enable, 0x18)
|
||||
microcontroller.delay_us(self.tsww)
|
||||
|
||||
self.adns_upload_srom()
|
||||
microcontroller.delay_us(2000)
|
||||
|
||||
laser_ctrl0 = self.adns_read(REG.LASER_CTRL0)
|
||||
microcontroller.delay_us(self.tsrw)
|
||||
self.adns_write(REG.LASER_CTRL0, laser_ctrl0 & 0xF0)
|
||||
microcontroller.delay_us(self.tsww)
|
||||
self.adns_write(REG.Configuration_I, 0x10)
|
||||
microcontroller.delay_us(self.tsww)
|
||||
|
||||
if debug.enabled:
|
||||
debug('ADNS: Product ID ', hex(self.adns_read(REG.Product_ID)))
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
debug('ADNS: Revision ID ', hex(self.adns_read(REG.Revision_ID)))
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
debug('ADNS: SROM ID ', hex(self.adns_read(REG.SROM_ID)))
|
||||
microcontroller.delay_us(self.tsrr)
|
||||
if self.adns_read(REG.Observation) & 0x20:
|
||||
debug('ADNS: Sensor is running SROM')
|
||||
else:
|
||||
debug('ADNS: Error! Sensor is not running SROM!')
|
||||
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
motion = self.adns_read_motion()
|
||||
if motion[0] & 0x80:
|
||||
delta_x = self.delta_to_int(motion[3], motion[2])
|
||||
delta_y = self.delta_to_int(motion[5], motion[4])
|
||||
|
||||
if self.invert_x:
|
||||
delta_x *= -1
|
||||
if self.invert_y:
|
||||
delta_y *= -1
|
||||
|
||||
if delta_x:
|
||||
AX.X.move(keyboard, delta_x)
|
||||
|
||||
if delta_y:
|
||||
AX.Y.move(keyboard, delta_y)
|
||||
|
||||
if debug.enabled:
|
||||
debug('Delta: ', delta_x, ' ', delta_y)
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
133
hackpads/Kalpad/firmware/kmk/modules/analogin/__init__.py
Normal file
133
hackpads/Kalpad/firmware/kmk/modules/analogin/__init__.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
from kmk.keys import KC
|
||||
from kmk.modules import Module
|
||||
from kmk.scheduler import create_task
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
def noop(*args):
|
||||
pass
|
||||
|
||||
|
||||
class AnalogEvent:
|
||||
def __init__(self, on_change=noop, on_stop=noop):
|
||||
self._on_change = on_change
|
||||
self._on_stop = on_stop
|
||||
|
||||
def on_change(self, event, keyboard):
|
||||
self._on_change(self, event, keyboard)
|
||||
|
||||
def on_stop(self, event, keyboard):
|
||||
self._on_stop(self, event, keyboard)
|
||||
|
||||
|
||||
class AnalogKey(AnalogEvent):
|
||||
def __init__(self, key, threshold=127):
|
||||
self.key = key
|
||||
self.threshold = threshold
|
||||
self.pressed = False
|
||||
|
||||
def on_change(self, event, keyboard):
|
||||
if event.value >= self.threshold and not self.pressed:
|
||||
self.pressed = True
|
||||
keyboard.pre_process_key(self.key, True)
|
||||
|
||||
elif event.value < self.threshold and self.pressed:
|
||||
self.pressed = False
|
||||
keyboard.pre_process_key(self.key, False)
|
||||
|
||||
def on_stop(self, event, keyboard):
|
||||
pass
|
||||
|
||||
|
||||
class AnalogInput:
|
||||
def __init__(self, input, filter=lambda input: input.value >> 8):
|
||||
self.input = input
|
||||
self.value = 0
|
||||
self.delta = 0
|
||||
self.filter = filter
|
||||
|
||||
def update(self):
|
||||
'''
|
||||
Read a new value from an analogio compatible input, apply
|
||||
transformation, then return either the new value if it changed or `None`
|
||||
otherwise.
|
||||
'''
|
||||
value = self.filter(self.input)
|
||||
self.delta = value - self.value
|
||||
if self.delta != 0:
|
||||
self.value = value
|
||||
return value
|
||||
|
||||
|
||||
class AnalogInputs(Module):
|
||||
def __init__(self, inputs, evtmap, update_interval=10):
|
||||
self._active = {}
|
||||
self.inputs = inputs
|
||||
self.evtmap = evtmap
|
||||
self.update_interval = update_interval
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
self.task = create_task(
|
||||
lambda: self.update(keyboard),
|
||||
period_ms=self.update_interval,
|
||||
)
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def update(self, keyboard):
|
||||
for idx, input in enumerate(self.inputs):
|
||||
value = input.update()
|
||||
|
||||
# No change in value: stop or pass
|
||||
if value is None:
|
||||
if idx in self._active:
|
||||
if debug.enabled:
|
||||
debug('on_stop', input, self._active[idx])
|
||||
self._active[idx].on_stop(input, keyboard)
|
||||
del self._active[idx]
|
||||
continue
|
||||
|
||||
# Resolve event handler
|
||||
if idx in self._active:
|
||||
key = self._active[idx]
|
||||
else:
|
||||
key = None
|
||||
for layer in keyboard.active_layers:
|
||||
try:
|
||||
key = self.evtmap[layer][idx]
|
||||
except IndexError:
|
||||
if debug.enabled:
|
||||
debug('evtmap IndexError: idx=', idx, ' layer=', layer)
|
||||
if key and key != KC.TRNS:
|
||||
break
|
||||
|
||||
if key == KC.NO:
|
||||
continue
|
||||
|
||||
# Forward change to event handler
|
||||
try:
|
||||
self._active[idx] = key
|
||||
if debug.enabled:
|
||||
debug('on_change', input, key, value)
|
||||
key.on_change(input, keyboard)
|
||||
except Exception as e:
|
||||
if debug.enabled:
|
||||
debug(type(e), ': ', e, ' in ', key.on_change)
|
||||
20
hackpads/Kalpad/firmware/kmk/modules/analogin/keys.py
Normal file
20
hackpads/Kalpad/firmware/kmk/modules/analogin/keys.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from . import AnalogEvent
|
||||
|
||||
|
||||
class AnalogKey(AnalogEvent):
|
||||
def __init__(self, key, threshold=127):
|
||||
self.key = key
|
||||
self.threshold = threshold
|
||||
self.pressed = False
|
||||
|
||||
def on_change(self, event, keyboard):
|
||||
if event.value >= self.threshold and not self.pressed:
|
||||
self.pressed = True
|
||||
keyboard.pre_process_key(self.key, True)
|
||||
|
||||
elif event.value < self.threshold and self.pressed:
|
||||
self.pressed = False
|
||||
keyboard.pre_process_key(self.key, False)
|
||||
|
||||
def on_stop(self, event, keyboard):
|
||||
pass
|
||||
82
hackpads/Kalpad/firmware/kmk/modules/autoshift.py
Normal file
82
hackpads/Kalpad/firmware/kmk/modules/autoshift.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
from kmk.keys import KC, KeyboardKey
|
||||
from kmk.modules import Module
|
||||
from kmk.scheduler import cancel_task, create_task
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class Autoshift(Module):
|
||||
def __init__(self, tap_time=300):
|
||||
self.tap_time = tap_time
|
||||
|
||||
self._active = False
|
||||
self._task = None
|
||||
self._key = None
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
self._task = create_task(lambda: self._shift(keyboard), after_ms=-1)
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
pass
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
pass
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
# Unshift on any key event
|
||||
if self._active:
|
||||
self._unshift(keyboard)
|
||||
return key
|
||||
|
||||
# Only shift from an unshifted state
|
||||
if KC.LSFT in keyboard.keys_pressed:
|
||||
return key
|
||||
|
||||
# Ignore rolls from tapped to hold
|
||||
if not is_pressed and key is not self._key:
|
||||
return key
|
||||
|
||||
# Only shift alpha keys, iff there's no pending potential shift
|
||||
if (
|
||||
is_pressed
|
||||
and not self._key
|
||||
and isinstance(key, KeyboardKey)
|
||||
and KC.A.code <= key.code <= KC.Z.code
|
||||
):
|
||||
create_task(self._task, after_ms=self.tap_time)
|
||||
self._key = key
|
||||
else:
|
||||
cancel_task(self._task)
|
||||
keyboard.resume_process_key(self, self._key, True)
|
||||
if key is self._key:
|
||||
keyboard.resume_process_key(self, self._key, False)
|
||||
else:
|
||||
keyboard.resume_process_key(self, key, True)
|
||||
self._key = None
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
pass
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
pass
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
pass
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
pass
|
||||
|
||||
def _shift(self, keyboard):
|
||||
if debug.enabled:
|
||||
debug('activate')
|
||||
self._active = True
|
||||
keyboard.keys_pressed.add(KC.LSFT)
|
||||
keyboard.resume_process_key(self, self._key, True)
|
||||
|
||||
def _unshift(self, keyboard):
|
||||
if debug.enabled:
|
||||
debug('deactivate')
|
||||
self._active = False
|
||||
self._key = None
|
||||
keyboard.keys_pressed.remove(KC.LSFT)
|
||||
102
hackpads/Kalpad/firmware/kmk/modules/capsword.py
Normal file
102
hackpads/Kalpad/firmware/kmk/modules/capsword.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
from kmk.keys import KC, KeyboardKey, ModifierKey, make_key
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class CapsWord(Module):
|
||||
# default timeout is 8000
|
||||
# alphabets, numbers and few more keys will not disable capsword
|
||||
def __init__(self, timeout=8000):
|
||||
self._alphabets = range(KC.A.code, KC.Z.code + 1)
|
||||
self._numbers = range(KC.N1.code, KC.N0.code + 1)
|
||||
self.keys_ignored = [
|
||||
KC.MINS,
|
||||
KC.BSPC,
|
||||
KC.UNDS,
|
||||
]
|
||||
self._timeout_key = False
|
||||
self._cw_active = False
|
||||
self.timeout = timeout
|
||||
make_key(
|
||||
names=(
|
||||
'CAPSWORD',
|
||||
'CW',
|
||||
),
|
||||
on_press=self.cw_pressed,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
if not self._cw_active or key == KC.CW:
|
||||
return key
|
||||
|
||||
continue_cw = False
|
||||
|
||||
# capitalize alphabets
|
||||
if isinstance(key, KeyboardKey) and key.code in self._alphabets:
|
||||
keyboard.process_key(KC.LSFT, is_pressed)
|
||||
continue_cw = True
|
||||
elif (
|
||||
not isinstance(key, KeyboardKey)
|
||||
or isinstance(key, ModifierKey)
|
||||
or key.code in self._numbers
|
||||
or key in self.keys_ignored
|
||||
):
|
||||
continue_cw = True
|
||||
|
||||
# requests and cancels existing timeouts
|
||||
if is_pressed:
|
||||
if continue_cw:
|
||||
self.discard_timeout(keyboard)
|
||||
self.request_timeout(keyboard)
|
||||
else:
|
||||
self.process_timeout()
|
||||
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_timeout(self):
|
||||
self._cw_active = False
|
||||
self._timeout_key = False
|
||||
|
||||
def request_timeout(self, keyboard):
|
||||
if self._cw_active:
|
||||
if self.timeout:
|
||||
self._timeout_key = keyboard.set_timeout(
|
||||
self.timeout, lambda: self.process_timeout()
|
||||
)
|
||||
|
||||
def discard_timeout(self, keyboard):
|
||||
if self._timeout_key:
|
||||
if self.timeout:
|
||||
keyboard.cancel_timeout(self._timeout_key)
|
||||
self._timeout_key = False
|
||||
|
||||
def cw_pressed(self, key, keyboard, *args, **kwargs):
|
||||
# enables/disables capsword
|
||||
if key == KC.CW:
|
||||
if not self._cw_active:
|
||||
self._cw_active = True
|
||||
self.discard_timeout(keyboard)
|
||||
self.request_timeout(keyboard)
|
||||
else:
|
||||
self.discard_timeout(keyboard)
|
||||
self.process_timeout()
|
||||
70
hackpads/Kalpad/firmware/kmk/modules/cg_swap.py
Normal file
70
hackpads/Kalpad/firmware/kmk/modules/cg_swap.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from kmk.keys import KC, ModifierKey, make_key
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class CgSwap(Module):
|
||||
# default cg swap is disabled, can be eanbled too if needed
|
||||
def __init__(self, cg_swap_enable=False):
|
||||
self.cg_swap_enable = cg_swap_enable
|
||||
self._cg_mapping = {
|
||||
KC.LCTL: KC.LGUI,
|
||||
KC.RCTL: KC.RGUI,
|
||||
KC.LGUI: KC.LCTL,
|
||||
KC.RGUI: KC.RCTL,
|
||||
}
|
||||
make_key(
|
||||
names=('CG_SWAP',),
|
||||
)
|
||||
make_key(
|
||||
names=('CG_NORM',),
|
||||
)
|
||||
make_key(
|
||||
names=('CG_TOGG',),
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def matrix_detected_press(self, keyboard):
|
||||
return keyboard.matrix_update is None
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
if is_pressed:
|
||||
# enables or disables or toggles cg swap
|
||||
if key == KC.CG_SWAP:
|
||||
self.cg_swap_enable = True
|
||||
elif key == KC.CG_NORM:
|
||||
self.cg_swap_enable = False
|
||||
elif key == KC.CG_TOGG:
|
||||
if not self.cg_swap_enable:
|
||||
self.cg_swap_enable = True
|
||||
else:
|
||||
self.cg_swap_enable = False
|
||||
# performs cg swap
|
||||
if (
|
||||
self.cg_swap_enable
|
||||
and key not in (KC.CG_SWAP, KC.CG_NORM, KC.CG_TOGG)
|
||||
and isinstance(key, ModifierKey)
|
||||
and key in self._cg_mapping
|
||||
):
|
||||
key = self._cg_mapping.get(key)
|
||||
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
330
hackpads/Kalpad/firmware/kmk/modules/combos.py
Normal file
330
hackpads/Kalpad/firmware/kmk/modules/combos.py
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
try:
|
||||
from typing import Optional, Tuple, Union
|
||||
except ImportError:
|
||||
pass
|
||||
from micropython import const
|
||||
|
||||
from kmk.keys import Key, make_key
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class _ComboState:
|
||||
RESET = const(0)
|
||||
MATCHING = const(1)
|
||||
ACTIVE = const(2)
|
||||
IDLE = const(3)
|
||||
|
||||
|
||||
class Combo:
|
||||
fast_reset = False
|
||||
per_key_timeout = False
|
||||
timeout = 50
|
||||
_remaining = []
|
||||
_timeout = None
|
||||
_state = _ComboState.IDLE
|
||||
_match_coord = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
match: Tuple[Union[Key, int], ...],
|
||||
result: Key,
|
||||
fast_reset=None,
|
||||
per_key_timeout=None,
|
||||
timeout=None,
|
||||
match_coord=None,
|
||||
):
|
||||
'''
|
||||
match: tuple of keys (KC.A, KC.B)
|
||||
result: key KC.C
|
||||
'''
|
||||
self.match = match
|
||||
self.result = result
|
||||
if fast_reset is not None:
|
||||
self.fast_reset = fast_reset
|
||||
if per_key_timeout is not None:
|
||||
self.per_key_timeout = per_key_timeout
|
||||
if timeout is not None:
|
||||
self.timeout = timeout
|
||||
if match_coord is not None:
|
||||
self._match_coord = match_coord
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.__class__.__name__}({list(self.match)})'
|
||||
|
||||
def matches(self, key: Key, int_coord: int):
|
||||
raise NotImplementedError
|
||||
|
||||
def has_match(self, key: Key, int_coord: int):
|
||||
return self._match_coord and int_coord in self.match or key in self.match
|
||||
|
||||
def insert(self, key: Key, int_coord: int):
|
||||
if self._match_coord:
|
||||
self._remaining.insert(0, int_coord)
|
||||
else:
|
||||
self._remaining.insert(0, key)
|
||||
|
||||
def reset(self):
|
||||
self._remaining = list(self.match)
|
||||
|
||||
|
||||
class Chord(Combo):
|
||||
def matches(self, key: Key, int_coord: int):
|
||||
if not self._match_coord and key in self._remaining:
|
||||
self._remaining.remove(key)
|
||||
return True
|
||||
elif self._match_coord and int_coord in self._remaining:
|
||||
self._remaining.remove(int_coord)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Sequence(Combo):
|
||||
fast_reset = True
|
||||
per_key_timeout = True
|
||||
timeout = 1000
|
||||
|
||||
def matches(self, key: Key, int_coord: int):
|
||||
if (
|
||||
not self._match_coord and self._remaining and self._remaining[0] == key
|
||||
) or (
|
||||
self._match_coord and self._remaining and self._remaining[0] == int_coord
|
||||
):
|
||||
self._remaining.pop(0)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Combos(Module):
|
||||
def __init__(self, combos=[]):
|
||||
self.combos = combos
|
||||
self._key_buffer = []
|
||||
|
||||
make_key(names=('LEADER', 'LDR'))
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
self.reset(keyboard)
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key: Key, is_pressed, int_coord):
|
||||
if is_pressed:
|
||||
return self.on_press(keyboard, key, int_coord)
|
||||
else:
|
||||
return self.on_release(keyboard, key, int_coord)
|
||||
|
||||
def on_press(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
|
||||
# refill potential matches from timed-out matches
|
||||
if self.count_matching() == 0:
|
||||
for combo in self.combos:
|
||||
if combo._state == _ComboState.RESET:
|
||||
combo._state = _ComboState.MATCHING
|
||||
|
||||
# filter potential matches
|
||||
for combo in self.combos:
|
||||
if combo._state != _ComboState.MATCHING:
|
||||
continue
|
||||
if combo.matches(key, int_coord):
|
||||
continue
|
||||
combo._state = _ComboState.IDLE
|
||||
if combo._timeout:
|
||||
keyboard.cancel_timeout(combo._timeout)
|
||||
combo._timeout = keyboard.set_timeout(
|
||||
combo.timeout, lambda c=combo: self.reset_combo(keyboard, c)
|
||||
)
|
||||
|
||||
match_count = self.count_matching()
|
||||
|
||||
if match_count:
|
||||
# At least one combo matches current key: append key to buffer.
|
||||
self._key_buffer.append((int_coord, key, True))
|
||||
key = None
|
||||
|
||||
for first_match in self.combos:
|
||||
if first_match._state == _ComboState.MATCHING:
|
||||
break
|
||||
|
||||
# Single match left: don't wait on timeout to activate
|
||||
if match_count == 1 and not any(first_match._remaining):
|
||||
combo = first_match
|
||||
self.activate(keyboard, combo)
|
||||
if combo._timeout:
|
||||
keyboard.cancel_timeout(combo._timeout)
|
||||
combo._timeout = None
|
||||
self._key_buffer = []
|
||||
self.reset(keyboard)
|
||||
|
||||
# Start or reset individual combo timeouts.
|
||||
for combo in self.combos:
|
||||
if combo._state != _ComboState.MATCHING:
|
||||
continue
|
||||
if combo._timeout:
|
||||
if combo.per_key_timeout:
|
||||
keyboard.cancel_timeout(combo._timeout)
|
||||
else:
|
||||
continue
|
||||
combo._timeout = keyboard.set_timeout(
|
||||
combo.timeout, lambda c=combo: self.on_timeout(keyboard, c)
|
||||
)
|
||||
else:
|
||||
# There's no matching combo: send and reset key buffer
|
||||
if self._key_buffer:
|
||||
self._key_buffer.append((int_coord, key, True))
|
||||
self.send_key_buffer(keyboard)
|
||||
self._key_buffer = []
|
||||
key = None
|
||||
|
||||
return key
|
||||
|
||||
def on_release(self, keyboard: KMKKeyboard, key: Key, int_coord: Optional[int]):
|
||||
for combo in self.combos:
|
||||
if combo._state != _ComboState.ACTIVE:
|
||||
continue
|
||||
if combo.has_match(key, int_coord):
|
||||
# Deactivate combo if it matches current key.
|
||||
self.deactivate(keyboard, combo)
|
||||
|
||||
if combo.fast_reset:
|
||||
self.reset_combo(keyboard, combo)
|
||||
self._key_buffer = []
|
||||
else:
|
||||
combo.insert(key, int_coord)
|
||||
combo._state = _ComboState.MATCHING
|
||||
|
||||
key = None
|
||||
break
|
||||
|
||||
else:
|
||||
# Non-active but matching combos can either activate on key release
|
||||
# if they're the only match, or "un-match" the released key but stay
|
||||
# matching if they're a repeatable combo.
|
||||
for combo in self.combos:
|
||||
if combo._state != _ComboState.MATCHING:
|
||||
continue
|
||||
if not combo.has_match(key, int_coord):
|
||||
continue
|
||||
|
||||
# Combo matches, but first key released before timeout.
|
||||
elif not any(combo._remaining) and self.count_matching() == 1:
|
||||
keyboard.cancel_timeout(combo._timeout)
|
||||
self.activate(keyboard, combo)
|
||||
self._key_buffer = []
|
||||
keyboard._send_hid()
|
||||
self.deactivate(keyboard, combo)
|
||||
if combo.fast_reset:
|
||||
self.reset_combo(keyboard, combo)
|
||||
else:
|
||||
combo.insert(key, int_coord)
|
||||
combo._state = _ComboState.MATCHING
|
||||
self.reset(keyboard)
|
||||
|
||||
elif not any(combo._remaining):
|
||||
continue
|
||||
|
||||
# Skip combos that allow tapping.
|
||||
elif combo.fast_reset:
|
||||
continue
|
||||
|
||||
# This was the last key released of a repeatable combo.
|
||||
elif len(combo._remaining) == len(combo.match) - 1:
|
||||
self.reset_combo(keyboard, combo)
|
||||
if not self.count_matching():
|
||||
self._key_buffer.append((int_coord, key, False))
|
||||
self.send_key_buffer(keyboard)
|
||||
self._key_buffer = []
|
||||
key = None
|
||||
|
||||
# Anything between first and last key released.
|
||||
else:
|
||||
combo.insert(key, int_coord)
|
||||
|
||||
# Don't propagate key-release events for keys that have been
|
||||
# buffered. Append release events only if corresponding press is in
|
||||
# buffer.
|
||||
pressed = self._key_buffer.count((int_coord, key, True))
|
||||
released = self._key_buffer.count((int_coord, key, False))
|
||||
if (pressed - released) > 0:
|
||||
self._key_buffer.append((int_coord, key, False))
|
||||
key = None
|
||||
|
||||
# Reset on non-combo key up
|
||||
if not self.count_matching():
|
||||
self.reset(keyboard)
|
||||
|
||||
return key
|
||||
|
||||
def on_timeout(self, keyboard, combo):
|
||||
# If combo reaches timeout and has no remaining keys, activate it;
|
||||
# else, drop it from the match list.
|
||||
combo._timeout = None
|
||||
|
||||
if not any(combo._remaining):
|
||||
self.activate(keyboard, combo)
|
||||
# check if the last buffered key event was a 'release'
|
||||
if not self._key_buffer[-1][2]:
|
||||
keyboard._send_hid()
|
||||
self.deactivate(keyboard, combo)
|
||||
self._key_buffer = []
|
||||
self.reset(keyboard)
|
||||
else:
|
||||
if self.count_matching() == 1:
|
||||
# This was the last pending combo: flush key buffer.
|
||||
self.send_key_buffer(keyboard)
|
||||
self._key_buffer = []
|
||||
self.reset_combo(keyboard, combo)
|
||||
|
||||
def send_key_buffer(self, keyboard):
|
||||
for int_coord, key, is_pressed in self._key_buffer:
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord)
|
||||
|
||||
def activate(self, keyboard, combo):
|
||||
if debug.enabled:
|
||||
debug('activate', combo)
|
||||
combo.result.on_press(keyboard)
|
||||
combo._state = _ComboState.ACTIVE
|
||||
|
||||
def deactivate(self, keyboard, combo):
|
||||
if debug.enabled:
|
||||
debug('deactivate', combo)
|
||||
combo.result.on_release(keyboard)
|
||||
combo._state = _ComboState.IDLE
|
||||
|
||||
def reset_combo(self, keyboard, combo):
|
||||
combo.reset()
|
||||
if combo._timeout is not None:
|
||||
keyboard.cancel_timeout(combo._timeout)
|
||||
combo._timeout = None
|
||||
combo._state = _ComboState.RESET
|
||||
|
||||
def reset(self, keyboard):
|
||||
for combo in self.combos:
|
||||
if combo._state != _ComboState.ACTIVE:
|
||||
self.reset_combo(keyboard, combo)
|
||||
|
||||
def count_matching(self):
|
||||
match_count = 0
|
||||
for combo in self.combos:
|
||||
if combo._state == _ComboState.MATCHING:
|
||||
match_count += 1
|
||||
return match_count
|
||||
261
hackpads/Kalpad/firmware/kmk/modules/dynamic_sequences.py
Normal file
261
hackpads/Kalpad/firmware/kmk/modules/dynamic_sequences.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
from micropython import const
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from kmk.keys import KC, Key, make_argumented_key
|
||||
from kmk.kmktime import check_deadline, ticks_diff
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class DynamicSequenceKey(Key):
|
||||
def __init__(self, sequence_select=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.sequence_select = sequence_select
|
||||
|
||||
|
||||
class SequenceStatus:
|
||||
STOPPED = const(0)
|
||||
RECORDING = const(1)
|
||||
PLAYING = const(2)
|
||||
SET_REPEPITIONS = const(3)
|
||||
SET_INTERVAL = const(4)
|
||||
|
||||
|
||||
# Keycodes for number keys
|
||||
_numbers = range(KC.N1.code, KC.N0.code + 1)
|
||||
|
||||
SequenceFrame = namedtuple('SequenceFrame', ['keys_pressed', 'timestamp'])
|
||||
|
||||
|
||||
class Sequence:
|
||||
def __init__(self):
|
||||
self.repetitions = 1
|
||||
self.interval = 0
|
||||
self.sequence_data = [SequenceFrame(set(), 0) for i in range(3)]
|
||||
|
||||
|
||||
class DynamicSequences(Module):
|
||||
def __init__(
|
||||
self, slots=1, timeout=60000, key_interval=0, use_recorded_speed=False
|
||||
):
|
||||
self.sequences = [Sequence() for i in range(slots)]
|
||||
self.current_slot = self.sequences[0]
|
||||
self.status = SequenceStatus.STOPPED
|
||||
|
||||
self.index = 0
|
||||
self.start_time = 0
|
||||
self.current_repetition = 0
|
||||
self.last_config_frame = set()
|
||||
|
||||
self.timeout = timeout
|
||||
self.key_interval = key_interval
|
||||
self.use_recorded_speed = use_recorded_speed
|
||||
|
||||
# Create keycodes
|
||||
make_argumented_key(
|
||||
names=('RECORD_SEQUENCE',),
|
||||
constructor=DynamicSequenceKey,
|
||||
on_press=self._record_sequence,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('PLAY_SEQUENCE',),
|
||||
constructor=DynamicSequenceKey,
|
||||
on_press=self._play_sequence,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('SET_SEQUENCE', 'STOP_SEQUENCE'),
|
||||
constructor=DynamicSequenceKey,
|
||||
on_press=self._stop_sequence,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('SET_SEQUENCE_REPETITIONS',),
|
||||
constructor=DynamicSequenceKey,
|
||||
on_press=self._set_sequence_repetitions,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('SET_SEQUENCE_INTERVAL',),
|
||||
constructor=DynamicSequenceKey,
|
||||
on_press=self._set_sequence_interval,
|
||||
)
|
||||
|
||||
def _record_sequence(self, key, keyboard, *args, **kwargs):
|
||||
self._stop_sequence(key, keyboard)
|
||||
self.status = SequenceStatus.RECORDING
|
||||
self.start_time = ticks_ms()
|
||||
self.current_slot.sequence_data = [SequenceFrame(set(), 0)]
|
||||
self.index = 0
|
||||
|
||||
def _play_sequence(self, key, keyboard, *args, **kwargs):
|
||||
self._stop_sequence(key, keyboard)
|
||||
self.status = SequenceStatus.PLAYING
|
||||
self.start_time = ticks_ms()
|
||||
self.index = 0
|
||||
self.current_repetition = 0
|
||||
|
||||
def _stop_sequence(self, key, keyboard, *args, **kwargs):
|
||||
if self.status == SequenceStatus.RECORDING:
|
||||
self.stop_recording()
|
||||
elif self.status == SequenceStatus.SET_INTERVAL:
|
||||
self.stop_config()
|
||||
self.status = SequenceStatus.STOPPED
|
||||
|
||||
# Change sequences here because stop is always called
|
||||
if key.sequence_select is not None:
|
||||
self.current_slot = self.sequences[key.sequence_select]
|
||||
|
||||
# Configure repeat settings
|
||||
def _set_sequence_repetitions(self, key, keyboard, *args, **kwargs):
|
||||
self._stop_sequence(key, keyboard)
|
||||
self.status = SequenceStatus.SET_REPEPITIONS
|
||||
self.last_config_frame = set()
|
||||
self.current_slot.repetitions = 0
|
||||
self.start_time = ticks_ms()
|
||||
|
||||
def _set_sequence_interval(self, key, keyboard, *args, **kwargs):
|
||||
self._stop_sequence(key, keyboard)
|
||||
self.status = SequenceStatus.SET_INTERVAL
|
||||
self.last_config_frame = set()
|
||||
self.current_slot.interval = 0
|
||||
self.start_time = ticks_ms()
|
||||
|
||||
# Add the current keypress state to the sequence
|
||||
def record_frame(self, keys_pressed):
|
||||
if self.current_slot.sequence_data[self.index].keys_pressed != keys_pressed:
|
||||
self.index += 1
|
||||
|
||||
# Recorded speed
|
||||
if self.use_recorded_speed:
|
||||
self.current_slot.sequence_data.append(
|
||||
SequenceFrame(
|
||||
keys_pressed.copy(), ticks_diff(ticks_ms(), self.start_time)
|
||||
)
|
||||
)
|
||||
|
||||
# Constant speed
|
||||
else:
|
||||
self.current_slot.sequence_data.append(
|
||||
SequenceFrame(keys_pressed.copy(), self.index * self.key_interval)
|
||||
)
|
||||
|
||||
if not check_deadline(ticks_ms(), self.start_time, self.timeout):
|
||||
self.stop_recording()
|
||||
|
||||
# Add the ending frames to the sequence
|
||||
def stop_recording(self):
|
||||
# Clear the remaining keys
|
||||
self.current_slot.sequence_data.append(
|
||||
SequenceFrame(set(), self.current_slot.sequence_data[-1].timestamp + 20)
|
||||
)
|
||||
|
||||
# Wait for the specified interval
|
||||
prev_timestamp = self.current_slot.sequence_data[-1].timestamp
|
||||
self.current_slot.sequence_data.append(
|
||||
SequenceFrame(
|
||||
set(),
|
||||
prev_timestamp + self.current_slot.interval * 1000,
|
||||
)
|
||||
)
|
||||
|
||||
self.status = SequenceStatus.STOPPED
|
||||
|
||||
def play_frame(self, keyboard):
|
||||
# Send the keypresses at this point in the sequence
|
||||
if not check_deadline(
|
||||
ticks_ms(),
|
||||
self.start_time,
|
||||
self.current_slot.sequence_data[self.index].timestamp,
|
||||
):
|
||||
if self.index:
|
||||
prev = self.current_slot.sequence_data[self.index - 1].keys_pressed
|
||||
cur = self.current_slot.sequence_data[self.index].keys_pressed
|
||||
|
||||
for key in prev.difference(cur):
|
||||
keyboard.remove_key(key)
|
||||
for key in cur.difference(prev):
|
||||
keyboard.add_key(key)
|
||||
|
||||
self.index += 1
|
||||
if self.index >= len(self.current_slot.sequence_data): # Reached the end
|
||||
self.current_repetition += 1
|
||||
if self.current_repetition == self.current_slot.repetitions:
|
||||
self.status = SequenceStatus.STOPPED
|
||||
else:
|
||||
self.index = 0
|
||||
self.start_time = ticks_ms()
|
||||
|
||||
# Configuration for repeating sequences
|
||||
def config_mode(self, keyboard):
|
||||
for key in keyboard.keys_pressed.difference(self.last_config_frame):
|
||||
if key.code in _numbers:
|
||||
digit = (key.code - KC.N1.code + 1) % 10
|
||||
if self.status == SequenceStatus.SET_REPEPITIONS:
|
||||
self.current_slot.repetitions = (
|
||||
self.current_slot.repetitions * 10 + digit
|
||||
)
|
||||
elif self.status == SequenceStatus.SET_INTERVAL:
|
||||
self.current_slot.interval = self.current_slot.interval * 10 + digit
|
||||
|
||||
elif key.code == KC.ENTER.code:
|
||||
self.stop_config()
|
||||
|
||||
self.last_config_frame = keyboard.keys_pressed.copy()
|
||||
keyboard.hid_pending = False # Disable typing
|
||||
|
||||
if not check_deadline(ticks_ms(), self.start_time, self.timeout):
|
||||
self.stop_config()
|
||||
|
||||
# Finish configuring repetitions
|
||||
def stop_config(self):
|
||||
self.current_slot.sequence_data[-1] = SequenceFrame(
|
||||
self.current_slot.sequence_data[-1].keys_pressed,
|
||||
self.current_slot.sequence_data[-2].timestamp
|
||||
+ self.current_slot.interval * 1000,
|
||||
)
|
||||
self.current_slot.repetitions = max(self.current_slot.repetitions, 1)
|
||||
self.status = SequenceStatus.STOPPED
|
||||
|
||||
def on_runtime_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
|
||||
if not self.status:
|
||||
return
|
||||
|
||||
elif self.status == SequenceStatus.RECORDING:
|
||||
self.record_frame(keyboard.keys_pressed)
|
||||
|
||||
elif self.status == SequenceStatus.PLAYING:
|
||||
self.play_frame(keyboard)
|
||||
|
||||
elif (
|
||||
self.status == SequenceStatus.SET_REPEPITIONS
|
||||
or self.status == SequenceStatus.SET_INTERVAL
|
||||
):
|
||||
self.config_mode(keyboard)
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
130
hackpads/Kalpad/firmware/kmk/modules/easypoint.py
Normal file
130
hackpads/Kalpad/firmware/kmk/modules/easypoint.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
'''
|
||||
Extension handles usage of AS5013 by AMS
|
||||
'''
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from kmk.keys import AX
|
||||
from kmk.modules import Module
|
||||
|
||||
I2C_ADDRESS = 0x40
|
||||
I2X_ALT_ADDRESS = 0x41
|
||||
|
||||
X = 0x10
|
||||
Y_RES_INT = 0x11
|
||||
|
||||
XP = 0x12
|
||||
XN = 0x13
|
||||
YP = 0x14
|
||||
YN = 0x15
|
||||
|
||||
M_CTRL = 0x2B
|
||||
T_CTRL = 0x2D
|
||||
|
||||
Y_OFFSET = 17
|
||||
X_OFFSET = 7
|
||||
|
||||
DEAD_X = 5
|
||||
DEAD_Y = 5
|
||||
|
||||
|
||||
class Easypoint(Module):
|
||||
'''Module handles usage of AS5013 by AMS'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
i2c,
|
||||
address=I2C_ADDRESS,
|
||||
y_offset=Y_OFFSET,
|
||||
x_offset=X_OFFSET,
|
||||
dead_x=DEAD_X,
|
||||
dead_y=DEAD_Y,
|
||||
):
|
||||
self._i2c_address = address
|
||||
self._i2c_bus = i2c
|
||||
|
||||
# HID parameters
|
||||
self.polling_interval = 20
|
||||
self.last_tick = ticks_ms()
|
||||
|
||||
# Offsets for poor soldering
|
||||
self.y_offset = y_offset
|
||||
self.x_offset = x_offset
|
||||
|
||||
# Deadzone
|
||||
self.dead_x = DEAD_X
|
||||
self.dead_y = DEAD_Y
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be injected as an extra matrix update
|
||||
'''
|
||||
now = ticks_ms()
|
||||
if now - self.last_tick < self.polling_interval:
|
||||
return
|
||||
self.last_tick = now
|
||||
|
||||
x, y = self._read_raw_state()
|
||||
|
||||
# I'm a shit coder, so offset is handled in software side
|
||||
s_x = self.getSignedNumber(x, 8) - self.x_offset
|
||||
s_y = self.getSignedNumber(y, 8) - self.y_offset
|
||||
|
||||
# Evaluate Deadzone
|
||||
if s_x in range(-self.dead_x, self.dead_x) and s_y in range(
|
||||
-self.dead_y, self.dead_y
|
||||
):
|
||||
# Within bounds, just die
|
||||
return
|
||||
else:
|
||||
# Set the X/Y from easypoint
|
||||
AX.X.move(keyboard, x)
|
||||
AX.Y.move(keyboard, y)
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def _read_raw_state(self):
|
||||
'''Read data from AS5013'''
|
||||
x, y = self._i2c_rdwr([X], length=2)
|
||||
return x, y
|
||||
|
||||
def getSignedNumber(self, number, bitLength=8):
|
||||
mask = (2**bitLength) - 1
|
||||
if number & (1 << (bitLength - 1)):
|
||||
return number | ~mask
|
||||
else:
|
||||
return number & mask
|
||||
|
||||
def _i2c_rdwr(self, data, length=1):
|
||||
'''Write and optionally read I2C data.'''
|
||||
while not self._i2c_bus.try_lock():
|
||||
pass
|
||||
|
||||
try:
|
||||
if length > 0:
|
||||
result = bytearray(length)
|
||||
self._i2c_bus.writeto_then_readfrom(
|
||||
self._i2c_address, bytes(data), result
|
||||
)
|
||||
return result
|
||||
else:
|
||||
self._i2c_bus.writeto(self._i2c_address, bytes(data))
|
||||
return []
|
||||
finally:
|
||||
self._i2c_bus.unlock()
|
||||
334
hackpads/Kalpad/firmware/kmk/modules/encoder.py
Normal file
334
hackpads/Kalpad/firmware/kmk/modules/encoder.py
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
# See docs/encoder.md for how to use
|
||||
|
||||
import busio
|
||||
import digitalio
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
# NB : not using rotaryio as it requires the pins to be consecutive
|
||||
|
||||
|
||||
class BaseEncoder:
|
||||
|
||||
VELOCITY_MODE = True
|
||||
|
||||
def __init__(self, is_inverted=False, divisor=4):
|
||||
|
||||
self.is_inverted = is_inverted
|
||||
self.divisor = divisor
|
||||
|
||||
self._state = None
|
||||
self._start_state = None
|
||||
self._direction = None
|
||||
self._pos = 0
|
||||
self._button_state = True
|
||||
self._button_held = None
|
||||
self._velocity = 0
|
||||
|
||||
self._movement = 0
|
||||
self._timestamp = ticks_ms()
|
||||
|
||||
# callback functions on events. Need to be defined externally
|
||||
self.on_move_do = None
|
||||
self.on_button_do = None
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
'direction': self.is_inverted and -self._direction or self._direction,
|
||||
'position': self.is_inverted and -self._pos or self._pos,
|
||||
'is_pressed': not self._button_state,
|
||||
'velocity': self._velocity,
|
||||
}
|
||||
|
||||
# Called in a loop to refresh encoder state
|
||||
|
||||
def update_state(self):
|
||||
# Rotation events
|
||||
new_state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
|
||||
if new_state != self._state:
|
||||
# encoder moved
|
||||
self._movement += 1
|
||||
# false / false and true / true are common half steps
|
||||
# looking on the step just before helps determining
|
||||
# the direction
|
||||
if new_state[0] == new_state[1] and self._state[0] != self._state[1]:
|
||||
if new_state[1] == self._state[0]:
|
||||
self._direction = 1
|
||||
else:
|
||||
self._direction = -1
|
||||
|
||||
# when the encoder settles on a position (every 2 steps)
|
||||
if new_state[0] == new_state[1]:
|
||||
# an encoder returned to the previous
|
||||
# position halfway, cancel rotation
|
||||
if (
|
||||
self._start_state[0] == new_state[0]
|
||||
and self._start_state[1] == new_state[1]
|
||||
and self._movement <= 2
|
||||
):
|
||||
self._movement = 0
|
||||
self._direction = 0
|
||||
|
||||
# when the encoder made a full loop according to its divisor
|
||||
elif self._movement >= self.divisor - 1:
|
||||
# 1 full step is 4 movements (2 for high-resolution encoder),
|
||||
# however, when rotated quickly, some steps may be missed.
|
||||
# This makes it behave more naturally
|
||||
real_movement = self._movement // self.divisor
|
||||
self._pos += self._direction * real_movement
|
||||
if self.on_move_do is not None:
|
||||
for i in range(real_movement):
|
||||
self.on_move_do(self.get_state())
|
||||
|
||||
# Rotation finished, reset to identify new movement
|
||||
self._movement = 0
|
||||
self._direction = 0
|
||||
self._start_state = new_state
|
||||
|
||||
self._state = new_state
|
||||
|
||||
# Velocity
|
||||
self.velocity_event()
|
||||
|
||||
# Button event
|
||||
self.button_event()
|
||||
|
||||
def velocity_event(self):
|
||||
if self.VELOCITY_MODE:
|
||||
new_timestamp = ticks_ms()
|
||||
self._velocity = new_timestamp - self._timestamp
|
||||
self._timestamp = new_timestamp
|
||||
|
||||
def button_event(self):
|
||||
raise NotImplementedError('subclasses must override button_event()!')
|
||||
|
||||
# return knob velocity as milliseconds between position changes (detents)
|
||||
# for backwards compatibility
|
||||
def vel_report(self):
|
||||
return self._velocity
|
||||
|
||||
|
||||
class GPIOEncoder(BaseEncoder):
|
||||
def __init__(
|
||||
self,
|
||||
pin_a,
|
||||
pin_b,
|
||||
pin_button=None,
|
||||
is_inverted=False,
|
||||
divisor=None,
|
||||
button_pull=digitalio.Pull.UP,
|
||||
):
|
||||
super().__init__(is_inverted)
|
||||
|
||||
# Divisor can be 4 or 2 depending on whether the detent
|
||||
# on the encoder is defined by 2 or 4 pulses
|
||||
self.divisor = divisor
|
||||
|
||||
self.pin_a = EncoderPin(pin_a)
|
||||
self.pin_b = EncoderPin(pin_b)
|
||||
if pin_button:
|
||||
self.pin_button = EncoderPin(pin_button, button_type=True, pull=button_pull)
|
||||
else:
|
||||
self.pin_button = None
|
||||
|
||||
self._state = (self.pin_a.get_value(), self.pin_b.get_value())
|
||||
self._start_state = self._state
|
||||
|
||||
def button_event(self):
|
||||
if self.pin_button:
|
||||
new_button_state = self.pin_button.get_value()
|
||||
if new_button_state != self._button_state:
|
||||
self._button_state = new_button_state
|
||||
if self.on_button_do is not None:
|
||||
self.on_button_do(self.get_state())
|
||||
|
||||
|
||||
class EncoderPin:
|
||||
def __init__(self, pin, button_type=False, pull=digitalio.Pull.UP):
|
||||
self.pin = pin
|
||||
self.button_type = button_type
|
||||
self.pull = pull
|
||||
self.prepare_pin()
|
||||
|
||||
def prepare_pin(self):
|
||||
if self.pin is not None:
|
||||
if isinstance(self.pin, digitalio.DigitalInOut):
|
||||
self.io = self.pin
|
||||
else:
|
||||
self.io = digitalio.DigitalInOut(self.pin)
|
||||
self.io.direction = digitalio.Direction.INPUT
|
||||
self.io.pull = self.pull
|
||||
else:
|
||||
self.io = None
|
||||
|
||||
def get_value(self):
|
||||
io = self.io
|
||||
result = io.value
|
||||
if digitalio.Pull.UP != io.pull:
|
||||
result = not result
|
||||
return result
|
||||
|
||||
|
||||
class I2CEncoder(BaseEncoder):
|
||||
def __init__(self, i2c, address, is_inverted=False):
|
||||
|
||||
try:
|
||||
from adafruit_seesaw import digitalio, neopixel, rotaryio, seesaw
|
||||
except ImportError:
|
||||
if debug.enabled:
|
||||
debug('seesaw missing')
|
||||
return
|
||||
|
||||
super().__init__(is_inverted)
|
||||
|
||||
self.seesaw = seesaw.Seesaw(i2c, address)
|
||||
|
||||
# Check for correct product
|
||||
|
||||
seesaw_product = (self.seesaw.get_version() >> 16) & 0xFFFF
|
||||
if seesaw_product != 4991:
|
||||
if debug.enabled:
|
||||
debug('Wrong firmware loaded? Expected 4991')
|
||||
|
||||
self.encoder = rotaryio.IncrementalEncoder(self.seesaw)
|
||||
self.seesaw.pin_mode(24, self.seesaw.INPUT_PULLUP)
|
||||
self.switch = digitalio.DigitalIO(self.seesaw, 24)
|
||||
self.pixel = neopixel.NeoPixel(self.seesaw, 6, 1)
|
||||
|
||||
self._state = self.encoder.position
|
||||
|
||||
def update_state(self):
|
||||
|
||||
# Rotation events
|
||||
new_state = self.encoder.position
|
||||
if new_state != self._state:
|
||||
# it moves !
|
||||
self._movement += 1
|
||||
# false / false and true / true are common half steps
|
||||
# looking on the step just before helps determining
|
||||
# the direction
|
||||
if self.encoder.position > self._state:
|
||||
self._direction = 1
|
||||
else:
|
||||
self._direction = -1
|
||||
self._state = new_state
|
||||
self.on_move_do(self.get_state())
|
||||
|
||||
# Velocity
|
||||
self.velocity_event()
|
||||
|
||||
# Button events
|
||||
self.button_event()
|
||||
|
||||
def button_event(self):
|
||||
if not self.switch.value and not self._button_held:
|
||||
# Pressed
|
||||
self._button_held = True
|
||||
if self.on_button_do is not None:
|
||||
self.on_button_do(self.get_state())
|
||||
|
||||
if self.switch.value and self._button_held:
|
||||
# Released
|
||||
self._button_held = False
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
'direction': self.is_inverted and -self._direction or self._direction,
|
||||
'position': self._state,
|
||||
'is_pressed': not self.switch.value,
|
||||
'is_held': self._button_held,
|
||||
'velocity': self._velocity,
|
||||
}
|
||||
|
||||
|
||||
class EncoderHandler(Module):
|
||||
def __init__(self):
|
||||
self.encoders = []
|
||||
self.pins = None
|
||||
self.map = None
|
||||
self.divisor = 4
|
||||
|
||||
def on_runtime_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
if self.pins and self.map:
|
||||
for idx, pins in enumerate(self.pins):
|
||||
try:
|
||||
# Check for busio.I2C
|
||||
if isinstance(pins[0], busio.I2C):
|
||||
new_encoder = I2CEncoder(*pins)
|
||||
|
||||
# Else fall back to GPIO
|
||||
else:
|
||||
new_encoder = GPIOEncoder(*pins)
|
||||
# Set default divisor if unset
|
||||
if new_encoder.divisor is None:
|
||||
new_encoder.divisor = self.divisor
|
||||
|
||||
# In our case, we need to define keybord and encoder_id for callbacks
|
||||
new_encoder.on_move_do = lambda x, bound_idx=idx: self.on_move_do(
|
||||
keyboard, bound_idx, x
|
||||
)
|
||||
new_encoder.on_button_do = (
|
||||
lambda x, bound_idx=idx: self.on_button_do(
|
||||
keyboard, bound_idx, x
|
||||
)
|
||||
)
|
||||
self.encoders.append(new_encoder)
|
||||
except Exception as e:
|
||||
if debug.enabled:
|
||||
debug(e)
|
||||
return
|
||||
|
||||
def on_move_do(self, keyboard, encoder_id, state):
|
||||
if self.map:
|
||||
layer_id = keyboard.active_layers[0]
|
||||
# if Left, key index 0 else key index 1
|
||||
if state['direction'] == -1:
|
||||
key_index = 0
|
||||
else:
|
||||
key_index = 1
|
||||
key = self.map[layer_id][encoder_id][key_index]
|
||||
keyboard.tap_key(key)
|
||||
|
||||
def on_button_do(self, keyboard, encoder_id, state):
|
||||
if state['is_pressed'] is True:
|
||||
layer_id = keyboard.active_layers[0]
|
||||
key = self.map[layer_id][encoder_id][2]
|
||||
keyboard.tap_key(key)
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be injected as an extra matrix update
|
||||
'''
|
||||
for encoder in self.encoders:
|
||||
encoder.update_state()
|
||||
|
||||
return keyboard
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be replace matrix update if supplied
|
||||
'''
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
272
hackpads/Kalpad/firmware/kmk/modules/holdtap.py
Normal file
272
hackpads/Kalpad/firmware/kmk/modules/holdtap.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
from micropython import const
|
||||
|
||||
from kmk.keys import Key, make_argumented_key
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class ActivationType:
|
||||
PRESSED = const(0)
|
||||
RELEASED = const(1)
|
||||
HOLD_TIMEOUT = const(2)
|
||||
INTERRUPTED = const(3)
|
||||
REPEAT = const(4)
|
||||
|
||||
|
||||
class HoldTapRepeat:
|
||||
NONE = const(0)
|
||||
TAP = const(1)
|
||||
HOLD = const(2)
|
||||
ALL = const(3)
|
||||
|
||||
|
||||
class HoldTapKeyState:
|
||||
def __init__(self, timeout_key, *args, **kwargs):
|
||||
self.timeout_key = timeout_key
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.activated = ActivationType.PRESSED
|
||||
|
||||
|
||||
class HoldTapKey(Key):
|
||||
def __init__(
|
||||
self,
|
||||
tap,
|
||||
hold,
|
||||
prefer_hold=True,
|
||||
tap_interrupted=False,
|
||||
tap_time=None,
|
||||
repeat=HoldTapRepeat.NONE,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.tap = tap
|
||||
self.hold = hold
|
||||
self.prefer_hold = prefer_hold
|
||||
self.tap_interrupted = tap_interrupted
|
||||
self.tap_time = tap_time
|
||||
self.repeat = repeat
|
||||
|
||||
|
||||
class HoldTap(Module):
|
||||
tap_time = 300
|
||||
|
||||
def __init__(self, _make_key=True):
|
||||
self.key_buffer = []
|
||||
self.key_states = {}
|
||||
|
||||
if _make_key:
|
||||
make_argumented_key(
|
||||
names=('HT',),
|
||||
constructor=HoldTapKey,
|
||||
on_press=self.ht_pressed,
|
||||
on_release=self.ht_released,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
'''Handle holdtap being interrupted by another key press/release.'''
|
||||
current_key = key
|
||||
send_buffer = False
|
||||
append_buffer = False
|
||||
|
||||
for key, state in self.key_states.items():
|
||||
if key == current_key:
|
||||
continue
|
||||
if state.activated != ActivationType.PRESSED:
|
||||
continue
|
||||
|
||||
# holdtap isn't interruptable, resolves on ht_release or timeout.
|
||||
if not key.tap_interrupted and not key.prefer_hold:
|
||||
append_buffer = is_pressed or self.key_buffer
|
||||
continue
|
||||
|
||||
# holdtap is interrupted by another key event.
|
||||
if (is_pressed and not key.tap_interrupted) or (
|
||||
not is_pressed and key.tap_interrupted and self.key_buffer
|
||||
):
|
||||
|
||||
keyboard.cancel_timeout(state.timeout_key)
|
||||
self.key_states[key].activated = ActivationType.INTERRUPTED
|
||||
self.ht_activate_on_interrupt(
|
||||
key,
|
||||
keyboard,
|
||||
*state.args,
|
||||
**state.kwargs,
|
||||
)
|
||||
append_buffer = True
|
||||
send_buffer = True
|
||||
|
||||
# if interrupt on release: store interrupting keys until one of them
|
||||
# is released.
|
||||
if key.tap_interrupted and is_pressed:
|
||||
append_buffer = True
|
||||
|
||||
# apply changes with 'side-effects' on key_states or the loop behaviour
|
||||
# outside the loop.
|
||||
if append_buffer:
|
||||
self.key_buffer.append((int_coord, current_key, is_pressed))
|
||||
current_key = None
|
||||
|
||||
if send_buffer:
|
||||
self.send_key_buffer(keyboard)
|
||||
|
||||
return current_key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def ht_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''Unless in repeat mode, do nothing yet, action resolves when key is released, timer expires or other key is pressed.'''
|
||||
if key in self.key_states:
|
||||
state = self.key_states[key]
|
||||
keyboard.cancel_timeout(self.key_states[key].timeout_key)
|
||||
|
||||
if state.activated == ActivationType.RELEASED:
|
||||
state.activated = ActivationType.REPEAT
|
||||
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
||||
elif state.activated == ActivationType.HOLD_TIMEOUT:
|
||||
self.ht_activate_hold(key, keyboard, *args, **kwargs)
|
||||
elif state.activated == ActivationType.INTERRUPTED:
|
||||
self.ht_activate_on_interrupt(key, keyboard, *args, **kwargs)
|
||||
return
|
||||
|
||||
if key.tap_time is None:
|
||||
tap_time = self.tap_time
|
||||
else:
|
||||
tap_time = key.tap_time
|
||||
timeout_key = keyboard.set_timeout(
|
||||
tap_time,
|
||||
lambda: self.on_tap_time_expired(key, keyboard, *args, **kwargs),
|
||||
)
|
||||
self.key_states[key] = HoldTapKeyState(timeout_key, *args, **kwargs)
|
||||
return keyboard
|
||||
|
||||
def ht_released(self, key, keyboard, *args, **kwargs):
|
||||
'''On keyup, release mod or tap key.'''
|
||||
if key not in self.key_states:
|
||||
return keyboard
|
||||
|
||||
state = self.key_states[key]
|
||||
keyboard.cancel_timeout(state.timeout_key)
|
||||
repeat = key.repeat & HoldTapRepeat.TAP
|
||||
|
||||
if state.activated == ActivationType.HOLD_TIMEOUT:
|
||||
# release hold
|
||||
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
|
||||
repeat = key.repeat & HoldTapRepeat.HOLD
|
||||
elif state.activated == ActivationType.INTERRUPTED:
|
||||
# release tap
|
||||
self.ht_deactivate_on_interrupt(key, keyboard, *args, **kwargs)
|
||||
if key.prefer_hold:
|
||||
repeat = key.repeat & HoldTapRepeat.HOLD
|
||||
elif state.activated == ActivationType.PRESSED:
|
||||
# press and release tap because key released within tap time
|
||||
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
||||
self.send_key_buffer(keyboard)
|
||||
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
||||
state.activated = ActivationType.RELEASED
|
||||
self.send_key_buffer(keyboard)
|
||||
elif state.activated == ActivationType.REPEAT:
|
||||
state.activated = ActivationType.RELEASED
|
||||
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
||||
|
||||
# don't delete the key state right now in this case
|
||||
if repeat:
|
||||
if key.tap_time is None:
|
||||
tap_time = self.tap_time
|
||||
else:
|
||||
tap_time = key.tap_time
|
||||
state.timeout_key = keyboard.set_timeout(
|
||||
tap_time, lambda: self.key_states.pop(key)
|
||||
)
|
||||
else:
|
||||
del self.key_states[key]
|
||||
|
||||
return keyboard
|
||||
|
||||
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
|
||||
'''When tap time expires activate hold if key is still being pressed.
|
||||
Remove key if ActivationType is RELEASED.'''
|
||||
try:
|
||||
state = self.key_states[key]
|
||||
except KeyError:
|
||||
if debug.enabled:
|
||||
debug(f'on_tap_time_expired: no such key {key}')
|
||||
return
|
||||
|
||||
if self.key_states[key].activated == ActivationType.PRESSED:
|
||||
# press hold because timer expired after tap time
|
||||
self.key_states[key].activated = ActivationType.HOLD_TIMEOUT
|
||||
self.ht_activate_hold(key, keyboard, *args, **kwargs)
|
||||
self.send_key_buffer(keyboard)
|
||||
elif state.activated == ActivationType.RELEASED:
|
||||
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
||||
del self.key_states[key]
|
||||
|
||||
def send_key_buffer(self, keyboard):
|
||||
if not self.key_buffer:
|
||||
return
|
||||
|
||||
reprocess = False
|
||||
for int_coord, key, is_pressed in self.key_buffer:
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord, reprocess)
|
||||
if isinstance(key, HoldTapKey):
|
||||
reprocess = True
|
||||
|
||||
self.key_buffer.clear()
|
||||
|
||||
def ht_activate_hold(self, key, keyboard, *args, **kwargs):
|
||||
if debug.enabled:
|
||||
debug('ht_activate_hold')
|
||||
keyboard.resume_process_key(self, key.hold, True)
|
||||
|
||||
def ht_deactivate_hold(self, key, keyboard, *args, **kwargs):
|
||||
if debug.enabled:
|
||||
debug('ht_deactivate_hold')
|
||||
keyboard.resume_process_key(self, key.hold, False)
|
||||
|
||||
def ht_activate_tap(self, key, keyboard, *args, **kwargs):
|
||||
if debug.enabled:
|
||||
debug('ht_activate_tap')
|
||||
keyboard.resume_process_key(self, key.tap, True)
|
||||
|
||||
def ht_deactivate_tap(self, key, keyboard, *args, **kwargs):
|
||||
if debug.enabled:
|
||||
debug('ht_deactivate_tap')
|
||||
keyboard.resume_process_key(self, key.tap, False)
|
||||
|
||||
def ht_activate_on_interrupt(self, key, keyboard, *args, **kwargs):
|
||||
if debug.enabled:
|
||||
debug('ht_activate_on_interrupt')
|
||||
if key.prefer_hold:
|
||||
self.ht_activate_hold(key, keyboard, *args, **kwargs)
|
||||
else:
|
||||
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
||||
|
||||
def ht_deactivate_on_interrupt(self, key, keyboard, *args, **kwargs):
|
||||
if debug.enabled:
|
||||
debug('ht_deactivate_on_interrupt')
|
||||
if key.prefer_hold:
|
||||
self.ht_deactivate_hold(key, keyboard, *args, **kwargs)
|
||||
else:
|
||||
self.ht_deactivate_tap(key, keyboard, *args, **kwargs)
|
||||
193
hackpads/Kalpad/firmware/kmk/modules/layers.py
Normal file
193
hackpads/Kalpad/firmware/kmk/modules/layers.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
'''One layer isn't enough. Adds keys to get to more of them'''
|
||||
|
||||
from kmk.keys import KC, Key, make_argumented_key
|
||||
from kmk.modules.holdtap import HoldTap, HoldTapKey
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
def lt_key(layer, key, prefer_hold=False, **kwargs):
|
||||
return HoldTapKey(tap=key, hold=KC.MO(layer), prefer_hold=prefer_hold, **kwargs)
|
||||
|
||||
|
||||
def tt_key(layer, prefer_hold=True, **kwargs):
|
||||
return HoldTapKey(
|
||||
tap=KC.TG(layer),
|
||||
hold=KC.MO(layer),
|
||||
prefer_hold=prefer_hold,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class LayerKey(Key):
|
||||
def __init__(self, layer, key=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.layer = layer
|
||||
self.key = key
|
||||
|
||||
|
||||
class Layers(HoldTap):
|
||||
'''Gives access to the keys used to enable the layer system'''
|
||||
|
||||
_active_combo = None
|
||||
|
||||
def __init__(self, combo_layers=None):
|
||||
# Layers
|
||||
super().__init__(_make_key=False)
|
||||
self.combo_layers = combo_layers
|
||||
make_argumented_key(
|
||||
names=('MO',),
|
||||
constructor=LayerKey,
|
||||
on_press=self._mo_pressed,
|
||||
on_release=self._mo_released,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('FD',),
|
||||
constructor=LayerKey,
|
||||
on_press=self._fd_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('DF',),
|
||||
constructor=LayerKey,
|
||||
on_press=self._df_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('LM',),
|
||||
constructor=LayerKey,
|
||||
on_press=self._lm_pressed,
|
||||
on_release=self._lm_released,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('TG',),
|
||||
constructor=LayerKey,
|
||||
on_press=self._tg_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('TO',),
|
||||
constructor=LayerKey,
|
||||
on_press=self._to_pressed,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('LT',),
|
||||
constructor=lt_key,
|
||||
on_press=self.ht_pressed,
|
||||
on_release=self.ht_released,
|
||||
)
|
||||
make_argumented_key(
|
||||
names=('TT',),
|
||||
constructor=tt_key,
|
||||
on_press=self.ht_pressed,
|
||||
on_release=self.ht_released,
|
||||
)
|
||||
|
||||
def _fd_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Switches the top layer
|
||||
'''
|
||||
self.activate_layer(keyboard, key.layer, idx=0)
|
||||
|
||||
def _df_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Switches the default layer
|
||||
'''
|
||||
self.activate_layer(keyboard, key.layer, idx=-1)
|
||||
|
||||
def _mo_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Momentarily activates layer, switches off when you let go
|
||||
'''
|
||||
self.activate_layer(keyboard, key.layer)
|
||||
|
||||
def _mo_released(self, key, keyboard, *args, **kwargs):
|
||||
self.deactivate_layer(keyboard, key.layer)
|
||||
|
||||
def _lm_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
As MO(layer) but with mod active
|
||||
'''
|
||||
keyboard.hid_pending = True
|
||||
keyboard.keys_pressed.add(key.key)
|
||||
self.activate_layer(keyboard, key.layer)
|
||||
|
||||
def _lm_released(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
As MO(layer) but with mod active
|
||||
'''
|
||||
keyboard.hid_pending = True
|
||||
keyboard.keys_pressed.discard(key.key)
|
||||
self.deactivate_layer(keyboard, key.layer)
|
||||
|
||||
def _tg_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Toggles the layer (enables it if not active, and vise versa)
|
||||
'''
|
||||
# See mo_released for implementation details around this
|
||||
if key.layer in keyboard.active_layers:
|
||||
self.deactivate_layer(keyboard, key.layer)
|
||||
else:
|
||||
self.activate_layer(keyboard, key.layer)
|
||||
|
||||
def _to_pressed(self, key, keyboard, *args, **kwargs):
|
||||
'''
|
||||
Activates layer and deactivates all other layers
|
||||
'''
|
||||
self._active_combo = None
|
||||
keyboard.active_layers.clear()
|
||||
self.activate_layer(keyboard, key.layer)
|
||||
|
||||
def _print_debug(self, keyboard):
|
||||
if debug.enabled:
|
||||
debug(f'active_layers={keyboard.active_layers}')
|
||||
|
||||
def activate_layer(self, keyboard, layer, idx=None):
|
||||
if idx is None:
|
||||
keyboard.active_layers.insert(0, layer)
|
||||
else:
|
||||
keyboard.active_layers[idx] = layer
|
||||
|
||||
if self.combo_layers:
|
||||
self._activate_combo_layer(keyboard)
|
||||
|
||||
self._print_debug(keyboard)
|
||||
|
||||
def deactivate_layer(self, keyboard, layer):
|
||||
# Remove the first instance of the target layer from the active list
|
||||
# under almost all normal use cases, this will disable the layer (but
|
||||
# preserve it if it was triggered as a default layer, etc.).
|
||||
# This also resolves an issue where using DF() on a layer
|
||||
# triggered by MO() and then defaulting to the MO()'s layer
|
||||
# would result in no layers active.
|
||||
if len(keyboard.active_layers) > 1:
|
||||
try:
|
||||
idx = keyboard.active_layers.index(layer)
|
||||
del keyboard.active_layers[idx]
|
||||
except ValueError:
|
||||
if debug.enabled:
|
||||
debug(f'_mo_released: layer {layer} not active')
|
||||
|
||||
if self.combo_layers:
|
||||
self._deactivate_combo_layer(keyboard, layer)
|
||||
|
||||
self._print_debug(keyboard)
|
||||
|
||||
def _activate_combo_layer(self, keyboard):
|
||||
if self._active_combo:
|
||||
return
|
||||
|
||||
for combo, result in self.combo_layers.items():
|
||||
matching = True
|
||||
for layer in combo:
|
||||
if layer not in keyboard.active_layers:
|
||||
matching = False
|
||||
break
|
||||
|
||||
if matching:
|
||||
self._active_combo = combo
|
||||
keyboard.active_layers.insert(0, result)
|
||||
break
|
||||
|
||||
def _deactivate_combo_layer(self, keyboard, layer):
|
||||
if self._active_combo and layer in self._active_combo:
|
||||
keyboard.active_layers.remove(self.combo_layers[self._active_combo])
|
||||
self._active_combo = None
|
||||
315
hackpads/Kalpad/firmware/kmk/modules/macros.py
Normal file
315
hackpads/Kalpad/firmware/kmk/modules/macros.py
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
from micropython import const
|
||||
|
||||
from kmk.keys import KC, Key, make_argumented_key, make_key
|
||||
from kmk.modules import Module
|
||||
from kmk.scheduler import create_task
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
_IDLE = const(0)
|
||||
_ON_PRESS = const(1)
|
||||
_ON_HOLD = const(2)
|
||||
_RELEASE = const(3)
|
||||
_ON_RELEASE = const(4)
|
||||
|
||||
|
||||
class MacroKey(Key):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
on_press=None,
|
||||
on_hold=None,
|
||||
on_release=None,
|
||||
blocking=True,
|
||||
_on_press=None,
|
||||
_on_release=None,
|
||||
):
|
||||
super().__init__(on_press=_on_press, on_release=_on_release)
|
||||
|
||||
if on_press is not None:
|
||||
self.on_press_macro = on_press
|
||||
else:
|
||||
self.on_press_macro = args
|
||||
self.on_hold_macro = on_hold
|
||||
self.on_release_macro = on_release
|
||||
self.blocking = blocking
|
||||
self.state = _IDLE
|
||||
self._task = None
|
||||
|
||||
|
||||
class UnicodeModeKey(Key):
|
||||
def __init__(self, mode, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.mode = mode
|
||||
|
||||
|
||||
def Delay(delay):
|
||||
return lambda keyboard: delay
|
||||
|
||||
|
||||
def Press(key):
|
||||
return lambda keyboard: key.on_press(keyboard)
|
||||
|
||||
|
||||
def Release(key):
|
||||
return lambda keyboard: key.on_release(keyboard)
|
||||
|
||||
|
||||
def Tap(key):
|
||||
def _(keyboard):
|
||||
key.on_press(keyboard)
|
||||
yield
|
||||
key.on_release(keyboard)
|
||||
|
||||
return _
|
||||
|
||||
|
||||
class UnicodeModeIBus:
|
||||
@staticmethod
|
||||
def pre(keyboard):
|
||||
macro = (KC.LCTL, KC.LSFT, KC.U)
|
||||
for k in macro:
|
||||
k.on_press(keyboard)
|
||||
yield
|
||||
for k in macro:
|
||||
k.on_release(keyboard)
|
||||
|
||||
@staticmethod
|
||||
def post(keyboard):
|
||||
KC.ENTER.on_press(keyboard)
|
||||
yield
|
||||
KC.ENTER.on_release(keyboard)
|
||||
|
||||
|
||||
class UnicodeModeMacOS:
|
||||
@staticmethod
|
||||
def pre(keyboard):
|
||||
KC.LALT.on_press(keyboard)
|
||||
yield
|
||||
|
||||
@staticmethod
|
||||
def post(keyboard):
|
||||
KC.LALT.on_release(keyboard)
|
||||
yield
|
||||
|
||||
|
||||
class UnicodeModeWinC:
|
||||
@staticmethod
|
||||
def pre(keyboard):
|
||||
macro = (KC.RALT, KC.U)
|
||||
for k in macro:
|
||||
k.on_press(keyboard)
|
||||
yield
|
||||
for k in macro:
|
||||
k.on_release(keyboard)
|
||||
|
||||
@staticmethod
|
||||
def post(keyboard):
|
||||
KC.ENTER.on_press(keyboard)
|
||||
yield
|
||||
KC.ENTER.on_release(keyboard)
|
||||
|
||||
|
||||
def MacroIter(keyboard, macro, unicode_mode):
|
||||
for item in macro:
|
||||
if callable(item):
|
||||
item = item(keyboard)
|
||||
|
||||
if item is None:
|
||||
yield
|
||||
|
||||
elif isinstance(item, int):
|
||||
yield item
|
||||
|
||||
elif isinstance(item, str):
|
||||
for char in item:
|
||||
if ord(char) <= 127:
|
||||
# ANSII key codes
|
||||
key = KC[char]
|
||||
if char.isupper():
|
||||
KC.LSHIFT.on_press(keyboard)
|
||||
key.on_press(keyboard)
|
||||
yield
|
||||
|
||||
if char.isupper():
|
||||
KC.LSHIFT.on_release(keyboard)
|
||||
key.on_release(keyboard)
|
||||
yield
|
||||
|
||||
else:
|
||||
# unicode code points
|
||||
yield from unicode_mode.pre(keyboard)
|
||||
yield
|
||||
|
||||
for digit in hex(ord(char))[2:]:
|
||||
key = KC[digit]
|
||||
key.on_press(keyboard)
|
||||
yield
|
||||
key.on_release(keyboard)
|
||||
yield
|
||||
|
||||
yield from unicode_mode.post(keyboard)
|
||||
yield
|
||||
|
||||
elif item.__class__.__name__ == 'generator':
|
||||
yield from MacroIter(keyboard, item, unicode_mode)
|
||||
yield
|
||||
|
||||
elif debug.enabled:
|
||||
debug('unsupported macro type ', item.__class__.__name__)
|
||||
|
||||
|
||||
class Macros(Module):
|
||||
def __init__(self, unicode_mode=UnicodeModeIBus, delay=10):
|
||||
self._active = []
|
||||
self.key_buffer = []
|
||||
self.unicode_mode = unicode_mode
|
||||
self.delay = delay
|
||||
|
||||
make_argumented_key(
|
||||
names=('MACRO',),
|
||||
constructor=MacroKey,
|
||||
_on_press=self.on_press_macro,
|
||||
_on_release=self.on_release_macro,
|
||||
)
|
||||
make_key(
|
||||
names=('UC_MODE_IBUS',),
|
||||
constructor=UnicodeModeKey,
|
||||
mode=UnicodeModeIBus,
|
||||
on_press=self.on_press_unicode_mode,
|
||||
)
|
||||
make_key(
|
||||
names=('UC_MODE_MACOS',),
|
||||
constructor=UnicodeModeKey,
|
||||
mode=UnicodeModeMacOS,
|
||||
on_press=self.on_press_unicode_mode,
|
||||
)
|
||||
make_key(
|
||||
names=('UC_MODE_WINC',),
|
||||
constructor=UnicodeModeKey,
|
||||
mode=UnicodeModeWinC,
|
||||
on_press=self.on_press_unicode_mode,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
# Passthrough if there are no active macros, or the key belongs to an
|
||||
# active macro, or all active macros or non-blocking.
|
||||
if not self._active or key in self._active or not self._active[-1].blocking:
|
||||
return key
|
||||
|
||||
self.key_buffer.append((int_coord, key, is_pressed))
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_press_unicode_mode(self, key, keyboard, *args, **kwargs):
|
||||
self.unicode_mode = key.mode
|
||||
|
||||
def on_press_macro(self, key, keyboard, *args, **kwargs):
|
||||
if key.state == _IDLE:
|
||||
key.state = _ON_PRESS
|
||||
self.process_macro_async(keyboard, key)
|
||||
else:
|
||||
self.key_buffer.append((args[1], key, True))
|
||||
|
||||
def on_release_macro(self, key, keyboard, *args, **kwargs):
|
||||
if key.state == _ON_PRESS or key.state == _ON_HOLD:
|
||||
key.state = _RELEASE
|
||||
if key._task is None:
|
||||
self.process_macro_async(keyboard, key)
|
||||
else:
|
||||
self.key_buffer.append((args[1], key, False))
|
||||
|
||||
def process_macro_async(self, keyboard, key, _iter=None):
|
||||
# There's no active macro iterator: select the next one.
|
||||
if _iter is None:
|
||||
key._task = None
|
||||
|
||||
if key.state == _ON_PRESS:
|
||||
self._active.append(key)
|
||||
if (macro := key.on_press_macro) is None:
|
||||
key.state = _ON_HOLD
|
||||
elif debug.enabled:
|
||||
debug('on_press')
|
||||
|
||||
if key.state == _ON_HOLD:
|
||||
if (macro := key.on_hold_macro) is None:
|
||||
return
|
||||
elif debug.enabled:
|
||||
debug('on_hold')
|
||||
|
||||
if key.state == _RELEASE:
|
||||
key.state = _ON_RELEASE
|
||||
|
||||
if key.state == _ON_RELEASE:
|
||||
if (macro := key.on_release_macro) is None:
|
||||
macro = ()
|
||||
elif debug.enabled:
|
||||
debug('on_release')
|
||||
|
||||
_iter = MacroIter(keyboard, macro, self.unicode_mode)
|
||||
|
||||
# Run one step in the macro sequence.
|
||||
delay = self.delay
|
||||
try:
|
||||
# any not None value the iterator yields is a delay value in ms.
|
||||
ret = next(_iter)
|
||||
if ret is not None:
|
||||
delay = ret
|
||||
keyboard._send_hid()
|
||||
|
||||
# The sequence has reached its end: advance the macro state.
|
||||
except StopIteration:
|
||||
_iter = None
|
||||
delay = 0
|
||||
key._task = None
|
||||
|
||||
if key.state == _ON_PRESS:
|
||||
key.state = _ON_HOLD
|
||||
|
||||
elif key.state == _ON_RELEASE:
|
||||
if debug.enabled:
|
||||
debug('deactivate')
|
||||
key.state = _IDLE
|
||||
self._active.remove(key)
|
||||
self.send_key_buffer(keyboard)
|
||||
return
|
||||
|
||||
# Schedule the next step.
|
||||
# Reuse existing task objects and save a couple of bytes and cycles for the gc.
|
||||
if key._task:
|
||||
task = key._task
|
||||
else:
|
||||
|
||||
def task():
|
||||
self.process_macro_async(keyboard, key, _iter)
|
||||
|
||||
key._task = create_task(task, after_ms=delay)
|
||||
|
||||
def send_key_buffer(self, keyboard):
|
||||
if not self.key_buffer or self._active:
|
||||
return
|
||||
|
||||
for int_coord, key, is_pressed in self.key_buffer:
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord, False)
|
||||
|
||||
self.key_buffer.clear()
|
||||
114
hackpads/Kalpad/firmware/kmk/modules/midi.py
Normal file
114
hackpads/Kalpad/firmware/kmk/modules/midi.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import adafruit_midi
|
||||
import usb_midi
|
||||
from adafruit_midi.control_change import ControlChange
|
||||
from adafruit_midi.note_off import NoteOff
|
||||
from adafruit_midi.note_on import NoteOn
|
||||
from adafruit_midi.pitch_bend import PitchBend
|
||||
from adafruit_midi.program_change import ProgramChange
|
||||
from adafruit_midi.start import Start
|
||||
from adafruit_midi.stop import Stop
|
||||
|
||||
from kmk.keys import Key, make_argumented_key
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class MidiKey(Key):
|
||||
def __init__(self, *args, command, channel=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.on_press_msg = command(*args, channel=channel)
|
||||
self.on_release_msg = None
|
||||
|
||||
|
||||
def midi_note_key(note=69, velocity=127, channel=None, **kwargs):
|
||||
key = MidiKey(note, velocity, command=NoteOn, channel=channel, **kwargs)
|
||||
key.on_release_msg = NoteOff(note, velocity, channel=channel)
|
||||
return key
|
||||
|
||||
|
||||
class MidiKeys(Module):
|
||||
def __init__(self):
|
||||
make_argumented_key(
|
||||
names=('MIDI_CC',),
|
||||
constructor=MidiKey,
|
||||
command=ControlChange,
|
||||
on_press=self.on_press,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('MIDI_NOTE',),
|
||||
constructor=midi_note_key,
|
||||
on_press=self.on_press,
|
||||
on_release=self.on_release,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('MIDI_PB',),
|
||||
constructor=MidiKey,
|
||||
command=PitchBend,
|
||||
on_press=self.on_press,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('MIDI_PC',),
|
||||
constructor=MidiKey,
|
||||
command=ProgramChange,
|
||||
on_press=self.on_press,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('MIDI_START',),
|
||||
constructor=MidiKey,
|
||||
command=Start,
|
||||
on_press=self.on_press,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('MIDI_STOP',),
|
||||
constructor=MidiKey,
|
||||
command=Stop,
|
||||
on_press=self.on_press,
|
||||
)
|
||||
|
||||
try:
|
||||
self.midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
|
||||
except IndexError:
|
||||
self.midi = None
|
||||
if debug.enabled:
|
||||
debug('No midi device found.')
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return None
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return None
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return None
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return None
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return None
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return None
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return None
|
||||
|
||||
def send(self, message):
|
||||
if self.midi:
|
||||
self.midi.send(message)
|
||||
|
||||
def on_press(self, key, keyboard, *args, **kwargs):
|
||||
self.send(key.on_press_msg)
|
||||
|
||||
def on_release(self, key, keyboard, *args, **kwargs):
|
||||
self.send(key.on_release_msg)
|
||||
152
hackpads/Kalpad/firmware/kmk/modules/mouse_keys.py
Normal file
152
hackpads/Kalpad/firmware/kmk/modules/mouse_keys.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
from micropython import const
|
||||
|
||||
from kmk.keys import AX, MouseKey, make_key
|
||||
from kmk.modules import Module
|
||||
from kmk.scheduler import cancel_task, create_task
|
||||
|
||||
_MU = const(0x01)
|
||||
_MD = const(0x02)
|
||||
_ML = const(0x04)
|
||||
_MR = const(0x08)
|
||||
_WU = const(0x10)
|
||||
_WD = const(0x20)
|
||||
_WL = const(0x40)
|
||||
_WR = const(0x80)
|
||||
|
||||
|
||||
class MouseKeys(Module):
|
||||
def __init__(self, max_speed=10, acc_interval=20, move_step=1):
|
||||
self._movement = 0
|
||||
self.max_speed = max_speed
|
||||
self.acc_interval = acc_interval
|
||||
self.move_step = move_step
|
||||
|
||||
codes = (
|
||||
(0x01, ('MB_LMB',)),
|
||||
(0x02, ('MB_RMB',)),
|
||||
(0x04, ('MB_MMB',)),
|
||||
(0x08, ('MB_BTN4',)),
|
||||
(0x10, ('MB_BTN5',)),
|
||||
)
|
||||
for code, names in codes:
|
||||
make_key(names=names, constructor=MouseKey, code=code)
|
||||
|
||||
keys = (
|
||||
(('MW_UP',), self._mw_up_press, self._mw_up_release),
|
||||
(('MW_DOWN', 'MW_DN'), self._mw_down_press, self._mw_down_release),
|
||||
(('MW_LEFT', 'MW_LT'), self._mw_left_press, self._mw_left_release),
|
||||
(('MW_RIGHT', 'MW_RT'), self._mw_right_press, self._mw_right_release),
|
||||
(('MS_UP',), self._ms_up_press, self._ms_up_release),
|
||||
(('MS_DOWN', 'MS_DN'), self._ms_down_press, self._ms_down_release),
|
||||
(('MS_LEFT', 'MS_LT'), self._ms_left_press, self._ms_left_release),
|
||||
(('MS_RIGHT', 'MS_RT'), self._ms_right_press, self._ms_right_release),
|
||||
)
|
||||
for names, on_press, on_release in keys:
|
||||
make_key(names=names, on_press=on_press, on_release=on_release)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
self._task = create_task(
|
||||
lambda: self._move(keyboard),
|
||||
period_ms=self.acc_interval,
|
||||
)
|
||||
cancel_task(self._task)
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def _move(self, keyboard):
|
||||
if self._movement & (_MR + _ML + _MD + _MU):
|
||||
if self.move_step < self.max_speed:
|
||||
self.move_step = self.move_step + 1
|
||||
if self._movement & _MU:
|
||||
AX.Y.move(keyboard, -self.move_step)
|
||||
if self._movement & _MD:
|
||||
AX.Y.move(keyboard, self.move_step)
|
||||
if self._movement & _ML:
|
||||
AX.X.move(keyboard, -self.move_step)
|
||||
if self._movement & _MR:
|
||||
AX.X.move(keyboard, self.move_step)
|
||||
|
||||
if self._movement & _WU:
|
||||
AX.W.move(keyboard, 1)
|
||||
if self._movement & _WD:
|
||||
AX.W.move(keyboard, -1)
|
||||
if self._movement & _WL:
|
||||
AX.P.move(keyboard, -1)
|
||||
if self._movement & _WR:
|
||||
AX.P.move(keyboard, 1)
|
||||
|
||||
def _maybe_start_move(self, mask):
|
||||
self._movement |= mask
|
||||
if self._movement == mask:
|
||||
self._task.restart()
|
||||
|
||||
def _maybe_stop_move(self, mask):
|
||||
self._movement &= ~mask
|
||||
if not self._movement & (_MR + _ML + _MD + _MU):
|
||||
self.move_step = 1
|
||||
if not self._movement:
|
||||
cancel_task(self._task)
|
||||
|
||||
def _mw_up_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_WU)
|
||||
|
||||
def _mw_up_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_WU)
|
||||
|
||||
def _mw_down_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_WD)
|
||||
|
||||
def _mw_down_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_WD)
|
||||
|
||||
def _mw_left_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_WL)
|
||||
|
||||
def _mw_left_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_WL)
|
||||
|
||||
def _mw_right_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_WR)
|
||||
|
||||
def _mw_right_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_WR)
|
||||
|
||||
def _ms_up_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_MU)
|
||||
|
||||
def _ms_up_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_MU)
|
||||
|
||||
def _ms_down_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_MD)
|
||||
|
||||
def _ms_down_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_MD)
|
||||
|
||||
def _ms_left_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_ML)
|
||||
|
||||
def _ms_left_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_ML)
|
||||
|
||||
def _ms_right_press(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_start_move(_MR)
|
||||
|
||||
def _ms_right_release(self, key, keyboard, *args, **kwargs):
|
||||
self._maybe_stop_move(_MR)
|
||||
318
hackpads/Kalpad/firmware/kmk/modules/pimoroni_trackball.py
Normal file
318
hackpads/Kalpad/firmware/kmk/modules/pimoroni_trackball.py
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
'''
|
||||
Extension handles usage of Trackball Breakout by Pimoroni
|
||||
'''
|
||||
|
||||
from micropython import const
|
||||
|
||||
import math
|
||||
import struct
|
||||
from adafruit_pixelbuf import PixelBuf
|
||||
|
||||
from kmk.keys import AX, KC, Key, make_argumented_key, make_key
|
||||
from kmk.kmktime import PeriodicTimer
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
_I2C_ADDRESS = const(0x0A)
|
||||
_I2C_ADDRESS_ALTERNATIVE = const(0x0B)
|
||||
|
||||
_CHIP_ID = const(0xBA11)
|
||||
_VERSION = const(1)
|
||||
|
||||
_REG_LED_RED = const(0x00)
|
||||
_REG_LED_GRN = const(0x01)
|
||||
_REG_LED_BLU = const(0x02)
|
||||
_REG_LED_WHT = const(0x03)
|
||||
|
||||
_REG_LEFT = const(0x04)
|
||||
_REG_RIGHT = const(0x05)
|
||||
_REG_UP = const(0x06)
|
||||
_REG_DOWN = const(0x07)
|
||||
_REG_SWITCH = const(0x08)
|
||||
_MSK_SWITCH_STATE = const(0b10000000)
|
||||
|
||||
_REG_USER_FLASH = const(0xD0)
|
||||
_REG_FLASH_PAGE = const(0xF0)
|
||||
_REG_INT = const(0xF9)
|
||||
_MSK_INT_TRIGGERED = const(0b00000001)
|
||||
_MSK_INT_OUT_EN = const(0b00000010)
|
||||
_REG_CHIP_ID_L = const(0xFA)
|
||||
_REG_CHIP_ID_H = const(0xFB)
|
||||
_REG_VERSION = const(0xFC)
|
||||
_REG_I2C_ADDR = const(0xFD)
|
||||
_REG_CTRL = const(0xFE)
|
||||
_MSK_CTRL_SLEEP = const(0b00000001)
|
||||
_MSK_CTRL_RESET = const(0b00000010)
|
||||
_MSK_CTRL_FREAD = const(0b00000100)
|
||||
_MSK_CTRL_FWRITE = const(0b00001000)
|
||||
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class TrackballMode:
|
||||
'''Behaviour mode of trackball: mouse movement or vertical scroll'''
|
||||
|
||||
MOUSE_MODE = const(0)
|
||||
SCROLL_MODE = const(1)
|
||||
|
||||
|
||||
class ScrollDirection:
|
||||
'''Behaviour mode of scrolling: natural or reverse scrolling'''
|
||||
|
||||
NATURAL = const(0)
|
||||
REVERSE = const(1)
|
||||
|
||||
|
||||
class TrackballHandlerKey(Key):
|
||||
def __init__(self, handler=TrackballMode.MOUSE_MODE, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.handler = handler
|
||||
|
||||
|
||||
class TrackballHandler:
|
||||
def handle(self, keyboard, trackball, x, y, switch, state):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PointingHandler(TrackballHandler):
|
||||
def __init__(self, on_press=KC.MB_LMB):
|
||||
self.on_press = on_press
|
||||
|
||||
def handle(self, keyboard, trackball, x, y, switch, state):
|
||||
if x:
|
||||
AX.X.move(keyboard, x)
|
||||
if y:
|
||||
AX.Y.move(keyboard, y)
|
||||
|
||||
if switch == 1: # Button changed state
|
||||
keyboard.pre_process_key(self.on_press, is_pressed=state)
|
||||
|
||||
|
||||
class ScrollHandler(TrackballHandler):
|
||||
def __init__(self, scroll_direction=ScrollDirection.NATURAL, on_press=KC.MB_LMB):
|
||||
self.scroll_direction = scroll_direction
|
||||
self.on_press = on_press
|
||||
|
||||
def handle(self, keyboard, trackball, x, y, switch, state):
|
||||
if self.scroll_direction == ScrollDirection.REVERSE:
|
||||
y = -y
|
||||
|
||||
if y != 0:
|
||||
AX.W.move(keyboard, y)
|
||||
|
||||
if switch == 1: # Button changed state
|
||||
keyboard.pre_process_key(self.on_press, is_pressed=state)
|
||||
|
||||
|
||||
class KeyHandler(TrackballHandler):
|
||||
x = 0
|
||||
y = 0
|
||||
|
||||
def __init__(self, up, right, down, left, press, axis_snap=0.25, steps=8):
|
||||
self.up = up
|
||||
self.right = right
|
||||
self.down = down
|
||||
self.left = left
|
||||
self.press = press
|
||||
self.axis_snap = axis_snap
|
||||
self.steps = steps
|
||||
|
||||
def handle(self, keyboard, trackball, x, y, switch, state):
|
||||
if y and abs(x / y) < self.axis_snap:
|
||||
x = 0
|
||||
if x and abs(y / x) < self.axis_snap:
|
||||
y = 0
|
||||
|
||||
self.x += x
|
||||
self.y += y
|
||||
x_taps = self.x // self.steps
|
||||
y_taps = self.y // self.steps
|
||||
self.x %= self.steps
|
||||
self.y %= self.steps
|
||||
for i in range(x_taps, 0, 1):
|
||||
keyboard.tap_key(self.left)
|
||||
for i in range(x_taps, 0, -1):
|
||||
keyboard.tap_key(self.right)
|
||||
for i in range(y_taps, 0, 1):
|
||||
keyboard.tap_key(self.up)
|
||||
for i in range(y_taps, 0, -1):
|
||||
keyboard.tap_key(self.down)
|
||||
if switch and state:
|
||||
keyboard.tap_key(self.press)
|
||||
|
||||
|
||||
class Trackball(Module):
|
||||
'''Module handles usage of Trackball Breakout by Pimoroni'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
i2c,
|
||||
mode=TrackballMode.MOUSE_MODE,
|
||||
address=_I2C_ADDRESS,
|
||||
angle_offset=0,
|
||||
handlers=None,
|
||||
):
|
||||
self.angle_offset = angle_offset
|
||||
if not handlers:
|
||||
handlers = [PointingHandler(), ScrollHandler()]
|
||||
if mode == TrackballMode.SCROLL_MODE:
|
||||
handlers.reverse()
|
||||
self._i2c_address = address
|
||||
self._i2c_bus = i2c
|
||||
|
||||
self.mode = mode
|
||||
self.handlers = handlers
|
||||
self.current_handler = self.handlers[0]
|
||||
self.polling_interval = 20
|
||||
|
||||
make_key(
|
||||
names=('TB_MODE', 'TB_NEXT_HANDLER', 'TB_N'),
|
||||
on_press=self._tb_handler_next_press,
|
||||
)
|
||||
|
||||
make_argumented_key(
|
||||
names=('TB_HANDLER', 'TB_H'),
|
||||
constructor=TrackballHandlerKey,
|
||||
on_press=self._tb_handler_press,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
chip_id = struct.unpack('<H', bytearray(self._i2c_rdwr([_REG_CHIP_ID_L], 2)))[0]
|
||||
if chip_id != _CHIP_ID:
|
||||
raise RuntimeError(
|
||||
f'Invalid chip ID: 0x{chip_id:04X}, expected 0x{_CHIP_ID:04X}'
|
||||
)
|
||||
|
||||
self._timer = PeriodicTimer(self.polling_interval)
|
||||
|
||||
a = math.pi * self.angle_offset / 180
|
||||
self.rot = [[math.cos(a), math.sin(a)], [-math.sin(a), math.cos(a)]]
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be injected as an extra matrix update
|
||||
'''
|
||||
if not self._timer.tick():
|
||||
return
|
||||
|
||||
if not (self._i2c_rdwr([_REG_INT], 1)[0] & _MSK_INT_TRIGGERED):
|
||||
return
|
||||
|
||||
up, down, left, right, switch, state = self._read_raw_state()
|
||||
|
||||
x, y = self._calculate_movement(right - left, down - up)
|
||||
|
||||
self.current_handler.handle(keyboard, self, x, y, switch, state)
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def set_rgbw(self, r, g, b, w):
|
||||
'''Set all LED brightness as RGBW.'''
|
||||
self._i2c_rdwr([_REG_LED_RED, r, g, b, w])
|
||||
|
||||
def set_red(self, value):
|
||||
'''Set brightness of trackball red LED.'''
|
||||
self._i2c_rdwr([_REG_LED_RED, value & 0xFF])
|
||||
|
||||
def set_green(self, value):
|
||||
'''Set brightness of trackball green LED.'''
|
||||
self._i2c_rdwr([_REG_LED_GRN, value & 0xFF])
|
||||
|
||||
def set_blue(self, value):
|
||||
'''Set brightness of trackball blue LED.'''
|
||||
self._i2c_rdwr([_REG_LED_BLU, value & 0xFF])
|
||||
|
||||
def set_white(self, value):
|
||||
'''Set brightness of trackball white LED.'''
|
||||
self._i2c_rdwr([_REG_LED_WHT, value & 0xFF])
|
||||
|
||||
def activate_handler(self, handler):
|
||||
if isinstance(handler, TrackballHandler):
|
||||
self.current_handler = handler
|
||||
else:
|
||||
try:
|
||||
self.current_handler = self.handlers[handler]
|
||||
except KeyError:
|
||||
if debug.enabled:
|
||||
debug(f'no handler found with id {handler}')
|
||||
|
||||
def next_handler(self):
|
||||
next_index = self.handlers.index(self.current_handler) + 1
|
||||
if next_index >= len(self.handlers):
|
||||
next_index = 0
|
||||
self.activate_handler(next_index)
|
||||
|
||||
def _read_raw_state(self):
|
||||
'''Read up, down, left, right and switch data from trackball.'''
|
||||
left, right, up, down, switch = self._i2c_rdwr([_REG_LEFT], 5)
|
||||
switch_changed, switch_state = (
|
||||
switch & ~_MSK_SWITCH_STATE,
|
||||
(switch & _MSK_SWITCH_STATE) > 0,
|
||||
)
|
||||
return up, down, left, right, switch_changed, switch_state
|
||||
|
||||
def _i2c_rdwr(self, data, length=0):
|
||||
'''Write and optionally read I2C data.'''
|
||||
if not self._i2c_bus.try_lock():
|
||||
return
|
||||
|
||||
try:
|
||||
if length > 0:
|
||||
result = bytearray(length)
|
||||
self._i2c_bus.writeto_then_readfrom(
|
||||
self._i2c_address, bytes(data), result
|
||||
)
|
||||
return list(result)
|
||||
else:
|
||||
self._i2c_bus.writeto(self._i2c_address, bytes(data))
|
||||
|
||||
return []
|
||||
|
||||
finally:
|
||||
self._i2c_bus.unlock()
|
||||
|
||||
def _tb_handler_press(self, key, keyboard, *args, **kwargs):
|
||||
self.activate_handler(key.handler)
|
||||
|
||||
def _tb_handler_next_press(self, key, keyboard, *args, **kwargs):
|
||||
self.next_handler()
|
||||
|
||||
def _calculate_movement(self, raw_x, raw_y):
|
||||
'''Calculate accelerated movement vector from raw data'''
|
||||
if raw_x == 0 and raw_y == 0:
|
||||
return 0, 0
|
||||
|
||||
scale = math.sqrt(raw_x**2 + raw_y**2)
|
||||
x = (self.rot[0][0] * raw_x + self.rot[0][1] * raw_y) * scale
|
||||
y = (self.rot[1][0] * raw_x + self.rot[1][1] * raw_y) * scale
|
||||
|
||||
return int(x), int(y)
|
||||
|
||||
|
||||
class TrackballPixel(PixelBuf):
|
||||
'''PixelBuf interface for the Trackball RGBW LED'''
|
||||
|
||||
def __init__(self, trackball, **kwargs):
|
||||
self.trackball = trackball
|
||||
kwargs['byteorder'] = 'RGBW'
|
||||
super().__init__(1, **kwargs)
|
||||
|
||||
def deinit(self):
|
||||
self.trackball.set_rgbw(0, 0, 0, 0)
|
||||
|
||||
def _transmit(self, b):
|
||||
self.trackball.set_rgbw(b[0], b[1], b[2], b[3])
|
||||
94
hackpads/Kalpad/firmware/kmk/modules/potentiometer.py
Normal file
94
hackpads/Kalpad/firmware/kmk/modules/potentiometer.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from analogio import AnalogIn
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class PotentiometerState:
|
||||
def __init__(self, direction: int, position: int):
|
||||
self.direction = direction
|
||||
self.position = position
|
||||
|
||||
|
||||
class Potentiometer:
|
||||
def __init__(self, pin, move_callback, is_inverted=False):
|
||||
self.is_inverted = is_inverted
|
||||
self.read_pin = AnalogIn(pin)
|
||||
self._direction = None
|
||||
self._pos = self.get_pos()
|
||||
self._timestamp = ticks_ms()
|
||||
self.cb = move_callback
|
||||
|
||||
# callback function on events.
|
||||
self.on_move_do = lambda state: self.cb(state)
|
||||
|
||||
def get_state(self) -> PotentiometerState:
|
||||
return PotentiometerState(
|
||||
direction=(self.is_inverted and -self._direction or self._direction),
|
||||
position=(self.is_inverted and -self._pos or self._pos),
|
||||
)
|
||||
|
||||
def get_pos(self):
|
||||
'''
|
||||
Read from the analog pin assingned, truncate to 7 bits,
|
||||
average over 10 readings, and return a value 0-127
|
||||
'''
|
||||
return int(sum([(self.read_pin.value >> 9) for i in range(10)]) / 10)
|
||||
|
||||
def update_state(self):
|
||||
self._direction = 0
|
||||
new_pos = self.get_pos()
|
||||
if abs(new_pos - self._pos) > 2:
|
||||
# movement detected!
|
||||
if new_pos > self._pos:
|
||||
self._direction = 1
|
||||
else:
|
||||
self._direction = -1
|
||||
self._pos = new_pos
|
||||
if self.on_move_do is not None:
|
||||
self.on_move_do(self.get_state())
|
||||
|
||||
|
||||
class PotentiometerHandler(Module):
|
||||
def __init__(self):
|
||||
self.potentiometers = []
|
||||
self.pins = None
|
||||
|
||||
def on_runtime_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_runtime_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
if self.pins:
|
||||
for args in self.pins:
|
||||
self.potentiometers.append(Potentiometer(*args))
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be injected as an extra matrix update
|
||||
'''
|
||||
for potentiometer in self.potentiometers:
|
||||
potentiometer.update_state()
|
||||
|
||||
return keyboard
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
'''
|
||||
Return value will be replace matrix update if supplied
|
||||
'''
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
143
hackpads/Kalpad/firmware/kmk/modules/power.py
Normal file
143
hackpads/Kalpad/firmware/kmk/modules/power.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import board
|
||||
import digitalio
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from time import sleep
|
||||
|
||||
from kmk.keys import make_key
|
||||
from kmk.kmktime import check_deadline
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class Power(Module):
|
||||
def __init__(self, powersave_pin=None):
|
||||
self.enable = False
|
||||
self.powersave_pin = powersave_pin # Powersave pin board object
|
||||
self._powersave_start = ticks_ms()
|
||||
self._usb_last_scan = ticks_ms() - 5000
|
||||
self._psp = None # Powersave pin object
|
||||
self._i2c = 0
|
||||
self._i2c_deinit_count = 0
|
||||
self._loopcounter = 0
|
||||
|
||||
make_key(names=('PS_TOG',), on_press=self._ps_tog)
|
||||
make_key(names=('PS_ON',), on_press=self._ps_enable)
|
||||
make_key(names=('PS_OFF',), on_press=self._ps_disable)
|
||||
|
||||
def __repr__(self):
|
||||
return f'Power({self._to_dict()})'
|
||||
|
||||
def _to_dict(self):
|
||||
return {
|
||||
'enable': self.enable,
|
||||
'powersave_pin': self.powersave_pin,
|
||||
'_powersave_start': self._powersave_start,
|
||||
'_usb_last_scan': self._usb_last_scan,
|
||||
'_psp': self._psp,
|
||||
}
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
self._i2c_scan()
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
if keyboard.matrix_update or keyboard.secondary_matrix_update:
|
||||
self.psave_time_reset()
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
if self.enable:
|
||||
self.psleep()
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
'''Gives 10 cycles to allow other extensions to clean up before powersave'''
|
||||
if self._loopcounter > 10:
|
||||
self.enable_powersave(keyboard)
|
||||
self._loopcounter = 0
|
||||
else:
|
||||
self._loopcounter += 1
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
self.disable_powersave(keyboard)
|
||||
return
|
||||
|
||||
def enable_powersave(self, keyboard):
|
||||
'''Enables power saving features'''
|
||||
if self._i2c_deinit_count >= self._i2c and self.powersave_pin:
|
||||
# Allows power save to prevent RGB drain.
|
||||
# Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic
|
||||
|
||||
if not self._psp:
|
||||
self._psp = digitalio.DigitalInOut(self.powersave_pin)
|
||||
self._psp.direction = digitalio.Direction.OUTPUT
|
||||
if self._psp:
|
||||
self._psp.value = True
|
||||
|
||||
self.enable = True
|
||||
keyboard._trigger_powersave_enable = False
|
||||
return
|
||||
|
||||
def disable_powersave(self, keyboard):
|
||||
'''Disables power saving features'''
|
||||
if self._psp:
|
||||
self._psp.value = False
|
||||
# Allows power save to prevent RGB drain.
|
||||
# Example here https://docs.nicekeyboards.com/#/nice!nano/pinout_schematic
|
||||
|
||||
keyboard._trigger_powersave_disable = False
|
||||
self.enable = False
|
||||
return
|
||||
|
||||
def psleep(self):
|
||||
'''
|
||||
Sleeps longer and longer to save power the more time in between updates.
|
||||
'''
|
||||
if check_deadline(ticks_ms(), self._powersave_start, 60000):
|
||||
sleep(8 / 1000)
|
||||
elif check_deadline(ticks_ms(), self._powersave_start, 240000) is False:
|
||||
sleep(180 / 1000)
|
||||
return
|
||||
|
||||
def psave_time_reset(self):
|
||||
self._powersave_start = ticks_ms()
|
||||
|
||||
def _i2c_scan(self):
|
||||
i2c = board.I2C()
|
||||
while not i2c.try_lock():
|
||||
pass
|
||||
try:
|
||||
self._i2c = len(i2c.scan())
|
||||
finally:
|
||||
i2c.unlock()
|
||||
return
|
||||
|
||||
def usb_rescan_timer(self):
|
||||
return bool(check_deadline(ticks_ms(), self._usb_last_scan, 5000) is False)
|
||||
|
||||
def usb_time_reset(self):
|
||||
self._usb_last_scan = ticks_ms()
|
||||
return
|
||||
|
||||
def usb_scan(self):
|
||||
# TODO Add USB detection here. Currently lies that it's connected
|
||||
# https://github.com/adafruit/circuitpython/pull/3513
|
||||
return True
|
||||
|
||||
def _ps_tog(self, key, keyboard, *args, **kwargs):
|
||||
if self.enable:
|
||||
keyboard._trigger_powersave_disable = True
|
||||
else:
|
||||
keyboard._trigger_powersave_enable = True
|
||||
|
||||
def _ps_enable(self, key, keyboard, *args, **kwargs):
|
||||
if not self.enable:
|
||||
keyboard._trigger_powersave_enable = True
|
||||
|
||||
def _ps_disable(self, key, keyboard, *args, **kwargs):
|
||||
if self.enable:
|
||||
keyboard._trigger_powersave_disable = True
|
||||
118
hackpads/Kalpad/firmware/kmk/modules/rapidfire.py
Normal file
118
hackpads/Kalpad/firmware/kmk/modules/rapidfire.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
from micropython import const
|
||||
|
||||
from random import randint
|
||||
|
||||
from kmk.keys import Key, make_argumented_key
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
_INACTIVE = const(0)
|
||||
_HOLD = const(1)
|
||||
_ACTIVE = const(2)
|
||||
|
||||
|
||||
class RapidFireKey(Key):
|
||||
def __init__(
|
||||
self,
|
||||
key,
|
||||
interval=100,
|
||||
timeout=200,
|
||||
enable_interval_randomization=False,
|
||||
randomization_magnitude=15,
|
||||
toggle=False,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.key = key
|
||||
self.interval = interval
|
||||
self.timeout = timeout
|
||||
self.enable_interval_randomization = enable_interval_randomization
|
||||
self.randomization_magnitude = randomization_magnitude
|
||||
self.toggle = toggle
|
||||
self._state = _INACTIVE
|
||||
self._timeout = None
|
||||
|
||||
|
||||
class RapidFire(Module):
|
||||
def __init__(self):
|
||||
make_argumented_key(
|
||||
names=('RF',),
|
||||
constructor=RapidFireKey,
|
||||
on_press=self._rf_pressed,
|
||||
on_release=self._rf_released,
|
||||
)
|
||||
|
||||
def _on_timer_timeout(self, key, keyboard):
|
||||
if key._state == _HOLD:
|
||||
key._state = _ACTIVE
|
||||
keyboard.remove_key(key.key)
|
||||
key._timeout = keyboard.set_timeout(
|
||||
1, lambda: self._on_timer_timeout(key, keyboard)
|
||||
)
|
||||
return
|
||||
|
||||
keyboard.add_key(key.key)
|
||||
keyboard.set_timeout(1, lambda: keyboard.remove_key(key.key))
|
||||
|
||||
interval = key.interval
|
||||
if key.enable_interval_randomization:
|
||||
interval += randint(
|
||||
-key.randomization_magnitude, key.randomization_magnitude
|
||||
)
|
||||
key._timeout = keyboard.set_timeout(
|
||||
interval, lambda: self._on_timer_timeout(key, keyboard)
|
||||
)
|
||||
|
||||
if debug.enabled:
|
||||
debug(key.key, ' @', interval, 'ms')
|
||||
|
||||
def _rf_pressed(self, key, keyboard, *args, **kwargs):
|
||||
if key._state == _ACTIVE:
|
||||
self._deactivate_key(key, keyboard)
|
||||
return
|
||||
|
||||
keyboard.add_key(key.key)
|
||||
key._state = _HOLD
|
||||
key._timeout = keyboard.set_timeout(
|
||||
key.timeout, lambda: self._on_timer_timeout(key, keyboard)
|
||||
)
|
||||
|
||||
def _rf_released(self, key, keyboard, *args, **kwargs):
|
||||
if key._state == _ACTIVE:
|
||||
if key.toggle:
|
||||
return
|
||||
key._state = _INACTIVE
|
||||
elif key._state == _INACTIVE:
|
||||
return
|
||||
else:
|
||||
keyboard.remove_key(key.key)
|
||||
|
||||
self._deactivate_key(key, keyboard)
|
||||
|
||||
def _deactivate_key(self, key, keyboard):
|
||||
keyboard.cancel_timeout(key._timeout)
|
||||
key._state = _INACTIVE
|
||||
key._timeout = None
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
63
hackpads/Kalpad/firmware/kmk/modules/serialace.py
Normal file
63
hackpads/Kalpad/firmware/kmk/modules/serialace.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
from usb_cdc import data
|
||||
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class SerialACE(Module):
|
||||
buffer = bytearray()
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
try:
|
||||
data.timeout = 0
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
pass
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
pass
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
# Serial.data isn't initialized.
|
||||
if not data:
|
||||
return
|
||||
|
||||
# Nothing to parse.
|
||||
if data.in_waiting == 0:
|
||||
return
|
||||
|
||||
self.buffer.extend(data.read())
|
||||
idx = self.buffer.find(b'\n')
|
||||
|
||||
# No full command yet.
|
||||
if idx == -1:
|
||||
return
|
||||
|
||||
# Split off command and evaluate.
|
||||
line = self.buffer[:idx]
|
||||
self.buffer = self.buffer[idx + 1 :] # noqa: E203
|
||||
|
||||
try:
|
||||
if debug.enabled:
|
||||
debug(f'eval({line})')
|
||||
ret = eval(line, {'keyboard': keyboard})
|
||||
data.write(bytearray(str(ret) + '\n'))
|
||||
except Exception as err:
|
||||
if debug.enabled:
|
||||
debug(f'error: {err}')
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
pass
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
pass
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
pass
|
||||
391
hackpads/Kalpad/firmware/kmk/modules/split.py
Normal file
391
hackpads/Kalpad/firmware/kmk/modules/split.py
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
'''Enables splitting keyboards wirelessly or wired'''
|
||||
|
||||
import busio
|
||||
from micropython import const
|
||||
from supervisor import runtime, ticks_ms
|
||||
|
||||
from keypad import Event as KeyEvent
|
||||
from storage import getmount
|
||||
|
||||
from kmk.hid import HIDModes
|
||||
from kmk.kmktime import check_deadline
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
class SplitSide:
|
||||
LEFT = const(1)
|
||||
RIGHT = const(2)
|
||||
|
||||
|
||||
class SplitType:
|
||||
UART = const(1)
|
||||
I2C = const(2) # unused
|
||||
ONEWIRE = const(3) # unused
|
||||
BLE = const(4)
|
||||
|
||||
|
||||
class Split(Module):
|
||||
'''Enables splitting keyboards wirelessly, or wired'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
split_flip=True,
|
||||
split_side=None,
|
||||
split_type=SplitType.UART,
|
||||
split_target_left=True,
|
||||
uart_interval=20,
|
||||
data_pin=None,
|
||||
data_pin2=None,
|
||||
uart_flip=True,
|
||||
use_pio=False,
|
||||
):
|
||||
self._is_target = True
|
||||
self._uart_buffer = []
|
||||
self.split_flip = split_flip
|
||||
self.split_side = split_side
|
||||
self.split_type = split_type
|
||||
self.split_target_left = split_target_left
|
||||
self.split_offset = None
|
||||
self.data_pin = data_pin
|
||||
self.data_pin2 = data_pin2
|
||||
self.uart_flip = uart_flip
|
||||
self._use_pio = use_pio
|
||||
self._uart = None
|
||||
self._uart_interval = uart_interval
|
||||
self.uart_header = bytearray([0xB2]) # Any non-zero byte should work
|
||||
|
||||
if self.split_type == SplitType.BLE:
|
||||
try:
|
||||
from adafruit_ble import BLERadio
|
||||
from adafruit_ble.advertising.standard import (
|
||||
ProvideServicesAdvertisement,
|
||||
)
|
||||
from adafruit_ble.services.nordic import UARTService
|
||||
|
||||
self.BLERadio = BLERadio
|
||||
self.ProvideServicesAdvertisement = ProvideServicesAdvertisement
|
||||
self.UARTService = UARTService
|
||||
except ImportError:
|
||||
if debug.enabled:
|
||||
debug('BLE Import error')
|
||||
return # BLE isn't supported on this platform
|
||||
self._ble_last_scan = ticks_ms() - 5000
|
||||
self._connection_count = 0
|
||||
self._split_connected = False
|
||||
self._uart_connection = None
|
||||
self._advertisment = None # Seems to not be used anywhere
|
||||
self._advertising = False
|
||||
self._psave_enable = False
|
||||
|
||||
if self._use_pio:
|
||||
from kmk.transports.pio_uart import PIO_UART
|
||||
|
||||
self.PIO_UART = PIO_UART
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
# Set up name for target side detection and BLE advertisment
|
||||
name = str(getmount('/').label)
|
||||
if self.split_type == SplitType.BLE:
|
||||
if keyboard.hid_type == HIDModes.BLE:
|
||||
self._ble = keyboard._hid_helper.ble
|
||||
else:
|
||||
self._ble = self.BLERadio()
|
||||
self._ble.name = name
|
||||
else:
|
||||
# Try to guess data pins if not supplied
|
||||
if not self.data_pin:
|
||||
self.data_pin = keyboard.data_pin
|
||||
|
||||
# if split side was given, find target from split_side.
|
||||
if self.split_side == SplitSide.LEFT:
|
||||
self._is_target = bool(self.split_target_left)
|
||||
elif self.split_side == SplitSide.RIGHT:
|
||||
self._is_target = not bool(self.split_target_left)
|
||||
else:
|
||||
# Detect split side from name
|
||||
if (
|
||||
self.split_type == SplitType.UART
|
||||
or self.split_type == SplitType.ONEWIRE
|
||||
):
|
||||
self._is_target = runtime.usb_connected
|
||||
elif self.split_type == SplitType.BLE:
|
||||
self._is_target = name.endswith('L') == self.split_target_left
|
||||
|
||||
if name.endswith('L'):
|
||||
self.split_side = SplitSide.LEFT
|
||||
elif name.endswith('R'):
|
||||
self.split_side = SplitSide.RIGHT
|
||||
|
||||
if not self._is_target:
|
||||
keyboard._hid_send_enabled = False
|
||||
|
||||
if self.split_offset is None:
|
||||
self.split_offset = keyboard.matrix[-1].coord_mapping[-1] + 1
|
||||
|
||||
if self.split_type == SplitType.UART and self.data_pin is not None:
|
||||
if self._is_target or not self.uart_flip:
|
||||
if self._use_pio:
|
||||
self._uart = self.PIO_UART(tx=self.data_pin2, rx=self.data_pin)
|
||||
else:
|
||||
self._uart = busio.UART(
|
||||
tx=self.data_pin2, rx=self.data_pin, timeout=self._uart_interval
|
||||
)
|
||||
else:
|
||||
if self._use_pio:
|
||||
self._uart = self.PIO_UART(tx=self.data_pin, rx=self.data_pin2)
|
||||
else:
|
||||
self._uart = busio.UART(
|
||||
tx=self.data_pin, rx=self.data_pin2, timeout=self._uart_interval
|
||||
)
|
||||
|
||||
# Attempt to sanely guess a coord_mapping if one is not provided.
|
||||
if not keyboard.coord_mapping and keyboard.row_pins and keyboard.col_pins:
|
||||
cm = []
|
||||
|
||||
rows_to_calc = len(keyboard.row_pins)
|
||||
cols_to_calc = len(keyboard.col_pins)
|
||||
|
||||
# Flips the col order if PCB is the same but flipped on right
|
||||
cols_rhs = list(range(cols_to_calc))
|
||||
if self.split_flip:
|
||||
cols_rhs = list(reversed(cols_rhs))
|
||||
|
||||
for ridx in range(rows_to_calc):
|
||||
for cidx in range(cols_to_calc):
|
||||
cm.append(cols_to_calc * ridx + cidx)
|
||||
for cidx in cols_rhs:
|
||||
cm.append(cols_to_calc * (rows_to_calc + ridx) + cidx)
|
||||
|
||||
keyboard.coord_mapping = tuple(cm)
|
||||
|
||||
if not keyboard.coord_mapping and debug.enabled:
|
||||
debug('Error: please provide coord_mapping for custom scanner')
|
||||
|
||||
if self.split_side == SplitSide.RIGHT:
|
||||
offset = self.split_offset
|
||||
for matrix in keyboard.matrix:
|
||||
matrix.offset = offset
|
||||
offset += matrix.key_count
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
if self.split_type == SplitType.BLE:
|
||||
self._check_all_connections(keyboard)
|
||||
self._receive_ble(keyboard)
|
||||
elif self.split_type == SplitType.UART:
|
||||
if self._is_target or self.data_pin2:
|
||||
self._receive_uart(keyboard)
|
||||
elif self.split_type == SplitType.ONEWIRE:
|
||||
pass # Protocol needs written
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
if keyboard.matrix_update:
|
||||
if self.split_type == SplitType.UART:
|
||||
if not self._is_target or self.data_pin2:
|
||||
self._send_uart(keyboard.matrix_update)
|
||||
else:
|
||||
pass # explicit pass just for dev sanity...
|
||||
elif self.split_type == SplitType.BLE:
|
||||
self._send_ble(keyboard.matrix_update)
|
||||
elif self.split_type == SplitType.ONEWIRE:
|
||||
pass # Protocol needs written
|
||||
else:
|
||||
if debug.enabled:
|
||||
debug('Unexpected case in after_matrix_scan')
|
||||
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
if not self._is_target:
|
||||
keyboard.hid_pending = False
|
||||
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
if self.split_type == SplitType.BLE:
|
||||
if self._uart_connection and not self._psave_enable:
|
||||
self._uart_connection.connection_interval = self._uart_interval
|
||||
self._psave_enable = True
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
if self.split_type == SplitType.BLE:
|
||||
if self._uart_connection and self._psave_enable:
|
||||
self._uart_connection.connection_interval = 11.25
|
||||
self._psave_enable = False
|
||||
|
||||
def _check_all_connections(self, keyboard):
|
||||
'''Validates the correct number of BLE connections'''
|
||||
self._previous_connection_count = self._connection_count
|
||||
self._connection_count = len(self._ble.connections)
|
||||
if self._is_target:
|
||||
if self._advertising or not self._check_if_split_connected():
|
||||
self._target_advertise()
|
||||
elif self._connection_count < 2 and keyboard.hid_type == HIDModes.BLE:
|
||||
keyboard._hid_helper.start_advertising()
|
||||
|
||||
elif not self._is_target and self._connection_count < 1:
|
||||
self._initiator_scan()
|
||||
|
||||
def _check_if_split_connected(self):
|
||||
# I'm looking for a way how to recognize which connection is on and which one off
|
||||
# For now, I found that service name relation to having other CP device
|
||||
if self._connection_count == 0:
|
||||
return False
|
||||
if self._connection_count == 2:
|
||||
self._split_connected = True
|
||||
return True
|
||||
|
||||
# Polling this takes some time so I check only if connection_count changed
|
||||
if self._previous_connection_count == self._connection_count:
|
||||
return self._split_connected
|
||||
|
||||
bleio_connection = self._ble.connections[0]._bleio_connection
|
||||
connection_services = bleio_connection.discover_remote_services()
|
||||
for service in connection_services:
|
||||
if str(service.uuid).startswith("UUID('adaf0001"):
|
||||
self._split_connected = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def _initiator_scan(self):
|
||||
'''Scans for target device'''
|
||||
self._uart = None
|
||||
self._uart_connection = None
|
||||
# See if any existing connections are providing UARTService.
|
||||
self._connection_count = len(self._ble.connections)
|
||||
if self._connection_count > 0 and not self._uart:
|
||||
for connection in self._ble.connections:
|
||||
if self.UARTService in connection:
|
||||
self._uart_connection = connection
|
||||
self._uart_connection.connection_interval = 11.25
|
||||
self._uart = self._uart_connection[self.UARTService]
|
||||
break
|
||||
|
||||
if not self._uart:
|
||||
if debug.enabled:
|
||||
debug('Scanning')
|
||||
self._ble.stop_scan()
|
||||
for adv in self._ble.start_scan(
|
||||
self.ProvideServicesAdvertisement, timeout=20
|
||||
):
|
||||
if debug.enabled:
|
||||
debug('Scanning')
|
||||
if self.UARTService in adv.services and adv.rssi > -70:
|
||||
self._uart_connection = self._ble.connect(adv)
|
||||
self._uart_connection.connection_interval = 11.25
|
||||
self._uart = self._uart_connection[self.UARTService]
|
||||
self._ble.stop_scan()
|
||||
if debug.enabled:
|
||||
debug('Scan complete')
|
||||
break
|
||||
self._ble.stop_scan()
|
||||
|
||||
def _target_advertise(self):
|
||||
'''Advertises the target for the initiator to find'''
|
||||
# Give previous advertising some time to complete
|
||||
if self._advertising:
|
||||
if self._check_if_split_connected():
|
||||
if debug.enabled:
|
||||
debug('Advertising complete')
|
||||
self._ble.stop_advertising()
|
||||
self._advertising = False
|
||||
return
|
||||
|
||||
if not self.ble_rescan_timer():
|
||||
return
|
||||
|
||||
if debug.enabled:
|
||||
debug('Advertising not answered')
|
||||
|
||||
self._ble.stop_advertising()
|
||||
if debug.enabled:
|
||||
debug('Advertising')
|
||||
# Uart must not change on this connection if reconnecting
|
||||
if not self._uart:
|
||||
self._uart = self.UARTService()
|
||||
advertisement = self.ProvideServicesAdvertisement(self._uart)
|
||||
|
||||
self._ble.start_advertising(advertisement)
|
||||
self._advertising = True
|
||||
self.ble_time_reset()
|
||||
|
||||
def ble_rescan_timer(self):
|
||||
'''If true, the rescan timer is up'''
|
||||
return not bool(check_deadline(ticks_ms(), self._ble_last_scan, 5000))
|
||||
|
||||
def ble_time_reset(self):
|
||||
'''Resets the rescan timer'''
|
||||
self._ble_last_scan = ticks_ms()
|
||||
|
||||
def _serialize_update(self, update):
|
||||
buffer = bytearray(2)
|
||||
buffer[0] = update.key_number
|
||||
buffer[1] = update.pressed
|
||||
return buffer
|
||||
|
||||
def _deserialize_update(self, update):
|
||||
kevent = KeyEvent(key_number=update[0], pressed=update[1])
|
||||
return kevent
|
||||
|
||||
def _send_ble(self, update):
|
||||
if self._uart:
|
||||
try:
|
||||
self._uart.write(self._serialize_update(update))
|
||||
except OSError:
|
||||
try:
|
||||
self._uart.disconnect()
|
||||
except: # noqa: E722
|
||||
if debug.enabled:
|
||||
debug('UART disconnect failed')
|
||||
|
||||
if debug.enabled:
|
||||
debug('Connection error')
|
||||
self._uart_connection = None
|
||||
self._uart = None
|
||||
|
||||
def _receive_ble(self, keyboard):
|
||||
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
|
||||
while self._uart.in_waiting >= 2:
|
||||
update = self._deserialize_update(self._uart.read(2))
|
||||
self._uart_buffer.append(update)
|
||||
if self._uart_buffer:
|
||||
keyboard.secondary_matrix_update = self._uart_buffer.pop(0)
|
||||
|
||||
def _checksum(self, update):
|
||||
checksum = bytes([sum(update) & 0xFF])
|
||||
|
||||
return checksum
|
||||
|
||||
def _send_uart(self, update):
|
||||
# Change offsets depending on where the data is going to match the correct
|
||||
# matrix location of the receiever
|
||||
if self._uart is not None:
|
||||
update = self._serialize_update(update)
|
||||
self._uart.write(self.uart_header)
|
||||
self._uart.write(update)
|
||||
self._uart.write(self._checksum(update))
|
||||
|
||||
def _receive_uart(self, keyboard):
|
||||
if self._uart is not None and self._uart.in_waiting > 0 or self._uart_buffer:
|
||||
if self._uart.in_waiting >= 60:
|
||||
# This is a dirty hack to prevent crashes in unrealistic cases
|
||||
import microcontroller
|
||||
|
||||
microcontroller.reset()
|
||||
|
||||
while self._uart.in_waiting >= 4:
|
||||
# Check the header
|
||||
if self._uart.read(1) == self.uart_header:
|
||||
update = self._uart.read(2)
|
||||
|
||||
# check the checksum
|
||||
if self._checksum(update) == self._uart.read(1):
|
||||
self._uart_buffer.append(self._deserialize_update(update))
|
||||
if self._uart_buffer:
|
||||
keyboard.secondary_matrix_update = self._uart_buffer.pop(0)
|
||||
107
hackpads/Kalpad/firmware/kmk/modules/steno.py
Normal file
107
hackpads/Kalpad/firmware/kmk/modules/steno.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import usb_cdc
|
||||
|
||||
from kmk.keys import make_key
|
||||
from kmk.modules import Module
|
||||
|
||||
# key order from https://github.com/openstenoproject/plover/blob/main/plover/machine/geminipr.py
|
||||
# do not rearrange
|
||||
STENO_KEYS = (
|
||||
'STN_FN',
|
||||
'STN_N1',
|
||||
'STN_N2',
|
||||
'STN_N3',
|
||||
'STN_N4',
|
||||
'STN_N5',
|
||||
'STN_N6',
|
||||
'STN_LS1',
|
||||
'STN_LS2',
|
||||
'STN_LT',
|
||||
'STN_LK',
|
||||
'STN_LP',
|
||||
'STN_LW',
|
||||
'STN_LH',
|
||||
'STN_LR',
|
||||
'STN_A',
|
||||
'STN_O',
|
||||
'STN_AS1',
|
||||
'STN_AS2',
|
||||
'STN_RES1',
|
||||
'STN_RES2',
|
||||
'STN_PWR',
|
||||
'STN_AS3',
|
||||
'STN_AS4',
|
||||
'STN_E',
|
||||
'STN_U',
|
||||
'STN_RF',
|
||||
'STN_RR',
|
||||
'STN_RP',
|
||||
'STN_RB',
|
||||
'STN_RL',
|
||||
'STN_RG',
|
||||
'STN_RT',
|
||||
'STN_RS',
|
||||
'STN_RD',
|
||||
'STN_N7',
|
||||
'STN_N8',
|
||||
'STN_N9',
|
||||
'STN_NA',
|
||||
'STN_NB',
|
||||
'STN_NC',
|
||||
'STN_RZ',
|
||||
)
|
||||
|
||||
|
||||
class Steno(Module):
|
||||
def __init__(self):
|
||||
self._should_write = False
|
||||
|
||||
self._buffer = bytearray(6)
|
||||
self._initialize_buffer()
|
||||
|
||||
for idx, key in enumerate(STENO_KEYS):
|
||||
make_key(
|
||||
code=((idx // 7) << 8) | (0x40 >> (idx % 7)),
|
||||
names=(key,),
|
||||
on_press=self._steno_press,
|
||||
on_release=self._steno_release,
|
||||
)
|
||||
|
||||
def _initialize_buffer(self):
|
||||
self._buffer[:] = b'\x80\x00\x00\x00\x00\x00'
|
||||
|
||||
# flip a key's bit in the buffer
|
||||
def _steno_press(self, key, *_):
|
||||
self._should_write = True
|
||||
self._buffer[key.code >> 8] |= key.code & 0xFF
|
||||
|
||||
# send all keys that were pressed, and reset the buffer
|
||||
def _steno_release(self, *_):
|
||||
if self._should_write:
|
||||
usb_cdc.data.write(self._buffer)
|
||||
|
||||
self._should_write = False
|
||||
self._initialize_buffer()
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
pass
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
pass
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
pass
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
pass
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
pass
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
pass
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
pass
|
||||
151
hackpads/Kalpad/firmware/kmk/modules/sticky_keys.py
Normal file
151
hackpads/Kalpad/firmware/kmk/modules/sticky_keys.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from micropython import const
|
||||
|
||||
from kmk.keys import Key, make_argumented_key
|
||||
from kmk.modules import Module
|
||||
from kmk.utils import Debug
|
||||
|
||||
debug = Debug(__name__)
|
||||
|
||||
|
||||
_SK_IDLE = const(0)
|
||||
_SK_PRESSED = const(1)
|
||||
_SK_RELEASED = const(2)
|
||||
_SK_HOLD = const(3)
|
||||
_SK_STICKY = const(4)
|
||||
|
||||
|
||||
class StickyKey(Key):
|
||||
def __init__(self, key, defer_release=False, retap_cancel=True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.key = key
|
||||
self.defer_release = defer_release
|
||||
self.timeout = None
|
||||
self.state = _SK_IDLE
|
||||
self.retap_cancel = retap_cancel
|
||||
|
||||
|
||||
class StickyKeys(Module):
|
||||
def __init__(self, release_after=1000):
|
||||
self.active_keys = []
|
||||
self.release_after = release_after
|
||||
|
||||
make_argumented_key(
|
||||
names=('SK', 'STICKY'),
|
||||
constructor=StickyKey,
|
||||
on_press=self.on_press,
|
||||
on_release=self.on_release,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, current_key, is_pressed, int_coord):
|
||||
delay_current = False
|
||||
|
||||
for key in self.active_keys.copy():
|
||||
# Ignore keys that will resolve to and emit a different key
|
||||
# eventually, potentially triggering twice.
|
||||
# Handle interactions among sticky keys (stacking) in `on_press`
|
||||
# instead of `process_key` to avoid race conditions / causal
|
||||
# reordering when resetting timeouts.
|
||||
if (
|
||||
isinstance(current_key, StickyKey)
|
||||
or current_key.__class__.__name__ == 'TapDanceKey'
|
||||
or current_key.__class__.__name__ == 'HoldTapKey'
|
||||
or current_key.__class__.__name__ == 'LayerTapKeyMeta'
|
||||
):
|
||||
continue
|
||||
|
||||
if key.state == _SK_PRESSED and is_pressed:
|
||||
key.state = _SK_HOLD
|
||||
elif key.state == _SK_RELEASED and is_pressed:
|
||||
key.state = _SK_STICKY
|
||||
elif key.state == _SK_STICKY:
|
||||
# Defer sticky release until last other key is released.
|
||||
if key.defer_release:
|
||||
if not is_pressed and len(keyboard._coordkeys_pressed) == 0:
|
||||
self.deactivate(keyboard, key)
|
||||
# Release sticky key; if it's a new key pressed: delay
|
||||
# propagation until after the sticky release.
|
||||
else:
|
||||
self.deactivate(keyboard, key)
|
||||
delay_current = is_pressed
|
||||
|
||||
if delay_current:
|
||||
keyboard.resume_process_key(self, current_key, is_pressed, int_coord, False)
|
||||
else:
|
||||
return current_key
|
||||
|
||||
def set_timeout(self, keyboard, key):
|
||||
key.timeout = keyboard.set_timeout(
|
||||
self.release_after,
|
||||
lambda: self.on_release_after(keyboard, key),
|
||||
)
|
||||
|
||||
def on_press(self, key, keyboard, *args, **kwargs):
|
||||
# Let sticky keys stack while renewing timeouts.
|
||||
for sk in self.active_keys:
|
||||
keyboard.cancel_timeout(sk.timeout)
|
||||
|
||||
# If active sticky key is tapped again, cancel.
|
||||
if key.retap_cancel and (key.state == _SK_RELEASED or key.state == _SK_STICKY):
|
||||
self.deactivate(keyboard, key)
|
||||
# Reset on repeated taps.
|
||||
elif key.state != _SK_IDLE:
|
||||
key.state = _SK_PRESSED
|
||||
else:
|
||||
self.activate(keyboard, key)
|
||||
|
||||
for sk in self.active_keys:
|
||||
self.set_timeout(keyboard, sk)
|
||||
|
||||
def on_release(self, key, keyboard, *args, **kwargs):
|
||||
# No interrupt or timeout happend, mark key as RELEASED, ready to get
|
||||
# STICKY.
|
||||
if key.state == _SK_PRESSED:
|
||||
key.state = _SK_RELEASED
|
||||
# Key in HOLD state is handled like a regular release.
|
||||
elif key.state == _SK_HOLD:
|
||||
keyboard.cancel_timeout(key.timeout)
|
||||
self.deactivate(keyboard, key)
|
||||
|
||||
def on_release_after(self, keyboard, key):
|
||||
# Key is still pressed but nothing else happend: set to HOLD.
|
||||
if key.state == _SK_PRESSED:
|
||||
key.state = _SK_HOLD
|
||||
keyboard.cancel_timeout(key.timeout)
|
||||
# Key got released but nothing else happend: deactivate.
|
||||
elif key.state == _SK_RELEASED:
|
||||
self.deactivate(keyboard, key)
|
||||
|
||||
def activate(self, keyboard, key):
|
||||
if debug.enabled:
|
||||
debug('activate')
|
||||
key.state = _SK_PRESSED
|
||||
self.active_keys.insert(0, key)
|
||||
keyboard.resume_process_key(self, key.key, True)
|
||||
|
||||
def deactivate(self, keyboard, key):
|
||||
if debug.enabled:
|
||||
debug('deactivate')
|
||||
key.state = _SK_IDLE
|
||||
self.active_keys.remove(key)
|
||||
keyboard.resume_process_key(self, key.key, False)
|
||||
64
hackpads/Kalpad/firmware/kmk/modules/sticky_mod.py
Normal file
64
hackpads/Kalpad/firmware/kmk/modules/sticky_mod.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from kmk.keys import Key, make_argumented_key
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class StickyModKey(Key):
|
||||
def __init__(self, key, mod, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.key = key
|
||||
self.mod = mod
|
||||
|
||||
|
||||
class StickyMod(Module):
|
||||
def __init__(self):
|
||||
self._active = False
|
||||
self._active_key = None
|
||||
make_argumented_key(
|
||||
names=('SM',),
|
||||
constructor=StickyModKey,
|
||||
on_press=self.sm_pressed,
|
||||
on_release=self.sm_released,
|
||||
)
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
# release previous key if any other key is pressed
|
||||
if self._active and self._active_key is not None:
|
||||
self.release_key(keyboard, self._active_key)
|
||||
|
||||
return key
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def release_key(self, keyboard, key):
|
||||
keyboard.process_key(key.mod, False)
|
||||
self._active = False
|
||||
self._active_key = None
|
||||
|
||||
def sm_pressed(self, key, keyboard, *args, **kwargs):
|
||||
keyboard.process_key(key.mod, True)
|
||||
keyboard.process_key(key.key, True)
|
||||
self._active_key = key
|
||||
|
||||
def sm_released(self, key, keyboard, *args, **kwargs):
|
||||
keyboard.process_key(key.key, False)
|
||||
self._active_key = key
|
||||
self._active = True
|
||||
229
hackpads/Kalpad/firmware/kmk/modules/string_substitution.py
Normal file
229
hackpads/Kalpad/firmware/kmk/modules/string_substitution.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
try:
|
||||
from typing import Optional
|
||||
except ImportError:
|
||||
# we're not in a dev environment, so we don't need to worry about typing
|
||||
pass
|
||||
from micropython import const
|
||||
|
||||
from kmk.keys import KC, Key, ModifiedKey, ModifierKey
|
||||
from kmk.modules import Module
|
||||
|
||||
|
||||
class State:
|
||||
LISTENING = const(0)
|
||||
DELETING = const(1)
|
||||
SENDING = const(2)
|
||||
IGNORING = const(3)
|
||||
|
||||
|
||||
class Character:
|
||||
'''Helper class for making a left-shifted key identical to a right-shifted key'''
|
||||
|
||||
is_shifted: bool = False
|
||||
|
||||
def __init__(self, key_code: Key, is_shifted: bool) -> None:
|
||||
self.is_shifted = is_shifted
|
||||
self.key_code = KC.LSHIFT(key_code) if is_shifted else key_code
|
||||
|
||||
def __eq__(self, other: any) -> bool: # type: ignore
|
||||
try:
|
||||
if isinstance(self.key_code, ModifiedKey):
|
||||
return (
|
||||
self.key_code.key.code == other.key_code.key.code
|
||||
and self.is_shifted == other.is_shifted
|
||||
)
|
||||
return (
|
||||
self.key_code.code == other.key_code.code
|
||||
and self.is_shifted == other.is_shifted
|
||||
)
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
class Phrase:
|
||||
'''Manages a collection of characters and keeps an index of them so that potential matches can be tracked'''
|
||||
|
||||
def __init__(self, string: str) -> None:
|
||||
self._characters: list[Character] = []
|
||||
self._index: int = 0
|
||||
for char in string:
|
||||
key_code = KC[char]
|
||||
if key_code == KC.NO:
|
||||
raise ValueError(f'Invalid character in dictionary: {char}')
|
||||
shifted = char.isupper() or (
|
||||
isinstance(key_code, ModifiedKey) and key_code.modifier.code == 0x02
|
||||
)
|
||||
self._characters.append(Character(key_code, shifted))
|
||||
|
||||
def next_character(self) -> None:
|
||||
'''Increment the current index for this phrase'''
|
||||
if not self.index_at_end():
|
||||
self._index += 1
|
||||
|
||||
def get_character_at_index(self, index: int) -> Character:
|
||||
'''Returns the character at the given index'''
|
||||
return self._characters[index]
|
||||
|
||||
def get_character_at_current_index(self) -> Character:
|
||||
'''Returns the character at the current index for this phrase'''
|
||||
return self._characters[self._index]
|
||||
|
||||
def reset_index(self) -> None:
|
||||
'''Reset the index to the start of the phrase'''
|
||||
self._index = 0
|
||||
|
||||
def index_at_end(self) -> bool:
|
||||
'''Returns True if the index is at the end of the phrase'''
|
||||
return self._index == len(self._characters)
|
||||
|
||||
def character_is_at_current_index(self, character) -> bool:
|
||||
'''Returns True if the given character is the next character in the phrase'''
|
||||
return self.get_character_at_current_index() == character
|
||||
|
||||
|
||||
class Rule:
|
||||
'''Represents the relationship between a phrase to be substituted and its substitution'''
|
||||
|
||||
def __init__(self, to_substitute: Phrase, substitution: Phrase) -> None:
|
||||
self.to_substitute: Phrase = to_substitute
|
||||
self.substitution: Phrase = substitution
|
||||
|
||||
def restart(self) -> None:
|
||||
'''Resets this rule's to_substitute and substitution phrases'''
|
||||
self.to_substitute.reset_index()
|
||||
self.substitution.reset_index()
|
||||
|
||||
|
||||
class StringSubstitution(Module):
|
||||
_shifted: bool = False
|
||||
_rules: list = []
|
||||
_state: State = State.LISTENING
|
||||
_matched_rule: Optional[Phrase] = None
|
||||
_active_modifiers: list[ModifierKey] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dictionary: dict,
|
||||
):
|
||||
for key, value in dictionary.items():
|
||||
self._rules.append(Rule(Phrase(key), Phrase(value)))
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
if key is KC.LSFT or key is KC.RSFT:
|
||||
if is_pressed:
|
||||
self._shifted = True
|
||||
else:
|
||||
self._shifted = False
|
||||
|
||||
# control ignoring state if the key is a non-shift modifier
|
||||
elif type(key) is ModifierKey:
|
||||
if is_pressed and key not in self._active_modifiers:
|
||||
self._active_modifiers.append(key)
|
||||
self._state = State.IGNORING
|
||||
elif key in self._active_modifiers:
|
||||
self._active_modifiers.remove(key)
|
||||
if not self._active_modifiers:
|
||||
self._state = State.LISTENING
|
||||
# reset rules because pressing a modifier combination
|
||||
# should interrupt any current matches
|
||||
for rule in self._rules:
|
||||
rule.restart()
|
||||
|
||||
if not self._state == State.LISTENING:
|
||||
return key
|
||||
|
||||
if is_pressed:
|
||||
character = Character(key, self._shifted)
|
||||
|
||||
# run through the dictionary to check for a possible match on each new keypress
|
||||
for rule in self._rules:
|
||||
if rule.to_substitute.character_is_at_current_index(character):
|
||||
rule.to_substitute.next_character()
|
||||
else:
|
||||
rule.restart()
|
||||
# if character is not a match at the current index,
|
||||
# it could still be a match at the start of the sequence
|
||||
# so redo the check after resetting the sequence
|
||||
if rule.to_substitute.character_is_at_current_index(character):
|
||||
rule.to_substitute.next_character()
|
||||
# we've matched all of the characters in a phrase to be substituted
|
||||
if rule.to_substitute.index_at_end():
|
||||
rule.restart()
|
||||
# set the phrase indexes to where they differ
|
||||
# so that only the characters that differ are replaced
|
||||
for character in rule.to_substitute._characters:
|
||||
if (
|
||||
character
|
||||
== rule.substitution.get_character_at_current_index()
|
||||
):
|
||||
rule.to_substitute.next_character()
|
||||
rule.substitution.next_character()
|
||||
else:
|
||||
break
|
||||
if rule.to_substitute.index_at_end():
|
||||
break
|
||||
self._matched_rule = rule
|
||||
self._state = State.DELETING
|
||||
# if we have a match there's no reason to continue the full key processing, so return out
|
||||
return
|
||||
return key
|
||||
|
||||
def during_bootup(self, keyboard):
|
||||
return
|
||||
|
||||
def before_matrix_scan(self, keyboard):
|
||||
return
|
||||
|
||||
def before_hid_send(self, keyboard):
|
||||
|
||||
if self._state == State.LISTENING:
|
||||
return
|
||||
|
||||
if self._state == State.DELETING:
|
||||
# force-release modifiers so sending the replacement text doesn't interact with them
|
||||
# it should not be possible for any modifiers other than shift to be held upon rule activation
|
||||
# as a modified key won't send a keycode that is matched against the user's dictionary,
|
||||
# but, just in case, we'll release those too
|
||||
modifiers_to_release = [
|
||||
KC.LSFT,
|
||||
KC.RSFT,
|
||||
KC.LCTL,
|
||||
KC.LGUI,
|
||||
KC.LALT,
|
||||
KC.RCTL,
|
||||
KC.RGUI,
|
||||
KC.RALT,
|
||||
]
|
||||
for modifier in modifiers_to_release:
|
||||
keyboard.remove_key(modifier)
|
||||
|
||||
# send backspace taps equivalent to the length of the phrase to be substituted
|
||||
to_substitute: Phrase = self._matched_rule.to_substitute # type: ignore
|
||||
to_substitute.next_character()
|
||||
if not to_substitute.index_at_end():
|
||||
keyboard.tap_key(KC.BSPC)
|
||||
else:
|
||||
self._state = State.SENDING
|
||||
|
||||
if self._state == State.SENDING:
|
||||
substitution = self._matched_rule.substitution # type: ignore
|
||||
if not substitution.index_at_end():
|
||||
keyboard.tap_key(substitution.get_character_at_current_index().key_code)
|
||||
substitution.next_character()
|
||||
else:
|
||||
self._state = State.LISTENING
|
||||
self._matched_rule = None
|
||||
for rule in self._rules:
|
||||
rule.restart()
|
||||
|
||||
def after_hid_send(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_enable(self, keyboard):
|
||||
return
|
||||
|
||||
def on_powersave_disable(self, keyboard):
|
||||
return
|
||||
|
||||
def after_matrix_scan(self, keyboard):
|
||||
return
|
||||
121
hackpads/Kalpad/firmware/kmk/modules/tapdance.py
Normal file
121
hackpads/Kalpad/firmware/kmk/modules/tapdance.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
from kmk.keys import Key, make_argumented_key
|
||||
from kmk.modules.holdtap import ActivationType, HoldTap, HoldTapKey
|
||||
|
||||
|
||||
class TapDanceKey(Key):
|
||||
def __init__(self, *keys, tap_time=None, **kwargs):
|
||||
'''
|
||||
Any key in the tapdance sequence that is not already a holdtap
|
||||
key gets converted to a holdtap key with identical tap and hold
|
||||
attributes.
|
||||
'''
|
||||
super().__init__(**kwargs)
|
||||
self.tap_time = tap_time
|
||||
self.keys = []
|
||||
|
||||
for key in keys:
|
||||
if not isinstance(key, HoldTapKey):
|
||||
ht_key = HoldTapKey(
|
||||
tap=key,
|
||||
hold=key,
|
||||
prefer_hold=True,
|
||||
tap_interrupted=False,
|
||||
tap_time=self.tap_time,
|
||||
)
|
||||
self.keys.append(ht_key)
|
||||
else:
|
||||
self.keys.append(key)
|
||||
self.keys = tuple(self.keys)
|
||||
|
||||
|
||||
class TapDance(HoldTap):
|
||||
def __init__(self):
|
||||
super().__init__(_make_key=False)
|
||||
make_argumented_key(
|
||||
names=('TD',),
|
||||
constructor=TapDanceKey,
|
||||
on_press=self.td_pressed,
|
||||
on_release=self.td_released,
|
||||
)
|
||||
|
||||
self.td_counts = {}
|
||||
|
||||
def process_key(self, keyboard, key, is_pressed, int_coord):
|
||||
if isinstance(key, TapDanceKey):
|
||||
if key in self.td_counts:
|
||||
return key
|
||||
|
||||
for _key, state in self.key_states.copy().items():
|
||||
if state.activated == ActivationType.RELEASED:
|
||||
keyboard.cancel_timeout(state.timeout_key)
|
||||
self.ht_activate_tap(_key, keyboard)
|
||||
self.send_key_buffer(keyboard)
|
||||
self.ht_deactivate_tap(_key, keyboard)
|
||||
keyboard.resume_process_key(self, key, is_pressed, int_coord)
|
||||
key = None
|
||||
|
||||
del self.key_states[_key]
|
||||
del self.td_counts[state.tap_dance]
|
||||
|
||||
if key:
|
||||
key = super().process_key(keyboard, key, is_pressed, int_coord)
|
||||
|
||||
return key
|
||||
|
||||
def td_pressed(self, key, keyboard, *args, **kwargs):
|
||||
# active tap dance
|
||||
if key in self.td_counts:
|
||||
count = self.td_counts[key]
|
||||
kc = key.keys[count]
|
||||
keyboard.cancel_timeout(self.key_states[kc].timeout_key)
|
||||
|
||||
count += 1
|
||||
|
||||
# Tap dance reached the end of the list: send last tap in sequence
|
||||
# and start from the beginning.
|
||||
if count >= len(key.keys):
|
||||
self.key_states[kc].activated = ActivationType.RELEASED
|
||||
self.on_tap_time_expired(kc, keyboard)
|
||||
count = 0
|
||||
else:
|
||||
del self.key_states[kc]
|
||||
|
||||
# new tap dance
|
||||
else:
|
||||
count = 0
|
||||
|
||||
current_key = key.keys[count]
|
||||
|
||||
self.ht_pressed(current_key, keyboard, *args, **kwargs)
|
||||
self.td_counts[key] = count
|
||||
|
||||
# Add the active tap dance to key_states; `on_tap_time_expired` needs
|
||||
# the back-reference.
|
||||
self.key_states[current_key].tap_dance = key
|
||||
|
||||
def td_released(self, key, keyboard, *args, **kwargs):
|
||||
kc = key.keys[self.td_counts[key]]
|
||||
state = self.key_states[kc]
|
||||
if state.activated == ActivationType.HOLD_TIMEOUT:
|
||||
# release hold
|
||||
self.ht_deactivate_hold(kc, keyboard, *args, **kwargs)
|
||||
del self.key_states[kc]
|
||||
del self.td_counts[key]
|
||||
elif state.activated == ActivationType.INTERRUPTED:
|
||||
# release tap
|
||||
self.ht_deactivate_on_interrupt(kc, keyboard, *args, **kwargs)
|
||||
del self.key_states[kc]
|
||||
del self.td_counts[key]
|
||||
else:
|
||||
# keep counting
|
||||
state.activated = ActivationType.RELEASED
|
||||
|
||||
def on_tap_time_expired(self, key, keyboard, *args, **kwargs):
|
||||
# Note: the `key` argument is the current holdtap key in the sequence,
|
||||
# not the tapdance key.
|
||||
state = self.key_states[key]
|
||||
if state.activated == ActivationType.RELEASED:
|
||||
self.ht_activate_tap(key, keyboard, *args, **kwargs)
|
||||
self.send_key_buffer(keyboard)
|
||||
del self.td_counts[state.tap_dance]
|
||||
super().on_tap_time_expired(key, keyboard, *args, **kwargs)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
translate = {
|
||||
'D3': 0,
|
||||
'D2': 1,
|
||||
'D1': 4,
|
||||
'D0': 5,
|
||||
'D4': 6,
|
||||
'C6': 7,
|
||||
'D7': 8,
|
||||
'E6': 9,
|
||||
'B4': 10,
|
||||
'B5': 11,
|
||||
'B6': 12,
|
||||
'B2': 13,
|
||||
'B3': 14,
|
||||
'B1': 15,
|
||||
'F7': 16,
|
||||
'F6': 17,
|
||||
'F5': 18,
|
||||
'F4': 19,
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import board
|
||||
|
||||
# Bit-C-Pro RP2040 pinout for reference, see https://nullbits.co/bit-c-pro/
|
||||
# (unused)
|
||||
pinout = [
|
||||
board.D0, # Enc 3
|
||||
board.D1, # Enc 3
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.D2, # Enc 2
|
||||
board.D3, # Enc 2
|
||||
board.D4, # Row 4 + breakout SDA
|
||||
board.D5, # Row 3 + breakout SCL
|
||||
board.D6, # Row 2
|
||||
board.D7, # Row 1
|
||||
board.D8, # Enc 1
|
||||
board.D9, # Enc 1
|
||||
# Unconnected breakout pins D11, D12, GND, D13, D14
|
||||
board.D21, # WS2812 LEDs labeled D10/GP21 but only board.D21 is defined
|
||||
board.D23, # MOSI - Enc 0
|
||||
board.D20, # MISO - Enc 0
|
||||
board.D22, # SCK - Row 0
|
||||
board.D26, # A0 - Col 3
|
||||
board.D27, # A1 - Col 2
|
||||
board.D28, # A2 - Col 1
|
||||
board.D29, # A3 - Col 0
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
# also defined: board.LED_RED, board.LED_GREEN, and board.LED_BLUE == board.LED
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.TX,
|
||||
board.RX,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.SDA,
|
||||
board.SCL,
|
||||
board.GP04,
|
||||
board.GP05,
|
||||
board.GP06,
|
||||
board.GP07,
|
||||
board.GP08,
|
||||
board.GP09,
|
||||
board.GP21,
|
||||
board.GP23,
|
||||
board.GP20,
|
||||
board.GP22,
|
||||
board.GP26,
|
||||
board.GP27,
|
||||
board.GP28,
|
||||
board.GP29,
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/elite_pi.py
Normal file
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/elite_pi.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.D0,
|
||||
board.D1,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.D2,
|
||||
board.D3,
|
||||
board.D4,
|
||||
board.D5,
|
||||
board.D6,
|
||||
board.D7,
|
||||
board.D8,
|
||||
board.D9,
|
||||
board.D21,
|
||||
board.D23,
|
||||
board.D20,
|
||||
board.D22,
|
||||
board.D26,
|
||||
board.D27,
|
||||
board.D28,
|
||||
board.D29,
|
||||
None, # VCC
|
||||
None, # RUN
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
33
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/frood.py
Normal file
33
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/frood.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.TX,
|
||||
board.RX,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.D2,
|
||||
board.D3,
|
||||
board.D4,
|
||||
board.D5,
|
||||
board.D6,
|
||||
board.D7,
|
||||
board.D8,
|
||||
board.D9,
|
||||
board.D12,
|
||||
board.D13,
|
||||
board.D14,
|
||||
board.D15,
|
||||
board.D16,
|
||||
board.D21,
|
||||
board.MOSI,
|
||||
board.MISO,
|
||||
board.SCK,
|
||||
board.D26,
|
||||
board.D27,
|
||||
board.D28,
|
||||
board.D29,
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/helios.py
Normal file
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/helios.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.TX,
|
||||
board.RX,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.GP2,
|
||||
board.GP3,
|
||||
board.GP4,
|
||||
board.GP5,
|
||||
board.GP6,
|
||||
board.GP7,
|
||||
board.GP8,
|
||||
board.GP9,
|
||||
board.CS,
|
||||
board.SDO,
|
||||
board.SDI,
|
||||
board.SCK,
|
||||
board.GP26,
|
||||
board.GP27,
|
||||
board.GP28,
|
||||
board.GP29,
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/kb2040.py
Normal file
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/kb2040.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.D0,
|
||||
board.D1,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.D2,
|
||||
board.D3,
|
||||
board.D4,
|
||||
board.D5,
|
||||
board.D6,
|
||||
board.D7,
|
||||
board.D8,
|
||||
board.D9,
|
||||
board.D10,
|
||||
board.MOSI,
|
||||
board.MISO,
|
||||
board.SCK,
|
||||
board.A0,
|
||||
board.A1,
|
||||
board.A2,
|
||||
board.A3,
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
43
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/liatris.py
Normal file
43
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/liatris.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
# Left, top->bottom
|
||||
board.TX,
|
||||
board.RX,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.SDA,
|
||||
board.SCL,
|
||||
board.D4,
|
||||
board.D5, # C6
|
||||
board.D6, # D7
|
||||
board.D7, # E6
|
||||
board.D8, # B4
|
||||
board.D9, # B5
|
||||
# Right, bottom->top
|
||||
board.D21, # B6
|
||||
board.D23, # B2
|
||||
board.D20, # B3
|
||||
board.D22, # B1
|
||||
board.D26, # F7
|
||||
board.D27, # F6
|
||||
board.D28, # F5
|
||||
board.D29, # F4
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
# Bottom, left->right
|
||||
board.D12,
|
||||
board.D13,
|
||||
board.D14,
|
||||
board.D15,
|
||||
board.D16,
|
||||
# Internal
|
||||
board.NEOPIXEL,
|
||||
board.VBUS_SENSE,
|
||||
board.POWER_LED,
|
||||
board.I2C,
|
||||
board.SPI,
|
||||
board.UART,
|
||||
]
|
||||
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/nice_nano.py
Normal file
28
hackpads/Kalpad/firmware/kmk/quickpin/pro_micro/nice_nano.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.TX,
|
||||
board.RX,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.SDA,
|
||||
board.SCL,
|
||||
board.P0_22,
|
||||
board.P0_24,
|
||||
board.P1_00,
|
||||
board.P0_11,
|
||||
board.P1_04,
|
||||
board.P1_06,
|
||||
board.P0_09,
|
||||
board.P0_10,
|
||||
board.P1_11,
|
||||
board.P1_13,
|
||||
board.P1_15,
|
||||
board.P0_02,
|
||||
board.P0_29,
|
||||
board.P0_31,
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # Battery+
|
||||
]
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import board
|
||||
|
||||
pinout = [
|
||||
board.TX,
|
||||
board.RX,
|
||||
None, # GND
|
||||
None, # GND
|
||||
board.D2,
|
||||
board.D3,
|
||||
board.D4,
|
||||
board.D5,
|
||||
board.D6,
|
||||
board.D7,
|
||||
board.D8,
|
||||
board.D9,
|
||||
board.D21,
|
||||
board.MOSI,
|
||||
board.MISO,
|
||||
board.SCK,
|
||||
board.D26,
|
||||
board.D27,
|
||||
board.D28,
|
||||
board.D29,
|
||||
None, # 3.3v
|
||||
None, # RST
|
||||
None, # GND
|
||||
None, # RAW
|
||||
]
|
||||
38
hackpads/Kalpad/firmware/kmk/scanners/__init__.py
Normal file
38
hackpads/Kalpad/firmware/kmk/scanners/__init__.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
class DiodeOrientation:
|
||||
'''
|
||||
Orientation of diodes on handwired boards. You can think of:
|
||||
COLUMNS = vertical
|
||||
ROWS = horizontal
|
||||
|
||||
COL2ROW and ROW2COL are equivalent to their meanings in QMK.
|
||||
'''
|
||||
|
||||
COLUMNS = 1
|
||||
ROWS = 0
|
||||
COL2ROW = COLUMNS
|
||||
ROW2COL = ROWS
|
||||
|
||||
|
||||
class Scanner:
|
||||
'''
|
||||
Base class for scanners.
|
||||
'''
|
||||
|
||||
# for split keyboards, the offset value will be assigned in Split module
|
||||
offset = 0
|
||||
|
||||
@property
|
||||
def coord_mapping(self):
|
||||
return tuple(range(self.offset, self.offset + self.key_count))
|
||||
|
||||
@property
|
||||
def key_count(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def scan_for_changes(self):
|
||||
'''
|
||||
Scan for key events and return a key report if an event exists.
|
||||
|
||||
The key report is a byte array with contents [row, col, True if pressed else False]
|
||||
'''
|
||||
raise NotImplementedError
|
||||
144
hackpads/Kalpad/firmware/kmk/scanners/digitalio.py
Normal file
144
hackpads/Kalpad/firmware/kmk/scanners/digitalio.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import digitalio
|
||||
|
||||
from keypad import Event as KeyEvent
|
||||
|
||||
from kmk.scanners import DiodeOrientation, Scanner
|
||||
|
||||
|
||||
def ensure_DIO(x):
|
||||
# __class__.__name__ is used instead of isinstance as the MCP230xx lib
|
||||
# does not use the digitalio.DigitalInOut, but rather a self defined one:
|
||||
# https://github.com/adafruit/Adafruit_CircuitPython_MCP230xx/blob/3f04abbd65ba5fa938fcb04b99e92ae48a8c9406/adafruit_mcp230xx/digital_inout.py#L33
|
||||
if x.__class__.__name__ == 'DigitalInOut':
|
||||
return x
|
||||
else:
|
||||
return digitalio.DigitalInOut(x)
|
||||
|
||||
|
||||
class MatrixScanner(Scanner):
|
||||
def __init__(
|
||||
self,
|
||||
cols,
|
||||
rows,
|
||||
diode_orientation=DiodeOrientation.COL2ROW,
|
||||
pull=digitalio.Pull.UP,
|
||||
rollover_cols_every_rows=None,
|
||||
offset=0,
|
||||
):
|
||||
self.len_cols = len(cols)
|
||||
self.len_rows = len(rows)
|
||||
self.pull = pull
|
||||
self.offset = offset
|
||||
|
||||
# A pin cannot be both a row and column, detect this by combining the
|
||||
# two tuples into a set and validating that the length did not drop
|
||||
#
|
||||
# repr() hackery is because CircuitPython Pin objects are not hashable
|
||||
unique_pins = {repr(c) for c in cols} | {repr(r) for r in rows}
|
||||
assert (
|
||||
len(unique_pins) == self.len_cols + self.len_rows
|
||||
), 'Cannot use a pin as both a column and row'
|
||||
del unique_pins
|
||||
|
||||
self.diode_orientation = diode_orientation
|
||||
|
||||
if self.diode_orientation == DiodeOrientation.COL2ROW:
|
||||
self.anodes = [ensure_DIO(x) for x in cols]
|
||||
self.cathodes = [ensure_DIO(x) for x in rows]
|
||||
self.translate_coords = True
|
||||
elif self.diode_orientation == DiodeOrientation.ROW2COL:
|
||||
self.anodes = [ensure_DIO(x) for x in rows]
|
||||
self.cathodes = [ensure_DIO(x) for x in cols]
|
||||
self.translate_coords = False
|
||||
else:
|
||||
raise ValueError(f'Invalid DiodeOrientation: {self.diode_orienttaion}')
|
||||
|
||||
if self.pull == digitalio.Pull.DOWN:
|
||||
self.outputs = self.anodes
|
||||
self.inputs = self.cathodes
|
||||
elif self.pull == digitalio.Pull.UP:
|
||||
self.outputs = self.cathodes
|
||||
self.inputs = self.anodes
|
||||
self.translate_coords = not self.translate_coords
|
||||
else:
|
||||
raise ValueError(f'Invalid pull: {self.pull}')
|
||||
|
||||
for pin in self.outputs:
|
||||
pin.switch_to_output()
|
||||
|
||||
for pin in self.inputs:
|
||||
pin.switch_to_input(pull=self.pull)
|
||||
|
||||
self.rollover_cols_every_rows = rollover_cols_every_rows
|
||||
if self.rollover_cols_every_rows is None:
|
||||
self.rollover_cols_every_rows = self.len_rows
|
||||
|
||||
self._key_count = self.len_cols * self.len_rows
|
||||
initial_state_value = b'\x01' if self.pull is digitalio.Pull.UP else b'\x00'
|
||||
self.state = bytearray(initial_state_value) * self.key_count
|
||||
|
||||
@property
|
||||
def key_count(self):
|
||||
return self._key_count
|
||||
|
||||
def scan_for_changes(self):
|
||||
'''
|
||||
Poll the matrix for changes and return either None (if nothing updated)
|
||||
or a bytearray (reused in later runs so copy this if you need the raw
|
||||
array itself for some crazy reason) consisting of (row, col, pressed)
|
||||
which are (int, int, bool)
|
||||
'''
|
||||
ba_idx = 0
|
||||
any_changed = False
|
||||
|
||||
for oidx, opin in enumerate(self.outputs):
|
||||
opin.value = self.pull is not digitalio.Pull.UP
|
||||
|
||||
for iidx, ipin in enumerate(self.inputs):
|
||||
# cast to int to avoid
|
||||
#
|
||||
# >>> xyz = bytearray(3)
|
||||
# >>> xyz[2] = True
|
||||
# Traceback (most recent call last):
|
||||
# File "<stdin>", line 1, in <module>
|
||||
# OverflowError: value would overflow a 1 byte buffer
|
||||
#
|
||||
# I haven't dived too far into what causes this, but it's
|
||||
# almost certainly because bool types in Python aren't just
|
||||
# aliases to int values, but are proper pseudo-types
|
||||
new_val = int(ipin.value)
|
||||
old_val = self.state[ba_idx]
|
||||
|
||||
if old_val != new_val:
|
||||
if self.translate_coords:
|
||||
new_oidx = oidx + self.len_cols * (
|
||||
iidx // self.rollover_cols_every_rows
|
||||
)
|
||||
new_iidx = iidx - self.rollover_cols_every_rows * (
|
||||
iidx // self.rollover_cols_every_rows
|
||||
)
|
||||
|
||||
row = new_iidx
|
||||
col = new_oidx
|
||||
else:
|
||||
row = oidx
|
||||
col = iidx
|
||||
|
||||
if self.pull is digitalio.Pull.UP:
|
||||
pressed = not new_val
|
||||
else:
|
||||
pressed = new_val
|
||||
self.state[ba_idx] = new_val
|
||||
|
||||
any_changed = True
|
||||
break
|
||||
|
||||
ba_idx += 1
|
||||
|
||||
opin.value = self.pull is digitalio.Pull.UP
|
||||
if any_changed:
|
||||
break
|
||||
|
||||
if any_changed:
|
||||
key_number = self.len_cols * row + col + self.offset
|
||||
return KeyEvent(key_number, pressed)
|
||||
43
hackpads/Kalpad/firmware/kmk/scanners/encoder.py
Normal file
43
hackpads/Kalpad/firmware/kmk/scanners/encoder.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import keypad
|
||||
import rotaryio
|
||||
|
||||
from kmk.scanners import Scanner
|
||||
|
||||
|
||||
class RotaryioEncoder(Scanner):
|
||||
def __init__(self, pin_a, pin_b, divisor=4):
|
||||
self.encoder = rotaryio.IncrementalEncoder(pin_a, pin_b, divisor)
|
||||
self.position = 0
|
||||
self._pressed = False
|
||||
self._queue = []
|
||||
|
||||
@property
|
||||
def key_count(self):
|
||||
return 2
|
||||
|
||||
def scan_for_changes(self):
|
||||
position = self.encoder.position
|
||||
|
||||
if position != self.position:
|
||||
self._queue.append(position - self.position)
|
||||
self.position = position
|
||||
|
||||
if not self._queue:
|
||||
return
|
||||
|
||||
key_number = self.offset
|
||||
if self._queue[0] > 0:
|
||||
key_number += 1
|
||||
|
||||
if self._pressed:
|
||||
self._queue[0] -= 1 if self._queue[0] > 0 else -1
|
||||
|
||||
if self._queue[0] == 0:
|
||||
self._queue.pop(0)
|
||||
|
||||
self._pressed = False
|
||||
|
||||
else:
|
||||
self._pressed = True
|
||||
|
||||
return keypad.Event(key_number, self._pressed)
|
||||
59
hackpads/Kalpad/firmware/kmk/scanners/keypad.py
Normal file
59
hackpads/Kalpad/firmware/kmk/scanners/keypad.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import keypad
|
||||
|
||||
from kmk.scanners import Scanner
|
||||
|
||||
|
||||
class KeypadScanner(Scanner):
|
||||
'''
|
||||
Translation layer around a CircuitPython 7 keypad scanner.
|
||||
|
||||
:param pin_map: A sequence of (row, column) tuples for each key.
|
||||
:param kp: An instance of the keypad class.
|
||||
'''
|
||||
|
||||
@property
|
||||
def key_count(self):
|
||||
return self.keypad.key_count
|
||||
|
||||
def scan_for_changes(self):
|
||||
'''
|
||||
Scan for key events and return a key report if an event exists.
|
||||
|
||||
The key report is a byte array with contents [row, col, True if pressed else False]
|
||||
'''
|
||||
ev = self.keypad.events.get()
|
||||
if ev and self.offset:
|
||||
return keypad.Event(ev.key_number + self.offset, ev.pressed)
|
||||
return ev
|
||||
|
||||
|
||||
class MatrixScanner(KeypadScanner):
|
||||
'''
|
||||
Row/Column matrix using the CircuitPython 7 keypad scanner.
|
||||
|
||||
:param row_pins: A sequence of pins used for rows.
|
||||
:param col_pins: A sequence of pins used for columns.
|
||||
:param direction: The diode orientation of the matrix.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.keypad = keypad.KeyMatrix(*args, **kwargs)
|
||||
super().__init__()
|
||||
|
||||
|
||||
class KeysScanner(KeypadScanner):
|
||||
'''
|
||||
GPIO-per-key 'matrix' using the native CircuitPython 7 keypad scanner.
|
||||
|
||||
:param pins: An array of arrays of CircuitPython Pin objects, such that pins[r][c] is the pin for row r, column c.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.keypad = keypad.Keys(*args, **kwargs)
|
||||
super().__init__()
|
||||
|
||||
|
||||
class ShiftRegisterKeys(KeypadScanner):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.keypad = keypad.ShiftRegisterKeys(*args, **kwargs)
|
||||
super().__init__()
|
||||
75
hackpads/Kalpad/firmware/kmk/scheduler.py
Normal file
75
hackpads/Kalpad/firmware/kmk/scheduler.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
'''
|
||||
Here we're abusing _asyncios TaskQueue to implement a very simple priority
|
||||
queue task scheduler.
|
||||
Despite documentation, Circuitpython doesn't usually ship with a min-heap
|
||||
module; it does however implement a pairing-heap for `TaskQueue` in native code.
|
||||
'''
|
||||
|
||||
try:
|
||||
from typing import Callable
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from _asyncio import Task, TaskQueue
|
||||
|
||||
from kmk.kmktime import ticks_add, ticks_diff
|
||||
|
||||
_task_queue = TaskQueue()
|
||||
|
||||
|
||||
class PeriodicTaskMeta:
|
||||
def __init__(self, func: Callable[[None], None], period: int) -> None:
|
||||
self._task = Task(self.call)
|
||||
self._coro = func
|
||||
self.period = period
|
||||
|
||||
def call(self) -> None:
|
||||
after_ms = ticks_add(self._task.ph_key, self.period)
|
||||
_task_queue.push_sorted(self._task, after_ms)
|
||||
self._coro()
|
||||
|
||||
def restart(self) -> None:
|
||||
_task_queue.push_sorted(self._task)
|
||||
|
||||
|
||||
def create_task(
|
||||
func: [Callable[[None], None], Task, PeriodicTaskMeta],
|
||||
*,
|
||||
after_ms: int = 0,
|
||||
period_ms: int = 0,
|
||||
) -> [Task, PeriodicTaskMeta]:
|
||||
if isinstance(func, Task):
|
||||
t = r = func
|
||||
elif isinstance(func, PeriodicTaskMeta):
|
||||
r = func
|
||||
t = r._task
|
||||
elif period_ms:
|
||||
r = PeriodicTaskMeta(func, period_ms)
|
||||
t = r._task
|
||||
else:
|
||||
t = r = Task(func)
|
||||
|
||||
if after_ms > 0:
|
||||
_task_queue.push_sorted(t, ticks_add(ticks_ms(), after_ms))
|
||||
elif after_ms == 0:
|
||||
_task_queue.push_head(t)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def get_due_task() -> [Callable, None]:
|
||||
now = ticks_ms()
|
||||
while True:
|
||||
t = _task_queue.peek()
|
||||
if not t or ticks_diff(t.ph_key, now) > 0:
|
||||
break
|
||||
_task_queue.pop_head()
|
||||
yield t.coro
|
||||
|
||||
|
||||
def cancel_task(t: [Task, PeriodicTaskMeta]) -> None:
|
||||
if isinstance(t, PeriodicTaskMeta):
|
||||
t = t._task
|
||||
_task_queue.remove(t)
|
||||
0
hackpads/Kalpad/firmware/kmk/transports/__init__.py
Normal file
0
hackpads/Kalpad/firmware/kmk/transports/__init__.py
Normal file
93
hackpads/Kalpad/firmware/kmk/transports/pio_uart.py
Normal file
93
hackpads/Kalpad/firmware/kmk/transports/pio_uart.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
'''
|
||||
Circuit Python wrapper around PIO implementation of UART
|
||||
Original source of these examples: https://github.com/adafruit/Adafruit_CircuitPython_PIOASM/tree/main/examples (MIT)
|
||||
'''
|
||||
|
||||
import rp2pio
|
||||
from array import array
|
||||
|
||||
'''
|
||||
.program uart_tx
|
||||
.side_set 1 opt
|
||||
; An 8n1 UART transmit program.
|
||||
; OUT pin 0 and side-set pin 0 are both mapped to UART TX pin.
|
||||
pull side 1 [7] ; Assert stop bit, or stall with line in idle state
|
||||
set x, 7 side 0 [7] ; Preload bit counter, assert start bit for 8 clocks
|
||||
bitloop: ; This loop will run 8 times (8n1 UART)
|
||||
out pins, 1 ; Shift 1 bit from OSR to the first OUT pin
|
||||
jmp x-- bitloop [6] ; Each loop iteration is 8 cycles.
|
||||
|
||||
; compiles to:
|
||||
'''
|
||||
tx_code = array('H', [40864, 63271, 24577, 1602])
|
||||
|
||||
|
||||
'''
|
||||
.program uart_rx_mini
|
||||
|
||||
; Minimum viable 8n1 UART receiver. Wait for the start bit, then sample 8 bits
|
||||
; with the correct timing.
|
||||
; IN pin 0 is mapped to the GPIO used as UART RX.
|
||||
; Autopush must be enabled, with a threshold of 8.
|
||||
|
||||
wait 0 pin 0 ; Wait for start bit
|
||||
set x, 7 [10] ; Preload bit counter, delay until eye of first data bit
|
||||
bitloop: ; Loop 8 times
|
||||
in pins, 1 ; Sample data
|
||||
jmp x-- bitloop [6] ; Each iteration is 8 cycles
|
||||
|
||||
; compiles to:
|
||||
'''
|
||||
rx_code = array('H', [8224, 59943, 16385, 1602])
|
||||
|
||||
|
||||
class PIO_UART:
|
||||
def __init__(self, *, tx, rx, baudrate=9600):
|
||||
if tx:
|
||||
self.tx_pio = rp2pio.StateMachine(
|
||||
tx_code,
|
||||
first_out_pin=tx,
|
||||
first_sideset_pin=tx,
|
||||
frequency=8 * baudrate,
|
||||
initial_sideset_pin_state=1,
|
||||
initial_sideset_pin_direction=1,
|
||||
initial_out_pin_state=1,
|
||||
initial_out_pin_direction=1,
|
||||
sideset_enable=True,
|
||||
)
|
||||
if rx:
|
||||
self.rx_pio = rp2pio.StateMachine(
|
||||
rx_code,
|
||||
first_in_pin=rx,
|
||||
frequency=8 * baudrate,
|
||||
auto_push=True,
|
||||
push_threshold=8,
|
||||
)
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
return 0
|
||||
|
||||
@property
|
||||
def baudrate(self):
|
||||
return self.tx_pio.frequency // 8
|
||||
|
||||
@baudrate.setter
|
||||
def baudrate(self, frequency):
|
||||
self.tx_pio.frequency = frequency * 8
|
||||
self.rx_pio.frequency = frequency * 8
|
||||
|
||||
def write(self, buf):
|
||||
return self.tx_pio.write(buf)
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
return self.rx_pio.in_waiting
|
||||
|
||||
def read(self, n):
|
||||
b = bytearray(n)
|
||||
n = self.rx_pio.readinto(b)
|
||||
return b[:n]
|
||||
|
||||
def readinto(self, buf):
|
||||
return self.rx_pio.readinto(buf)
|
||||
49
hackpads/Kalpad/firmware/kmk/utils.py
Normal file
49
hackpads/Kalpad/firmware/kmk/utils.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
try:
|
||||
from typing import Optional
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from supervisor import ticks_ms
|
||||
|
||||
from usb_cdc import console
|
||||
|
||||
|
||||
def clamp(x: int, bottom: int = 0, top: int = 100) -> int:
|
||||
return min(max(bottom, x), top)
|
||||
|
||||
|
||||
_debug_enabled = None
|
||||
|
||||
|
||||
class Debug:
|
||||
'''default usage:
|
||||
debug = Debug(__name__)
|
||||
'''
|
||||
|
||||
def __init__(self, name: str = __name__):
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *message: str, name: Optional[str] = None) -> None:
|
||||
if not name:
|
||||
name = self.name
|
||||
print(ticks_ms(), end=' ')
|
||||
print(name, end=': ')
|
||||
print(*message, sep='')
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
global _debug_enabled
|
||||
if (
|
||||
_debug_enabled is None
|
||||
and console
|
||||
and console.connected
|
||||
and console.out_waiting == 0
|
||||
):
|
||||
return True
|
||||
return bool(_debug_enabled)
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, enabled: bool):
|
||||
global _debug_enabled
|
||||
_debug_enabled = enabled
|
||||
self('debug.enabled=', enabled)
|
||||
50
hackpads/Kalpad/firmware/main.py
Normal file
50
hackpads/Kalpad/firmware/main.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# import all the IOs of your board
|
||||
import board
|
||||
|
||||
# KMK library imports
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.scanners.keypad import MatrixScanner
|
||||
from kmk.keys import KC
|
||||
from kmk.modules.encoder import EncoderHandler
|
||||
|
||||
# main instance of keyboard
|
||||
keyboard = KMKKeyboard()
|
||||
|
||||
# define encoders
|
||||
encoder_handler = EncoderHandler()
|
||||
keyboard.modules.append(encoder_handler)
|
||||
|
||||
encoder_handler.pins = [
|
||||
(1, 2), # first encoder (A, B pins)
|
||||
(3, 4), # second encoder (A, B pins)
|
||||
]
|
||||
|
||||
# define key matrix
|
||||
keyboard.matrix = MatrixScanner(
|
||||
rows=(5, 6, 7),
|
||||
cols=(8, 9, 10, 11)
|
||||
)
|
||||
|
||||
# MARK: - mappings
|
||||
# keycodes: https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/keycodes.md
|
||||
# macros: https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/macros.md
|
||||
|
||||
# map encoders
|
||||
encoder_handler.map = [
|
||||
((KC.LEFT, KC.RIGHT)), # left encoder - brush size
|
||||
((KC.MINUS, KC.EQUAL)), # right encoder - zoom
|
||||
]
|
||||
|
||||
# map matrix
|
||||
keyboard.keymap = [
|
||||
[
|
||||
KC.BRIGHTNESS_DOWN, KC.BRIGHTNESS_UP, KC.LCMD(KC.Z), KC.LCMD(KC.LSFT(KC.Z)), # Row 1: brightness down, brightness up, undo, redo
|
||||
KC.LCMD(KC.X), KC.LCMD(KC.C), KC.LCMD(KC.V), KC.L, # Row 2: selection (Cmd+X), transform (Cmd+C), HSB (Cmd+V), layer menu
|
||||
KC.LCMD, KC.TAB, KC.G, KC.E # Row 3: Cmd, slide over, eyedropper, color menu
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
# start KMK!
|
||||
if __name__ == '__main__':
|
||||
keyboard.go()
|
||||
BIN
hackpads/Kalpad/production/PCB.zip
Normal file
BIN
hackpads/Kalpad/production/PCB.zip
Normal file
Binary file not shown.
1769
hackpads/Kalpad/production/bottom.step
Normal file
1769
hackpads/Kalpad/production/bottom.step
Normal file
File diff suppressed because it is too large
Load diff
50
hackpads/Kalpad/production/main.py
Normal file
50
hackpads/Kalpad/production/main.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# import all the IOs of your board
|
||||
import board
|
||||
|
||||
# KMK library imports
|
||||
from kmk.kmk_keyboard import KMKKeyboard
|
||||
from kmk.scanners.keypad import MatrixScanner
|
||||
from kmk.keys import KC
|
||||
from kmk.modules.encoder import EncoderHandler
|
||||
|
||||
# main instance of keyboard
|
||||
keyboard = KMKKeyboard()
|
||||
|
||||
# define encoders
|
||||
encoder_handler = EncoderHandler()
|
||||
keyboard.modules.append(encoder_handler)
|
||||
|
||||
encoder_handler.pins = [
|
||||
(1, 2), # first encoder (A, B pins)
|
||||
(3, 4), # second encoder (A, B pins)
|
||||
]
|
||||
|
||||
# define key matrix
|
||||
keyboard.matrix = MatrixScanner(
|
||||
rows=(5, 6, 7),
|
||||
cols=(8, 9, 10, 11)
|
||||
)
|
||||
|
||||
# MARK: - mappings
|
||||
# keycodes: https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/keycodes.md
|
||||
# macros: https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/macros.md
|
||||
|
||||
# map encoders
|
||||
encoder_handler.map = [
|
||||
((KC.LEFT, KC.RIGHT)), # left encoder - brush size
|
||||
((KC.MINUS, KC.EQUAL)), # right encoder - zoom
|
||||
]
|
||||
|
||||
# map matrix
|
||||
keyboard.keymap = [
|
||||
[
|
||||
KC.BRIGHTNESS_DOWN, KC.BRIGHTNESS_UP, KC.LCMD(KC.Z), KC.LCMD(KC.LSFT(KC.Z)), # Row 1: brightness down, brightness up, undo, redo
|
||||
KC.LCMD(KC.X), KC.LCMD(KC.C), KC.LCMD(KC.V), KC.L, # Row 2: selection (Cmd+X), transform (Cmd+C), HSB (Cmd+V), layer menu
|
||||
KC.LCMD, KC.TAB, KC.G, KC.E # Row 3: Cmd, slide over, eyedropper, color menu
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
# start KMK!
|
||||
if __name__ == '__main__':
|
||||
keyboard.go()
|
||||
4040
hackpads/Kalpad/production/middle.step
Normal file
4040
hackpads/Kalpad/production/middle.step
Normal file
File diff suppressed because it is too large
Load diff
3132
hackpads/Kalpad/production/top.step
Normal file
3132
hackpads/Kalpad/production/top.step
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue