[{"data":1,"prerenderedAt":2313},["ShallowReactive",2],{"writing-\u002Fwriting\u002Fadding-haptics-to-your-kb":3},{"id":4,"title":5,"body":6,"date":2306,"description":12,"extension":2307,"meta":2308,"navigation":939,"path":2309,"seo":2310,"stem":2311,"__hash__":2312},"writing\u002Fwriting\u002F20250219.adding-haptics-to-your-kb.md","The Symphony of Sensation: Adding Haptic Feedback to Your Keyboard",{"type":7,"value":8,"toc":2289},"minimark",[9,13,26,29,34,100,103,106,109,112,139,142,145,174,177,197,206,210,229,255,260,288,292,295,298,358,361,370,373,423,427,438,441,452,455,465,468,471,501,508,750,758,761,803,820,831,1161,1167,1252,1265,1444,1455,1629,1632,1645,2000,2003,2259,2275,2279,2282,2285],[10,11,12],"p",{},"Most modern phones give you a subtle buzz with each tap on the screen. This haptic feedback makes typing feel more precise and satisfying — yet it's notably absent from most custom keyboards. While the keyboard community has created countless tutorials on nearly every aspect of switches, finding resources on adding haptic feedback is surprisingly difficult.",[14,15,16,17,16,22],"figure",{},"\n  ",[18,19],"img",{"src":20,"alt":21},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002Feos.webp","Eos, a 30-key split ergonomic keyboard.",[23,24,25],"figcaption",{},"The Eos Keyboard, a 30-key split ergonomic board, designed by me!",[10,27,28],{},"This guide covers the full process of adding a haptic system to a custom keyboard. I'll walk through component selection, installation, and writing the ZMK firmware to control the linear resonant actuators (LRAs). Whether you're planning your next build or just curious about what's possible, you'll learn what it takes to add this extra dimension to your typing experience.",[30,31,33],"h2",{"id":32},"table-of-contents","Table of Contents",[35,36,37,45,60,80],"ol",{},[38,39,40],"li",{},[41,42,44],"a",{"href":43},"#why-add-haptic-feedback","Why add Haptic Feedback?",[38,46,47,51],{},[41,48,50],{"href":49},"#understanding-the-schematics","Understanding the Schematics",[52,53,54],"ul",{},[38,55,56],{},[41,57,59],{"href":58},"#component-breakdown","Component Breakdown",[38,61,62,66],{},[41,63,65],{"href":64},"#laying-down-the-circuit-pcb-layout-essentials","PCB Layout Essentials",[52,67,68,74],{},[38,69,70],{},[41,71,73],{"href":72},"#footprints","Footprints",[38,75,76],{},[41,77,79],{"href":78},"#design-tips","Design Tips",[38,81,82,86],{},[41,83,85],{"href":84},"#the-firmware","The Firmware",[52,87,88,94],{},[38,89,90],{},[41,91,93],{"href":92},"#shield-definition","Shield Definition",[38,95,96],{},[41,97,99],{"href":98},"#keymap-definition","Keymap Definition",[30,101,44],{"id":102},"why-add-haptic-feedback",[10,104,105],{},"If you've used a split mechanical keyboard, you're probably familiar with layers and combos. These aren't just convenient features; they're essential for maintaining efficiency with a reduced key count. However, there's a subtle usability cost: they often lack immediate feedback.",[10,107,108],{},"Think about it. When you hold a home row mod key or activate a layer, there's no immediate visual indication on your screen. You only discover if the mod or layer activated correctly after you press the next key.",[10,110,111],{},"This is most useful in a few places:",[52,113,114,121,127,133],{},[38,115,116,120],{},[117,118,119],"strong",{},"Layer activation:"," Confirm that you've switched layers before hitting the next key.",[38,122,123,126],{},[117,124,125],{},"Home row mods:"," Tell when a hold registered as a modifier instead of a tap.",[38,128,129,132],{},[117,130,131],{},"Combos:"," Confirm that the key combination actually fired.",[38,134,135,138],{},[117,136,137],{},"Timing-sensitive keys:"," Make hold-tap behavior easier to learn and adjust.",[10,140,141],{},"The beauty of custom haptics is that you can tune the feedback to your needs. You can set light vibrations for layer changes, stronger ones for combo activations, or different patterns for specific events.",[30,143,50],{"id":144},"understanding-the-schematics",[146,147,148,155],"content-alert",{},[149,150,152],"template",{"v-slot:title":151},"",[10,153,154],{},"Heads up!",[10,156,157,158,163,164,168,169,173],{},"Before diving into custom schematics, consider that ",[41,159,162],{"href":160,"target":161},"https:\u002F\u002Fshop.pimoroni.com\u002Fproducts\u002Fdrv2605l-linear-actuator-haptic-breakout?variant=27859486867539","_blank","Pimoroni"," and ",[41,165,167],{"href":166,"target":161},"https:\u002F\u002Fwww.adafruit.com\u002Fproduct\u002F2305","Adafruit"," offer DRV2605L haptic breakout boards that are perfect for most builds. If you're using these boards, you can skip straight to the ",[41,170,172],{"href":171},"#firmware","firmware"," section.\nThe custom implementation we'll cover here is mainly for builds with tight space constraints, those who want a single-board solution, or if you just enjoy building things from scratch (like me).",[10,175,176],{},"The haptic feedback system consists of three main components:",[35,178,179,185,191],{},[38,180,181,184],{},[117,182,183],{},"MCU:"," The brain of the operation, handling I2C communication and timing control. Lately I've been enjoying the Seeeduino XIAO nRF52840.",[38,186,187,190],{},[117,188,189],{},"DRV2605LDGS Driver:"," A specialized haptic driver IC that generates the precise waveforms needed for the LRA.",[38,192,193,196],{},[117,194,195],{},"Linear Resonant Actuator:"," The physical component that creates the vibration. I prefer using the ELV1411A, but other LRAs work just fine.",[14,198,16,199,16,203],{},[18,200],{"src":201,"alt":202},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002Fhaptic-schematics.webp","Haptic Schematics.",[23,204,205],{},"Schematic showing the haptic feedback circuit. A DRV2605L haptic driver interfaces with the Seeeduino XIAO MCU via I2C and drives an ELV1411A linear resonant actuator (LRA).",[207,208,59],"h3",{"id":209},"component-breakdown",[10,211,212,213,163,217,220,221,224,225,228],{},"The DRV2605L is connected to the MCU via I2C. For the XIAO, the default ",[214,215,216],"code",{},"SDA",[214,218,219],{},"SCL"," lines are at pin ",[214,222,223],{},"P0.04-D4"," and pin ",[214,226,227],{},"P0.05-D5"," respectively. The power connections are as follows:",[52,230,231,237,243,249],{},[38,232,233,236],{},[214,234,235],{},"VDD\u002FVNC"," connects to +3.3V with a 1µF decoupling capacitor (C1).",[38,238,239,242],{},[214,240,241],{},"REG"," pin has a 1µF capacitor (C2) to ground.",[38,244,245,248],{},[214,246,247],{},"IN\u002FTRIG"," is grounded since we're using I2C mode.",[38,250,251,254],{},[214,252,253],{},"EN"," pin is pulled high to enable the device.",[10,256,257],{},[117,258,259],{},"Key Design Considerations:",[52,261,262,271],{},[38,263,264,265,267,268,270],{},"We connect the ",[214,266,253],{}," pin to +3.3V instead of a GPIO. Currently, the ZMK Driver for DRV2605L doesn't support the ",[214,269,253],{}," pin (yet?), so save yourself the extra GPIO 😉. The power draw is also minimal (~2µA).",[38,272,273,274,278,279,282,283,163,285,287],{},"While the ",[41,275,277],{"href":276,"target":161},"https:\u002F\u002Fwww.ti.com\u002Flit\u002Fds\u002Fsymlink\u002Fdrv2605.pdf","datasheet"," specifies a 0.1µF capacitor for ",[214,280,281],{},"VDD",", this is a minimum requirement. We can use 1µF capacitors for both ",[214,284,281],{},[214,286,241],{}," pins to simplify our bill of materials and make hand-soldering easier.",[30,289,291],{"id":290},"laying-down-the-circuit-pcb-layout-essentials","Laying Down the Circuit: PCB Layout Essentials",[10,293,294],{},"After finalizing our schematic, we need to carefully consider how to arrange these components on our PCB. While the circuit itself is straightforward, a few key layout decisions can make the difference between crisp, reliable haptic feedback and inconsistent performance.",[207,296,73],{"id":297},"footprints",[52,299,300,315,330,345],{},[38,301,302,305,306,309,310,314],{},[117,303,304],{},"DRV2605LDGS",": KiCad's default ",[214,307,308],{},"VSSOP-10_3x3mm_P0.5mm"," footprint works well. If you need reversible PCBs, use this ",[41,311,313],{"href":312,"target":161},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FVSSOP-10_3x3mm_P0.5mm_Reversible.kicad_mod","reversible version",".",[38,316,317,320,321,163,325,329],{},[117,318,319],{},"ELV1411A LRA",": You can find both ",[41,322,324],{"href":323,"target":161},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FELV1411A.kicad_mod","standard",[41,326,328],{"href":327,"target":161},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FELV1411A_Reversible.kicad_mod","reversible"," footprints in my repository.",[38,331,332,335,336,339,340,344],{},[117,333,334],{},"Seeeduino XIAO nRF52840",": The XIAO has underside battery pins which require some creativity. Use this ",[41,337,313],{"href":338,"target":161},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FSeeeduino_Xiao_nRF52840_Reversible.kicad_mod"," (requires hot-plate) for reversible PCBs, or this through-hole ",[41,341,343],{"href":342,"target":161},"https:\u002F\u002Fgithub.com\u002Fcrides\u002Fkleeb\u002Fblob\u002Fmaster\u002Fmcu.pretty\u002Fxiao-ble-smd-cutout.kicad_mod","standard version"," for traditional layouts.",[38,346,347,350,351,354,355,314],{},[117,348,349],{},"Capacitors",": KiCad's standard ",[214,352,353],{},"C_1206_3216Metric"," footprints work perfectly here, but here's a ",[41,356,313],{"href":357,"target":161},"https:\u002F\u002Fgithub.com\u002Fflumelabs\u002Fkeebrary\u002Fblob\u002Fmain\u002Fkeebrary.pretty\u002FC_1206_3216Metric_Reversible.kicad_mod",[10,359,360],{},"The reversible footprints are designed for PCBs that use the same design for both left and right halves. For keyboards with dedicated left\u002Fright PCBs or single-piece designs, use the standard versions.",[14,362,16,363,16,367],{},[18,364],{"src":365,"alt":366},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002Fhaptic-pcb-routing.webp","Haptic Kicad Routing of DRV2605L.",[23,368,369],{},"PCB layout example showing key component placement. The DRV2605L (U1) has its decoupling capacitors (C1 and C2) placed close by, with short traces connecting them. On the right is a tightly packed ground pad layout for the LRA, ensuring good mechanical contact and signal integrity.",[207,371,79],{"id":372},"design-tips",[35,374,375,400,413],{},[38,376,377,380],{},[117,378,379],{},"Component Placement",[52,381,382,390],{},[38,383,384,385,163,387,389],{},"The DRV2605L's decoupling capacitors must be placed as close as possible to their respective pins (",[214,386,281],{},[214,388,241],{},").",[38,391,392,393,163,396,399],{},"Keep the ",[214,394,395],{},"LRA+",[214,397,398],{},"LRA-"," traces symmetrical (as much as possible) and avoid crossing them.",[38,401,402,405],{},[117,403,404],{},"Power Routing",[52,406,407,410],{},[38,408,409],{},"If space allows, use wider traces (e.g 0.3mm) for ground and power connections.",[38,411,412],{},"Keep the traces connecting the DRV2605L to its capacitors as short and direct as possible.",[38,414,415,418],{},[117,416,417],{},"LRA Mounting",[52,419,420],{},[38,421,422],{},"The LRA must have solid mechanical contact with the case\u002Fplate to effectively transfer vibrations.",[207,424,426],{"id":425},"assembly","Assembly",[146,428,430,435],{"variant":429},"destructive",[149,431,432],{"v-slot:title":151},[10,433,434],{},"Attention!",[10,436,437],{},"A hot plate is mandatory for this assembly. It is essential for the ELV1411A LRA and potentially for the XIAO, depending on the footprint choice.",[10,439,440],{},"The DRV2605L's tiny VSSOP-10 package makes assembly challenging. Using a stencil and hot plate is strongly recommended, but hand-soldering is possible with generous flux and the drag soldering technique.",[14,442,16,445,16,449],{"className":443},[444],"w-4\u002F5",[18,446],{"src":447,"alt":448},"\u002Fwriting\u002F20250219.adding-haptics-to-your-kb\u002FDRV2605L-Solder.webp","Circuit board closeup showing DRV2605L haptic driver IC.",[23,450,451],{},"Close-up of a PCB showing the DRV2605L haptic driver IC in the VSSOP-10 package.",[10,453,454],{},"The other components (capacitors, LRA, and XIAO) are relatively straightforward to solder. After assembly, use a multimeter to verify there are no shorts between power and ground, and check continuity of the I2C lines.",[146,456,457,462],{},[149,458,459],{"v-slot:title":151},[10,460,461],{},"Tip",[10,463,464],{},"Work under good lighting with magnification. A microscope, or even your phone's camera can be incredibly helpful for inspecting your joints.",[30,466,85],{"id":467},"the-firmware",[10,469,470],{},"Since ZMK mainline doesn't have haptic support yet, we need to use the following three modules.",[52,472,473,483,492],{},[38,474,475],{},[41,476,480],{"href":477,"rel":478},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-drv2605-driver",[479],"nofollow",[214,481,482],{},"zmk-drv2605-driver",[38,484,485],{},[41,486,489],{"href":487,"rel":488},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-output-behavior-listener",[479],[214,490,491],{},"zmk-output-behavior-listener",[38,493,494],{},[41,495,498],{"href":496,"rel":497},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-split-peripheral-output-relay",[479],[214,499,500],{},"zmk-split-peripheral-output-relay",[10,502,503,504,507],{},"To add the modules to your ZMK config, update your ",[214,505,506],{},"config\u002Fwest.yml"," file.",[509,510,514],"pre",{"className":511,"code":512,"language":513,"meta":151,"style":151},"language-yaml shiki shiki-themes github-dark","manifest:\n  remotes:\n    - name: zmkfirmware\n      url-base: https:\u002F\u002Fgithub.com\u002Fzmkfirmware\n    - name: badjeff\n      url-base: https:\u002F\u002Fgithub.com\u002Fbadjeff\n  projects:\n    - name: zmk\n      remote: zmkfirmware\n      revision: v0.1\n      import: app\u002Fwest.yml\n    - name: zmk-output-behavior-listener\n      remote: badjeff\n      revision: d6f2f4c\n    - name: zmk-split-peripheral-output-relay\n      remote: badjeff\n      revision: 014b549\n    - name: zmk-drv2605-driver\n      remote: badjeff\n      revision: 81a386a\n  self:\n    path: config\n","yaml",[214,515,516,529,537,553,564,576,586,594,606,616,627,638,650,659,669,681,690,700,712,721,731,739],{"__ignoreMap":151},[517,518,521,525],"span",{"class":519,"line":520},"line",1,[517,522,524],{"class":523},"s4JwU","manifest",[517,526,528],{"class":527},"s95oV",":\n",[517,530,532,535],{"class":519,"line":531},2,[517,533,534],{"class":523},"  remotes",[517,536,528],{"class":527},[517,538,540,543,546,549],{"class":519,"line":539},3,[517,541,542],{"class":527},"    - ",[517,544,545],{"class":523},"name",[517,547,548],{"class":527},": ",[517,550,552],{"class":551},"sU2Wk","zmkfirmware\n",[517,554,556,559,561],{"class":519,"line":555},4,[517,557,558],{"class":523},"      url-base",[517,560,548],{"class":527},[517,562,563],{"class":551},"https:\u002F\u002Fgithub.com\u002Fzmkfirmware\n",[517,565,567,569,571,573],{"class":519,"line":566},5,[517,568,542],{"class":527},[517,570,545],{"class":523},[517,572,548],{"class":527},[517,574,575],{"class":551},"badjeff\n",[517,577,579,581,583],{"class":519,"line":578},6,[517,580,558],{"class":523},[517,582,548],{"class":527},[517,584,585],{"class":551},"https:\u002F\u002Fgithub.com\u002Fbadjeff\n",[517,587,589,592],{"class":519,"line":588},7,[517,590,591],{"class":523},"  projects",[517,593,528],{"class":527},[517,595,597,599,601,603],{"class":519,"line":596},8,[517,598,542],{"class":527},[517,600,545],{"class":523},[517,602,548],{"class":527},[517,604,605],{"class":551},"zmk\n",[517,607,609,612,614],{"class":519,"line":608},9,[517,610,611],{"class":523},"      remote",[517,613,548],{"class":527},[517,615,552],{"class":551},[517,617,619,622,624],{"class":519,"line":618},10,[517,620,621],{"class":523},"      revision",[517,623,548],{"class":527},[517,625,626],{"class":551},"v0.1\n",[517,628,630,633,635],{"class":519,"line":629},11,[517,631,632],{"class":523},"      import",[517,634,548],{"class":527},[517,636,637],{"class":551},"app\u002Fwest.yml\n",[517,639,641,643,645,647],{"class":519,"line":640},12,[517,642,542],{"class":527},[517,644,545],{"class":523},[517,646,548],{"class":527},[517,648,649],{"class":551},"zmk-output-behavior-listener\n",[517,651,653,655,657],{"class":519,"line":652},13,[517,654,611],{"class":523},[517,656,548],{"class":527},[517,658,575],{"class":551},[517,660,662,664,666],{"class":519,"line":661},14,[517,663,621],{"class":523},[517,665,548],{"class":527},[517,667,668],{"class":551},"d6f2f4c\n",[517,670,672,674,676,678],{"class":519,"line":671},15,[517,673,542],{"class":527},[517,675,545],{"class":523},[517,677,548],{"class":527},[517,679,680],{"class":551},"zmk-split-peripheral-output-relay\n",[517,682,684,686,688],{"class":519,"line":683},16,[517,685,611],{"class":523},[517,687,548],{"class":527},[517,689,575],{"class":551},[517,691,693,695,697],{"class":519,"line":692},17,[517,694,621],{"class":523},[517,696,548],{"class":527},[517,698,699],{"class":551},"014b549\n",[517,701,703,705,707,709],{"class":519,"line":702},18,[517,704,542],{"class":527},[517,706,545],{"class":523},[517,708,548],{"class":527},[517,710,711],{"class":551},"zmk-drv2605-driver\n",[517,713,715,717,719],{"class":519,"line":714},19,[517,716,611],{"class":523},[517,718,548],{"class":527},[517,720,575],{"class":551},[517,722,724,726,728],{"class":519,"line":723},20,[517,725,621],{"class":523},[517,727,548],{"class":527},[517,729,730],{"class":551},"81a386a\n",[517,732,734,737],{"class":519,"line":733},21,[517,735,736],{"class":523},"  self",[517,738,528],{"class":527},[517,740,742,745,747],{"class":519,"line":741},22,[517,743,744],{"class":523},"    path",[517,746,548],{"class":527},[517,748,749],{"class":551},"config\n",[146,751,752],{},[10,753,754,755,757],{},"The ",[214,756,500],{}," module is used for communication of output events between the central and peripheral side. For unibody builds, this module is not required.",[207,759,93],{"id":760},"shield-definition",[10,762,763,764,163,766,768,769,772,773,776,777,779,780,776,783,785,786,788,789,791,792,795,796,788,798,791,800,314],{},"To set up the DRV2605L driver, we need to configure the ",[214,765,216],{},[214,767,219],{}," lines. For a pin labeled ",[214,770,771],{},"PX.Y",", the configuration format is ",[214,774,775],{},"\u003CNRF_PSEL(TWIM_SDA, X, Y)>"," for the ",[214,778,216],{}," line and ",[214,781,782],{},"\u003CNRF_PSEL(TWIM_SCL, X, Y)>",[214,784,219],{}," line. In our case, the ",[214,787,216],{}," line is connected to pin ",[214,790,223],{},", so we use ",[214,793,794],{},"\u003CNRF_PSEL(TWIM_SDA, 0, 4)>",". Similarly, the ",[214,797,219],{},[214,799,227],{},[214,801,802],{},"\u003CNRF_PSEL(TWIM_SCL, 0, 5)>",[10,804,805,806,809,810,813,814,819],{},"Next, we must set up the I2C bus with the DRV2605L driver, specifying it's address. For the XIAO, the ",[214,807,808],{},"&xiao_i2c"," node label is exposed. Similarly, for Pro-Micro compatible boards, you may use the ",[214,811,812],{},"&pro_micro_i2c"," node. Check the ",[41,815,818],{"href":816,"rel":817},"https:\u002F\u002Fzmk.dev\u002Fdocs\u002Fdevelopment\u002Fhardware-integration\u002Fpinctrl#predefined-nodes",[479],"ZMK documentation"," for more details.",[10,821,822,823,826,827,830],{},"Thus, the following code snippet can be added to your ",[214,824,825],{},"board.overlay"," file (for unibody builds), or the ",[214,828,829],{},"board.dtsi"," file (for split keyboards).",[509,832,836],{"className":833,"code":834,"language":835,"meta":151,"style":151},"language-c shiki shiki-themes github-dark","&pinctrl {\n    i2c0_default: i2c0_default {\n        group1 {\n            psels = \u003CNRF_PSEL(TWIM_SDA, 0, 4)>, \u002F\u002F define your SDA pin.\n                    \u003CNRF_PSEL(TWIM_SCL, 0, 5)>; \u002F\u002F define your SCL pin.\n        };\n    };\n\n    i2c0_sleep: i2c0_sleep {\n        group1 {\n            psels = \u003CNRF_PSEL(TWIM_SDA, 0, 4)>, \u002F\u002F define your SDA pin.\n                    \u003CNRF_PSEL(TWIM_SCL, 0, 5)>; \u002F\u002F define your SCL pin.\n            low-power-enable;\n        };\n    };\n};\n\n&xiao_i2c {\n    status = \"okay\";\n    compatible = \"nordic,nrf-twim\";\n\n    drv2605_0: drv2605@5a {\n        compatible = \"ti,drv2605\";\n        reg = \u003C0x5a>;\n        library = \u003C6>;  \u002F\u002F LRA\n        standby-ms = \u003C1000>;\n    };\n};\n","c",[214,837,838,847,852,857,898,925,930,935,941,946,950,976,998,1014,1018,1022,1027,1031,1038,1051,1063,1067,1079,1092,1109,1129,1151,1156],{"__ignoreMap":151},[517,839,840,844],{"class":519,"line":520},[517,841,843],{"class":842},"snl16","&",[517,845,846],{"class":527},"pinctrl {\n",[517,848,849],{"class":519,"line":531},[517,850,851],{"class":527},"    i2c0_default: i2c0_default {\n",[517,853,854],{"class":519,"line":539},[517,855,856],{"class":527},"        group1 {\n",[517,858,859,862,865,868,872,875,879,882,885,888,891,894],{"class":519,"line":555},[517,860,861],{"class":527},"            psels ",[517,863,864],{"class":842},"=",[517,866,867],{"class":842}," \u003C",[517,869,871],{"class":870},"svObZ","NRF_PSEL",[517,873,874],{"class":527},"(TWIM_SDA, ",[517,876,878],{"class":877},"sDLfK","0",[517,880,881],{"class":527},", ",[517,883,884],{"class":877},"4",[517,886,887],{"class":527},")",[517,889,890],{"class":842},">",[517,892,893],{"class":527},",",[517,895,897],{"class":896},"sAwPA"," \u002F\u002F define your SDA pin.\n",[517,899,900,903,905,908,910,912,915,917,919,922],{"class":519,"line":566},[517,901,902],{"class":842},"                    \u003C",[517,904,871],{"class":870},[517,906,907],{"class":527},"(TWIM_SCL, ",[517,909,878],{"class":877},[517,911,881],{"class":527},[517,913,914],{"class":877},"5",[517,916,887],{"class":527},[517,918,890],{"class":842},[517,920,921],{"class":527},";",[517,923,924],{"class":896}," \u002F\u002F define your SCL pin.\n",[517,926,927],{"class":519,"line":578},[517,928,929],{"class":527},"        };\n",[517,931,932],{"class":519,"line":588},[517,933,934],{"class":527},"    };\n",[517,936,937],{"class":519,"line":596},[517,938,940],{"emptyLinePlaceholder":939},true,"\n",[517,942,943],{"class":519,"line":608},[517,944,945],{"class":527},"    i2c0_sleep: i2c0_sleep {\n",[517,947,948],{"class":519,"line":618},[517,949,856],{"class":527},[517,951,952,954,956,958,960,962,964,966,968,970,972,974],{"class":519,"line":629},[517,953,861],{"class":527},[517,955,864],{"class":842},[517,957,867],{"class":842},[517,959,871],{"class":870},[517,961,874],{"class":527},[517,963,878],{"class":877},[517,965,881],{"class":527},[517,967,884],{"class":877},[517,969,887],{"class":527},[517,971,890],{"class":842},[517,973,893],{"class":527},[517,975,897],{"class":896},[517,977,978,980,982,984,986,988,990,992,994,996],{"class":519,"line":640},[517,979,902],{"class":842},[517,981,871],{"class":870},[517,983,907],{"class":527},[517,985,878],{"class":877},[517,987,881],{"class":527},[517,989,914],{"class":877},[517,991,887],{"class":527},[517,993,890],{"class":842},[517,995,921],{"class":527},[517,997,924],{"class":896},[517,999,1000,1003,1006,1009,1011],{"class":519,"line":652},[517,1001,1002],{"class":527},"            low",[517,1004,1005],{"class":842},"-",[517,1007,1008],{"class":527},"power",[517,1010,1005],{"class":842},[517,1012,1013],{"class":527},"enable;\n",[517,1015,1016],{"class":519,"line":661},[517,1017,929],{"class":527},[517,1019,1020],{"class":519,"line":671},[517,1021,934],{"class":527},[517,1023,1024],{"class":519,"line":683},[517,1025,1026],{"class":527},"};\n",[517,1028,1029],{"class":519,"line":692},[517,1030,940],{"emptyLinePlaceholder":939},[517,1032,1033,1035],{"class":519,"line":702},[517,1034,843],{"class":842},[517,1036,1037],{"class":527},"xiao_i2c {\n",[517,1039,1040,1043,1045,1048],{"class":519,"line":714},[517,1041,1042],{"class":527},"    status ",[517,1044,864],{"class":842},[517,1046,1047],{"class":551}," \"okay\"",[517,1049,1050],{"class":527},";\n",[517,1052,1053,1056,1058,1061],{"class":519,"line":723},[517,1054,1055],{"class":527},"    compatible ",[517,1057,864],{"class":842},[517,1059,1060],{"class":551}," \"nordic,nrf-twim\"",[517,1062,1050],{"class":527},[517,1064,1065],{"class":519,"line":733},[517,1066,940],{"emptyLinePlaceholder":939},[517,1068,1069,1072,1076],{"class":519,"line":741},[517,1070,1071],{"class":527},"    drv2605_0: drv2605@",[517,1073,1075],{"class":1074},"s6RL2","5a",[517,1077,1078],{"class":527}," {\n",[517,1080,1082,1085,1087,1090],{"class":519,"line":1081},23,[517,1083,1084],{"class":527},"        compatible ",[517,1086,864],{"class":842},[517,1088,1089],{"class":551}," \"ti,drv2605\"",[517,1091,1050],{"class":527},[517,1093,1095,1098,1100,1103,1105,1107],{"class":519,"line":1094},24,[517,1096,1097],{"class":527},"        reg ",[517,1099,864],{"class":842},[517,1101,1102],{"class":842}," \u003C0x",[517,1104,1075],{"class":877},[517,1106,890],{"class":842},[517,1108,1050],{"class":527},[517,1110,1112,1115,1117,1119,1122,1124,1126],{"class":519,"line":1111},25,[517,1113,1114],{"class":527},"        library ",[517,1116,864],{"class":842},[517,1118,867],{"class":842},[517,1120,1121],{"class":877},"6",[517,1123,890],{"class":842},[517,1125,921],{"class":527},[517,1127,1128],{"class":896},"  \u002F\u002F LRA\n",[517,1130,1132,1135,1137,1140,1142,1144,1147,1149],{"class":519,"line":1131},26,[517,1133,1134],{"class":527},"        standby",[517,1136,1005],{"class":842},[517,1138,1139],{"class":527},"ms ",[517,1141,864],{"class":842},[517,1143,867],{"class":842},[517,1145,1146],{"class":877},"1000",[517,1148,890],{"class":842},[517,1150,1050],{"class":527},[517,1152,1154],{"class":519,"line":1153},27,[517,1155,934],{"class":527},[517,1157,1159],{"class":519,"line":1158},28,[517,1160,1026],{"class":527},[10,1162,1163,1164,1166],{},"Next we define the devices. For unibody builds, add the following snippet to ",[214,1165,825],{},":",[509,1168,1170],{"className":833,"code":1169,"language":835,"meta":151,"style":151},"\u002F {\n    haptic: haptic {\n        compatible = \"zmk,output-haptic-feedback\";\n        #binding-cells = \u003C0>;\n        driver = \"drv2605\";\n        device = \u003C&drv2605_0>;\n    };\n};\n",[214,1171,1172,1179,1184,1195,1215,1227,1244,1248],{"__ignoreMap":151},[517,1173,1174,1177],{"class":519,"line":520},[517,1175,1176],{"class":842},"\u002F",[517,1178,1078],{"class":527},[517,1180,1181],{"class":519,"line":531},[517,1182,1183],{"class":527},"    haptic: haptic {\n",[517,1185,1186,1188,1190,1193],{"class":519,"line":539},[517,1187,1084],{"class":527},[517,1189,864],{"class":842},[517,1191,1192],{"class":551}," \"zmk,output-haptic-feedback\"",[517,1194,1050],{"class":527},[517,1196,1197,1200,1202,1205,1207,1209,1211,1213],{"class":519,"line":555},[517,1198,1199],{"class":527},"        #binding",[517,1201,1005],{"class":842},[517,1203,1204],{"class":527},"cells ",[517,1206,864],{"class":842},[517,1208,867],{"class":842},[517,1210,878],{"class":877},[517,1212,890],{"class":842},[517,1214,1050],{"class":527},[517,1216,1217,1220,1222,1225],{"class":519,"line":566},[517,1218,1219],{"class":527},"        driver ",[517,1221,864],{"class":842},[517,1223,1224],{"class":551}," \"drv2605\"",[517,1226,1050],{"class":527},[517,1228,1229,1232,1234,1237,1240,1242],{"class":519,"line":578},[517,1230,1231],{"class":527},"        device ",[517,1233,864],{"class":842},[517,1235,1236],{"class":842}," \u003C&",[517,1238,1239],{"class":527},"drv2605_0",[517,1241,890],{"class":842},[517,1243,1050],{"class":527},[517,1245,1246],{"class":519,"line":588},[517,1247,934],{"class":527},[517,1249,1250],{"class":519,"line":596},[517,1251,1026],{"class":527},[10,1253,1254,1255,1257,1258,1260,1261,1264],{},"For split builds, ensure that the ",[214,1256,500],{}," module is included in your ",[214,1259,506],{},". Add the following to ",[214,1262,1263],{},"board-left.overlay"," (central side):",[509,1266,1268],{"className":833,"code":1267,"language":835,"meta":151,"style":151},"\u002F{\n    haptic_l: haptic_l {\n        compatible = \"zmk,output-haptic-feedback\";\n        #binding-cells = \u003C0>;\n        driver = \"drv2605\";\n        device = \u003C&drv2605_0>;\n    };\n\n    haptic_r: haptic_r {\n        compatible = \"zmk,output-split-output-relay\";\n        #binding-cells = \u003C0>;\n    };\n\n    output_relay_config_201_l {\n        compatible = \"zmk,split-peripheral-output-relay\";\n        device = \u003C&haptic_r>;\n        relay-channel = \u003C201>;\n    };\n};\n",[214,1269,1270,1277,1282,1292,1310,1320,1334,1338,1342,1347,1358,1376,1380,1384,1389,1400,1415,1436,1440],{"__ignoreMap":151},[517,1271,1272,1274],{"class":519,"line":520},[517,1273,1176],{"class":842},[517,1275,1276],{"class":527},"{\n",[517,1278,1279],{"class":519,"line":531},[517,1280,1281],{"class":527},"    haptic_l: haptic_l {\n",[517,1283,1284,1286,1288,1290],{"class":519,"line":539},[517,1285,1084],{"class":527},[517,1287,864],{"class":842},[517,1289,1192],{"class":551},[517,1291,1050],{"class":527},[517,1293,1294,1296,1298,1300,1302,1304,1306,1308],{"class":519,"line":555},[517,1295,1199],{"class":527},[517,1297,1005],{"class":842},[517,1299,1204],{"class":527},[517,1301,864],{"class":842},[517,1303,867],{"class":842},[517,1305,878],{"class":877},[517,1307,890],{"class":842},[517,1309,1050],{"class":527},[517,1311,1312,1314,1316,1318],{"class":519,"line":566},[517,1313,1219],{"class":527},[517,1315,864],{"class":842},[517,1317,1224],{"class":551},[517,1319,1050],{"class":527},[517,1321,1322,1324,1326,1328,1330,1332],{"class":519,"line":578},[517,1323,1231],{"class":527},[517,1325,864],{"class":842},[517,1327,1236],{"class":842},[517,1329,1239],{"class":527},[517,1331,890],{"class":842},[517,1333,1050],{"class":527},[517,1335,1336],{"class":519,"line":588},[517,1337,934],{"class":527},[517,1339,1340],{"class":519,"line":596},[517,1341,940],{"emptyLinePlaceholder":939},[517,1343,1344],{"class":519,"line":608},[517,1345,1346],{"class":527},"    haptic_r: haptic_r {\n",[517,1348,1349,1351,1353,1356],{"class":519,"line":618},[517,1350,1084],{"class":527},[517,1352,864],{"class":842},[517,1354,1355],{"class":551}," \"zmk,output-split-output-relay\"",[517,1357,1050],{"class":527},[517,1359,1360,1362,1364,1366,1368,1370,1372,1374],{"class":519,"line":629},[517,1361,1199],{"class":527},[517,1363,1005],{"class":842},[517,1365,1204],{"class":527},[517,1367,864],{"class":842},[517,1369,867],{"class":842},[517,1371,878],{"class":877},[517,1373,890],{"class":842},[517,1375,1050],{"class":527},[517,1377,1378],{"class":519,"line":640},[517,1379,934],{"class":527},[517,1381,1382],{"class":519,"line":652},[517,1383,940],{"emptyLinePlaceholder":939},[517,1385,1386],{"class":519,"line":661},[517,1387,1388],{"class":527},"    output_relay_config_201_l {\n",[517,1390,1391,1393,1395,1398],{"class":519,"line":671},[517,1392,1084],{"class":527},[517,1394,864],{"class":842},[517,1396,1397],{"class":551}," \"zmk,split-peripheral-output-relay\"",[517,1399,1050],{"class":527},[517,1401,1402,1404,1406,1408,1411,1413],{"class":519,"line":683},[517,1403,1231],{"class":527},[517,1405,864],{"class":842},[517,1407,1236],{"class":842},[517,1409,1410],{"class":527},"haptic_r",[517,1412,890],{"class":842},[517,1414,1050],{"class":527},[517,1416,1417,1420,1422,1425,1427,1429,1432,1434],{"class":519,"line":692},[517,1418,1419],{"class":527},"        relay",[517,1421,1005],{"class":842},[517,1423,1424],{"class":527},"channel ",[517,1426,864],{"class":842},[517,1428,867],{"class":842},[517,1430,1431],{"class":877},"201",[517,1433,890],{"class":842},[517,1435,1050],{"class":527},[517,1437,1438],{"class":519,"line":702},[517,1439,934],{"class":527},[517,1441,1442],{"class":519,"line":714},[517,1443,1026],{"class":527},[10,1445,1446,1447,1450,1451,1454],{},"Similarly, we add the following to ",[214,1448,1449],{},"board-right.overlay"," (peripheral side). Here, you must create a dummy node ",[214,1452,1453],{},"&haptic_l"," to assist in compiling the firmware:",[509,1456,1458],{"className":833,"code":1457,"language":835,"meta":151,"style":151},"\u002F{\n    \u002F\u002F dummy device to make the overlay compile\n    haptic_l: haptic_l {\n        compatible = \"zmk,output-split-output-relay\";\n        #binding-cells = \u003C0>;\n    };\n\n    haptic_r: haptic_r {\n        compatible = \"zmk,output-haptic-feedback\";\n        #binding-cells = \u003C0>;\n        driver = \"drv2605\";\n        device = \u003C&drv2605_0>;\n    };\n\n    output_relay_config_201_l {\n        compatible = \"zmk,split-peripheral-output-relay\";\n        device = \u003C&haptic_r>;\n        relay-channel = \u003C201>;\n    };\n};\n",[214,1459,1460,1466,1471,1475,1485,1503,1507,1511,1515,1525,1543,1553,1567,1571,1575,1579,1589,1603,1621,1625],{"__ignoreMap":151},[517,1461,1462,1464],{"class":519,"line":520},[517,1463,1176],{"class":842},[517,1465,1276],{"class":527},[517,1467,1468],{"class":519,"line":531},[517,1469,1470],{"class":896},"    \u002F\u002F dummy device to make the overlay compile\n",[517,1472,1473],{"class":519,"line":539},[517,1474,1281],{"class":527},[517,1476,1477,1479,1481,1483],{"class":519,"line":555},[517,1478,1084],{"class":527},[517,1480,864],{"class":842},[517,1482,1355],{"class":551},[517,1484,1050],{"class":527},[517,1486,1487,1489,1491,1493,1495,1497,1499,1501],{"class":519,"line":566},[517,1488,1199],{"class":527},[517,1490,1005],{"class":842},[517,1492,1204],{"class":527},[517,1494,864],{"class":842},[517,1496,867],{"class":842},[517,1498,878],{"class":877},[517,1500,890],{"class":842},[517,1502,1050],{"class":527},[517,1504,1505],{"class":519,"line":578},[517,1506,934],{"class":527},[517,1508,1509],{"class":519,"line":588},[517,1510,940],{"emptyLinePlaceholder":939},[517,1512,1513],{"class":519,"line":596},[517,1514,1346],{"class":527},[517,1516,1517,1519,1521,1523],{"class":519,"line":608},[517,1518,1084],{"class":527},[517,1520,864],{"class":842},[517,1522,1192],{"class":551},[517,1524,1050],{"class":527},[517,1526,1527,1529,1531,1533,1535,1537,1539,1541],{"class":519,"line":618},[517,1528,1199],{"class":527},[517,1530,1005],{"class":842},[517,1532,1204],{"class":527},[517,1534,864],{"class":842},[517,1536,867],{"class":842},[517,1538,878],{"class":877},[517,1540,890],{"class":842},[517,1542,1050],{"class":527},[517,1544,1545,1547,1549,1551],{"class":519,"line":629},[517,1546,1219],{"class":527},[517,1548,864],{"class":842},[517,1550,1224],{"class":551},[517,1552,1050],{"class":527},[517,1554,1555,1557,1559,1561,1563,1565],{"class":519,"line":640},[517,1556,1231],{"class":527},[517,1558,864],{"class":842},[517,1560,1236],{"class":842},[517,1562,1239],{"class":527},[517,1564,890],{"class":842},[517,1566,1050],{"class":527},[517,1568,1569],{"class":519,"line":652},[517,1570,934],{"class":527},[517,1572,1573],{"class":519,"line":661},[517,1574,940],{"emptyLinePlaceholder":939},[517,1576,1577],{"class":519,"line":671},[517,1578,1388],{"class":527},[517,1580,1581,1583,1585,1587],{"class":519,"line":683},[517,1582,1084],{"class":527},[517,1584,864],{"class":842},[517,1586,1397],{"class":551},[517,1588,1050],{"class":527},[517,1590,1591,1593,1595,1597,1599,1601],{"class":519,"line":692},[517,1592,1231],{"class":527},[517,1594,864],{"class":842},[517,1596,1236],{"class":842},[517,1598,1410],{"class":527},[517,1600,890],{"class":842},[517,1602,1050],{"class":527},[517,1604,1605,1607,1609,1611,1613,1615,1617,1619],{"class":519,"line":702},[517,1606,1419],{"class":527},[517,1608,1005],{"class":842},[517,1610,1424],{"class":527},[517,1612,864],{"class":842},[517,1614,867],{"class":842},[517,1616,1431],{"class":877},[517,1618,890],{"class":842},[517,1620,1050],{"class":527},[517,1622,1623],{"class":519,"line":714},[517,1624,934],{"class":527},[517,1626,1627],{"class":519,"line":723},[517,1628,1026],{"class":527},[207,1630,99],{"id":1631},"keymap-definition",[10,1633,754,1634,1636,1637,1644],{},[214,1635,491],{}," module has excellent documentation and examples in its ",[41,1638,1641],{"href":1639,"rel":1640},"https:\u002F\u002Fgithub.com\u002Fbadjeff\u002Fzmk-output-behavior-listener\u002Fblob\u002Fmain\u002FREADME.md",[479],[214,1642,1643],{},"README.md"," file. To simplify our keymaps and minimize repetitive code, I created some macros for a cleaner setup.",[509,1646,1648],{"className":833,"code":1647,"language":835,"meta":151,"style":151},"#define HAPTIC_OBG(node_name, device_ref, force_val) \\\n    node_name: node_name {                           \\\n        compatible = \"zmk,output-behavior-generic\";  \\\n        #binding-cells = \u003C0>;                        \\\n        device = \u003Cdevice_ref>;                       \\\n        force = \u003Cforce_val>;                         \\\n    };\n\n#define OUTPUT_SOURCE_LAYER_STATE_CHANGE 1\n#define HAPTIC_LAYER(node_name, bindings_list, layers_list)  \\\n    node_name: node_name {                                   \\\n        compatible = \"zmk,output-behavior-listener\";         \\\n        bindings = bindings_list;                            \\\n        layers = layers_list;                                \\\n        sources = \u003COUTPUT_SOURCE_LAYER_STATE_CHANGE>;        \\\n        all-state;                                           \\\n    };\n\n#define OUTPUT_SOURCE_KEYCODE_STATE_CHANGE 3\n#define HAPTIC_KEYCODE(node_name, keycode, bindings_list, layers_list) \\\n    node_name: node_name {                                             \\\n        compatible = \"zmk,output-behavior-listener\";                   \\\n        bindings = bindings_list;                                      \\\n        position = keycode;                                            \\\n        layers = layers_list;                                          \\\n        sources = \u003COUTPUT_SOURCE_KEYCODE_STATE_CHANGE>;                \\\n    };\n",[214,1649,1650,1681,1688,1702,1723,1740,1758,1762,1766,1776,1802,1809,1823,1835,1847,1866,1878,1882,1886,1896,1924,1931,1944,1955,1967,1978,1996],{"__ignoreMap":151},[517,1651,1652,1655,1658,1661,1665,1667,1670,1672,1675,1678],{"class":519,"line":520},[517,1653,1654],{"class":842},"#define",[517,1656,1657],{"class":870}," HAPTIC_OBG",[517,1659,1660],{"class":527},"(",[517,1662,1664],{"class":1663},"s9osk","node_name",[517,1666,881],{"class":527},[517,1668,1669],{"class":1663},"device_ref",[517,1671,881],{"class":527},[517,1673,1674],{"class":1663},"force_val",[517,1676,1677],{"class":527},") ",[517,1679,1680],{"class":877},"\\\n",[517,1682,1683,1686],{"class":519,"line":531},[517,1684,1685],{"class":527},"    node_name: node_name {                           ",[517,1687,1680],{"class":877},[517,1689,1690,1692,1694,1697,1700],{"class":519,"line":539},[517,1691,1084],{"class":527},[517,1693,864],{"class":842},[517,1695,1696],{"class":551}," \"zmk,output-behavior-generic\"",[517,1698,1699],{"class":527},";  ",[517,1701,1680],{"class":877},[517,1703,1704,1706,1708,1710,1712,1714,1716,1718,1721],{"class":519,"line":555},[517,1705,1199],{"class":527},[517,1707,1005],{"class":842},[517,1709,1204],{"class":527},[517,1711,864],{"class":842},[517,1713,867],{"class":842},[517,1715,878],{"class":877},[517,1717,890],{"class":842},[517,1719,1720],{"class":527},";                        ",[517,1722,1680],{"class":877},[517,1724,1725,1727,1729,1731,1733,1735,1738],{"class":519,"line":566},[517,1726,1231],{"class":527},[517,1728,864],{"class":842},[517,1730,867],{"class":842},[517,1732,1669],{"class":527},[517,1734,890],{"class":842},[517,1736,1737],{"class":527},";                       ",[517,1739,1680],{"class":877},[517,1741,1742,1745,1747,1749,1751,1753,1756],{"class":519,"line":578},[517,1743,1744],{"class":527},"        force ",[517,1746,864],{"class":842},[517,1748,867],{"class":842},[517,1750,1674],{"class":527},[517,1752,890],{"class":842},[517,1754,1755],{"class":527},";                         ",[517,1757,1680],{"class":877},[517,1759,1760],{"class":519,"line":588},[517,1761,934],{"class":527},[517,1763,1764],{"class":519,"line":596},[517,1765,940],{"emptyLinePlaceholder":939},[517,1767,1768,1770,1773],{"class":519,"line":608},[517,1769,1654],{"class":842},[517,1771,1772],{"class":870}," OUTPUT_SOURCE_LAYER_STATE_CHANGE",[517,1774,1775],{"class":877}," 1\n",[517,1777,1778,1780,1783,1785,1787,1789,1792,1794,1797,1800],{"class":519,"line":618},[517,1779,1654],{"class":842},[517,1781,1782],{"class":870}," HAPTIC_LAYER",[517,1784,1660],{"class":527},[517,1786,1664],{"class":1663},[517,1788,881],{"class":527},[517,1790,1791],{"class":1663},"bindings_list",[517,1793,881],{"class":527},[517,1795,1796],{"class":1663},"layers_list",[517,1798,1799],{"class":527},")  ",[517,1801,1680],{"class":877},[517,1803,1804,1807],{"class":519,"line":629},[517,1805,1806],{"class":527},"    node_name: node_name {                                   ",[517,1808,1680],{"class":877},[517,1810,1811,1813,1815,1818,1821],{"class":519,"line":640},[517,1812,1084],{"class":527},[517,1814,864],{"class":842},[517,1816,1817],{"class":551}," \"zmk,output-behavior-listener\"",[517,1819,1820],{"class":527},";         ",[517,1822,1680],{"class":877},[517,1824,1825,1828,1830,1833],{"class":519,"line":652},[517,1826,1827],{"class":527},"        bindings ",[517,1829,864],{"class":842},[517,1831,1832],{"class":527}," bindings_list;                            ",[517,1834,1680],{"class":877},[517,1836,1837,1840,1842,1845],{"class":519,"line":661},[517,1838,1839],{"class":527},"        layers ",[517,1841,864],{"class":842},[517,1843,1844],{"class":527}," layers_list;                                ",[517,1846,1680],{"class":877},[517,1848,1849,1852,1854,1856,1859,1861,1864],{"class":519,"line":671},[517,1850,1851],{"class":527},"        sources ",[517,1853,864],{"class":842},[517,1855,867],{"class":842},[517,1857,1858],{"class":527},"OUTPUT_SOURCE_LAYER_STATE_CHANGE",[517,1860,890],{"class":842},[517,1862,1863],{"class":527},";        ",[517,1865,1680],{"class":877},[517,1867,1868,1871,1873,1876],{"class":519,"line":683},[517,1869,1870],{"class":527},"        all",[517,1872,1005],{"class":842},[517,1874,1875],{"class":527},"state;                                           ",[517,1877,1680],{"class":877},[517,1879,1880],{"class":519,"line":692},[517,1881,934],{"class":527},[517,1883,1884],{"class":519,"line":702},[517,1885,940],{"emptyLinePlaceholder":939},[517,1887,1888,1890,1893],{"class":519,"line":714},[517,1889,1654],{"class":842},[517,1891,1892],{"class":870}," OUTPUT_SOURCE_KEYCODE_STATE_CHANGE",[517,1894,1895],{"class":877}," 3\n",[517,1897,1898,1900,1903,1905,1907,1909,1912,1914,1916,1918,1920,1922],{"class":519,"line":723},[517,1899,1654],{"class":842},[517,1901,1902],{"class":870}," HAPTIC_KEYCODE",[517,1904,1660],{"class":527},[517,1906,1664],{"class":1663},[517,1908,881],{"class":527},[517,1910,1911],{"class":1663},"keycode",[517,1913,881],{"class":527},[517,1915,1791],{"class":1663},[517,1917,881],{"class":527},[517,1919,1796],{"class":1663},[517,1921,1677],{"class":527},[517,1923,1680],{"class":877},[517,1925,1926,1929],{"class":519,"line":733},[517,1927,1928],{"class":527},"    node_name: node_name {                                             ",[517,1930,1680],{"class":877},[517,1932,1933,1935,1937,1939,1942],{"class":519,"line":741},[517,1934,1084],{"class":527},[517,1936,864],{"class":842},[517,1938,1817],{"class":551},[517,1940,1941],{"class":527},";                   ",[517,1943,1680],{"class":877},[517,1945,1946,1948,1950,1953],{"class":519,"line":1081},[517,1947,1827],{"class":527},[517,1949,864],{"class":842},[517,1951,1952],{"class":527}," bindings_list;                                      ",[517,1954,1680],{"class":877},[517,1956,1957,1960,1962,1965],{"class":519,"line":1094},[517,1958,1959],{"class":527},"        position ",[517,1961,864],{"class":842},[517,1963,1964],{"class":527}," keycode;                                            ",[517,1966,1680],{"class":877},[517,1968,1969,1971,1973,1976],{"class":519,"line":1111},[517,1970,1839],{"class":527},[517,1972,864],{"class":842},[517,1974,1975],{"class":527}," layers_list;                                          ",[517,1977,1680],{"class":877},[517,1979,1980,1982,1984,1986,1989,1991,1994],{"class":519,"line":1131},[517,1981,1851],{"class":527},[517,1983,864],{"class":842},[517,1985,867],{"class":842},[517,1987,1988],{"class":527},"OUTPUT_SOURCE_KEYCODE_STATE_CHANGE",[517,1990,890],{"class":842},[517,1992,1993],{"class":527},";                ",[517,1995,1680],{"class":877},[517,1997,1998],{"class":519,"line":1153},[517,1999,934],{"class":527},[10,2001,2002],{},"Here's an example keymap with the macros:",[509,2004,2006],{"className":833,"code":2005,"language":835,"meta":151,"style":151},"\u002F* Layer Definitions *\u002F\n#define DEFAULT 0\n#define LAYER1  1\n#define LAYER2  2\n\n\u002F {\n    \u002F\u002F setup output behaviors for both the left, and right haptic devices.\n    HAPTIC_OBG(hl_dc_strong_1, &haptic_l, 27)\n    HAPTIC_OBG(hr_dc_strong_1, &haptic_r, 27)\n    HAPTIC_OBG(hl_strong_click_1, &haptic_l, 1)\n    HAPTIC_OBG(hr_strong_click_1, &haptic_r, 1)\n\n    \u002F\u002F setup output listeners for keycodes and layers.\n    HAPTIC_KEYCODE(haptic_lshift, \u003C0xE1>, \u003C&hl_dc_strong_1>, \u003CDEFAULT>)\n    HAPTIC_KEYCODE(haptic_rshift, \u003C0xE5>, \u003C&hr_dc_strong_1>, \u003CDEFAULT>)\n    HAPTIC_LAYER(haptic_l1, \u003C&hl_strong_click_1>, \u003CLAYER1>)\n    HAPTIC_LAYER(haptic_l2, \u003C&hr_strong_click_1>, \u003CLAYER2>)\n};\n",[214,2007,2008,2013,2023,2033,2043,2047,2053,2058,2077,2093,2109,2124,2128,2133,2171,2204,2230,2255],{"__ignoreMap":151},[517,2009,2010],{"class":519,"line":520},[517,2011,2012],{"class":896},"\u002F* Layer Definitions *\u002F\n",[517,2014,2015,2017,2020],{"class":519,"line":531},[517,2016,1654],{"class":842},[517,2018,2019],{"class":870}," DEFAULT",[517,2021,2022],{"class":877}," 0\n",[517,2024,2025,2027,2030],{"class":519,"line":539},[517,2026,1654],{"class":842},[517,2028,2029],{"class":870}," LAYER1",[517,2031,2032],{"class":877},"  1\n",[517,2034,2035,2037,2040],{"class":519,"line":555},[517,2036,1654],{"class":842},[517,2038,2039],{"class":870}," LAYER2",[517,2041,2042],{"class":877},"  2\n",[517,2044,2045],{"class":519,"line":566},[517,2046,940],{"emptyLinePlaceholder":939},[517,2048,2049,2051],{"class":519,"line":578},[517,2050,1176],{"class":842},[517,2052,1078],{"class":527},[517,2054,2055],{"class":519,"line":588},[517,2056,2057],{"class":896},"    \u002F\u002F setup output behaviors for both the left, and right haptic devices.\n",[517,2059,2060,2063,2066,2068,2071,2074],{"class":519,"line":596},[517,2061,2062],{"class":870},"    HAPTIC_OBG",[517,2064,2065],{"class":527},"(hl_dc_strong_1, ",[517,2067,843],{"class":842},[517,2069,2070],{"class":527},"haptic_l, ",[517,2072,2073],{"class":877},"27",[517,2075,2076],{"class":527},")\n",[517,2078,2079,2081,2084,2086,2089,2091],{"class":519,"line":608},[517,2080,2062],{"class":870},[517,2082,2083],{"class":527},"(hr_dc_strong_1, ",[517,2085,843],{"class":842},[517,2087,2088],{"class":527},"haptic_r, ",[517,2090,2073],{"class":877},[517,2092,2076],{"class":527},[517,2094,2095,2097,2100,2102,2104,2107],{"class":519,"line":618},[517,2096,2062],{"class":870},[517,2098,2099],{"class":527},"(hl_strong_click_1, ",[517,2101,843],{"class":842},[517,2103,2070],{"class":527},[517,2105,2106],{"class":877},"1",[517,2108,2076],{"class":527},[517,2110,2111,2113,2116,2118,2120,2122],{"class":519,"line":629},[517,2112,2062],{"class":870},[517,2114,2115],{"class":527},"(hr_strong_click_1, ",[517,2117,843],{"class":842},[517,2119,2088],{"class":527},[517,2121,2106],{"class":877},[517,2123,2076],{"class":527},[517,2125,2126],{"class":519,"line":640},[517,2127,940],{"emptyLinePlaceholder":939},[517,2129,2130],{"class":519,"line":652},[517,2131,2132],{"class":896},"    \u002F\u002F setup output listeners for keycodes and layers.\n",[517,2134,2135,2138,2141,2144,2147,2149,2151,2154,2157,2159,2161,2164,2167,2169],{"class":519,"line":661},[517,2136,2137],{"class":870},"    HAPTIC_KEYCODE",[517,2139,2140],{"class":527},"(haptic_lshift, ",[517,2142,2143],{"class":842},"\u003C0x",[517,2145,2146],{"class":877},"E1",[517,2148,890],{"class":842},[517,2150,881],{"class":527},[517,2152,2153],{"class":842},"\u003C&",[517,2155,2156],{"class":527},"hl_dc_strong_1",[517,2158,890],{"class":842},[517,2160,881],{"class":527},[517,2162,2163],{"class":842},"\u003C",[517,2165,2166],{"class":527},"DEFAULT",[517,2168,890],{"class":842},[517,2170,2076],{"class":527},[517,2172,2173,2175,2178,2180,2183,2185,2187,2189,2192,2194,2196,2198,2200,2202],{"class":519,"line":671},[517,2174,2137],{"class":870},[517,2176,2177],{"class":527},"(haptic_rshift, ",[517,2179,2143],{"class":842},[517,2181,2182],{"class":877},"E5",[517,2184,890],{"class":842},[517,2186,881],{"class":527},[517,2188,2153],{"class":842},[517,2190,2191],{"class":527},"hr_dc_strong_1",[517,2193,890],{"class":842},[517,2195,881],{"class":527},[517,2197,2163],{"class":842},[517,2199,2166],{"class":527},[517,2201,890],{"class":842},[517,2203,2076],{"class":527},[517,2205,2206,2209,2212,2214,2217,2219,2221,2223,2226,2228],{"class":519,"line":683},[517,2207,2208],{"class":870},"    HAPTIC_LAYER",[517,2210,2211],{"class":527},"(haptic_l1, ",[517,2213,2153],{"class":842},[517,2215,2216],{"class":527},"hl_strong_click_1",[517,2218,890],{"class":842},[517,2220,881],{"class":527},[517,2222,2163],{"class":842},[517,2224,2225],{"class":527},"LAYER1",[517,2227,890],{"class":842},[517,2229,2076],{"class":527},[517,2231,2232,2234,2237,2239,2242,2244,2246,2248,2251,2253],{"class":519,"line":692},[517,2233,2208],{"class":870},[517,2235,2236],{"class":527},"(haptic_l2, ",[517,2238,2153],{"class":842},[517,2240,2241],{"class":527},"hr_strong_click_1",[517,2243,890],{"class":842},[517,2245,881],{"class":527},[517,2247,2163],{"class":842},[517,2249,2250],{"class":527},"LAYER2",[517,2252,890],{"class":842},[517,2254,2076],{"class":527},[517,2256,2257],{"class":519,"line":702},[517,2258,1026],{"class":527},[10,2260,2261,2262,2265,2266,2270,2271,314],{},"Note that the ",[214,2263,2264],{},"force"," parameter in the output behavior definition corresponds to the waveform effect from the DRV2605L library. For a complete list of effects, refer to section 11.2 of the ",[41,2267,2269],{"href":2268,"target":161},"https:\u002F\u002Fwww.ti.com\u002Flit\u002Fds\u002Fsymlink\u002Fdrv2605.pdf#page=57","DRV2605L datasheet",". Additionally, the keycode values align with the usage IDs found in the ",[41,2272,2274],{"href":2273,"target":161},"https:\u002F\u002Fusb.org\u002Fsites\u002Fdefault\u002Ffiles\u002Fhut1_2.pdf#page=83","HID Usages document",[30,2276,2278],{"id":2277},"conclusion","Conclusion",[10,2280,2281],{},"So... we are at the end of the journey! Adding haptic feedback to your custom keyboard is a rewarding project. By following this guide, you've learned how to select the right components, design and assemble the PCB, and configure the firmware to bring your keyboard to life with tactile feedback.",[10,2283,2284],{},"As you continue to explore and experiment with your keyboard builds, remember that the possibilities for personalization are endless. Thank you for joining me on this journey. I hope you found this guide helpful and inspiring. Happy building, and may your keyboard be as expressive as your creativity allows! Here's a keyboard I designed to prototype haptic feedback 😄.",[2286,2287,2288],"style",{},"html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s6RL2, html code.shiki .s6RL2{--shiki-default:#FDAEB7;--shiki-default-font-style:italic}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":151,"searchDepth":531,"depth":531,"links":2290},[2291,2292,2293,2296,2301,2305],{"id":32,"depth":531,"text":33},{"id":102,"depth":531,"text":44},{"id":144,"depth":531,"text":50,"children":2294},[2295],{"id":209,"depth":539,"text":59},{"id":290,"depth":531,"text":291,"children":2297},[2298,2299,2300],{"id":297,"depth":539,"text":73},{"id":372,"depth":539,"text":79},{"id":425,"depth":539,"text":426},{"id":467,"depth":531,"text":85,"children":2302},[2303,2304],{"id":760,"depth":539,"text":93},{"id":1631,"depth":539,"text":99},{"id":2277,"depth":531,"text":2278},"2025-02-19","md",{},"\u002Fwriting\u002Fadding-haptics-to-your-kb",{"title":5,"description":12},"writing\u002F20250219.adding-haptics-to-your-kb","Gxn822LFmU-F6Ntcm-LBF1dCBxafOFGoXazvhXnfxgA",1775828556658]