├── .gitignore ├── LICENSE ├── README.md ├── apt.txt ├── environment.yml ├── firmware ├── README.md ├── kb.py ├── main.py └── oled_python.bmp ├── hardware ├── build │ ├── case │ │ └── .gitignore │ ├── output │ │ ├── case.stl │ │ ├── oled_cover.stl │ │ └── pcb_gerber │ │ │ ├── kbd_python.DRL │ │ │ ├── kbd_python.GBL │ │ │ ├── kbd_python.GBO │ │ │ ├── kbd_python.GBP │ │ │ ├── kbd_python.GBS │ │ │ ├── kbd_python.GML │ │ │ ├── kbd_python.GTL │ │ │ ├── kbd_python.GTO │ │ │ ├── kbd_python.GTP │ │ │ ├── kbd_python.GTS │ │ │ ├── kbd_python_NPTH.DRL │ │ │ ├── kbd_python_PTH.DRL │ │ │ ├── kbd_python_bot.GBR │ │ │ └── kbd_python_top.GBR │ ├── pcb_gerber │ │ └── .gitignore │ └── pcb_png │ │ └── .gitignore ├── case.py ├── imgs │ ├── QR.png │ ├── dm9-logo.png │ ├── hsgw-logo.png │ ├── logo-python-powered-w-logo.png │ ├── logo-python-powered-w-logoBG.png │ ├── logo-python-powered-w-text.png │ ├── python-logo.png │ └── python-powered-w.svg ├── kicad_libs │ ├── kicad.pretty │ │ ├── D_SOD123_hand.kicad_mod │ │ ├── HOLE_D2.2.kicad_mod │ │ ├── SW_Cherry_MX_1.00u_PCB.kicad_mod │ │ ├── oled_i2c.kicad_mod │ │ └── xiao_rp2040.kicad_mod │ ├── kicad_symbols.bck │ ├── kicad_symbols.dcm │ └── kicad_symbols.lib ├── pcb.py └── prepare_silks.py └── notebook ├── en ├── README.md ├── case.ipynb ├── firmware.md ├── pcb.ipynb └── what_is_colaboratory.ipynb ├── imgs ├── case.jpg ├── case_usb.jpg ├── exported_case.png ├── kbd_python_preview_bot.png ├── kbd_python_preview_top.png ├── keyboard_made_by_python.jpg ├── kicad_gerber_to_pcbnew.png ├── kicad_gerberviewer.png ├── kicad_pcbnew.png ├── pcb.jpg ├── slicer.png ├── soldered_pcb_bottom.jpg └── soldered_pcb_top.jpg └── jp ├── README.md ├── case.ipynb ├── firmware.md ├── pcb.ipynb └── what_is_colaboratory.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | .vscode/ 3 | 4 | pcbflow/ 5 | __pycache__/ 6 | 7 | *_sklib.py 8 | *.erc 9 | *.log 10 | *.net 11 | 12 | *.bck 13 | *.dcm 14 | 15 | *.Identifier 16 | 17 | keyboard-made-by-python/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2022 Takuya Urakawa, Dm9 Records, 5z6p Instruments 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CO -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keyboard made by Python 2 | > This repository is the 20th article in [キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529) 3 | 4 | ## License 5 | ### Python logo images 6 | https://www.python.org/psf/trademarks/ 7 | 8 | ### kicad library 9 | https://www.kicad.org/libraries/license/ 10 | 11 | ### Dm9Records logo 12 | Do not use in any other 13 | 14 | ### All other files 15 | MIT License((c) 2022 Takuya Urakawa, Dm9 Records, 5z6p Instruments) -------------------------------------------------------------------------------- /apt.txt: -------------------------------------------------------------------------------- 1 | libgl1-mesa-glx 2 | libglu1-mesa 3 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - defaults 3 | - cadquery 4 | - conda-forge 5 | dependencies: 6 | - python=3.10 7 | - cadquery=master 8 | - matplotlib=3.6 9 | - pip 10 | - pip: 11 | - jupyter-cadquery==3.4.0 12 | - cadquery-massembly==1.0.0rc0 -------------------------------------------------------------------------------- /firmware/README.md: -------------------------------------------------------------------------------- 1 | # files for kmk firmware 2 | -------------------------------------------------------------------------------- /firmware/kb.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard 4 | from kmk.scanners import DiodeOrientation 5 | 6 | 7 | class KMKKeyboard(_KMKKeyboard): 8 | row_pins = (board.D0, board.D5, board.D6) 9 | col_pins = ( 10 | board.D7, 11 | board.D2, 12 | board.D3, 13 | board.D4, 14 | ) 15 | diode_orientation = DiodeOrientation.COL2ROW 16 | 17 | # oled 18 | SCL = board.D10 19 | SDA = board.D8 20 | 21 | # fmt: off 22 | coord_mapping = [ 23 | 1, 2, 3, 24 | 5, 6, 7, 25 | 8, 9, 10, 11 26 | ] 27 | # fmt: on 28 | -------------------------------------------------------------------------------- /firmware/main.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | from kb import KMKKeyboard 4 | 5 | from kmk.handlers.sequences import send_string, simple_key_sequence 6 | from kmk.keys import KC 7 | from kmk.modules.layers import Layers 8 | from kmk.extensions.peg_oled_Display import ( 9 | Oled, 10 | OledDisplayMode, 11 | OledReactionType, 12 | OledData, 13 | ) 14 | 15 | keyboard = KMKKeyboard() 16 | layers_ext = Layers() 17 | keyboard.modules.append(layers_ext) 18 | 19 | keyboard.tap_time = 250 20 | keyboard.debug_enabled = False 21 | 22 | oled_ext = Oled( 23 | OledData(image={0: OledReactionType.STATIC, 1: ["oled_python.bmp"]}), 24 | toDisplay=OledDisplayMode.IMG, 25 | flip=True, 26 | ) 27 | keyboard.extensions.append(oled_ext) 28 | 29 | ____ = KC.TRNS 30 | 31 | L1_P0 = KC.LT(1, KC.P0) 32 | 33 | # fmt: off 34 | keyboard.keymap = [ 35 | # default layer 36 | [ 37 | KC.P7, KC.P8, KC.P9, 38 | KC.P4, KC.P5, KC.P6, 39 | L1_P0, KC.P1, KC.P2, KC.P3 40 | ], 41 | # Fn layer 42 | [ 43 | KC.ESC, KC.DEL, KC.BSPC, 44 | KC.COMM, KC.NO, KC.TAB, 45 | ____, KC.DOT, KC.SPC, KC.ENT 46 | ], 47 | ] 48 | # fmt: on 49 | 50 | if __name__ == "__main__": 51 | keyboard.go() 52 | -------------------------------------------------------------------------------- /firmware/oled_python.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/firmware/oled_python.bmp -------------------------------------------------------------------------------- /hardware/build/case/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /hardware/build/output/case.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/build/output/case.stl -------------------------------------------------------------------------------- /hardware/build/output/oled_cover.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/build/output/oled_cover.stl -------------------------------------------------------------------------------- /hardware/build/output/pcb_gerber/kbd_python.GBP: -------------------------------------------------------------------------------- 1 | G04 pcbflow Gerber RS-274X export* 2 | G75* 3 | %MOMM*% 4 | %FSLAX34Y34*% 5 | %LPD*% 6 | %INBottom Paste*% 7 | %IPPOS*% 8 | %AMOC8* 9 | 5,1,8,0,0,1.08239X$1,22.5*% 10 | G01* 11 | %ADD10C,0.010000*% 12 | %TF.FileFunction,Paste,Bot*%G36* 13 | X0150250Y0133750D02* 14 | X0159750Y0133750D01* 15 | X0159750Y0119750D01* 16 | X0150250Y0119750D01* 17 | X0150250Y0133750D01* 18 | G37* 19 | 20 | G36* 21 | X0340250Y0133750D02* 22 | X0349750Y0133750D01* 23 | X0349750Y0119750D01* 24 | X0340250Y0119750D01* 25 | X0340250Y0133750D01* 26 | G37* 27 | 28 | G36* 29 | X0150250Y0170250D02* 30 | X0159750Y0170250D01* 31 | X0159750Y0156250D01* 32 | X0150250Y0156250D01* 33 | X0150250Y0170250D01* 34 | G37* 35 | 36 | G36* 37 | X0349750Y0170250D02* 38 | X0349750Y0156250D01* 39 | X0340250Y0156250D01* 40 | X0340250Y0170250D01* 41 | X0349750Y0170250D01* 42 | G37* 43 | 44 | G36* 45 | X0349750Y0323750D02* 46 | X0349750Y0309750D01* 47 | X0340250Y0309750D01* 48 | X0340250Y0323750D01* 49 | X0349750Y0323750D01* 50 | G37* 51 | 52 | G36* 53 | X0340250Y0360250D02* 54 | X0349750Y0360250D01* 55 | X0349750Y0346250D01* 56 | X0340250Y0346250D01* 57 | X0340250Y0360250D01* 58 | G37* 59 | 60 | G36* 61 | X0340250Y0513750D02* 62 | X0349750Y0513750D01* 63 | X0349750Y0499750D01* 64 | X0340250Y0499750D01* 65 | X0340250Y0513750D01* 66 | G37* 67 | 68 | G36* 69 | X0530250Y0513750D02* 70 | X0539750Y0513750D01* 71 | X0539750Y0499750D01* 72 | X0530250Y0499750D01* 73 | X0530250Y0513750D01* 74 | G37* 75 | 76 | G36* 77 | X0349750Y0550250D02* 78 | X0349750Y0536250D01* 79 | X0340250Y0536250D01* 80 | X0340250Y0550250D01* 81 | X0349750Y0550250D01* 82 | G37* 83 | 84 | G36* 85 | X0539750Y0550250D02* 86 | X0539750Y0536250D01* 87 | X0530250Y0536250D01* 88 | X0530250Y0550250D01* 89 | X0539750Y0550250D01* 90 | G37* 91 | 92 | G36* 93 | X0530250Y0133750D02* 94 | X0539750Y0133750D01* 95 | X0539750Y0119750D01* 96 | X0530250Y0119750D01* 97 | X0530250Y0133750D01* 98 | G37* 99 | 100 | G36* 101 | X0720250Y0133750D02* 102 | X0729750Y0133750D01* 103 | X0729750Y0119750D01* 104 | X0720250Y0119750D01* 105 | X0720250Y0133750D01* 106 | G37* 107 | 108 | G36* 109 | X0530250Y0170250D02* 110 | X0539750Y0170250D01* 111 | X0539750Y0156250D01* 112 | X0530250Y0156250D01* 113 | X0530250Y0170250D01* 114 | G37* 115 | 116 | G36* 117 | X0729750Y0170250D02* 118 | X0729750Y0156250D01* 119 | X0720250Y0156250D01* 120 | X0720250Y0170250D01* 121 | X0729750Y0170250D01* 122 | G37* 123 | 124 | G36* 125 | X0539750Y0323750D02* 126 | X0539750Y0309750D01* 127 | X0530250Y0309750D01* 128 | X0530250Y0323750D01* 129 | X0539750Y0323750D01* 130 | G37* 131 | 132 | G36* 133 | X0720250Y0323750D02* 134 | X0729750Y0323750D01* 135 | X0729750Y0309750D01* 136 | X0720250Y0309750D01* 137 | X0720250Y0323750D01* 138 | G37* 139 | 140 | G36* 141 | X0530250Y0360250D02* 142 | X0539750Y0360250D01* 143 | X0539750Y0346250D01* 144 | X0530250Y0346250D01* 145 | X0530250Y0360250D01* 146 | G37* 147 | 148 | G36* 149 | X0720250Y0360250D02* 150 | X0729750Y0360250D01* 151 | X0729750Y0346250D01* 152 | X0720250Y0346250D01* 153 | X0720250Y0360250D01* 154 | G37* 155 | 156 | G36* 157 | X0729750Y0513750D02* 158 | X0729750Y0499750D01* 159 | X0720250Y0499750D01* 160 | X0720250Y0513750D01* 161 | X0729750Y0513750D01* 162 | G37* 163 | 164 | G36* 165 | X0729750Y0550250D02* 166 | X0729750Y0536250D01* 167 | X0720250Y0536250D01* 168 | X0720250Y0550250D01* 169 | X0729750Y0550250D01* 170 | G37* 171 | 172 | M02* 173 | -------------------------------------------------------------------------------- /hardware/build/output/pcb_gerber/kbd_python.GML: -------------------------------------------------------------------------------- 1 | G04 pcbflow Gerber RS-274X export* 2 | G75* 3 | %MOMM*% 4 | %FSLAX34Y34*% 5 | %LPD*% 6 | %INMechanical*% 7 | %IPPOS*% 8 | %AMOC8* 9 | 5,1,8,0,0,1.08239X$1,22.5*% 10 | G01* 11 | %ADD10C,0.010000*% 12 | D10* 13 | X0000000Y0000000D02* 14 | X0760000Y0000000D01* 15 | X0760000Y0570000D01* 16 | X0000000Y0570000D01* 17 | X0000000Y0000000D01* 18 | M02* 19 | -------------------------------------------------------------------------------- /hardware/build/output/pcb_gerber/kbd_python.GTP: -------------------------------------------------------------------------------- 1 | G04 pcbflow Gerber RS-274X export* 2 | G75* 3 | %MOMM*% 4 | %FSLAX34Y34*% 5 | %LPD*% 6 | %INTop Paste*% 7 | %IPPOS*% 8 | %AMOC8* 9 | 5,1,8,0,0,1.08239X$1,22.5*% 10 | G01* 11 | %ADD10C,0.010000*% 12 | %TF.FileFunction,Paste,Top*%M02* 13 | -------------------------------------------------------------------------------- /hardware/build/output/pcb_gerber/kbd_python_NPTH.DRL: -------------------------------------------------------------------------------- 1 | M48 2 | FMAT,2 3 | ICI,OFF 4 | METRIC,TZ,000.000 5 | T2C1.700 6 | T3C2.200 7 | T4C4.000 8 | % 9 | G90 10 | M71 11 | T2 12 | X33580Y47500 13 | X23420Y47500 14 | X52580Y47500 15 | X42420Y47500 16 | X71580Y47500 17 | X61420Y47500 18 | X33580Y28500 19 | X23420Y28500 20 | X52580Y28500 21 | X42420Y28500 22 | X71580Y28500 23 | X61420Y28500 24 | X14580Y9500 25 | X4420Y9500 26 | X33580Y9500 27 | X23420Y9500 28 | X52580Y9500 29 | X42420Y9500 30 | X71580Y9500 31 | X61420Y9500 32 | T3 33 | X38000Y38000 34 | X57000Y38000 35 | X57000Y19000 36 | X19000Y19000 37 | T4 38 | X28500Y47500 39 | X47500Y47500 40 | X66500Y47500 41 | X28500Y28500 42 | X47500Y28500 43 | X66500Y28500 44 | X9500Y9500 45 | X28500Y9500 46 | X47500Y9500 47 | X66500Y9500 48 | M30 49 | -------------------------------------------------------------------------------- /hardware/build/output/pcb_gerber/kbd_python_PTH.DRL: -------------------------------------------------------------------------------- 1 | M48 2 | FMAT,2 3 | ICI,OFF 4 | METRIC,TZ,000.000 5 | T2C0.600 6 | T3C0.800 7 | T4C0.850 8 | T5C1.500 9 | % 10 | G90 11 | M71 12 | T2 13 | X34500Y43747 14 | X53500Y43747 15 | X72500Y43747 16 | T3 17 | X18870Y53120 18 | X18870Y50580 19 | X18870Y48040 20 | X18870Y45500 21 | X18870Y42960 22 | X18870Y40420 23 | X18870Y37880 24 | X3630Y37880 25 | X3630Y40420 26 | X3630Y42960 27 | X3630Y45500 28 | X3630Y48040 29 | X3630Y50580 30 | X3630Y53120 31 | T4 32 | X5690Y21000 33 | X8230Y21000 34 | X10770Y21000 35 | X13310Y21000 36 | T5 37 | X24690Y50040 38 | X31040Y52580 39 | X43690Y50040 40 | X50040Y52580 41 | X62690Y50040 42 | X69040Y52580 43 | X24690Y31040 44 | X31040Y33580 45 | X43690Y31040 46 | X50040Y33580 47 | X62690Y31040 48 | X69040Y33580 49 | X5690Y12040 50 | X12040Y14580 51 | X24690Y12040 52 | X31040Y14580 53 | X43690Y12040 54 | X50040Y14580 55 | X62690Y12040 56 | X69040Y14580 57 | M30 58 | -------------------------------------------------------------------------------- /hardware/build/output/pcb_gerber/kbd_python_top.GBR: -------------------------------------------------------------------------------- 1 | G04 pcbflow Gerber RS-274X export* 2 | G75* 3 | %MOMM*% 4 | %FSLAX34Y34*% 5 | %LPD*% 6 | %INTop Documentation*% 7 | %IPPOS*% 8 | %AMOC8* 9 | 5,1,8,0,0,1.08239X$1,22.5*% 10 | G01* 11 | %ADD10C,0.010000*% 12 | %TF.FileFunction,AssemblyDrawing,Top*%G36* 13 | X-000500Y0000000D02* 14 | X-000500Y0000000D01* 15 | X-000500Y0190000D01* 16 | X-000462Y0190191D01* 17 | X-000354Y0190354D01* 18 | X-000191Y0190462D01* 19 | X0000000Y0190500D01* 20 | X0000000Y0190500D01* 21 | X0094625Y0190500D01* 22 | X0094625Y0189500D01* 23 | X0000500Y0189500D01* 24 | X0000500Y0000500D01* 25 | X0094625Y0000500D01* 26 | X0094625Y-000500D01* 27 | X0000000Y-000500D01* 28 | X0000000Y-000500D01* 29 | X-000020Y-000496D01* 30 | X-000191Y-000462D01* 31 | X-000191Y-000462D01* 32 | X-000191Y-000462D01* 33 | X-000313Y-000381D01* 34 | X-000354Y-000354D01* 35 | X-000354Y-000354D01* 36 | X-000354Y-000354D01* 37 | X-000462Y-000191D01* 38 | X-000462Y-000191D01* 39 | X-000490Y-000048D01* 40 | X-000500Y0000000D01* 41 | G37* 42 | 43 | G36* 44 | X0189500Y0190500D02* 45 | X0189500Y0380000D01* 46 | X0189500Y0570000D01* 47 | X0189538Y0570191D01* 48 | X0189646Y0570354D01* 49 | X0189750Y0570423D01* 50 | X0189750Y-000500D01* 51 | X0094625Y-000500D01* 52 | X0094625Y0000500D01* 53 | X0189500Y0000500D01* 54 | X0189500Y0189500D01* 55 | X0094625Y0189500D01* 56 | X0094625Y0190500D01* 57 | X0189500Y0190500D01* 58 | G37* 59 | 60 | G36* 61 | X0190000Y0570500D02* 62 | X0284875Y0570500D01* 63 | X0284875Y0569500D01* 64 | X0190500Y0569500D01* 65 | X0190500Y0380500D01* 66 | X0284875Y0380500D01* 67 | X0284875Y0379500D01* 68 | X0190500Y0379500D01* 69 | X0190500Y0190500D01* 70 | X0284875Y0190500D01* 71 | X0284875Y0189500D01* 72 | X0190500Y0189500D01* 73 | X0190500Y0000500D01* 74 | X0284875Y0000500D01* 75 | X0284875Y-000500D01* 76 | X0190000Y-000500D01* 77 | X0189750Y-000500D01* 78 | X0189750Y0570423D01* 79 | X0189809Y0570462D01* 80 | X0190000Y0570500D01* 81 | G37* 82 | 83 | G36* 84 | X0380000Y0570500D02* 85 | X0380000Y-000500D01* 86 | X0284875Y-000500D01* 87 | X0284875Y0000500D01* 88 | X0379500Y0000500D01* 89 | X0379500Y0189500D01* 90 | X0284875Y0189500D01* 91 | X0284875Y0190500D01* 92 | X0379500Y0190500D01* 93 | X0379500Y0379500D01* 94 | X0284875Y0379500D01* 95 | X0284875Y0380500D01* 96 | X0379500Y0380500D01* 97 | X0379500Y0569500D01* 98 | X0284875Y0569500D01* 99 | X0284875Y0570500D01* 100 | X0380000Y0570500D01* 101 | G37* 102 | 103 | G36* 104 | X0380000Y-000500D02* 105 | X0380000Y0570500D01* 106 | X0475125Y0570500D01* 107 | X0475125Y0569500D01* 108 | X0380500Y0569500D01* 109 | X0380500Y0380500D01* 110 | X0475125Y0380500D01* 111 | X0475125Y0379500D01* 112 | X0380500Y0379500D01* 113 | X0380500Y0190500D01* 114 | X0475125Y0190500D01* 115 | X0475125Y0189500D01* 116 | X0380500Y0189500D01* 117 | X0380500Y0000500D01* 118 | X0475125Y0000500D01* 119 | X0475125Y-000500D01* 120 | X0380000Y-000500D01* 121 | G37* 122 | 123 | G36* 124 | X0570250Y-000500D02* 125 | X0570000Y-000500D01* 126 | X0475125Y-000500D01* 127 | X0475125Y0000500D01* 128 | X0569500Y0000500D01* 129 | X0569500Y0189500D01* 130 | X0475125Y0189500D01* 131 | X0475125Y0190500D01* 132 | X0569500Y0190500D01* 133 | X0569500Y0379500D01* 134 | X0475125Y0379500D01* 135 | X0475125Y0380500D01* 136 | X0569500Y0380500D01* 137 | X0569500Y0569500D01* 138 | X0475125Y0569500D01* 139 | X0475125Y0570500D01* 140 | X0570000Y0570500D01* 141 | X0570250Y0570500D01* 142 | X0570250Y-000500D01* 143 | G37* 144 | 145 | G36* 146 | X0570250Y-000500D02* 147 | X0570250Y0570500D01* 148 | X0665375Y0570500D01* 149 | X0665375Y0569500D01* 150 | X0570500Y0569500D01* 151 | X0570500Y0380500D01* 152 | X0665375Y0380500D01* 153 | X0665375Y0379500D01* 154 | X0570500Y0379500D01* 155 | X0570500Y0190500D01* 156 | X0665375Y0190500D01* 157 | X0665375Y0189500D01* 158 | X0570500Y0189500D01* 159 | X0570500Y0000500D01* 160 | X0665375Y0000500D01* 161 | X0665375Y-000500D01* 162 | X0570250Y-000500D01* 163 | G37* 164 | 165 | G36* 166 | X0760191Y0570462D02* 167 | X0760354Y0570354D01* 168 | X0760462Y0570191D01* 169 | X0760500Y0570000D01* 170 | X0760500Y0380000D01* 171 | X0760500Y0190000D01* 172 | X0760500Y0000000D01* 173 | X0760462Y-000191D01* 174 | X0760354Y-000354D01* 175 | X0760191Y-000462D01* 176 | X0760000Y-000500D01* 177 | X0665375Y-000500D01* 178 | X0665375Y0000500D01* 179 | X0759500Y0000500D01* 180 | X0759500Y0189500D01* 181 | X0665375Y0189500D01* 182 | X0665375Y0190500D01* 183 | X0759500Y0190500D01* 184 | X0759500Y0379500D01* 185 | X0665375Y0379500D01* 186 | X0665375Y0380500D01* 187 | X0759500Y0380500D01* 188 | X0759500Y0569500D01* 189 | X0665375Y0569500D01* 190 | X0665375Y0570500D01* 191 | X0760000Y0570500D01* 192 | X0760191Y0570462D01* 193 | G37* 194 | 195 | G36* 196 | X0025000Y0024500D02* 197 | X0024809Y0024538D01* 198 | X0024646Y0024646D01* 199 | X0024538Y0024809D01* 200 | X0024500Y0025000D01* 201 | X0024500Y0165000D01* 202 | X0024538Y0165191D01* 203 | X0024646Y0165354D01* 204 | X0024809Y0165462D01* 205 | X0025000Y0165500D01* 206 | X0095000Y0165500D01* 207 | X0095000Y0164500D01* 208 | X0025500Y0164500D01* 209 | X0025500Y0025500D01* 210 | X0095000Y0025500D01* 211 | X0095000Y0024500D01* 212 | X0025000Y0024500D01* 213 | G37* 214 | 215 | G36* 216 | X0095000Y0024500D02* 217 | X0095000Y0025500D01* 218 | X0164500Y0025500D01* 219 | X0164500Y0164500D01* 220 | X0095000Y0164500D01* 221 | X0095000Y0165500D01* 222 | X0165000Y0165500D01* 223 | X0165191Y0165462D01* 224 | X0165354Y0165354D01* 225 | X0165462Y0165191D01* 226 | X0165500Y0165000D01* 227 | X0165500Y0025000D01* 228 | X0165462Y0024809D01* 229 | X0165354Y0024646D01* 230 | X0165191Y0024538D01* 231 | X0165000Y0024500D01* 232 | X0095000Y0024500D01* 233 | G37* 234 | 235 | G36* 236 | X0035000Y0194250D02* 237 | X0035000Y0194250D01* 238 | X0035000Y0194250D01* 239 | X0034927Y0194257D01* 240 | X0034854Y0194264D01* 241 | X0034854Y0194264D01* 242 | X0034854Y0194264D01* 243 | X0034783Y0194286D01* 244 | X0034713Y0194307D01* 245 | X0034713Y0194307D01* 246 | X0034713Y0194307D01* 247 | X0034648Y0194342D01* 248 | X0034583Y0194376D01* 249 | X0034583Y0194376D01* 250 | X0034583Y0194376D01* 251 | X0034527Y0194423D01* 252 | X0034470Y0194470D01* 253 | X0034470Y0194470D01* 254 | X0034470Y0194470D01* 255 | X0034423Y0194526D01* 256 | X0034376Y0194583D01* 257 | X0034376Y0194583D01* 258 | X0034376Y0194583D01* 259 | X0034342Y0194648D01* 260 | X0034307Y0194713D01* 261 | X0034307Y0194713D01* 262 | X0034307Y0194713D01* 263 | X0034286Y0194783D01* 264 | X0034264Y0194854D01* 265 | X0034264Y0194854D01* 266 | X0034264Y0194854D01* 267 | X0034257Y0194927D01* 268 | X0034250Y0195000D01* 269 | X0034250Y0195000D01* 270 | X0034250Y0195000D01* 271 | X0034250Y0575000D01* 272 | X0034264Y0575146D01* 273 | X0034307Y0575287D01* 274 | X0034376Y0575417D01* 275 | X0034470Y0575530D01* 276 | X0034583Y0575624D01* 277 | X0034713Y0575693D01* 278 | X0034854Y0575736D01* 279 | X0035000Y0575750D01* 280 | X0095000Y0575750D01* 281 | X0095000Y0574250D01* 282 | X0035750Y0574250D01* 283 | X0035750Y0195750D01* 284 | X0095000Y0195750D01* 285 | X0095000Y0194250D01* 286 | X0035000Y0194250D01* 287 | G37* 288 | 289 | G36* 290 | X0155714Y0194783D02* 291 | X0155693Y0194713D01* 292 | X0155693Y0194713D01* 293 | X0155693Y0194713D01* 294 | X0155658Y0194648D01* 295 | X0155624Y0194583D01* 296 | X0155624Y0194583D01* 297 | X0155624Y0194583D01* 298 | X0155577Y0194527D01* 299 | X0155530Y0194470D01* 300 | X0155530Y0194470D01* 301 | X0155530Y0194470D01* 302 | X0155474Y0194423D01* 303 | X0155417Y0194376D01* 304 | X0155417Y0194376D01* 305 | X0155417Y0194376D01* 306 | X0155352Y0194342D01* 307 | X0155287Y0194307D01* 308 | X0155287Y0194307D01* 309 | X0155287Y0194307D01* 310 | X0155217Y0194286D01* 311 | X0155146Y0194264D01* 312 | X0155146Y0194264D01* 313 | X0155146Y0194264D01* 314 | X0155073Y0194257D01* 315 | X0155000Y0194250D01* 316 | X0095000Y0194250D01* 317 | X0095000Y0195750D01* 318 | X0154250Y0195750D01* 319 | X0154250Y0574250D01* 320 | X0095000Y0574250D01* 321 | X0095000Y0575750D01* 322 | X0155000Y0575750D01* 323 | X0155146Y0575736D01* 324 | X0155287Y0575693D01* 325 | X0155417Y0575624D01* 326 | X0155530Y0575530D01* 327 | X0155624Y0575417D01* 328 | X0155693Y0575287D01* 329 | X0155736Y0575146D01* 330 | X0155750Y0575000D01* 331 | X0155750Y0195000D01* 332 | X0155750Y0195000D01* 333 | X0155743Y0194927D01* 334 | X0155736Y0194854D01* 335 | X0155736Y0194854D01* 336 | X0155736Y0194854D01* 337 | X0155714Y0194783D01* 338 | G37* 339 | 340 | G36* 341 | X0037500Y0244250D02* 342 | X0037500Y0244250D01* 343 | X0037500Y0244250D01* 344 | X0037427Y0244257D01* 345 | X0037354Y0244264D01* 346 | X0037354Y0244264D01* 347 | X0037354Y0244264D01* 348 | X0037283Y0244286D01* 349 | X0037213Y0244307D01* 350 | X0037213Y0244307D01* 351 | X0037213Y0244307D01* 352 | X0037148Y0244342D01* 353 | X0037083Y0244376D01* 354 | X0037083Y0244376D01* 355 | X0037083Y0244376D01* 356 | X0037027Y0244423D01* 357 | X0036970Y0244470D01* 358 | X0036970Y0244470D01* 359 | X0036970Y0244470D01* 360 | X0036923Y0244526D01* 361 | X0036876Y0244583D01* 362 | X0036876Y0244583D01* 363 | X0036876Y0244583D01* 364 | X0036842Y0244648D01* 365 | X0036807Y0244713D01* 366 | X0036807Y0244713D01* 367 | X0036807Y0244713D01* 368 | X0036786Y0244783D01* 369 | X0036764Y0244854D01* 370 | X0036764Y0244854D01* 371 | X0036764Y0244854D01* 372 | X0036757Y0244927D01* 373 | X0036750Y0245000D01* 374 | X0036750Y0245000D01* 375 | X0036750Y0245000D01* 376 | X0036750Y0545000D01* 377 | X0036764Y0545146D01* 378 | X0036807Y0545287D01* 379 | X0036876Y0545417D01* 380 | X0036970Y0545530D01* 381 | X0037083Y0545624D01* 382 | X0037213Y0545693D01* 383 | X0037354Y0545736D01* 384 | X0037500Y0545750D01* 385 | X0095000Y0545750D01* 386 | X0095000Y0544250D01* 387 | X0038250Y0544250D01* 388 | X0038250Y0245750D01* 389 | X0095000Y0245750D01* 390 | X0095000Y0244250D01* 391 | X0037500Y0244250D01* 392 | G37* 393 | 394 | G36* 395 | X0153214Y0244783D02* 396 | X0153193Y0244713D01* 397 | X0153193Y0244713D01* 398 | X0153193Y0244713D01* 399 | X0153158Y0244648D01* 400 | X0153124Y0244583D01* 401 | X0153124Y0244583D01* 402 | X0153124Y0244583D01* 403 | X0153077Y0244526D01* 404 | X0153030Y0244470D01* 405 | X0153030Y0244470D01* 406 | X0153030Y0244470D01* 407 | X0152974Y0244423D01* 408 | X0152917Y0244376D01* 409 | X0152917Y0244376D01* 410 | X0152917Y0244376D01* 411 | X0152852Y0244342D01* 412 | X0152787Y0244307D01* 413 | X0152787Y0244307D01* 414 | X0152787Y0244307D01* 415 | X0152717Y0244286D01* 416 | X0152646Y0244264D01* 417 | X0152646Y0244264D01* 418 | X0152646Y0244264D01* 419 | X0152573Y0244257D01* 420 | X0152500Y0244250D01* 421 | X0095000Y0244250D01* 422 | X0095000Y0245750D01* 423 | X0151750Y0245750D01* 424 | X0151750Y0544250D01* 425 | X0095000Y0544250D01* 426 | X0095000Y0545750D01* 427 | X0152500Y0545750D01* 428 | X0152646Y0545736D01* 429 | X0152787Y0545693D01* 430 | X0152917Y0545624D01* 431 | X0153030Y0545530D01* 432 | X0153124Y0545417D01* 433 | X0153193Y0545287D01* 434 | X0153236Y0545146D01* 435 | X0153250Y0545000D01* 436 | X0153250Y0245000D01* 437 | X0153250Y0245000D01* 438 | X0153243Y0244927D01* 439 | X0153236Y0244854D01* 440 | X0153236Y0244854D01* 441 | X0153236Y0244854D01* 442 | X0153214Y0244783D01* 443 | G37* 444 | 445 | G36* 446 | X0056600Y0265250D02* 447 | X0056454Y0265264D01* 448 | X0056313Y0265307D01* 449 | X0056183Y0265376D01* 450 | X0056070Y0265470D01* 451 | X0055976Y0265583D01* 452 | X0055907Y0265713D01* 453 | X0055864Y0265854D01* 454 | X0055850Y0266000D01* 455 | X0055850Y0489800D01* 456 | X0055864Y0489946D01* 457 | X0055907Y0490087D01* 458 | X0055976Y0490217D01* 459 | X0056070Y0490330D01* 460 | X0056183Y0490424D01* 461 | X0056313Y0490493D01* 462 | X0056454Y0490536D01* 463 | X0056600Y0490550D01* 464 | X0084500Y0490550D01* 465 | X0084500Y0489050D01* 466 | X0057350Y0489050D01* 467 | X0057350Y0266750D01* 468 | X0084500Y0266750D01* 469 | X0084500Y0265250D01* 470 | X0056600Y0265250D01* 471 | G37* 472 | 473 | G36* 474 | X0112546Y0265264D02* 475 | X0112400Y0265250D01* 476 | X0084500Y0265250D01* 477 | X0084500Y0266750D01* 478 | X0111650Y0266750D01* 479 | X0111650Y0489050D01* 480 | X0084500Y0489050D01* 481 | X0084500Y0490550D01* 482 | X0112400Y0490550D01* 483 | X0112546Y0490536D01* 484 | X0112687Y0490493D01* 485 | X0112817Y0490424D01* 486 | X0112930Y0490330D01* 487 | X0113024Y0490217D01* 488 | X0113093Y0490087D01* 489 | X0113136Y0489946D01* 490 | X0113150Y0489800D01* 491 | X0113150Y0266000D01* 492 | X0113136Y0265854D01* 493 | X0113093Y0265713D01* 494 | X0113024Y0265583D01* 495 | X0112930Y0265470D01* 496 | X0112817Y0265376D01* 497 | X0112687Y0265307D01* 498 | X0112546Y0265264D01* 499 | G37* 500 | 501 | G36* 502 | X0214809Y0024538D02* 503 | X0214646Y0024646D01* 504 | X0214538Y0024809D01* 505 | X0214500Y0025000D01* 506 | X0214500Y0165000D01* 507 | X0214538Y0165191D01* 508 | X0214646Y0165354D01* 509 | X0214809Y0165462D01* 510 | X0215000Y0165500D01* 511 | X0285000Y0165500D01* 512 | X0285000Y0164500D01* 513 | X0215500Y0164500D01* 514 | X0215500Y0025500D01* 515 | X0285000Y0025500D01* 516 | X0285000Y0024500D01* 517 | X0215000Y0024500D01* 518 | X0214809Y0024538D01* 519 | G37* 520 | 521 | G36* 522 | X0355000Y0165500D02* 523 | X0355191Y0165462D01* 524 | X0355354Y0165354D01* 525 | X0355462Y0165191D01* 526 | X0355500Y0165000D01* 527 | X0355500Y0025000D01* 528 | X0355462Y0024809D01* 529 | X0355354Y0024646D01* 530 | X0355191Y0024538D01* 531 | X0355000Y0024500D01* 532 | X0285000Y0024500D01* 533 | X0285000Y0025500D01* 534 | X0354500Y0025500D01* 535 | X0354500Y0164500D01* 536 | X0285000Y0164500D01* 537 | X0285000Y0165500D01* 538 | X0355000Y0165500D01* 539 | G37* 540 | 541 | G36* 542 | X0214646Y0214646D02* 543 | X0214538Y0214809D01* 544 | X0214500Y0215000D01* 545 | X0214500Y0355000D01* 546 | X0214538Y0355191D01* 547 | X0214646Y0355354D01* 548 | X0214809Y0355462D01* 549 | X0215000Y0355500D01* 550 | X0285000Y0355500D01* 551 | X0285000Y0354500D01* 552 | X0215500Y0354500D01* 553 | X0215500Y0215500D01* 554 | X0285000Y0215500D01* 555 | X0285000Y0214500D01* 556 | X0215000Y0214500D01* 557 | X0214809Y0214538D01* 558 | X0214646Y0214646D01* 559 | G37* 560 | 561 | G36* 562 | X0355000Y0355500D02* 563 | X0355191Y0355462D01* 564 | X0355354Y0355354D01* 565 | X0355462Y0355191D01* 566 | X0355500Y0355000D01* 567 | X0355500Y0215000D01* 568 | X0355462Y0214809D01* 569 | X0355354Y0214646D01* 570 | X0355191Y0214538D01* 571 | X0355000Y0214500D01* 572 | X0285000Y0214500D01* 573 | X0285000Y0215500D01* 574 | X0354500Y0215500D01* 575 | X0354500Y0354500D01* 576 | X0285000Y0354500D01* 577 | X0285000Y0355500D01* 578 | X0355000Y0355500D01* 579 | G37* 580 | 581 | G36* 582 | X0215000Y0404500D02* 583 | X0214809Y0404538D01* 584 | X0214646Y0404646D01* 585 | X0214538Y0404809D01* 586 | X0214500Y0405000D01* 587 | X0214500Y0545000D01* 588 | X0214538Y0545191D01* 589 | X0214646Y0545354D01* 590 | X0214809Y0545462D01* 591 | X0215000Y0545500D01* 592 | X0285000Y0545500D01* 593 | X0285000Y0544500D01* 594 | X0215500Y0544500D01* 595 | X0215500Y0405500D01* 596 | X0285000Y0405500D01* 597 | X0285000Y0404500D01* 598 | X0215000Y0404500D01* 599 | G37* 600 | 601 | G36* 602 | X0285000Y0404500D02* 603 | X0285000Y0405500D01* 604 | X0354500Y0405500D01* 605 | X0354500Y0544500D01* 606 | X0285000Y0544500D01* 607 | X0285000Y0545500D01* 608 | X0355000Y0545500D01* 609 | X0355191Y0545462D01* 610 | X0355354Y0545354D01* 611 | X0355462Y0545191D01* 612 | X0355500Y0545000D01* 613 | X0355500Y0405000D01* 614 | X0355462Y0404809D01* 615 | X0355354Y0404646D01* 616 | X0355191Y0404538D01* 617 | X0355000Y0404500D01* 618 | X0285000Y0404500D01* 619 | G37* 620 | 621 | G36* 622 | X0405000Y0024500D02* 623 | X0404809Y0024538D01* 624 | X0404646Y0024646D01* 625 | X0404538Y0024809D01* 626 | X0404500Y0025000D01* 627 | X0404500Y0165000D01* 628 | X0404538Y0165191D01* 629 | X0404646Y0165354D01* 630 | X0404809Y0165462D01* 631 | X0405000Y0165500D01* 632 | X0475000Y0165500D01* 633 | X0475000Y0164500D01* 634 | X0405500Y0164500D01* 635 | X0405500Y0025500D01* 636 | X0475000Y0025500D01* 637 | X0475000Y0024500D01* 638 | X0405000Y0024500D01* 639 | G37* 640 | 641 | G36* 642 | X0475000Y0024500D02* 643 | X0475000Y0025500D01* 644 | X0544500Y0025500D01* 645 | X0544500Y0164500D01* 646 | X0475000Y0164500D01* 647 | X0475000Y0165500D01* 648 | X0545000Y0165500D01* 649 | X0545191Y0165462D01* 650 | X0545354Y0165354D01* 651 | X0545462Y0165191D01* 652 | X0545500Y0165000D01* 653 | X0545500Y0025000D01* 654 | X0545462Y0024809D01* 655 | X0545354Y0024646D01* 656 | X0545191Y0024538D01* 657 | X0545000Y0024500D01* 658 | X0475000Y0024500D01* 659 | G37* 660 | 661 | G36* 662 | X0405000Y0214500D02* 663 | X0404809Y0214538D01* 664 | X0404646Y0214646D01* 665 | X0404538Y0214809D01* 666 | X0404500Y0215000D01* 667 | X0404500Y0355000D01* 668 | X0404538Y0355191D01* 669 | X0404646Y0355354D01* 670 | X0404809Y0355462D01* 671 | X0405000Y0355500D01* 672 | X0475000Y0355500D01* 673 | X0475000Y0354500D01* 674 | X0405500Y0354500D01* 675 | X0405500Y0215500D01* 676 | X0475000Y0215500D01* 677 | X0475000Y0214500D01* 678 | X0405000Y0214500D01* 679 | G37* 680 | 681 | G36* 682 | X0545000Y0214500D02* 683 | X0475000Y0214500D01* 684 | X0475000Y0215500D01* 685 | X0544500Y0215500D01* 686 | X0544500Y0354500D01* 687 | X0475000Y0354500D01* 688 | X0475000Y0355500D01* 689 | X0545000Y0355500D01* 690 | X0545191Y0355462D01* 691 | X0545354Y0355354D01* 692 | X0545462Y0355191D01* 693 | X0545500Y0355000D01* 694 | X0545500Y0215000D01* 695 | X0545462Y0214809D01* 696 | X0545354Y0214646D01* 697 | X0545191Y0214538D01* 698 | X0545000Y0214500D01* 699 | G37* 700 | 701 | G36* 702 | X0404646Y0404646D02* 703 | X0404538Y0404809D01* 704 | X0404500Y0405000D01* 705 | X0404500Y0545000D01* 706 | X0404538Y0545191D01* 707 | X0404646Y0545354D01* 708 | X0404809Y0545462D01* 709 | X0405000Y0545500D01* 710 | X0475000Y0545500D01* 711 | X0475000Y0544500D01* 712 | X0405500Y0544500D01* 713 | X0405500Y0405500D01* 714 | X0475000Y0405500D01* 715 | X0475000Y0404500D01* 716 | X0405000Y0404500D01* 717 | X0404809Y0404538D01* 718 | X0404646Y0404646D01* 719 | G37* 720 | 721 | G36* 722 | X0545000Y0545500D02* 723 | X0545191Y0545462D01* 724 | X0545354Y0545354D01* 725 | X0545462Y0545191D01* 726 | X0545500Y0545000D01* 727 | X0545500Y0405000D01* 728 | X0545462Y0404809D01* 729 | X0545354Y0404646D01* 730 | X0545191Y0404538D01* 731 | X0545000Y0404500D01* 732 | X0475000Y0404500D01* 733 | X0475000Y0405500D01* 734 | X0544500Y0405500D01* 735 | X0544500Y0544500D01* 736 | X0475000Y0544500D01* 737 | X0475000Y0545500D01* 738 | X0545000Y0545500D01* 739 | G37* 740 | 741 | G36* 742 | X0594809Y0024538D02* 743 | X0594646Y0024646D01* 744 | X0594538Y0024809D01* 745 | X0594500Y0025000D01* 746 | X0594500Y0165000D01* 747 | X0594538Y0165191D01* 748 | X0594646Y0165354D01* 749 | X0594809Y0165462D01* 750 | X0595000Y0165500D01* 751 | X0665000Y0165500D01* 752 | X0665000Y0164500D01* 753 | X0595500Y0164500D01* 754 | X0595500Y0025500D01* 755 | X0665000Y0025500D01* 756 | X0665000Y0024500D01* 757 | X0595000Y0024500D01* 758 | X0594809Y0024538D01* 759 | G37* 760 | 761 | G36* 762 | X0735000Y0165500D02* 763 | X0735191Y0165462D01* 764 | X0735354Y0165354D01* 765 | X0735462Y0165191D01* 766 | X0735500Y0165000D01* 767 | X0735500Y0025000D01* 768 | X0735462Y0024809D01* 769 | X0735354Y0024646D01* 770 | X0735191Y0024538D01* 771 | X0735000Y0024500D01* 772 | X0665000Y0024500D01* 773 | X0665000Y0025500D01* 774 | X0734500Y0025500D01* 775 | X0734500Y0164500D01* 776 | X0665000Y0164500D01* 777 | X0665000Y0165500D01* 778 | X0735000Y0165500D01* 779 | G37* 780 | 781 | G36* 782 | X0595000Y0214500D02* 783 | X0594809Y0214538D01* 784 | X0594646Y0214646D01* 785 | X0594538Y0214809D01* 786 | X0594500Y0215000D01* 787 | X0594500Y0355000D01* 788 | X0594538Y0355191D01* 789 | X0594646Y0355354D01* 790 | X0594809Y0355462D01* 791 | X0595000Y0355500D01* 792 | X0665000Y0355500D01* 793 | X0665000Y0354500D01* 794 | X0595500Y0354500D01* 795 | X0595500Y0215500D01* 796 | X0665000Y0215500D01* 797 | X0665000Y0214500D01* 798 | X0595000Y0214500D01* 799 | G37* 800 | 801 | G36* 802 | X0735000Y0214500D02* 803 | X0665000Y0214500D01* 804 | X0665000Y0215500D01* 805 | X0734500Y0215500D01* 806 | X0734500Y0354500D01* 807 | X0665000Y0354500D01* 808 | X0665000Y0355500D01* 809 | X0735000Y0355500D01* 810 | X0735191Y0355462D01* 811 | X0735354Y0355354D01* 812 | X0735462Y0355191D01* 813 | X0735500Y0355000D01* 814 | X0735500Y0215000D01* 815 | X0735462Y0214809D01* 816 | X0735354Y0214646D01* 817 | X0735191Y0214538D01* 818 | X0735000Y0214500D01* 819 | G37* 820 | 821 | G36* 822 | X0595000Y0404500D02* 823 | X0594809Y0404538D01* 824 | X0594646Y0404646D01* 825 | X0594538Y0404809D01* 826 | X0594500Y0405000D01* 827 | X0594500Y0545000D01* 828 | X0594538Y0545191D01* 829 | X0594646Y0545354D01* 830 | X0594809Y0545462D01* 831 | X0595000Y0545500D01* 832 | X0665000Y0545500D01* 833 | X0665000Y0544500D01* 834 | X0595500Y0544500D01* 835 | X0595500Y0405500D01* 836 | X0665000Y0405500D01* 837 | X0665000Y0404500D01* 838 | X0595000Y0404500D01* 839 | G37* 840 | 841 | G36* 842 | X0665000Y0404500D02* 843 | X0665000Y0405500D01* 844 | X0734500Y0405500D01* 845 | X0734500Y0544500D01* 846 | X0665000Y0544500D01* 847 | X0665000Y0545500D01* 848 | X0735000Y0545500D01* 849 | X0735191Y0545462D01* 850 | X0735354Y0545354D01* 851 | X0735462Y0545191D01* 852 | X0735500Y0545000D01* 853 | X0735500Y0405000D01* 854 | X0735462Y0404809D01* 855 | X0735354Y0404646D01* 856 | X0735191Y0404538D01* 857 | X0735000Y0404500D01* 858 | X0665000Y0404500D01* 859 | G37* 860 | 861 | M02* 862 | -------------------------------------------------------------------------------- /hardware/build/pcb_gerber/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /hardware/build/pcb_png/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /hardware/case.py: -------------------------------------------------------------------------------- 1 | import cadquery as cq 2 | 3 | # use cq-server as gui, uncomment below line 4 | # you can run cq-server `cd hardware && docker run -p 5000:5000 -v $(pwd):/data cadquery/cadquery-server run /data` 5 | # from cq_server.ui import ui, show_object 6 | 7 | PCB_WIDTH = 76.0 8 | PCB_HEIGHT = 57.0 9 | PCB_THICKNESS = 1.6 10 | 11 | CASE_MARGIN_TOP = 11.0 12 | CASE_MARGIN_BOTTOM = 3.5 13 | CASE_MARGIN_PCB = 0.5 14 | 15 | CASE_FRAME = 2.0 16 | CASE_BOTTOM = 3.0 17 | 18 | SCREW_POINTS = [(19, 9.5), (19, -9.5), (-19, -9.5), (0, 9.5)] 19 | 20 | USB_POS = (11.25, 1.2 + 3.15 / 2) 21 | USB_HOLE_SIZE = [9, 3.2, 7.5] 22 | USB_HOLE_MARGIN = 0.5 23 | USB_CONN_SIZE = (11, 8) 24 | 25 | INNER_HEIGHT = CASE_MARGIN_TOP + PCB_THICKNESS + CASE_MARGIN_BOTTOM 26 | CASE_HEIGHT = INNER_HEIGHT + CASE_BOTTOM 27 | 28 | # main body 29 | case = ( 30 | cq.Workplane("XY") 31 | .rect( 32 | PCB_WIDTH + (CASE_FRAME + CASE_MARGIN_PCB) * 2, 33 | PCB_HEIGHT + (CASE_FRAME + CASE_MARGIN_PCB) * 2, 34 | ) 35 | .extrude(CASE_BOTTOM + INNER_HEIGHT) 36 | .edges("|Z") 37 | .fillet(2) 38 | .edges("|X") 39 | .chamfer(1) 40 | ) 41 | # inner 42 | case = ( 43 | case.faces(">Z") 44 | .workplane() 45 | .rect(PCB_WIDTH + CASE_MARGIN_PCB * 2, PCB_HEIGHT + CASE_MARGIN_PCB * 2) 46 | .cutBlind(-INNER_HEIGHT) 47 | ) 48 | # mounting holes 49 | case = ( 50 | case.faces(">Z") 51 | .workplane(offset=-INNER_HEIGHT) 52 | .tag("InnerBottom") 53 | .pushPoints(SCREW_POINTS) 54 | .circle(2.5) 55 | .extrude(CASE_MARGIN_BOTTOM) 56 | .workplaneFromTagged("InnerBottom") 57 | .pushPoints(SCREW_POINTS) 58 | .circle(1.1) 59 | .cutThruAll() 60 | .workplaneFromTagged("InnerBottom") 61 | .workplane(offset=-1) 62 | .pushPoints(SCREW_POINTS) 63 | .rect(4.2, 4.8) 64 | .cutBlind(-2) 65 | ) 66 | # USB 67 | case = ( 68 | case.faces(">Y") 69 | .workplane(centerOption="CenterOfMass") 70 | .center( 71 | PCB_WIDTH / 2 - USB_POS[0], 72 | CASE_HEIGHT / 2 - CASE_MARGIN_TOP - PCB_THICKNESS - USB_POS[1], 73 | ) 74 | .tag("USBCutout") 75 | .rect(USB_HOLE_SIZE[0] + USB_HOLE_MARGIN, USB_HOLE_SIZE[1] + USB_HOLE_MARGIN) 76 | .cutBlind(-(CASE_FRAME + USB_HOLE_SIZE[2] + 2)) 77 | .workplaneFromTagged("USBCutout") 78 | .rect(USB_CONN_SIZE[0], USB_CONN_SIZE[1]) 79 | .cutBlind(-1) 80 | ) 81 | 82 | pcb = ( 83 | cq.Workplane("XY") 84 | .workplane(offset=CASE_BOTTOM + CASE_MARGIN_BOTTOM) 85 | .rect(PCB_WIDTH, PCB_HEIGHT) 86 | .extrude(PCB_THICKNESS) 87 | .faces(">Z") 88 | .workplane() 89 | .pushPoints(SCREW_POINTS) 90 | .hole(2.2) 91 | ) 92 | 93 | OLED_COVER_WIDTH = 19 94 | OLED_COVER_HEIGHT = 19 * 2 - 3 95 | OLED_COVER_THICKNESS = 10 96 | 97 | oled_cover = ( 98 | cq.Workplane("XY") 99 | .workplane(offset=CASE_BOTTOM + CASE_MARGIN_BOTTOM + PCB_THICKNESS) 100 | .center(-PCB_WIDTH / 2, PCB_HEIGHT / 2) 101 | .tag("PCB_ORIGIN") 102 | .center(OLED_COVER_WIDTH / 2, -OLED_COVER_HEIGHT / 2) 103 | .rect(OLED_COVER_WIDTH, OLED_COVER_HEIGHT) 104 | .extrude(OLED_COVER_THICKNESS) 105 | ) 106 | 107 | # if use cq-server, uncomment 108 | # show_object(case, name="case") 109 | # show_object(pcb, name="pcb", options=dict(color=(0, 1, 0))) 110 | # show_object(oled_cover) 111 | 112 | cq.exporters.export(case, "hardware/build/case/case.stl") 113 | cq.exporters.export(oled_cover, "hardware/build/case/oled_cover.stl") 114 | -------------------------------------------------------------------------------- /hardware/imgs/QR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/QR.png -------------------------------------------------------------------------------- /hardware/imgs/dm9-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/dm9-logo.png -------------------------------------------------------------------------------- /hardware/imgs/hsgw-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/hsgw-logo.png -------------------------------------------------------------------------------- /hardware/imgs/logo-python-powered-w-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/logo-python-powered-w-logo.png -------------------------------------------------------------------------------- /hardware/imgs/logo-python-powered-w-logoBG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/logo-python-powered-w-logoBG.png -------------------------------------------------------------------------------- /hardware/imgs/logo-python-powered-w-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/logo-python-powered-w-text.png -------------------------------------------------------------------------------- /hardware/imgs/python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/hardware/imgs/python-logo.png -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad.pretty/D_SOD123_hand.kicad_mod: -------------------------------------------------------------------------------- 1 | (module D_SOD123_hand (layer F.Cu) (tedit 62166172) 2 | (attr smd) 3 | (fp_text reference REF** (at -1.7 0 90) (layer F.SilkS) 4 | (effects (font (size 1 1) (thickness 0.15))) 5 | ) 6 | (fp_text value D_SOD123_hand (at 1.8 0 90) (layer F.Fab) hide 7 | (effects (font (size 1 1) (thickness 0.15))) 8 | ) 9 | (fp_line (start -0.8 0.762) (end 0.8 0.762) (layer F.SilkS) (width 0.2)) 10 | (fp_line (start 0.8 -1.3) (end 0.8 1.3) (layer F.SilkS) (width 0.2)) 11 | (fp_line (start -0.8 1.3) (end -0.8 -1.3) (layer F.SilkS) (width 0.2)) 12 | (pad 1 smd roundrect (at 0 1.575) (size 0.95 1.4) (drill (offset 0 0.25)) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 13 | (pad 2 smd roundrect (at 0 -1.575) (size 0.95 1.4) (drill (offset 0 -0.25)) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) 14 | (model ${KISYS3DMOD}/Diode_SMD.3dshapes/D_SOD-123.step 15 | (at (xyz 0 0 0)) 16 | (scale (xyz 1 1 1)) 17 | (rotate (xyz 0 0 -90)) 18 | ) 19 | ) 20 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad.pretty/HOLE_D2.2.kicad_mod: -------------------------------------------------------------------------------- 1 | (module HOLE_D2.2 (layer F.Cu) (tedit 6367AF4D) 2 | (fp_text reference REF** (at 0 1.95) (layer Dwgs.User) hide 3 | (effects (font (size 1 1) (thickness 0.15))) 4 | ) 5 | (fp_text value HOLE_D2.2 (at 0 -1.95) (layer F.Fab) hide 6 | (effects (font (size 1 1) (thickness 0.15))) 7 | ) 8 | (fp_circle (center 0 0) (end 0 2) (layer Dwgs.User) (width 0.12)) 9 | (pad "" np_thru_hole circle (at 0 0) (size 2.2 2.2) (drill 2.2) (layers *.Cu *.Mask)) 10 | ) 11 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad.pretty/SW_Cherry_MX_1.00u_PCB.kicad_mod: -------------------------------------------------------------------------------- 1 | (module SW_Cherry_MX_1.00u_PCB (layer F.Cu) (tedit 5F54FDC7) 2 | (descr "Cherry MX keyboard switch 1.00u PCB") 3 | (tags "cherry mx 1.00U PCB") 4 | (fp_text reference REF** (at 0 -7.7) (layer B.SilkS) 5 | (effects (font (size 1 1) (thickness 0.15))) 6 | ) 7 | (fp_text value MX (at 0 7.874) (layer F.Fab) hide 8 | (effects (font (size 1 1) (thickness 0.15))) 9 | ) 10 | (fp_line (start -7.8 7.8) (end -7.8 -7.8) (layer Cmts.User) (width 0.15)) 11 | (fp_line (start 7.8 7.8) (end -7.8 7.8) (layer Cmts.User) (width 0.15)) 12 | (fp_line (start 7.8 -7.8) (end 7.8 7.8) (layer Cmts.User) (width 0.15)) 13 | (fp_line (start -7.8 -7.8) (end 7.8 -7.8) (layer Cmts.User) (width 0.15)) 14 | (fp_line (start -7 -7) (end 7 -7) (layer Dwgs.User) (width 0.1)) 15 | (fp_line (start -7 7) (end -7 -7) (layer Dwgs.User) (width 0.1)) 16 | (fp_line (start 7 -7) (end 7 7) (layer Dwgs.User) (width 0.1)) 17 | (fp_line (start -7 7) (end 7 7) (layer Dwgs.User) (width 0.1)) 18 | (fp_line (start -9.5 9.5) (end -9.5 -9.5) (layer F.Fab) (width 0.1)) 19 | (fp_line (start 9.5 9.5) (end -9.5 9.5) (layer F.Fab) (width 0.1)) 20 | (fp_line (start 9.5 -9.5) (end 9.5 9.5) (layer F.Fab) (width 0.1)) 21 | (fp_line (start -9.5 -9.5) (end 9.5 -9.5) (layer F.Fab) (width 0.1)) 22 | (pad "" np_thru_hole circle (at 5.08 0) (size 1.7 1.7) (drill 1.7) (layers *.Cu *.Mask)) 23 | (pad "" np_thru_hole circle (at -5.08 0) (size 1.7 1.7) (drill 1.7) (layers *.Cu *.Mask)) 24 | (pad "" np_thru_hole circle (at 0 0) (size 4 4) (drill 4) (layers *.Cu *.Mask)) 25 | (pad 1 thru_hole circle (at -3.81 -2.54) (size 2.5 2.5) (drill 1.5) (layers *.Cu *.Mask)) 26 | (pad 2 thru_hole circle (at 2.54 -5.08) (size 2.5 2.5) (drill 1.5) (layers *.Cu *.Mask)) 27 | (model ${KISYS3DMOD}/SMKiJ_keyboard.3dshapes/cherry_mx.step 28 | (at (xyz 0 0 0)) 29 | (scale (xyz 1 1 1)) 30 | (rotate (xyz 0 0 0)) 31 | ) 32 | ) 33 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad.pretty/oled_i2c.kicad_mod: -------------------------------------------------------------------------------- 1 | (module oled_i2c (layer F.Cu) (tedit 63691D51) 2 | (fp_text reference REF** (at 0 -2.5 0) (layer B.SilkS) 3 | (effects (font (size 1 1) (thickness 0.15))) 4 | ) 5 | (fp_text value oled_i2c (at 2.54 0 90) (layer F.Fab) 6 | (effects (font (size 1 1) (thickness 0.15))) 7 | ) 8 | (fp_line (start -1.27 -2.54) (end 1.27 -2.54) (layer B.SilkS) (width 0.12)) 9 | (fp_line (start 1.27 -5.08) (end 1.27 5.08) (layer B.SilkS) (width 0.12)) 10 | (fp_line (start -1.27 -5.08) (end 1.27 -5.08) (layer B.SilkS) (width 0.12)) 11 | (fp_line (start -1.27 5.08) (end 1.27 5.08) (layer B.SilkS) (width 0.12)) 12 | (fp_line (start -1.27 5.08) (end -1.27 -5.08) (layer B.SilkS) (width 0.12)) 13 | (fp_line (start 36.5 -5.999999) (end -1.5 -5.999999) (layer Dwgs.User) (width 0.15)) 14 | (fp_line (start -1.5 -5.999999) (end -1.499999 6) (layer Dwgs.User) (width 0.15)) 15 | (fp_line (start -1.499999 6) (end 36.5 6) (layer Dwgs.User) (width 0.15)) 16 | (fp_line (start 36.5 6) (end 36.5 -5.999999) (layer Dwgs.User) (width 0.15)) 17 | (fp_line (start 33.5 -5.749999) (end 3.499999 -5.749999) (layer Dwgs.User) (width 0.15)) 18 | (fp_line (start 3.499999 -5.749999) (end 3.5 5.75) (layer Dwgs.User) (width 0.15)) 19 | (fp_line (start 3.5 5.75) (end 33.5 5.75) (layer Dwgs.User) (width 0.15)) 20 | (fp_line (start 33.5 5.75) (end 33.5 -5.749999) (layer Dwgs.User) (width 0.15)) 21 | (fp_line (start 27.98 -3.839999) (end 5.6 -3.839999) (layer Dwgs.User) (width 0.15)) 22 | (fp_line (start 5.6 -3.839999) (end 5.6 1.74) (layer Dwgs.User) (width 0.15)) 23 | (fp_line (start 5.6 1.74) (end 27.98 1.74) (layer Dwgs.User) (width 0.15)) 24 | (fp_line (start 27.98 1.74) (end 27.98 -3.839999) (layer Dwgs.User) (width 0.15)) 25 | (pad 1 thru_hole circle (at 0 -3.81) (size 1.65 1.65) (drill 0.85) (layers *.Cu *.Mask)) 26 | (pad 2 thru_hole circle (at 0 -1.27) (size 1.65 1.65) (drill 0.85) (layers *.Cu *.Mask)) 27 | (pad 3 thru_hole circle (at 0 1.27) (size 1.65 1.65) (drill 0.85) (layers *.Cu *.Mask)) 28 | (pad 4 thru_hole rect (at 0 3.81) (size 1.65 1.65) (drill 0.85) (layers *.Cu *.Mask)) 29 | (model ${KISYS3DMOD}/SMKJP.3dshapes/oled_keyboard.step 30 | (offset (xyz 0 0 6)) 31 | (scale (xyz 1 1 1)) 32 | (rotate (xyz -90 0 0)) 33 | ) 34 | ) 35 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad.pretty/xiao_rp2040.kicad_mod: -------------------------------------------------------------------------------- 1 | (module xiao_rp2040 (layer F.Cu) (tedit 61C4D5C3) 2 | (fp_text reference REF** (at 0 11.5) (layer F.SilkS) 3 | (effects (font (size 1 1) (thickness 0.15))) 4 | ) 5 | (fp_text value xiao_rp2040 (at 0 -11.3) (layer F.Fab) 6 | (effects (font (size 1 1) (thickness 0.15))) 7 | ) 8 | (fp_line (start -6.858 10.609599) (end 6.858 10.609599) (layer F.SilkS) (width 0.15)) 9 | (fp_line (start 6.858 -10.492) (end -6.858 -10.492) (layer F.SilkS) (width 0.15)) 10 | (fp_line (start -4.85775 -3.922) (end 4.85775 -3.922) (layer Dwgs.User) (width 0.15)) 11 | (fp_line (start 4.85775 -3.922) (end 4.85775 -11.9855) (layer Dwgs.User) (width 0.15)) 12 | (fp_line (start 4.85775 -11.9855) (end -4.85775 -11.9855) (layer Dwgs.User) (width 0.15)) 13 | (fp_line (start -4.85775 -11.9855) (end -4.85775 -3.922) (layer Dwgs.User) (width 0.15)) 14 | (fp_line (start -0.4445 7.4295) (end -1.8415 7.4295) (layer Dwgs.User) (width 0.15)) 15 | (fp_line (start -1.8415 7.4295) (end -1.8415 9.8425) (layer Dwgs.User) (width 0.15)) 16 | (fp_line (start -1.8415 9.8425) (end -0.4445 9.8425) (layer Dwgs.User) (width 0.15)) 17 | (fp_line (start -0.4445 9.8425) (end -0.4445 7.4295) (layer Dwgs.User) (width 0.15)) 18 | (fp_line (start 0.6985 9.8425) (end 2.0955 9.8425) (layer Dwgs.User) (width 0.15)) 19 | (fp_line (start 2.0955 9.8425) (end 2.0955 7.4295) (layer Dwgs.User) (width 0.15)) 20 | (fp_line (start 2.0955 7.4295) (end 0.6985 7.4295) (layer Dwgs.User) (width 0.15)) 21 | (fp_line (start 0.6985 7.4295) (end 0.6985 9.8425) (layer Dwgs.User) (width 0.15)) 22 | (fp_line (start 0.5715 -7.5565) (end 2.2225 -7.5565) (layer Dwgs.User) (width 0.15)) 23 | (fp_line (start 2.2225 -7.5565) (end 2.2225 -9.2075) (layer Dwgs.User) (width 0.15)) 24 | (fp_line (start 2.2225 -9.2075) (end 0.5715 -9.2075) (layer Dwgs.User) (width 0.15)) 25 | (fp_line (start 0.5715 -9.2075) (end 0.5715 -7.5565) (layer Dwgs.User) (width 0.15)) 26 | (fp_line (start -1.9685 -7.5565) (end -0.3175 -7.5565) (layer Dwgs.User) (width 0.15)) 27 | (fp_line (start -0.3175 -7.5565) (end -0.3175 -9.2075) (layer Dwgs.User) (width 0.15)) 28 | (fp_line (start -0.3175 -9.2075) (end -1.9685 -9.2075) (layer Dwgs.User) (width 0.15)) 29 | (fp_line (start -1.9685 -9.2075) (end -1.9685 -7.5565) (layer Dwgs.User) (width 0.15)) 30 | (fp_line (start -1.9685 -5.0165) (end -0.3175 -5.0165) (layer Dwgs.User) (width 0.15)) 31 | (fp_line (start -0.3175 -5.0165) (end -0.3175 -6.6675) (layer Dwgs.User) (width 0.15)) 32 | (fp_line (start -0.3175 -6.6675) (end -1.9685 -6.6675) (layer Dwgs.User) (width 0.15)) 33 | (fp_line (start -1.9685 -6.6675) (end -1.9685 -5.0165) (layer Dwgs.User) (width 0.15)) 34 | (fp_line (start 0.5715 -5.0165) (end 2.2225 -5.0165) (layer Dwgs.User) (width 0.15)) 35 | (fp_line (start 2.2225 -5.0165) (end 2.2225 -6.6675) (layer Dwgs.User) (width 0.15)) 36 | (fp_line (start 2.2225 -6.6675) (end 0.5715 -6.6675) (layer Dwgs.User) (width 0.15)) 37 | (fp_line (start 0.5715 -6.6675) (end 0.5715 -5.0165) (layer Dwgs.User) (width 0.15)) 38 | (fp_line (start 2.013999 10.5456) (end 2.013999 7.7932) (layer Dwgs.User) (width 0.15)) 39 | (fp_line (start 2.013999 7.7932) (end 6.114 7.7932) (layer Dwgs.User) (width 0.15)) 40 | (fp_line (start 6.114 7.7932) (end 6.114 10.5456) (layer Dwgs.User) (width 0.15)) 41 | (fp_line (start 6.114 10.5456) (end 2.013999 10.5456) (layer Dwgs.User) (width 0.15)) 42 | (fp_line (start -6.114 10.5456) (end -6.114 7.7932) (layer Dwgs.User) (width 0.15)) 43 | (fp_line (start -6.114 7.7932) (end -2.013999 7.7932) (layer Dwgs.User) (width 0.15)) 44 | (fp_line (start -2.013999 7.7932) (end -2.013999 10.5456) (layer Dwgs.User) (width 0.15)) 45 | (fp_line (start -2.013999 10.5456) (end -6.114 10.5456) (layer Dwgs.User) (width 0.15)) 46 | (fp_line (start 1.775 7.5444) (end 1.775 10.4444) (layer Dwgs.User) (width 0.15)) 47 | (fp_line (start 1.775 10.4444) (end -1.5 10.4444) (layer Dwgs.User) (width 0.15)) 48 | (fp_line (start -1.5 10.4444) (end -1.5 7.5444) (layer Dwgs.User) (width 0.15)) 49 | (fp_line (start -1.5 7.5444) (end 1.775 7.5444) (layer Dwgs.User) (width 0.15)) 50 | (fp_circle (center 2.89 -5.672) (end 3.19 -5.672) (layer Dwgs.User) (width 0.15)) 51 | (fp_circle (center -2.89 -5.672) (end -2.59 -5.672) (layer Dwgs.User) (width 0.15)) 52 | (fp_circle (center 4.064 9.1694) (end 4.414 9.1694) (layer Dwgs.User) (width 0.15)) 53 | (fp_circle (center -4.064 9.1694) (end -3.714 9.1694) (layer Dwgs.User) (width 0.15)) 54 | (fp_line (start 8.889999 5.395) (end 8.889999 7.305) (layer Dwgs.User) (width 0.15)) 55 | (fp_line (start 8.889999 7.935) (end 8.889999 8.475999) (layer Dwgs.User) (width 0.15)) 56 | (fp_line (start 6.858 10.609599) (end -6.858 10.609599) (layer Dwgs.User) (width 0.15)) 57 | (fp_line (start -8.89 8.476) (end -8.89 7.935) (layer Dwgs.User) (width 0.15)) 58 | (fp_line (start -8.89 7.305) (end -8.89 5.395) (layer Dwgs.User) (width 0.15)) 59 | (fp_line (start -8.89 4.765) (end -8.89 2.855) (layer Dwgs.User) (width 0.15)) 60 | (fp_line (start -8.89 2.225) (end -8.89 0.315) (layer Dwgs.User) (width 0.15)) 61 | (fp_line (start -8.89 -0.315) (end -8.89 -2.225) (layer Dwgs.User) (width 0.15)) 62 | (fp_line (start -8.89 -2.855) (end -8.89 -4.765) (layer Dwgs.User) (width 0.15)) 63 | (fp_line (start -8.89 -5.395) (end -8.89 -7.305) (layer Dwgs.User) (width 0.15)) 64 | (fp_line (start -8.89 -7.935) (end -8.89 -8.46) (layer Dwgs.User) (width 0.15)) 65 | (fp_line (start -6.858 -10.492) (end 6.858 -10.492) (layer Dwgs.User) (width 0.15)) 66 | (fp_line (start 8.89 -8.46) (end 8.889999 -7.935) (layer Dwgs.User) (width 0.15)) 67 | (fp_line (start 8.889999 -7.305) (end 8.889999 -5.395) (layer Dwgs.User) (width 0.15)) 68 | (fp_line (start 8.889999 -4.765) (end 8.889999 -2.855) (layer Dwgs.User) (width 0.15)) 69 | (fp_line (start 8.889999 -2.225) (end 8.889999 -0.315) (layer Dwgs.User) (width 0.15)) 70 | (fp_line (start 8.889999 0.315) (end 8.889999 2.225) (layer Dwgs.User) (width 0.15)) 71 | (fp_line (start 8.889999 2.855) (end 8.889999 4.765) (layer Dwgs.User) (width 0.15)) 72 | (fp_circle (center -7.62 2.54) (end -7.23 2.54) (layer Dwgs.User) (width 0.15)) 73 | (fp_circle (center 7.62 -2.54) (end 8.01 -2.54) (layer Dwgs.User) (width 0.15)) 74 | (fp_circle (center -7.62 -5.08) (end -7.23 -5.08) (layer Dwgs.User) (width 0.15)) 75 | (fp_circle (center 7.62 0) (end 8.01 0) (layer Dwgs.User) (width 0.15)) 76 | (fp_circle (center -7.62 5.08) (end -7.23 5.08) (layer Dwgs.User) (width 0.15)) 77 | (fp_circle (center 7.62 5.08) (end 8.01 5.08) (layer Dwgs.User) (width 0.15)) 78 | (fp_circle (center -7.62 -7.62) (end -7.23 -7.62) (layer Dwgs.User) (width 0.15)) 79 | (fp_circle (center 7.62 2.54) (end 8.01 2.54) (layer Dwgs.User) (width 0.15)) 80 | (fp_circle (center 7.62 7.62) (end 8.01 7.62) (layer Dwgs.User) (width 0.15)) 81 | (fp_circle (center 7.62 -7.62) (end 8.01 -7.62) (layer Dwgs.User) (width 0.15)) 82 | (fp_circle (center -7.62 -2.54) (end -7.23 -2.54) (layer Dwgs.User) (width 0.15)) 83 | (fp_circle (center 7.62 -5.08) (end 8.01 -5.08) (layer Dwgs.User) (width 0.15)) 84 | (fp_circle (center -7.62 7.62) (end -7.23 7.62) (layer Dwgs.User) (width 0.15)) 85 | (fp_circle (center -7.62 0) (end -7.23 0) (layer Dwgs.User) (width 0.15)) 86 | (fp_arc (start 6.8072 8.5268) (end 6.858 10.609599) (angle -72.06045078) (layer F.SilkS) (width 0.15)) 87 | (fp_arc (start -6.858 -8.46) (end -6.858 -10.492) (angle -71.0463507) (layer F.SilkS) (width 0.15)) 88 | (fp_arc (start -6.8072 8.5268) (end -8.804385 9.12) (angle -72.06045119) (layer F.SilkS) (width 0.15)) 89 | (fp_arc (start 6.858 -8.46) (end 8.779828 -9.12) (angle -71.0463507) (layer F.SilkS) (width 0.15)) 90 | (fp_arc (start 8.889999 5.08) (end 8.889999 4.765) (angle -180) (layer Dwgs.User) (width 0.15)) 91 | (fp_arc (start 8.889999 7.62) (end 8.889999 7.305) (angle -179.9999999) (layer Dwgs.User) (width 0.15)) 92 | (fp_arc (start 6.8072 8.5268) (end 6.858 10.609599) (angle -89.99999846) (layer Dwgs.User) (width 0.15)) 93 | (fp_arc (start -6.8072 8.5268) (end -8.889999 8.476) (angle -89.99999746) (layer Dwgs.User) (width 0.15)) 94 | (fp_arc (start -8.89 7.62) (end -8.89 7.935) (angle -180) (layer Dwgs.User) (width 0.15)) 95 | (fp_arc (start -8.89 5.08) (end -8.89 5.395) (angle -180) (layer Dwgs.User) (width 0.15)) 96 | (fp_arc (start -8.89 2.54) (end -8.89 2.855) (angle -180) (layer Dwgs.User) (width 0.15)) 97 | (fp_arc (start -8.89 0) (end -8.89 0.314999) (angle -180) (layer Dwgs.User) (width 0.15)) 98 | (fp_arc (start -8.89 -2.54) (end -8.89 -2.225) (angle -180) (layer Dwgs.User) (width 0.15)) 99 | (fp_arc (start -8.89 -5.08) (end -8.89 -4.765) (angle -180) (layer Dwgs.User) (width 0.15)) 100 | (fp_arc (start -8.89 -7.62) (end -8.89 -7.305) (angle -180) (layer Dwgs.User) (width 0.15)) 101 | (fp_arc (start -6.858 -8.46) (end -6.858 -10.492) (angle -89.99999846) (layer Dwgs.User) (width 0.15)) 102 | (fp_arc (start 6.858 -8.46) (end 8.89 -8.46) (angle -90) (layer Dwgs.User) (width 0.15)) 103 | (fp_arc (start 8.89 -7.62) (end 8.89 -7.935) (angle -180) (layer Dwgs.User) (width 0.15)) 104 | (fp_arc (start 8.89 -5.08) (end 8.89 -5.395) (angle -180) (layer Dwgs.User) (width 0.15)) 105 | (fp_arc (start 8.889999 -2.54) (end 8.889999 -2.855) (angle -180) (layer Dwgs.User) (width 0.15)) 106 | (fp_arc (start 8.889999 0) (end 8.889999 -0.315) (angle -179.9999999) (layer Dwgs.User) (width 0.15)) 107 | (fp_arc (start 8.889999 2.54) (end 8.889999 2.225) (angle -179.9999999) (layer Dwgs.User) (width 0.15)) 108 | (pad 1 smd rect (at -7.62 -7.62) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 109 | (zone_connect 0)) 110 | (pad 2 smd rect (at -7.62 -5.08) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 111 | (zone_connect 0)) 112 | (pad 3 smd rect (at -7.62 -2.54) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 113 | (zone_connect 0)) 114 | (pad 4 smd rect (at -7.62 0) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 115 | (zone_connect 0)) 116 | (pad 5 smd rect (at -7.62 2.54) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 117 | (zone_connect 0)) 118 | (pad 6 smd rect (at -7.62 5.08) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 119 | (zone_connect 0)) 120 | (pad 7 smd rect (at -7.62 7.62) (size 3 2) (drill (offset -1.5 0)) (layers F.Cu F.Mask) 121 | (zone_connect 0)) 122 | (pad 8 smd rect (at 7.62 7.62) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 123 | (zone_connect 0)) 124 | (pad 9 smd rect (at 7.62 5.08) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 125 | (zone_connect 0)) 126 | (pad 10 smd rect (at 7.62 2.54) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 127 | (zone_connect 0)) 128 | (pad 11 smd rect (at 7.62 0) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 129 | (zone_connect 0)) 130 | (pad 12 smd rect (at 7.62 -2.54) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 131 | (zone_connect 0)) 132 | (pad 13 smd rect (at 7.62 -5.08) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 133 | (zone_connect 0)) 134 | (pad 14 smd rect (at 7.62 -7.62) (size 3 2) (drill (offset 1.5 0)) (layers F.Cu F.Mask) 135 | (zone_connect 0)) 136 | (pad 1 thru_hole circle (at -7.62 -7.62) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 137 | (zone_connect 0)) 138 | (pad 2 thru_hole circle (at -7.62 -5.08) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 139 | (zone_connect 0)) 140 | (pad 3 thru_hole circle (at -7.62 -2.54) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 141 | (zone_connect 0)) 142 | (pad 4 thru_hole circle (at -7.62 0) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 143 | (zone_connect 0)) 144 | (pad 5 thru_hole circle (at -7.62 2.54) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 145 | (zone_connect 0)) 146 | (pad 6 thru_hole circle (at -7.62 5.08) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 147 | (zone_connect 0)) 148 | (pad 7 thru_hole circle (at -7.62 7.62) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 149 | (zone_connect 0)) 150 | (pad 8 thru_hole circle (at 7.62 7.62) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 151 | (zone_connect 0)) 152 | (pad 9 thru_hole circle (at 7.62 5.08) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 153 | (zone_connect 0)) 154 | (pad 10 thru_hole circle (at 7.62 2.54) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 155 | (zone_connect 0)) 156 | (pad 11 thru_hole circle (at 7.62 0) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 157 | (zone_connect 0)) 158 | (pad 12 thru_hole circle (at 7.62 -2.54) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 159 | (zone_connect 0)) 160 | (pad 13 thru_hole circle (at 7.62 -5.08) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 161 | (zone_connect 0)) 162 | (pad 14 thru_hole circle (at 7.62 -7.62) (size 2 2) (drill 0.8) (layers *.Cu *.Mask) 163 | (zone_connect 0)) 164 | (model "${KISYS3DMOD}/dm9.3dshapes/STEP214 Seeeduino XIAO Arduino.STEP" 165 | (offset (xyz 0 0 1)) 166 | (scale (xyz 1 1 1)) 167 | (rotate (xyz -90 0 180)) 168 | ) 169 | ) 170 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad_symbols.bck: -------------------------------------------------------------------------------- 1 | EESchema-DOCLIB Version 2.0 2 | # 3 | $CMP D_Small_ALT 4 | D Diode, small symbol, filled shape 5 | K diode 6 | F ~ 7 | $ENDCMP 8 | # 9 | $CMP SW_Push 10 | D Push button switch, generic, two pins 11 | K switch normally-open pushbutton push-button 12 | F ~ 13 | $ENDCMP 14 | # 15 | #End Doc Library 16 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad_symbols.dcm: -------------------------------------------------------------------------------- 1 | EESchema-DOCLIB Version 2.0 2 | # 3 | $CMP D_Small_ALT 4 | D Diode, small symbol, filled shape 5 | K diode 6 | F ~ 7 | $ENDCMP 8 | # 9 | $CMP MountingHole 10 | D Mounting Hole without connection 11 | K mounting hole 12 | F ~ 13 | $ENDCMP 14 | # 15 | $CMP SW_Push 16 | D Push button switch, generic, two pins 17 | K switch normally-open pushbutton push-button 18 | F ~ 19 | $ENDCMP 20 | # 21 | #End Doc Library 22 | -------------------------------------------------------------------------------- /hardware/kicad_libs/kicad_symbols.lib: -------------------------------------------------------------------------------- 1 | EESchema-LIBRARY Version 2.4 2 | #encoding utf-8 3 | # 4 | # D_Small_ALT 5 | # 6 | DEF D_Small_ALT D 0 10 N N 1 F N 7 | F0 "D" -50 80 50 H V L CNN 8 | F1 "D_Small_ALT" -150 -80 50 H V L CNN 9 | F2 "kicad:D_SOD123_hand" 0 0 50 V I C CNN 10 | F3 "" 0 0 50 V I C CNN 11 | $FPLIST 12 | TO-???* 13 | *_Diode_* 14 | *SingleDiode* 15 | D_* 16 | $ENDFPLIST 17 | DRAW 18 | P 2 0 1 10 -30 -40 -30 40 N 19 | P 2 0 1 0 -30 0 30 0 N 20 | P 4 0 1 10 30 -40 -30 0 30 40 30 -40 F 21 | X K 1 -100 0 70 R 50 50 1 1 P 22 | X A 2 100 0 70 L 50 50 1 1 P 23 | ENDDRAW 24 | ENDDEF 25 | # 26 | # MountingHole 27 | # 28 | DEF MountingHole H 0 40 Y Y 1 F N 29 | F0 "H" 0 200 50 H V C CNN 30 | F1 "MountingHole" 0 125 50 H V C CNN 31 | F2 "" 0 0 50 H I C CNN 32 | F3 "" 0 0 50 H I C CNN 33 | $FPLIST 34 | MountingHole* 35 | $ENDFPLIST 36 | DRAW 37 | C 0 0 50 0 1 50 N 38 | ENDDRAW 39 | ENDDEF 40 | # 41 | # SW_Push 42 | # 43 | DEF SW_Push SW 0 40 N N 1 F N 44 | F0 "SW" 50 100 50 H V L CNN 45 | F1 "SW_Push" 0 -60 50 H V C CNN 46 | F2 "kicad:SW_Cherry_MX_1.00u_PCB" 0 200 50 H I C CNN 47 | F3 "" 0 200 50 H I C CNN 48 | DRAW 49 | C -80 0 20 0 1 0 N 50 | C 80 0 20 0 1 0 N 51 | P 2 0 1 0 0 50 0 120 N 52 | P 2 0 1 0 100 50 -100 50 N 53 | X 1 1 -200 0 100 R 50 50 0 1 P 54 | X 2 2 200 0 100 L 50 50 0 1 P 55 | ENDDRAW 56 | ENDDEF 57 | # 58 | # oled_i2c 59 | # 60 | DEF oled_i2c DISP 0 40 Y Y 1 F N 61 | F0 "DISP" 0 250 50 H V C CNN 62 | F1 "oled_i2c" 0 -350 50 H V C CNN 63 | F2 "kicad:oled_i2c" -200 0 50 H I C CNN 64 | F3 "" -200 0 50 H I C CNN 65 | DRAW 66 | S -200 200 200 -300 0 1 0 f 67 | X SDA 1 -300 100 100 R 50 50 1 1 B 68 | X SCL 2 -300 0 100 R 50 50 1 1 I 69 | X Vcc 3 -300 -100 100 R 50 50 1 1 W 70 | X GND 4 -300 -200 100 R 50 50 1 1 W 71 | ENDDRAW 72 | ENDDEF 73 | # 74 | # xiao_rp2040 75 | # 76 | DEF xiao_rp2040 U 0 40 Y Y 1 F N 77 | F0 "U" -1500 450 50 H V L CNN 78 | F1 "xiao_rp2040" -1200 -450 50 H V C CNN 79 | F2 "kicad:xiao_rp2040" -1200 100 50 H I C CNN 80 | F3 "" -1200 100 50 H I C CNN 81 | DRAW 82 | S -1500 400 1500 -400 0 1 0 f 83 | X P26/ADC0/I2C1_SDA 1 -1600 300 100 R 50 50 1 1 B 84 | X UART1_TX/I2C0_SDA/SPI0_RX/P4 10 1600 -100 100 L 50 50 1 1 B 85 | X I2C1_SCL/SPI0_TX/P3 11 1600 0 100 L 50 50 1 1 B 86 | X 3.3V 12 1600 100 100 L 50 50 1 1 w 87 | X GND 13 1600 200 100 L 50 50 1 1 w 88 | X 5V 14 1600 300 100 L 50 50 1 1 w 89 | X P27/ADC1/I2C1_SCL 2 -1600 200 100 R 50 50 1 1 B 90 | X P28/ADC2 3 -1600 100 100 R 50 50 1 1 B 91 | X P29/ADC3 4 -1600 0 100 R 50 50 1 1 B 92 | X P6/I2C1_SDA/SPI0_SCK 5 -1600 -100 100 R 50 50 1 1 B 93 | X P7/I2C1_SCL/SPI0_TX 6 -1600 -200 100 R 50 50 1 1 B 94 | X P0/UART0_TX/I2C0_SDA/SPI0_RX 7 -1600 -300 100 R 50 50 1 1 B 95 | X UART0_RX/I2C0_SCL/SPI0_CSn/P1 8 1600 -300 100 L 50 50 1 1 B 96 | X I2C1_SDA/SPI0_SCK/P2 9 1600 -200 100 L 50 50 1 1 B 97 | ENDDRAW 98 | ENDDEF 99 | # 100 | #End Library 101 | -------------------------------------------------------------------------------- /hardware/pcb.py: -------------------------------------------------------------------------------- 1 | from skidl import * 2 | from pcbflow import * 3 | 4 | if __name__ == "__main__": 5 | ### make netlist(circuit) by skidl 6 | 7 | KEY_COUNT = 10 8 | COL_COUNT = 4 9 | ROW_COUNT = 3 10 | 11 | # fmt: off 12 | # switch matrix mapping 13 | MATRIX_MAP = [ 14 | (0,1),(0,2),(0,3), 15 | (1,1),(1,2),(1,3), 16 | (2,0),(2,1),(2,2),(2,3) 17 | ] 18 | # fmt: on 19 | 20 | # add local libraries 21 | lib_search_paths[KICAD].append("hardware/kicad_libs") 22 | footprint_search_paths[KICAD].append("hardware/kicad_libs/kicad.pretty") 23 | 24 | # components 25 | diode = Part( 26 | "kicad_symbols", "D_Small_ALT", TEMPLATE, footprint="kicad:D_SOD123_hand" 27 | ) 28 | switch = Part( 29 | "kicad_symbols", 30 | "SW_Push", 31 | TEMPLATE, 32 | footprint="kicad:SW_Cherry_MX_1.00u_PCB", 33 | ) 34 | 35 | diodes = diode(KEY_COUNT) 36 | switches = switch(KEY_COUNT) 37 | 38 | xiao = Part("kicad_symbols", "xiao_rp2040", footprint="kicad:xiao_rp2040") 39 | oled = Part("kicad_symbols", "oled_i2c", footprint="kicad:oled_i2c") 40 | 41 | # if use netlist with pcbnew 42 | # hole = Part("kicad_symbols", "MountingHole", TEMPLATE, footprint="kicad:HOLE_D2.2") 43 | # holes = hole(4) 44 | 45 | # create net for key-matrix 46 | netRows = [Net(f"ROW{i}") for i in range(ROW_COUNT)] 47 | netCols = [Net(f"COL{i}") for i in range(COL_COUNT)] 48 | 49 | # wiring 50 | # print part object for see the pin description 51 | # print(oled) 52 | # print(xiao) 53 | 54 | # row -> sw -> diode -> col 55 | for sw, d, mapping in zip(switches, diodes, MATRIX_MAP): 56 | netRows[mapping[0]] & sw["1 2"] & d["K A"] & netCols[mapping[1]] 57 | 58 | # matrix -> xiao 59 | netCols[0] += xiao[8] 60 | netCols[1] += xiao[3] 61 | netCols[2] += xiao[4] 62 | netCols[3] += xiao[5] 63 | 64 | netRows[0] += xiao[1] 65 | netRows[1] += xiao[6] 66 | netRows[2] += xiao[7] 67 | 68 | # oled -> xiao 69 | Net("3.3V") & oled["Vcc"] & xiao["3.3V"] 70 | Net("GND") & oled["GND"] & xiao["GND"] 71 | Net("SDA") & oled["SDA"] & xiao[9] 72 | Net("SCL") & oled["SCL"] & xiao[11] 73 | 74 | # ERC() 75 | generate_netlist() 76 | 77 | ### place parts and draw trace by pcbflow 78 | 79 | # Calculate coordinates with origin in the upper left corner 80 | def pos(x, y): 81 | return (x, BOARD_HEIGHT - y) 82 | 83 | BOARD_WIDTH = 76.0 84 | BOARD_HEIGHT = 57.0 85 | 86 | KEY_PITCH = 19.0 87 | 88 | SCREW_HOLE = 2.2 89 | 90 | LAYER_TOP = "GTL" 91 | LAYER_BOTTOM = "GBL" 92 | 93 | # DRC setting 94 | circuit = builtins.default_circuit 95 | # create board 96 | board = Board((BOARD_WIDTH, BOARD_HEIGHT)) 97 | board.drc.trace_width = 0.5 98 | board.drc.via_drill = 0.6 99 | board.drc.via_annular_ring = 0.4 100 | board.drc.clearance = 0.4 101 | 102 | # assign placement position to skidl parts 103 | for sw, d, mapping in zip(switches, diodes, MATRIX_MAP): 104 | sw.pos = pos( 105 | mapping[1] * KEY_PITCH + KEY_PITCH / 2, 106 | mapping[0] * KEY_PITCH + KEY_PITCH / 2, 107 | ) 108 | sw.side = "top" 109 | d.pos = (sw.pos[0] + 6, sw.pos[1] + 5) 110 | d.side = "bottom" 111 | d.rotate = 0 112 | 113 | xiao.pos = pos(11.25, 11.5) 114 | xiao.side = "bottom" 115 | 116 | oled.pos = pos(9.5, 36) 117 | oled.side = "top" 118 | oled.rotate = 270 119 | 120 | # add holes 121 | board.add_hole(pos(KEY_PITCH * 2, KEY_PITCH), SCREW_HOLE) 122 | board.add_hole(pos(KEY_PITCH * 3, KEY_PITCH), SCREW_HOLE) 123 | board.add_hole(pos(KEY_PITCH * 3, KEY_PITCH * 2), SCREW_HOLE) 124 | board.add_hole(pos(KEY_PITCH, KEY_PITCH * 2), SCREW_HOLE) 125 | 126 | # place parts on board 127 | for part in circuit.parts: 128 | try: 129 | rotate = part.rotate 130 | except: 131 | rotate = 0 132 | 133 | try: 134 | SkiPart(board.DC(part.pos).right(rotate), part, side=part.side) 135 | except AttributeError: 136 | print(f"{part.ref} has no pos or side") 137 | continue 138 | 139 | ### wiring 140 | xiaoRef = "U1" 141 | oledRef = "DISP1" 142 | 143 | # get connected pin number from skidl net 144 | # pin number is started from 1 in skidl, 0 in pcbflow 145 | def get_pin_number_from_net(netLabel, ref): 146 | net = Net.get(netLabel) 147 | return list((int(x.num) - 1 for x in net.pins if x.ref == ref))[0] 148 | 149 | # get access xiao pads by net label 150 | # convert to correct pin number for solving the complication of the kicad library 151 | # apply newpath() for bug fix (the initial value has a strange value) 152 | def xiao_pads(net: str): 153 | return ( 154 | board.get_part(xiaoRef) 155 | .pads[get_pin_number_from_net(net, xiaoRef) + 14] 156 | .newpath() 157 | ) 158 | 159 | # row xiao -> sw 160 | xiao_pads("ROW0").set_layer(LAYER_TOP).w("r 45").align_meet( 161 | board.get_part("SW1").pads[0], "x" 162 | ) 163 | xiao_pads("ROW1").set_layer(LAYER_TOP).w("r 45").align_meet( 164 | board.get_part("SW4").pads[0], "y" 165 | ) 166 | xiao_pads("ROW2").set_layer(LAYER_TOP).w("l 180 f 12 r 45").align_meet( 167 | board.get_part("SW8").pads[0], "y" 168 | ) 169 | 170 | # d -> sw 171 | for i in range(1, KEY_COUNT + 1): 172 | sw = board.get_part(f"SW{i}") 173 | d = board.get_part(f"D{i}") 174 | d.pads[0].set_layer(LAYER_BOTTOM).w("f 1 r 45").align_meet(sw.pads[1], "x") 175 | 176 | # row sw->sw 177 | for i in range(3): 178 | refs = list(x.ref for x in netRows[i].pins if x.ref.startswith("SW")) 179 | for j in range(len(refs) - 1): 180 | board.get_part(refs[j]).pads[0].newpath().set_layer(LAYER_TOP).w( 181 | "r 90 f 2 l 45 f 1 r 45 f 2.5 r 45 f1" 182 | ).meet(board.get_part(refs[j + 1]).pads[0]) 183 | 184 | # store via 185 | viaCol = [0] * 4 186 | # col d->d (col 1-3) 187 | for i in range(1, 4): 188 | d1 = board.get_part(f"D{i}") 189 | d2 = board.get_part(f"D{i+3}") 190 | d3 = board.get_part(f"D{i+7}") 191 | via = ( 192 | d1.pads[1] 193 | .set_layer(LAYER_BOTTOM) 194 | .w("l 180 f 1.25 r 45 f 2 l 45 f 6.5 l 45") 195 | .align(d2.pads[1], "y") 196 | .wire() 197 | .via() 198 | ) 199 | viaCol[i] = via 200 | via.set_layer(LAYER_BOTTOM).meet(d2.pads[1]) 201 | d2.pads[1].set_layer(LAYER_BOTTOM).w( 202 | "l 180 f 1 r 45 f 2 l 45 f 6.5 l 45" 203 | ).align_meet(d3.pads[1], "y") 204 | 205 | # col 1-3 via -> xiao 206 | viaCol[1].set_layer(LAYER_TOP).w("l 45 f 11 l 45").align_meet( 207 | xiao_pads("COL1"), "x" 208 | ) 209 | viaCol[2].set_layer(LAYER_TOP).w("f 2 l 45 f 29.5 l 45").align_meet( 210 | xiao_pads("COL2"), "x" 211 | ) 212 | viaCol[3].set_layer(LAYER_TOP).w("f 4 l 45 f 48.5 l 45").align_meet( 213 | xiao_pads("COL3"), "x" 214 | ) 215 | 216 | # xiao -> oled 217 | for net in ["GND", "3.3V", "SCL", "SDA"]: 218 | oledPin = board.get_part(oledRef).pads[get_pin_number_from_net(net, oledRef)] 219 | xiao_pads(net).set_layer(LAYER_TOP).left(135).align_meet(oledPin, "y") 220 | 221 | # for layer in [LAYER_TOP, LAYER_BOTTOM]: 222 | # xiao_pads("GND").set_layer(layer).w( 223 | # "r 90 f 1.5 r 180 f 3 r 180 f 1.5 r 90" 224 | # ).wire() 225 | # board.get_part(oledRef).pads[ 226 | # get_pin_number_from_net("GND", "DISP1") 227 | # ].newpath().set_layer(layer).w("r 90 f 1.5 r 180 f 3 r 180 f 1.5 r 90").wire() 228 | 229 | # place logos 230 | board.add_bitmap( 231 | (47.5, 28.5), "hardware/imgs/python-logo.png", side="top", scale=0.85 232 | ) 233 | 234 | board.add_bitmap( 235 | (11.25, 45), 236 | "hardware/imgs/QR.png", 237 | side="bottom", 238 | scale=0.75, 239 | ) 240 | 241 | board.add_bitmap( 242 | (11, 28.5), 243 | "hardware/imgs/logo-python-powered-w-logo.png", 244 | side="bottom", 245 | layer="GBS", 246 | scale=0.7, 247 | ) 248 | board.add_bitmap( 249 | (11, 28.5), 250 | "hardware/imgs/logo-python-powered-w-logoBG.png", 251 | side="bottom", 252 | layer="GBL", 253 | scale=0.7, 254 | ) 255 | board.add_bitmap( 256 | (11, 28.5), 257 | "hardware/imgs/logo-python-powered-w-text.png", 258 | side="bottom", 259 | scale=0.7, 260 | ) 261 | 262 | board.add_bitmap( 263 | (47.5, 22), 264 | "hardware/imgs/dm9-logo.png", 265 | side="bottom", 266 | scale=0.8, 267 | ) 268 | 269 | board.add_bitmap( 270 | (66.5, 22), 271 | "hardware/imgs/hsgw-logo.png", 272 | side="bottom", 273 | scale=0.4, 274 | ) 275 | 276 | # place texts 277 | board.add_text( 278 | (47.5, 41.5), 279 | "https://github.com/hsgw/keyboard_made_by_python", 280 | scale=1.25, 281 | side="bottom", 282 | ) 283 | 284 | board.add_text( 285 | (BOARD_WIDTH - 39, 5), 286 | "KEYBOARD MADE BY PYTHON", 287 | scale=2, 288 | side="bottom", 289 | justify="left", 290 | ) 291 | board.add_text( 292 | (BOARD_WIDTH - 44, 4.75), 293 | "Rev.1", 294 | scale=1.25, 295 | side="bottom", 296 | justify="left", 297 | ) 298 | board.add_text( 299 | (BOARD_WIDTH - 47.5, 2.25), 300 | "(c) 2022, Takuya Urakawa / Dm9Records / 5z6p.com", 301 | scale=1.25, 302 | side="bottom", 303 | justify="left", 304 | ) 305 | 306 | board.add_text( 307 | (11.5, 55), 308 | "JLCJLCJLCJLC", 309 | scale=1.1, 310 | side="bottom", 311 | ) 312 | 313 | board.add_outline() 314 | board.save_png("kbd_python", subdir="hardware/build/pcb_png") 315 | board.save_gerbers("kbd_python", subdir="hardware/build/pcb_gerber") 316 | -------------------------------------------------------------------------------- /hardware/prepare_silks.py: -------------------------------------------------------------------------------- 1 | from svglib.svglib import svg2rlg 2 | from reportlab.graphics import renderPM 3 | 4 | import numpy as np 5 | from PIL import Image, ImageDraw, ImageOps 6 | 7 | svgFile = "hardware/imgs/python-powered-w.svg" 8 | outputPath = "hardware/imgs/" 9 | 10 | imgArray = np.array(renderPM.drawToPIL(svg2rlg(svgFile)).convert("L")) 11 | imgArray = np.uint8((imgArray < 127) * 255) 12 | imgRaw = Image.fromarray(imgArray, mode="L") 13 | 14 | logoMaskPos = (20, 0, 280, 280) 15 | textMaskPos = (290, 0, 720, 280) 16 | 17 | # draw = ImageDraw.Draw(imgRaw) 18 | # draw.ellipse(logoMaskPos, outline=100) 19 | # draw.rectangle(textMaskPos, outline=100) 20 | 21 | # only logo 22 | img = imgRaw.copy() 23 | draw = ImageDraw.Draw(img) 24 | draw.rectangle(textMaskPos, fill=0) 25 | img.save(outputPath + "logo-python-powered-w-logo.png") 26 | 27 | # only logo bg 28 | draw.ellipse(logoMaskPos, fill=255) 29 | img.save(outputPath + "logo-python-powered-w-logoBG.png") 30 | 31 | # only text 32 | img = imgRaw.copy() 33 | draw = ImageDraw.Draw(img) 34 | draw.ellipse(logoMaskPos, fill=0) 35 | img.save(outputPath + "logo-python-powered-w-text.png") 36 | -------------------------------------------------------------------------------- /notebook/en/README.md: -------------------------------------------------------------------------------- 1 | ![top](../imgs/keyboard_made_by_python.jpg) 2 | 3 | # Making a keyboard using only Python. 4 | 5 | > This is day 20 of [Keyboard #1 Advent Calendar 2022](https://adventar.org/calendars/7529). 6 | > The article yesterday was [コンスルーピンヘッダの代わりを探して](https://74th.hateblo.jp/entry/2022/12/19/124809) by 74th. 7 | 8 | Hello. I'm [hsgw](https://twitter.com/hsgw). 9 | Every year I post useful tips for the advent calendar post. 10 | 11 | This year I will introduce how to design a keyboard using only Python without using kicad or 3DCAD. 12 | 13 | # TL;DR 14 | ※ PCB, case, and firmware data are not guaranteed. Please let me know if there are any mistakes. 15 | 16 | PCB, case, and firmware data are not guaranteed. Please let me know if there are any mistakes. 17 | 18 | - Design not only the firmware, but also the schematic (netlist), PCB, and case all in Python. 19 | - Mainly introduce the design flow (please read the documentation and source code for the detailed) 20 | - The code will be explained in a runnable jupyter notebook (google colaboratory) environment. 21 | - The libraries to be used are as follows 22 | 23 | | Purpose |library| 24 | |------------|-----| 25 | |Circuit |[skidl](https://github.com/devbisme/skidl)| 26 | |PCB |[pcbflow](https://github.com/michaelgale/pcbflow) ([my folk](https://github.com/hsgw/pcbflow/tree/fix_kicad))| 27 | |Case |[cadquery](https://github.com/CadQuery/cadquery)| 28 | |Firmware |[KMK Firmware](https://github.com/KMKfw/kmk_firmware), [circuitpython](https://circuitpython.org/)| 29 | 30 | # Why make a keyboard in Python? 31 | [Python](https://www.python.org/) is a programming language. It is designed with a focus on creating useful programs that are easy to write and read. 32 | 33 | Of course, it can be extended with all kinds of libraries to create not only computer-like behaviors such as complex calculations, image processing, and machine learning, but also games, music, pictures, and anything else you can want to create. 34 | Python is used in almost anything that has the name "AI", and the image generation AI that has been making the rounds recently is also running in Python. 35 | 36 | There are also libraries for drawing schematics, wiring PCBs, creating 3D models, and even running microcontrollers in Python. 37 | 38 | The advantage of `designing something` by `writing code`, not just Python, is that the design is character-based information. 39 | It can be understood and designed using only a text editor without special things (schematic symbols, etc.) to be aware of. 40 | When you collaborate with someone else, you can use git's powerful version control and diff extraction, so it is easy to do so. 41 | 42 | And since the code for the design is a program, let the computer do the complex calculations and repetition of the same values and sections. It is less mistake and easier to make changes. 43 | 44 | # Code and Instructions 45 | Click on the link below to open the code and instructions that can be run on google colaboratory. 46 | 47 | The case section uses [binder](https://mybinder.org) for environmental reasons. This is a similar runtime environment. 48 | 49 | - [What is google colaboratory](https://colab.research.google.com/github/hsgw/keyboard-made-by-python/blob/main/notebook/en/what_is_colaboratory.ipynb) 50 | - [Design PCB](https://colab.research.google.com/github/hsgw/keyboard-made-by-python/blob/main/notebook/en/pcb.ipynb) 51 | - [Design case](https://mybinder.org/v2/gh/hsgw/keyboard-made-by-python/HEAD?labpath=notebook%2Fen%2Fcase.ipynb) * [binder](https://mybinder.org). It may take some time to display. 52 | - [Create firmware](firmware.md) 53 | 54 | # Impressions 55 | It was possible to design the entire project, including the hardware, using only Python. 56 | Especially netlist design with skidl and 3D modeling with cadquery can be done parametrically and are easy to write and understand, so there will be many uses in the future. 57 | I recommend a workflow that reads the skidl netlist into kicad's pcbnew because pcbflow seems to be out of development, has many restrictions, and is difficult to preview. 58 | 59 | # Lastly. 60 | It has been a busy year and the only keyboard project I did this year was shipping Lain's GB. Next year, I hope to be able to relax and design keyboards since both the semiconductor/component shortage and the weak yen seem to be over. 61 | Have a great New Year! 62 | 63 | // This article was written by casasagi+3D printed case -------------------------------------------------------------------------------- /notebook/en/case.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "> Project page is [Keyboard as a Python Code](https://hackaday.io/project/188907-keyboard-as-a-python-code)\n" 9 | ] 10 | }, 11 | { 12 | "attachments": {}, 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# Make a case\n", 17 | "\n", 18 | "![target case](../imgs/exported_case.png)\n", 19 | "\n", 20 | "Modeling with cadquery to create a case that can be 3D printable.\n", 21 | "\n", 22 | "Since cadquery viewer is not available on google colaboratory due to OS, the notebook is opened with binder. \n", 23 | "The binder enables jupyter notebook to be used by preconfiguring the OS and necessary libraries using Docker. \n", 24 | "Therefore, there is no need to install any libraries." 25 | ] 26 | }, 27 | { 28 | "attachments": {}, 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "# What is cadquery?\n", 33 | "[cadquery](https://github.com/CadQuery/cadquery) is an environment for coding python to generate 3D models.\n", 34 | "Use [jupyter_cadquery](https://github.com/bernhard-42/jupyter-cadquery) as a viewer.\n", 35 | "\n", 36 | "- cadquery https://github.com/CadQuery/cadquery\n", 37 | "- cadquery documentation https://cadquery-ja.readthedocs.io/ja/latest/\n", 38 | "- jupyter-cadquery https://github.com/bernhard-42/jupyter-cadquery\n", 39 | "\n", 40 | "Run the cell below and make sure the viewer opens.\n", 41 | "\n", 42 | "To run a cell, click on it and press `Shift + Enter` or `▶` on the top menu" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "import cadquery as cq\n", 52 | "\n", 53 | "from jupyter_cadquery import (\n", 54 | " versions,\n", 55 | " show, PartGroup, Part, \n", 56 | " get_viewer, close_viewer, get_viewers, close_viewers, open_viewer, set_defaults, get_defaults, open_viewer,\n", 57 | " get_pick,\n", 58 | ")\n", 59 | "\n", 60 | "from jupyter_cadquery.replay import replay, enable_replay, disable_replay\n", 61 | "\n", 62 | "enable_replay(False)\n", 63 | "\n", 64 | "set_defaults(\n", 65 | " cad_width=640, \n", 66 | " height=480, \n", 67 | ")\n", 68 | "\n", 69 | "print()\n", 70 | "versions()\n", 71 | "\n", 72 | "cv = open_viewer(\"CadQuery\", anchor=\"right\")" 73 | ] 74 | }, 75 | { 76 | "attachments": {}, 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## Try cadquery\n", 81 | "Generates a simple cube and displays it in the viewer." 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "# Make a cube from the xy plane and fillet the edges.\n", 91 | "box = cq.Workplane('XY').box(1, 2, 3).edges().fillet(0.1)\n", 92 | "# Display in default viewer\n", 93 | "show(box)" 94 | ] 95 | }, 96 | { 97 | "attachments": {}, 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "# Design the case\n", 102 | "Design a tray-type case in which PCBs are screwed in place.\n", 103 | "\n", 104 | "## Rough shape\n", 105 | "Create a rough outline based on the shape of the PCB. As usual, declare and use constants." 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "# PCB Outline\n", 115 | "PCB_WIDTH = 76.0\n", 116 | "PCB_HEIGHT = 57.0\n", 117 | "PCB_THICKNESS = 1.6\n", 118 | "\n", 119 | "# Z margin between case and PCB\n", 120 | "CASE_MARGIN_TOP = 11.0\n", 121 | "CASE_MARGIN_BOTTOM = 3.5\n", 122 | "\n", 123 | "# XY margin between case and PCB\n", 124 | "CASE_MARGIN_PCB = 0.5\n", 125 | "\n", 126 | "# Case Thickness\n", 127 | "CASE_FRAME = 2.0\n", 128 | "CASE_BOTTOM = 3.0\n", 129 | "\n", 130 | "INNER_HEIGHT = CASE_MARGIN_TOP + PCB_THICKNESS + CASE_MARGIN_BOTTOM\n", 131 | "CASE_HEIGHT = INNER_HEIGHT + CASE_BOTTOM\n", 132 | "\n", 133 | "# Draw a rectangle based on the XY plane and extrude it\n", 134 | "case = (\n", 135 | " cq.Workplane(\"XY\")\n", 136 | " .rect(\n", 137 | " PCB_WIDTH + (CASE_FRAME + CASE_MARGIN_PCB) * 2,\n", 138 | " PCB_HEIGHT + (CASE_FRAME + CASE_MARGIN_PCB) * 2,\n", 139 | " )\n", 140 | " .extrude(CASE_BOTTOM + INNER_HEIGHT)\n", 141 | " .edges(\"|Z\")\n", 142 | " .fillet(2)\n", 143 | " .edges(\"|X\")\n", 144 | " .chamfer(1)\n", 145 | ")\n", 146 | "\n", 147 | "show(case)" 148 | ] 149 | }, 150 | { 151 | "attachments": {}, 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "## Cut out the inside of the case\n", 156 | "Cut out the inside of the case to make a tray shape. \n", 157 | "Begin by selecting the reference plane from the current case face" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "# Reference the topmost surface on the Z-axis from the case faces.\n", 167 | "# Draw a rectangle and cut off the inside height\n", 168 | "case = (\n", 169 | " case.faces(\">Z\")\n", 170 | " .workplane()\n", 171 | " .rect(PCB_WIDTH + CASE_MARGIN_PCB * 2, PCB_HEIGHT + CASE_MARGIN_PCB * 2)\n", 172 | " .cutBlind(-INNER_HEIGHT)\n", 173 | ")\n", 174 | "\n", 175 | "show(case)" 176 | ] 177 | }, 178 | { 179 | "attachments": {}, 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "## Screw bosses and holes\n", 184 | "The reference plane is saved and used with `tag(\"name\")`. \n", 185 | "Prepare an array of screw position coordinates in advance and repeat the same process." 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "# Screw coordinates\n", 195 | "SCREW_POINTS = [(19, 9.5), (19, -9.5), (-19, -9.5), (0, 9.5)]\n", 196 | "\n", 197 | "# Make the base plane by selecting the top plane on the z-axis (the edge of the case that is just barely left) from the face of the case and offsetting it down by `INNER_HEIGHT`.\n", 198 | "# Make a cylinder of radius 2.5mm at each coordinate\n", 199 | "# Make a hole of radius 1.1mm at each coordinate\n", 200 | "# Make a new reference plane offset 1mm down from the base plane and drill a square hole to the bottom (a hole to put a nut)\n", 201 | "case = (\n", 202 | " case.faces(\">Z\")\n", 203 | " .workplane(offset=-INNER_HEIGHT)\n", 204 | " .tag(\"InnerBottom\")\n", 205 | " .pushPoints(SCREW_POINTS)\n", 206 | " .circle(2.5)\n", 207 | " .extrude(CASE_MARGIN_BOTTOM)\n", 208 | " .workplaneFromTagged(\"InnerBottom\")\n", 209 | " .pushPoints(SCREW_POINTS)\n", 210 | " .circle(1.1)\n", 211 | " .cutThruAll()\n", 212 | " .workplaneFromTagged(\"InnerBottom\")\n", 213 | " .workplane(offset=-1)\n", 214 | " .pushPoints(SCREW_POINTS)\n", 215 | " .rect(4.2, 4.8)\n", 216 | " .cutBlind(-2)\n", 217 | ")\n", 218 | "\n", 219 | "show(case)" 220 | ] 221 | }, 222 | { 223 | "attachments": {}, 224 | "cell_type": "markdown", 225 | "metadata": {}, 226 | "source": [ 227 | "## USB connector\n", 228 | "Make a slot for the USB connector. \n", 229 | "The USB connector needs to be drilled on the rear side, so the reference plane is the furthest plane on the Y-axis. \n", 230 | "There are planes with the same distance in front and behind, however, the plane selected is the rear side, so it is left as it is." 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "# USB connector slot location and dimensions\n", 240 | "USB_POS = (11.25, 1.2 + 3.15 / 2)\n", 241 | "USB_HOLE_SIZE = [9, 3.2, 7.5]\n", 242 | "USB_HOLE_MARGIN = 0.5\n", 243 | "USB_CONN_SIZE = (11, 8)\n", 244 | "\n", 245 | "# Base on the furthest surface in the Y-axis direction\n", 246 | "# Cut out the USB connector slot deep enough to the inside to avoid hitting the USB connector\n", 247 | "# Cut out areas to avoid the USB cable housing\n", 248 | "case = (\n", 249 | " case.faces(\">Y\")\n", 250 | " .workplane(centerOption=\"CenterOfMass\")\n", 251 | " .center(\n", 252 | " PCB_WIDTH / 2 - USB_POS[0],\n", 253 | " CASE_HEIGHT / 2 - CASE_MARGIN_TOP - PCB_THICKNESS - USB_POS[1],\n", 254 | " )\n", 255 | " .tag(\"USBCutout\")\n", 256 | " .rect(USB_HOLE_SIZE[0] + USB_HOLE_MARGIN, USB_HOLE_SIZE[1] + USB_HOLE_MARGIN)\n", 257 | " .cutBlind(-(CASE_FRAME + USB_HOLE_SIZE[2] + 2))\n", 258 | " .workplaneFromTagged(\"USBCutout\")\n", 259 | " .rect(USB_CONN_SIZE[0], USB_CONN_SIZE[1])\n", 260 | " .cutBlind(-1)\n", 261 | ")\n", 262 | "\n", 263 | "show(case)" 264 | ] 265 | }, 266 | { 267 | "attachments": {}, 268 | "cell_type": "markdown", 269 | "metadata": {}, 270 | "source": [ 271 | "## Cover under OLED\n", 272 | "Create a cover to be placed between the PCB and the OLED. \n", 273 | "The cover will be made as a separate object, but its position will be aligned.\n", 274 | "The cover will be fixed with double-sided tape, so no screws are required." 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": null, 280 | "metadata": {}, 281 | "outputs": [], 282 | "source": [ 283 | "OLED_COVER_WIDTH = 19\n", 284 | "OLED_COVER_HEIGHT = 19 * 2 - 3\n", 285 | "OLED_COVER_THICKNESS = 10\n", 286 | "\n", 287 | "# Base on offset plane from the XY plane by the height above the PCB.\n", 288 | "oled_cover = (\n", 289 | " cq.Workplane(\"XY\")\n", 290 | " .workplane(offset=CASE_BOTTOM + CASE_MARGIN_BOTTOM + PCB_THICKNESS)\n", 291 | " .center(-PCB_WIDTH / 2, PCB_HEIGHT / 2)\n", 292 | " .tag(\"PCB_ORIGIN\")\n", 293 | " .center(OLED_COVER_WIDTH / 2, -OLED_COVER_HEIGHT / 2)\n", 294 | " .rect(OLED_COVER_WIDTH, OLED_COVER_HEIGHT)\n", 295 | " .extrude(OLED_COVER_THICKNESS)\n", 296 | " .faces(\"Z\")\n", 297 | " .tag(\"CASE_TOP\")\n", 298 | " .edges(\">Y or Project page is [Keyboard as a Python Code](https://hackaday.io/project/188907-keyboard-as-a-python-code) 2 | 3 | # Create firmware 4 | The firmware uses [KMK Firmware](https://github.com/KMKfw/kmk_firmware), based on [CircuitPython](https://circuitpython.org/), which is an embedded Python Implementation. 5 | - CircuitPython https://circuitpython.org/ 6 | - KMK Firmware https://github.com/KMKfw/kmk_firmware 7 | 8 | With the RP2040 and CircuitPython, you can simply write the Python code directly to the drive recognised. Of course, the code can be rewritten by editing the saved file itself. 9 | 10 | ## Install KMK Firmware 11 | Install KMK Firmware following [Getting started guide](https://github.com/KMKfw/kmk_firmware/blob/master/docs/en/Getting_Started.md). 12 | 13 | - Install CircuitPython 7.x for XIAO RP2040. (7.3.3 as of 2022/12) 14 | https://circuitpython.org/board/seeeduino_xiao_rp2040/ 15 | 16 | ## Install additional libraries 17 | Install the packages required to use the OLED feature of KMK Firmware. 18 | 19 | - Documentation for OLED feature 20 | https://github.com/KMKfw/kmk_firmware/blob/master/docs/en/peg_oled_display.md 21 | 22 | Download the bundle as described in the documentation. Copy `adafruit_display_text/` and `adafruit_displayio_ssd1306.mpy` in `adafruit-circuitpython-bundle-7.x~/lib/` to under `lib/` in the XIAO drive. 23 | 24 | # Create firmware 25 | KMK Firmware requires `kb.py` and `main.py`. 26 | The firmware for this keyboard can be found in [firmware/](. /... /firmware/). 27 | 28 | If you find `code.py` or `main.py` in the root of XIAO already, remove them once. 29 | Do not delete `boot.py`, `libs/`, and so on. 30 | 31 | ## kb.py 32 | In `kb.py`, define the hardware information for the keyboard. 33 | 34 | ```python 35 | # Board information defined in CircuitPython 36 | # It depends on CircuitPython installed. 37 | # print(dir(board)) to check the definition 38 | import board 39 | 40 | from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard 41 | from kmk.scanners import DiodeOrientation 42 | 43 | class KMKKeyboard(_KMKKeyboard): 44 | # pin assigns for switch matrix 45 | row_pins = (board.D0, board.D5, board.D6) 46 | col_pins = ( 47 | board.D7, 48 | board.D2, 49 | board.D3, 50 | board.D4, 51 | ) 52 | # diode orientation 53 | diode_orientation = DiodeOrientation.COL2ROW 54 | 55 | # I2C pin assign for OLED 56 | SCL = board.D10 57 | SDA = board.D8 58 | 59 | # Linking the matrix to the actual key position 60 | # like LAYOUT in QMK 61 | # fmt: off 62 | coord_mapping = [ 63 | 1, 2, 3, 64 | 5, 6, 7, 65 | 8, 9, 10, 11 66 | ] 67 | # fmt: on 68 | 69 | ``` 70 | 71 | ## main.py 72 | In `main.py`, implement the keyboard keymap and behavior. 73 | It also prepares the image to be displayed on the OLED as a bitmap. 74 | 75 | ```python 76 | import board 77 | 78 | from kb import KMKKeyboard 79 | 80 | from kmk.keys import KC 81 | from kmk.modules.layers import Layers 82 | # OLED extension 83 | from kmk.extensions.peg_oled_Display import ( 84 | Oled, 85 | OledDisplayMode, 86 | OledReactionType, 87 | OledData, 88 | ) 89 | 90 | keyboard = KMKKeyboard() 91 | 92 | # enable layer 93 | layers_ext = Layers() 94 | keyboard.modules.append(layers_ext) 95 | 96 | # keyboard setting 97 | keyboard.tap_time = 250 98 | # debug 99 | # If set to True, debug information is output to the serial port. 100 | keyboard.debug_enabled = False 101 | 102 | # OLED config 103 | # # The image should be a black and white bitmap. 104 | oled_ext = Oled( 105 | OledData(image={0: OledReactionType.STATIC, 1: ["oled_python.bmp"]}), 106 | toDisplay=OledDisplayMode.IMG, 107 | flip=True, 108 | ) 109 | keyboard.extensions.append(oled_ext) 110 | 111 | ____ = KC.TRNS 112 | L1_P0 = KC.LT(1, KC.P0) 113 | 114 | # keymap 115 | # fmt: off 116 | keyboard.keymap = [ 117 | # default layer 118 | [ 119 | KC.P7, KC.P8, KC.P9, 120 | KC.P4, KC.P5, KC.P6, 121 | L1_P0, KC.P1, KC.P2, KC.P3 122 | ], 123 | # Fn layer 124 | [ 125 | KC.ESC, KC.DEL, KC.BSPC, 126 | KC.COMM, KC.NO, KC.TAB, 127 | ____, KC.DOT, KC.SPC, KC.ENT 128 | ], 129 | ] 130 | # fmt: on 131 | 132 | if __name__ == "__main__": 133 | keyboard.go() 134 | 135 | ``` 136 | 137 | # Write to XIAO 138 | Write `kb.py`, `main.py` and the image file to XIAO and check the working. 139 | Now it is complete! 140 | -------------------------------------------------------------------------------- /notebook/en/pcb.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "colab_type": "text", 8 | "id": "view-in-github" 9 | }, 10 | "source": [ 11 | "\"Open" 12 | ] 13 | }, 14 | { 15 | "attachments": {}, 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "> Project page is [Keyboard as a Python Code](https://hackaday.io/project/188907-keyboard-as-a-python-code)\n" 20 | ] 21 | }, 22 | { 23 | "attachments": {}, 24 | "cell_type": "markdown", 25 | "metadata": { 26 | "id": "g6rr65y7TUEE" 27 | }, 28 | "source": [ 29 | "# Design PCB\n", 30 | "\n", 31 | "\n", 32 | "\n", 33 | "\n", 34 | "The goal of this notebook is to design the circuit for a keyboard and to get `Gerber files` for the production.\n", 35 | "To make a pcb in the normal way, first draw a `circuit diagram` in the `circuit diagram editor` and output it as a `netlist` with the components to be used and their wiring information, and then physically place and wire the components on the `PCB` using the `PCB editor`." 36 | ] 37 | }, 38 | { 39 | "attachments": {}, 40 | "cell_type": "markdown", 41 | "metadata": { 42 | "id": "RwgtrhL6DhoQ" 43 | }, 44 | "source": [ 45 | "## NOTE TO READ ARTICLE.\n", 46 | "This notebook contains cells that will not work properly if run twice.\n", 47 | "\n", 48 | "To prevent this, you should run `Runtime>Execute All Cells` from the menu above before starting to read the article. It will take some time for the execution to complete." 49 | ] 50 | }, 51 | { 52 | "attachments": {}, 53 | "cell_type": "markdown", 54 | "metadata": { 55 | "id": "Topn5GuhW6Z2" 56 | }, 57 | "source": [ 58 | "## Download resources\n", 59 | "Run the following cell to clone the repository with kicad libraries and other resources." 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": null, 65 | "metadata": { 66 | "colab": { 67 | "base_uri": "https://localhost:8080/" 68 | }, 69 | "id": "hEThVfBsW8RE", 70 | "outputId": "b4211474-89e8-4cf3-9779-fdbd6e895db2" 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "!git clone https://github.com/hsgw/keyboard-made-by-python/" 75 | ] 76 | }, 77 | { 78 | "attachments": {}, 79 | "cell_type": "markdown", 80 | "metadata": { 81 | "id": "Ancu-CO6W_Y4" 82 | }, 83 | "source": [ 84 | "# Design a netlist (schematic) with skidl\n", 85 | "If you design with Python, you can define components and wiring information by code without drawing a schematic, and output a netlist directly.\n", 86 | "\n", 87 | "[skidl](https://github.com/devbisme/skidl) uses the kicad component library as well as its own formats. The netlist is also compatible with the kicad and can be directly imported into pcbnew to make the pcb.\n", 88 | "\n", 89 | "- skidl https://github.com/devbisme/skidl\n", 90 | "- skidl Document https://devbisme.github.io/skidl/" 91 | ] 92 | }, 93 | { 94 | "attachments": {}, 95 | "cell_type": "markdown", 96 | "metadata": { 97 | "id": "-cW5lSiPYuUS" 98 | }, 99 | "source": [ 100 | "## Install and import skidl\n", 101 | "Install and import [skidl](https://github.com/devbisme/skidl)." 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": { 108 | "colab": { 109 | "base_uri": "https://localhost:8080/" 110 | }, 111 | "id": "Oy3vcMzUWq9g", 112 | "outputId": "aa8168c0-4184-4e8b-cd14-815035fc8882" 113 | }, 114 | "outputs": [], 115 | "source": [ 116 | "# Install will take some time\n", 117 | "!pip install skidl" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": { 124 | "colab": { 125 | "base_uri": "https://localhost:8080/" 126 | }, 127 | "id": "KMF3_yPHTF0L", 128 | "outputId": "e51fa261-a270-4d10-c6a9-b27fae81e2b9" 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "# WARNING because the default path for kicad is not set in the environment variable\n", 133 | "from skidl import *" 134 | ] 135 | }, 136 | { 137 | "attachments": {}, 138 | "cell_type": "markdown", 139 | "metadata": { 140 | "id": "LTeD6nJwogVT" 141 | }, 142 | "source": [ 143 | "## Declare constants\n", 144 | "Declare values that appear repeatedly as constants. " 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": { 151 | "id": "6jwqmkIjo5MH" 152 | }, 153 | "outputs": [], 154 | "source": [ 155 | "KEY_COUNT = 10\n", 156 | "COL_COUNT = 4\n", 157 | "ROW_COUNT = 3\n", 158 | "MATRIX_MAP = [\n", 159 | " (0,1),(0,2),(0,3), \n", 160 | " (1,1),(1,2),(1,3),\n", 161 | " (2,0),(2,1),(2,2),(2,3)\n", 162 | "]" 163 | ] 164 | }, 165 | { 166 | "attachments": {}, 167 | "cell_type": "markdown", 168 | "metadata": { 169 | "id": "QWaPdvrNmmzk" 170 | }, 171 | "source": [ 172 | "## Loading kicad library and define parts\n", 173 | "Add the path to the kicad library to be used.\n", 174 | "\n", 175 | "If kicad is installed on your computer, the path to the default library has been added. In this environment, pass the path to the required libraries only. \n", 176 | "They are in `keyboard-made-by-python/hardware/kicad_libs` that has just been cloned from github." 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "metadata": { 183 | "colab": { 184 | "base_uri": "https://localhost:8080/" 185 | }, 186 | "id": "EjCwQBClnF5q", 187 | "outputId": "0c0ad665-d336-4d2a-af5e-55f8886532ac" 188 | }, 189 | "outputs": [], 190 | "source": [ 191 | "# resistor library paths\n", 192 | "# require both symbol and footprint\n", 193 | "lib_search_paths[KICAD].append(\"keyboard-made-by-python/hardware/kicad_libs\")\n", 194 | "footprint_search_paths[KICAD].append(\"keyboard-made-by-python/hardware/kicad_libs/kicad.pretty\")\n", 195 | "\n", 196 | "# If you use the same part more than one, define it as template with a symbol and footprint\n", 197 | "diode = Part(\n", 198 | " \"kicad_symbols\", \"D_Small_ALT\", TEMPLATE, footprint=\"kicad:D_SOD123_hand\"\n", 199 | ")\n", 200 | "switch = Part(\n", 201 | " \"kicad_symbols\",\n", 202 | " \"SW_Push\",\n", 203 | " TEMPLATE,\n", 204 | " footprint=\"kicad:SW_Cherry_MX_1.00u_PCB\",\n", 205 | ")\n", 206 | "\n", 207 | "# define diodes and switches as array\n", 208 | "diodes = diode(KEY_COUNT)\n", 209 | "switches = switch(KEY_COUNT)\n", 210 | "\n", 211 | "xiao = Part(\"kicad_symbols\", \"xiao_rp2040\", footprint=\"kicad:xiao_rp2040\")\n", 212 | "oled = Part(\"kicad_symbols\", \"oled_i2c\", footprint=\"kicad:oled_i2c\")\n", 213 | "\n", 214 | "# You can see the Pin information of the symbol when you print\n", 215 | "print(diode, switch, xiao, oled)" 216 | ] 217 | }, 218 | { 219 | "attachments": {}, 220 | "cell_type": "markdown", 221 | "metadata": { 222 | "id": "oG5xuNjqq6qg" 223 | }, 224 | "source": [ 225 | "## Connecting Pins and Net\n", 226 | "Define a circuit by entering connections between the pins of the prepared components.\n", 227 | "\n", 228 | "If possible, you should name the wiring itself with `Net`. By connecting the `Pins` of the components based on the `Net`, you can make the code easier to read by organizing the connections to the same place (power supply, switch matrix, etc.). It also serves as a good guide when importing a netlist into kicad, as it will be displayed there.\n", 229 | "\n", 230 | "This design will also read the `pin numbers` from `Net` when wiring the PCB." 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": { 237 | "colab": { 238 | "base_uri": "https://localhost:8080/" 239 | }, 240 | "id": "PI9UK581tf5H", 241 | "outputId": "3cb4724e-8401-4655-a2e2-66fb5cb5368f" 242 | }, 243 | "outputs": [], 244 | "source": [ 245 | "# Viewing pin information is helpfull when wiring\n", 246 | "print(xiao)" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "metadata": { 253 | "colab": { 254 | "base_uri": "https://localhost:8080/" 255 | }, 256 | "id": "Ufo-bwPNtWzt", 257 | "outputId": "833c4292-eef8-46e5-c648-298eb0fe0c53" 258 | }, 259 | "outputs": [], 260 | "source": [ 261 | "# Make an array containing the netlist of ROW and COL of the switch matrix\n", 262 | "netRows = [Net(f\"ROW{i}\") for i in range(ROW_COUNT)]\n", 263 | "netCols = [Net(f\"COL{i}\") for i in range(COL_COUNT)]\n", 264 | "\n", 265 | "\n", 266 | "# Connect ROW's Net -> switch's Pin 1 switch's Pin 2 -> diode's cathode, diode's anode -> COL's Net\n", 267 | "# Net and Pin are connected by `&`\n", 268 | "# Pin of a component can be accessed by part[\"pin name\"]\n", 269 | "# Pin of a part can be accessed by part[\"pin name\"] \n", 270 | "# Two subscripts for in and out\n", 271 | "# Example: Net or Pin connected to sw[\"1\"] & sw[\"1 2\"] & Net or Pin connected to sw[\"2\"]\n", 272 | "for sw, d, mapping in zip(switches, diodes, MATRIX_MAP):\n", 273 | " netRows[mapping[0]] & sw[\"1 2\"] & d[\"K A\"] & netCols[mapping[1]]\n", 274 | "\n", 275 | "# Connect the switch matrix to xiao\n", 276 | "# Add Pin to Net by `+`\n", 277 | "netCols[0] += xiao[8]\n", 278 | "netCols[1] += xiao[3]\n", 279 | "netCols[2] += xiao[4]\n", 280 | "netCols[3] += xiao[5]\n", 281 | "\n", 282 | "netRows[0] += xiao[1]\n", 283 | "netRows[1] += xiao[6]\n", 284 | "netRows[2] += xiao[7]\n", 285 | "\n", 286 | "# Connect oled and xiao\n", 287 | "# You can connect by declaring Net directly like Net(\"3.3V\")\n", 288 | "Net(\"3.3V\") & oled[\"Vcc\"] & xiao[\"3.3V\"]\n", 289 | "Net(\"GND\") & oled[\"GND\"] & xiao[\"GND\"]\n", 290 | "Net(\"SDA\") & oled[\"SDA\"] & xiao[9]\n", 291 | "Net(\"SCL\") & oled[\"SCL\"] & xiao[11]\n", 292 | "\n", 293 | "# printing Net shows connected Pins\n", 294 | "print(netRows[0])" 295 | ] 296 | }, 297 | { 298 | "attachments": {}, 299 | "cell_type": "markdown", 300 | "metadata": { 301 | "id": "ygS8U7MywJ8k" 302 | }, 303 | "source": [ 304 | "## Run ERC and export netlist\n", 305 | "Run ERC (Electrical Rule Check, schematic rule check) and export the netlist.\n", 306 | "Now the schematic/netlist is complete!" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": null, 312 | "metadata": { 313 | "colab": { 314 | "base_uri": "https://localhost:8080/", 315 | "height": 330 316 | }, 317 | "id": "7UCaNTPgwJVV", 318 | "outputId": "209f3e2a-ff1e-4806-d9b4-13bd61d0f927" 319 | }, 320 | "outputs": [], 321 | "source": [ 322 | "# You may get an error if you run other cells more than once\n", 323 | "# In that case, restart and run all cells again\n", 324 | "# You will get a warning about un-wired cells, but no problem\n", 325 | "ERC()\n", 326 | "generate_netlist(file_=\"keyboard.net\")" 327 | ] 328 | }, 329 | { 330 | "attachments": {}, 331 | "cell_type": "markdown", 332 | "metadata": { 333 | "id": "5URJ6fd7xzse" 334 | }, 335 | "source": [ 336 | "The generated netlist can be imported by kicad pcbnew to create pcb as-is.\n", 337 | "\n", 338 | "![imported by kicad pcbnew](https://github.com/hsgw/keyboard-made-by-python/blob/main/notebook/imgs/kicad_pcbnew.png?raw=1)" 339 | ] 340 | }, 341 | { 342 | "attachments": {}, 343 | "cell_type": "markdown", 344 | "metadata": { 345 | "id": "rtU9DM-AD2hu" 346 | }, 347 | "source": [ 348 | "# Designing a PCB with pcbflow\n", 349 | "Using [pcbflow](https://github.com/michaelgale/pcbflow), read the footprint and pin connections designed in skidl and physically place the components and wiring on the PCB. As needed, you can preview the image files and finally export a complete set of Gerber files for manufacturing.\n", 350 | "\n", 351 | "The routing is based on the traditional [Turtle graphics](https://docs.python.org/ja/3/library/turtle.html)-like notations.\n", 352 | "\n", 353 | "pcbflow is under development(?). and had some problems and unexpected behavior, so I have fixed and updated it. [folk](https://github.com/hsgw/pcbflow/tree/fix_kicad)\n", 354 | "\n", 355 | "- pcbflow https://github.com/michaelgale/pcbflow\n", 356 | "- folked pcbflow https://github.com/hsgw/pcbflow/tree/fix_kicad" 357 | ] 358 | }, 359 | { 360 | "attachments": {}, 361 | "cell_type": "markdown", 362 | "metadata": { 363 | "id": "mXU5TZOeFBGI" 364 | }, 365 | "source": [ 366 | "## Install pcbflow\n", 367 | "Execute the following cells to install.\n", 368 | "Import and verify that there are no errors." 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": null, 374 | "metadata": { 375 | "colab": { 376 | "base_uri": "https://localhost:8080/" 377 | }, 378 | "id": "D4V_SArjWQXs", 379 | "outputId": "1d0507f1-bea5-4027-fa2c-08d44f90cbb0" 380 | }, 381 | "outputs": [], 382 | "source": [ 383 | "# Takes a few minutes to install\n", 384 | "!pip install git+https://github.com/hsgw/pcbflow/@fix_kicad" 385 | ] 386 | }, 387 | { 388 | "cell_type": "code", 389 | "execution_count": null, 390 | "metadata": { 391 | "id": "zZH-ygq3FlJu" 392 | }, 393 | "outputs": [], 394 | "source": [ 395 | "from pcbflow import *" 396 | ] 397 | }, 398 | { 399 | "attachments": {}, 400 | "cell_type": "markdown", 401 | "metadata": { 402 | "id": "qkWfPZF2I37R" 403 | }, 404 | "source": [ 405 | "## Declare constants\n", 406 | "As in the netlist, frequently used values are declared as constants. \n", 407 | "I also declare a function to convert the Y-axis of the coordinates because it was inverted and felt uncomfortable." 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": null, 413 | "metadata": { 414 | "id": "a031kS7yJEtJ" 415 | }, 416 | "outputs": [], 417 | "source": [ 418 | "BOARD_WIDTH = 76.0\n", 419 | "BOARD_HEIGHT = 57.0\n", 420 | "\n", 421 | "KEY_PITCH = 19.0\n", 422 | "\n", 423 | "SCREW_HOLE = 2.2\n", 424 | "\n", 425 | "LAYER_TOP = \"GTL\"\n", 426 | "LAYER_BOTTOM = \"GBL\"\n", 427 | "\n", 428 | "# Invert Y-axis\n", 429 | "def pos(x, y):\n", 430 | " return (x, BOARD_HEIGHT - y)" 431 | ] 432 | }, 433 | { 434 | "attachments": {}, 435 | "cell_type": "markdown", 436 | "metadata": { 437 | "id": "K2o9ziq3N2Bb" 438 | }, 439 | "source": [ 440 | "## Declare variables for the PCB and circuit\n", 441 | "Declare variables to access the PCB and the circuit.\n", 442 | "The PCB is from pcbflow and the circuit is from skidl.\n", 443 | "\n", 444 | "When declaring the `board`, specify the size of the outline and add an outline line." 445 | ] 446 | }, 447 | { 448 | "cell_type": "code", 449 | "execution_count": null, 450 | "metadata": { 451 | "id": "8bzraF-yOaIH" 452 | }, 453 | "outputs": [], 454 | "source": [ 455 | "# from skidl\n", 456 | "circuit = builtins.default_circuit\n", 457 | "# for pcbflow\n", 458 | "board = Board((BOARD_WIDTH, BOARD_HEIGHT))\n", 459 | "board.add_outline()" 460 | ] 461 | }, 462 | { 463 | "attachments": {}, 464 | "cell_type": "markdown", 465 | "metadata": { 466 | "id": "9nWBUCJJNdgE" 467 | }, 468 | "source": [ 469 | "## Configure design rules\n", 470 | "Configure the design rules for the PCB, such as drill size, wire width, and so on.\n", 471 | "\n", 472 | "Use these settings as the basis for routing the PCB." 473 | ] 474 | }, 475 | { 476 | "cell_type": "code", 477 | "execution_count": null, 478 | "metadata": { 479 | "id": "PdPPua_3Nuoi" 480 | }, 481 | "outputs": [], 482 | "source": [ 483 | "board.drc.trace_width = 0.5\n", 484 | "board.drc.via_drill = 0.6\n", 485 | "board.drc.via_annular_ring = 0.4\n", 486 | "board.drc.clearance = 0.4" 487 | ] 488 | }, 489 | { 490 | "attachments": {}, 491 | "cell_type": "markdown", 492 | "metadata": { 493 | "id": "NFHOh_4xaSMc" 494 | }, 495 | "source": [ 496 | "## Add drill holes\n", 497 | "Make a hole to mount the PCB." 498 | ] 499 | }, 500 | { 501 | "cell_type": "code", 502 | "execution_count": null, 503 | "metadata": { 504 | "id": "1AY3bcKpaa6M" 505 | }, 506 | "outputs": [], 507 | "source": [ 508 | "board.add_hole(pos(KEY_PITCH * 2, KEY_PITCH), SCREW_HOLE)\n", 509 | "board.add_hole(pos(KEY_PITCH * 3, KEY_PITCH), SCREW_HOLE)\n", 510 | "board.add_hole(pos(KEY_PITCH * 3, KEY_PITCH * 2), SCREW_HOLE)\n", 511 | "board.add_hole(pos(KEY_PITCH, KEY_PITCH * 2), SCREW_HOLE)" 512 | ] 513 | }, 514 | { 515 | "attachments": {}, 516 | "cell_type": "markdown", 517 | "metadata": { 518 | "id": "gxtMIg5IcLKb" 519 | }, 520 | "source": [ 521 | "### Generate image and preview\n", 522 | "Now let's generate image for preview" 523 | ] 524 | }, 525 | { 526 | "cell_type": "code", 527 | "execution_count": null, 528 | "metadata": { 529 | "colab": { 530 | "base_uri": "https://localhost:8080/", 531 | "height": 1000 532 | }, 533 | "id": "22HqPC8IcsFh", 534 | "outputId": "3c5cb1e1-9994-4dbc-9208-43b08752bbcb" 535 | }, 536 | "outputs": [], 537 | "source": [ 538 | "from IPython.display import Image\n", 539 | "\n", 540 | "# Export the current board as a png file\n", 541 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 542 | "\n", 543 | "# Show on output\n", 544 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 545 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 546 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 547 | ] 548 | }, 549 | { 550 | "attachments": {}, 551 | "cell_type": "markdown", 552 | "metadata": { 553 | "id": "t2mUZjAfY31k" 554 | }, 555 | "source": [ 556 | "## Add position info to a part\n", 557 | "Add position, rotation, and mounting surface info on the PCB to each of the skidl parts.\n", 558 | "\n", 559 | "In this article, the position is specified immediately, but I repeatedly placed the parts on the PCB and previewed them to determine the position." 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": null, 565 | "metadata": { 566 | "id": "R4cUuHhZZPm_" 567 | }, 568 | "outputs": [], 569 | "source": [ 570 | "# sw, d, xiao, oled are from skidl section\n", 571 | "\n", 572 | "# Switches and diodes are positioned the same for every block\n", 573 | "for sw, d, mapping in zip(switches, diodes, MATRIX_MAP):\n", 574 | " sw.pos = pos(\n", 575 | " mapping[1] * KEY_PITCH + KEY_PITCH / 2,\n", 576 | " mapping[0] * KEY_PITCH + KEY_PITCH / 2,\n", 577 | " )\n", 578 | " sw.side = \"top\"\n", 579 | " sw.rotate = 0\n", 580 | " d.pos = (sw.pos[0] + 6, sw.pos[1] + 5)\n", 581 | " d.side = \"bottom\"\n", 582 | " d.rotate = 0\n", 583 | "\n", 584 | "xiao.pos = pos(11.25, 11.5)\n", 585 | "xiao.side = \"bottom\"\n", 586 | "xiao.rotate = 0\n", 587 | "\n", 588 | "oled.pos = pos(9.5, 36)\n", 589 | "oled.side = \"top\"\n", 590 | "oled.rotate = 270" 591 | ] 592 | }, 593 | { 594 | "attachments": {}, 595 | "cell_type": "markdown", 596 | "metadata": { 597 | "id": "jQy4fcGbfAfd" 598 | }, 599 | "source": [ 600 | "## Place the components on the PCB\n", 601 | "Based on the location information added, place the parts in skidl while converting them to the PCB in pcbflow." 602 | ] 603 | }, 604 | { 605 | "cell_type": "code", 606 | "execution_count": null, 607 | "metadata": { 608 | "id": "MBdn1GA4e_ea" 609 | }, 610 | "outputs": [], 611 | "source": [ 612 | "for part in circuit.parts:\n", 613 | " try:\n", 614 | " # convert skidl parts to pcbflow\n", 615 | " SkiPart(board.DC(part.pos).right(part.rotate), part, side=part.side)\n", 616 | " except AttributeError:\n", 617 | " print(f\"{part.ref} has no pos or side\")\n", 618 | " continue" 619 | ] 620 | }, 621 | { 622 | "attachments": {}, 623 | "cell_type": "markdown", 624 | "metadata": { 625 | "id": "376guVBxgOOm" 626 | }, 627 | "source": [ 628 | "### Preview" 629 | ] 630 | }, 631 | { 632 | "cell_type": "code", 633 | "execution_count": null, 634 | "metadata": { 635 | "colab": { 636 | "base_uri": "https://localhost:8080/", 637 | "height": 1000 638 | }, 639 | "id": "pMku38a9gIMl", 640 | "outputId": "e09a38c7-d36a-4ed8-88b7-e3031accccbb" 641 | }, 642 | "outputs": [], 643 | "source": [ 644 | "from IPython.display import Image\n", 645 | "\n", 646 | "# Export the current board as a png file\n", 647 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 648 | "\n", 649 | "# Show\n", 650 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 651 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 652 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 653 | ] 654 | }, 655 | { 656 | "attachments": {}, 657 | "cell_type": "markdown", 658 | "metadata": { 659 | "id": "HIvuWJ6_sxcM" 660 | }, 661 | "source": [ 662 | "## Wiring: xiao to switch (ROW)\n", 663 | "Route the wires from xiao to ROW. \n", 664 | "Use Turtle syntax to draw wires from xiao and connect them to `Pad` on the switch.\n", 665 | "\n", 666 | "The `Pin` in skidl and `pads` in pcbflow refer to the pins of the component." 667 | ] 668 | }, 669 | { 670 | "cell_type": "code", 671 | "execution_count": null, 672 | "metadata": { 673 | "colab": { 674 | "base_uri": "https://localhost:8080/" 675 | }, 676 | "id": "QYuy9Np7sx9M", 677 | "outputId": "ed24ff82-e77b-46f8-fba6-2d8cdfdee9fe" 678 | }, 679 | "outputs": [], 680 | "source": [ 681 | "xiaoRef = \"U1\"\n", 682 | "\n", 683 | "# Function to return Pad number by name from skidl's Net\n", 684 | "# skidl's Pin number starts from 1, pcbflow's Pad number starts from 0\n", 685 | "def get_pin_number_from_net(netLabel, ref):\n", 686 | " net = Net.get(netLabel)\n", 687 | " return list((int(x.num) - 1 for x in net.pins if x.ref == ref))[0]\n", 688 | "\n", 689 | "# Function to return the connected xiao pad by name from skidl's Net\n", 690 | "# Fixes a wrong pad number due to the kicad library and returns it.\n", 691 | "# last newpath() fixes strange value in initial value\n", 692 | "def xiao_pads(net: str):\n", 693 | " return (\n", 694 | " board.get_part(xiaoRef)\n", 695 | " .pads[get_pin_number_from_net(net, xiaoRef) + 14]\n", 696 | " .newpath()\n", 697 | " )\n", 698 | "\n", 699 | "# Connect the pins of the xiao connected to Net to the pins of the switch\n", 700 | "# Pad of xiao on ROWn -> specify layer -> route using Turtle syntax -> align axis up to pad of SW\n", 701 | "xiao_pads(\"ROW0\").set_layer(LAYER_TOP).w(\"r 45\").align_meet(\n", 702 | " board.get_part(\"SW1\").pads[0], \"x\"\n", 703 | ")\n", 704 | "xiao_pads(\"ROW1\").set_layer(LAYER_TOP).w(\"r 45\").align_meet(\n", 705 | " board.get_part(\"SW4\").pads[0], \"y\"\n", 706 | ")\n", 707 | "xiao_pads(\"ROW2\").set_layer(LAYER_TOP).w(\"l 180 f 12 r 45\").align_meet(\n", 708 | " board.get_part(\"SW8\").pads[0], \"y\"\n", 709 | ")" 710 | ] 711 | }, 712 | { 713 | "attachments": {}, 714 | "cell_type": "markdown", 715 | "metadata": { 716 | "id": "fSKc2qDnu_c3" 717 | }, 718 | "source": [ 719 | "### Preview" 720 | ] 721 | }, 722 | { 723 | "cell_type": "code", 724 | "execution_count": null, 725 | "metadata": { 726 | "colab": { 727 | "base_uri": "https://localhost:8080/", 728 | "height": 965 729 | }, 730 | "id": "bA690BhQu_c8", 731 | "outputId": "dae75eb2-5ee9-47a2-8a80-598494b4f07e" 732 | }, 733 | "outputs": [], 734 | "source": [ 735 | "from IPython.display import Image\n", 736 | "\n", 737 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 738 | "\n", 739 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 740 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 741 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 742 | ] 743 | }, 744 | { 745 | "attachments": {}, 746 | "cell_type": "markdown", 747 | "metadata": { 748 | "id": "7g0j_Biziy3k" 749 | }, 750 | "source": [ 751 | "## Wiring: diode - switch, switch - switch (ROW), diode - diode (COL)\n", 752 | "If the positions of the connected pins are the same, they can be connected in the same code.\n", 753 | "\n", 754 | "In routing COL, a via is created on the way. Store the information of the via and connect it to xiao later." 755 | ] 756 | }, 757 | { 758 | "cell_type": "code", 759 | "execution_count": null, 760 | "metadata": { 761 | "id": "tfu1rTPnjlSW" 762 | }, 763 | "outputs": [], 764 | "source": [ 765 | "# Switches and diodes are arranged in the same way as a block so they can be routed with the same code\n", 766 | "# Starts from 1 because of accessing by REF No.\n", 767 | "for i in range(1, KEY_COUNT + 1):\n", 768 | " sw = board.get_part(f\"SW{i}\")\n", 769 | " d = board.get_part(f\"D{i}\")\n", 770 | " # Specify pad to start routing → Specify layer → Routing in Turtle syntax → Align pad and axis and connect.\n", 771 | " d.pads[0].set_layer(LAYER_BOTTOM).w(\"f 1 r 45\").align_meet(sw.pads[1], \"x\")\n", 772 | "\n", 773 | "# The pins of the switches on the ROW of the switch matrix are aligned in a straight line, so they can be wired in the same way.\n", 774 | "# Note that the number of switches differs depending on the ROW.\n", 775 | "for i in range(3):\n", 776 | " # Make an array of Ref No. of connected to SW from Net information of ROW.\n", 777 | " refs = list(x.ref for x in netRows[i].pins if x.ref.startswith(\"SW\"))\n", 778 | " for j in range(len(refs) - 1):\n", 779 | " # Specify pad to start routing → Create new path → Specify layer → Routing in Turtle syntax → Route in a straight line to the next pad\n", 780 | " board.get_part(refs[j]).pads[0].newpath().set_layer(LAYER_TOP).w(\n", 781 | " \"r 90 f 2 l 45 f 1 r 45 f 2.5 r 45 f1\"\n", 782 | " ).meet(board.get_part(refs[j + 1]).pads[0])\n", 783 | "\n", 784 | "# Array to store via information\n", 785 | "# Used to connect with xiao\n", 786 | "viaCol = [0] * 4\n", 787 | "\n", 788 | "# The diodes leading to COL1-3 in the switch matrix are aligned in a straight line, so they can be wired in the same way\n", 789 | "# Put a via across on the way\n", 790 | "for i in range(1, 4):\n", 791 | " d1 = board.get_part(f\"D{i}\")\n", 792 | " d2 = board.get_part(f\"D{i+3}\")\n", 793 | " d3 = board.get_part(f\"D{i+7}\")\n", 794 | " # Diode-Via is routed first to store via information\n", 795 | " via = (\n", 796 | " d1.pads[1]\n", 797 | " .set_layer(LAYER_BOTTOM)\n", 798 | " .w(\"l 180 f 1.25 r 45 f 2 l 45 f 6.5 l 45\")\n", 799 | " .align(d2.pads[1], \"y\")\n", 800 | " .wire()\n", 801 | " .via()\n", 802 | " )\n", 803 | " viaCol[i] = via\n", 804 | " # Route from via to pad of next diode\n", 805 | " via.set_layer(LAYER_BOTTOM).meet(d2.pads[1])\n", 806 | " d2.pads[1].set_layer(LAYER_BOTTOM).w(\n", 807 | " \"l 180 f 1 r 45 f 2 l 45 f 6.5 l 45\"\n", 808 | " ).align_meet(d3.pads[1], \"y\")" 809 | ] 810 | }, 811 | { 812 | "attachments": {}, 813 | "cell_type": "markdown", 814 | "metadata": { 815 | "id": "RHUfHju2lnf4" 816 | }, 817 | "source": [ 818 | "### Preview" 819 | ] 820 | }, 821 | { 822 | "cell_type": "code", 823 | "execution_count": null, 824 | "metadata": { 825 | "colab": { 826 | "base_uri": "https://localhost:8080/", 827 | "height": 965 828 | }, 829 | "id": "TdpxBEHelnf9", 830 | "outputId": "4505fad8-49ab-4c5d-b885-1b856bfea16c" 831 | }, 832 | "outputs": [], 833 | "source": [ 834 | "from IPython.display import Image\n", 835 | "\n", 836 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 837 | "\n", 838 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 839 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 840 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 841 | ] 842 | }, 843 | { 844 | "attachments": {}, 845 | "cell_type": "markdown", 846 | "metadata": { 847 | "id": "4sNQELnWwx5h" 848 | }, 849 | "source": [ 850 | "## Wiring: via to xiao (COL)\n", 851 | "Route from the stored via to xiao." 852 | ] 853 | }, 854 | { 855 | "cell_type": "code", 856 | "execution_count": null, 857 | "metadata": { 858 | "colab": { 859 | "base_uri": "https://localhost:8080/" 860 | }, 861 | "id": "uQ1xlQk8wx5m", 862 | "outputId": "66bcf5b0-6c5b-4774-fff5-11b4a61d82e8" 863 | }, 864 | "outputs": [], 865 | "source": [ 866 | "viaCol[1].set_layer(LAYER_TOP).w(\"l 45 f 11 l 45\").align_meet(\n", 867 | " xiao_pads(\"COL1\"), \"x\"\n", 868 | ")\n", 869 | "viaCol[2].set_layer(LAYER_TOP).w(\"f 2 l 45 f 29.5 l 45\").align_meet(\n", 870 | " xiao_pads(\"COL2\"), \"x\"\n", 871 | ")\n", 872 | "viaCol[3].set_layer(LAYER_TOP).w(\"f 4 l 45 f 48.5 l 45\").align_meet(\n", 873 | " xiao_pads(\"COL3\"), \"x\"\n", 874 | ")" 875 | ] 876 | }, 877 | { 878 | "attachments": {}, 879 | "cell_type": "markdown", 880 | "metadata": { 881 | "id": "BZp2rf73xjPt" 882 | }, 883 | "source": [ 884 | "### Preview" 885 | ] 886 | }, 887 | { 888 | "cell_type": "code", 889 | "execution_count": null, 890 | "metadata": { 891 | "colab": { 892 | "base_uri": "https://localhost:8080/", 893 | "height": 965 894 | }, 895 | "id": "-eCOnReyxjPt", 896 | "outputId": "ab969902-942c-4924-c529-4064fd769043" 897 | }, 898 | "outputs": [], 899 | "source": [ 900 | "from IPython.display import Image\n", 901 | "\n", 902 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 903 | "\n", 904 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 905 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 906 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 907 | ] 908 | }, 909 | { 910 | "attachments": {}, 911 | "cell_type": "markdown", 912 | "metadata": { 913 | "id": "ViFc_c0oxjPo" 914 | }, 915 | "source": [ 916 | "## Wiring: xiao to OLED\n", 917 | "Route from xiao to OLED.\n", 918 | "\n", 919 | "Enumerate the name of Net and look up the number of the pad connected from skidl's Net information." 920 | ] 921 | }, 922 | { 923 | "cell_type": "code", 924 | "execution_count": null, 925 | "metadata": { 926 | "id": "pEkSdb7YxjPs" 927 | }, 928 | "outputs": [], 929 | "source": [ 930 | "oledRef = \"DISP1\"\n", 931 | "\n", 932 | "for net in [\"GND\", \"3.3V\", \"SCL\", \"SDA\"]:\n", 933 | " oledPin = board.get_part(oledRef).pads[get_pin_number_from_net(net, oledRef)]\n", 934 | " xiao_pads(net).set_layer(LAYER_TOP).left(135).align_meet(oledPin, \"y\")" 935 | ] 936 | }, 937 | { 938 | "attachments": {}, 939 | "cell_type": "markdown", 940 | "metadata": { 941 | "id": "Xa0VwbDJwx5m" 942 | }, 943 | "source": [ 944 | "## Routing is completed\n", 945 | "Now all the routing is done!" 946 | ] 947 | }, 948 | { 949 | "cell_type": "code", 950 | "execution_count": null, 951 | "metadata": { 952 | "colab": { 953 | "base_uri": "https://localhost:8080/", 954 | "height": 965 955 | }, 956 | "id": "_z-12LbDwx5m", 957 | "outputId": "31258366-12f5-4a81-fa95-ff16facfab80" 958 | }, 959 | "outputs": [], 960 | "source": [ 961 | "from IPython.display import Image\n", 962 | "\n", 963 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 964 | "\n", 965 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 966 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 967 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 968 | ] 969 | }, 970 | { 971 | "attachments": {}, 972 | "cell_type": "markdown", 973 | "metadata": { 974 | "id": "KduW_1h4OLdU" 975 | }, 976 | "source": [ 977 | "## Place pictures on PCB\n", 978 | "Place a logo and other images on the silk layer of the PCB.\n", 979 | "The image data to be used is in the resource at the beginning." 980 | ] 981 | }, 982 | { 983 | "cell_type": "code", 984 | "execution_count": null, 985 | "metadata": { 986 | "id": "Ygvq-EqzOxDH" 987 | }, 988 | "outputs": [], 989 | "source": [ 990 | "DIR_IMGS = \"keyboard-made-by-python/\"\n", 991 | "\n", 992 | "board.add_bitmap(\n", 993 | " (47.5, 28.5), \n", 994 | " DIR_IMGS + \"hardware/imgs/python-logo.png\",\n", 995 | " side=\"top\",\n", 996 | " scale=0.85\n", 997 | ")\n", 998 | "\n", 999 | "board.add_bitmap(\n", 1000 | " (11.25, 45),\n", 1001 | " DIR_IMGS + \"hardware/imgs/QR.png\",\n", 1002 | " side=\"bottom\",\n", 1003 | " scale=0.75,\n", 1004 | ")\n", 1005 | "\n", 1006 | "board.add_bitmap(\n", 1007 | " (11, 28.5),\n", 1008 | " DIR_IMGS + \"hardware/imgs/logo-python-powered-w-logo.png\",\n", 1009 | " side=\"bottom\",\n", 1010 | " layer=\"GBS\",\n", 1011 | " scale=0.7,\n", 1012 | ")\n", 1013 | "board.add_bitmap(\n", 1014 | " (11, 28.5),\n", 1015 | " DIR_IMGS + \"hardware/imgs/logo-python-powered-w-logoBG.png\",\n", 1016 | " side=\"bottom\",\n", 1017 | " layer=\"GBL\",\n", 1018 | " scale=0.7,\n", 1019 | ")\n", 1020 | "board.add_bitmap(\n", 1021 | " (11, 28.5),\n", 1022 | " DIR_IMGS + \"hardware/imgs/logo-python-powered-w-text.png\",\n", 1023 | " side=\"bottom\",\n", 1024 | " scale=0.7,\n", 1025 | ")\n", 1026 | "\n", 1027 | "board.add_bitmap(\n", 1028 | " (47.5, 22),\n", 1029 | " DIR_IMGS + \"hardware/imgs/dm9-logo.png\",\n", 1030 | " side=\"bottom\",\n", 1031 | " scale=0.8,\n", 1032 | ")\n", 1033 | "\n", 1034 | "board.add_bitmap(\n", 1035 | " (66.5, 22),\n", 1036 | " DIR_IMGS + \"hardware/imgs/hsgw-logo.png\",\n", 1037 | " side=\"bottom\",\n", 1038 | " scale=0.4,\n", 1039 | ")" 1040 | ] 1041 | }, 1042 | { 1043 | "attachments": {}, 1044 | "cell_type": "markdown", 1045 | "metadata": { 1046 | "id": "32qY1ujHQRuA" 1047 | }, 1048 | "source": [ 1049 | "### Preview" 1050 | ] 1051 | }, 1052 | { 1053 | "cell_type": "code", 1054 | "execution_count": null, 1055 | "metadata": { 1056 | "colab": { 1057 | "base_uri": "https://localhost:8080/", 1058 | "height": 1000 1059 | }, 1060 | "id": "x6OEVJKUQO1W", 1061 | "outputId": "01b64bbd-db89-4d8a-f7b6-c1349efc29d4" 1062 | }, 1063 | "outputs": [], 1064 | "source": [ 1065 | "from IPython.display import Image\n", 1066 | "\n", 1067 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 1068 | "\n", 1069 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 1070 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 1071 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 1072 | ] 1073 | }, 1074 | { 1075 | "attachments": {}, 1076 | "cell_type": "markdown", 1077 | "metadata": { 1078 | "id": "HQ_stk2iQmiy" 1079 | }, 1080 | "source": [ 1081 | "## Place text on PCB\n", 1082 | "Place texts on the silk layer of the PCB.\n", 1083 | "\n", 1084 | "Specify the position where the order number will be placed because I will order to JLCPCB." 1085 | ] 1086 | }, 1087 | { 1088 | "cell_type": "code", 1089 | "execution_count": null, 1090 | "metadata": { 1091 | "id": "1Lf-MAn1Qmiy" 1092 | }, 1093 | "outputs": [], 1094 | "source": [ 1095 | "board.add_text(\n", 1096 | " (47.5, 41.5),\n", 1097 | " \"https://github.com/hsgw/keyboard_made_by_python\",\n", 1098 | " scale=1.25,\n", 1099 | " side=\"bottom\",\n", 1100 | ")\n", 1101 | "\n", 1102 | "board.add_text(\n", 1103 | " (BOARD_WIDTH - 39, 5),\n", 1104 | " \"KEYBOARD MADE BY PYTHON\",\n", 1105 | " scale=2,\n", 1106 | " side=\"bottom\",\n", 1107 | " justify=\"left\",\n", 1108 | ")\n", 1109 | "board.add_text(\n", 1110 | " (BOARD_WIDTH - 44, 4.75),\n", 1111 | " \"Rev.1\",\n", 1112 | " scale=1.25,\n", 1113 | " side=\"bottom\",\n", 1114 | " justify=\"left\",\n", 1115 | ")\n", 1116 | "board.add_text(\n", 1117 | " (BOARD_WIDTH - 47.5, 2.25),\n", 1118 | " \"(c) 2022, Takuya Urakawa / Dm9Records / 5z6p.com\",\n", 1119 | " scale=1.25,\n", 1120 | " side=\"bottom\",\n", 1121 | " justify=\"left\",\n", 1122 | ")\n", 1123 | "\n", 1124 | "# MAGIC WORD for JLCPCB\n", 1125 | "board.add_text(\n", 1126 | " (11.5, 55),\n", 1127 | " \"JLCJLCJLCJLC\",\n", 1128 | " scale=1.1,\n", 1129 | " side=\"bottom\",\n", 1130 | ")" 1131 | ] 1132 | }, 1133 | { 1134 | "attachments": {}, 1135 | "cell_type": "markdown", 1136 | "metadata": { 1137 | "id": "3FdBYSKrQmiz" 1138 | }, 1139 | "source": [ 1140 | "### Preview" 1141 | ] 1142 | }, 1143 | { 1144 | "cell_type": "code", 1145 | "execution_count": null, 1146 | "metadata": { 1147 | "colab": { 1148 | "base_uri": "https://localhost:8080/", 1149 | "height": 1000 1150 | }, 1151 | "id": "8lkcYwWVQmiz", 1152 | "outputId": "3c3bb898-f533-4105-f32b-46901d75f7fc" 1153 | }, 1154 | "outputs": [], 1155 | "source": [ 1156 | "from IPython.display import Image\n", 1157 | "\n", 1158 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 1159 | "\n", 1160 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 1161 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 1162 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 1163 | ] 1164 | }, 1165 | { 1166 | "attachments": {}, 1167 | "cell_type": "markdown", 1168 | "metadata": { 1169 | "id": "dvHvS6UHRLil" 1170 | }, 1171 | "source": [ 1172 | "# Complete PCB and generate gerber files\n", 1173 | "Finally, export the Gerber file for manufacturing and complete PCB." 1174 | ] 1175 | }, 1176 | { 1177 | "cell_type": "code", 1178 | "execution_count": null, 1179 | "metadata": { 1180 | "colab": { 1181 | "base_uri": "https://localhost:8080/" 1182 | }, 1183 | "id": "KIt69DLaRIgA", 1184 | "outputId": "6d7c135a-49d8-4036-d2b3-84f697309b49" 1185 | }, 1186 | "outputs": [], 1187 | "source": [ 1188 | "board.save_gerbers(\"kbd_python\", subdir=\"pcb_gerber\")" 1189 | ] 1190 | }, 1191 | { 1192 | "attachments": {}, 1193 | "cell_type": "markdown", 1194 | "metadata": { 1195 | "id": "TjFdKPM9Sont" 1196 | }, 1197 | "source": [ 1198 | "Use kicad's Gerber Viewer to review Gerber files.\n", 1199 | "\n", 1200 | "![kicad's gerber viewer](../imgs/kicad_gerberviewer.png)\n", 1201 | "\n", 1202 | "It is also possible to convert from kicad's gerber viewer to pcbnew. For ordering, I converted to pcbnew, modified the silk and generated the gerber file again.\n", 1203 | "\n", 1204 | "![PCB converted to pcbnew](../imgs/kicad_gerber_to_pcbnew.png)" 1205 | ] 1206 | }, 1207 | { 1208 | "attachments": {}, 1209 | "cell_type": "markdown", 1210 | "metadata": {}, 1211 | "source": [ 1212 | "# Order PCB\n", 1213 | "\n", 1214 | "! [PCB received](. /imgs/pcb.jpg)\n", 1215 | "\n", 1216 | "I ordered a batch of Gerber files to JLCPCB, about $8 for 5 boards, and they arrived in less than 2 weeks.\n", 1217 | "\n", 1218 | "# Soldering the components\n", 1219 | "\n", 1220 | "![soldered PCB](../imgs/soldered_pcb_top.jpg)\n", 1221 | "![soldered PCB](../imgs/soldered_pcb_bottom.jpg)\n", 1222 | "\n", 1223 | "I soldered the components. The PCB design is now complete." 1224 | ] 1225 | } 1226 | ], 1227 | "metadata": { 1228 | "colab": { 1229 | "include_colab_link": true, 1230 | "provenance": [] 1231 | }, 1232 | "kernelspec": { 1233 | "display_name": ".venv", 1234 | "language": "python", 1235 | "name": "python3" 1236 | }, 1237 | "language_info": { 1238 | "codemirror_mode": { 1239 | "name": "ipython", 1240 | "version": 3 1241 | }, 1242 | "file_extension": ".py", 1243 | "mimetype": "text/x-python", 1244 | "name": "python", 1245 | "nbconvert_exporter": "python", 1246 | "pygments_lexer": "ipython3", 1247 | "version": "3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]" 1248 | }, 1249 | "vscode": { 1250 | "interpreter": { 1251 | "hash": "b68c839e49670d3aa77ea8bdd6f541fdcce81585b85d6e1d97674ad5f32db92f" 1252 | } 1253 | } 1254 | }, 1255 | "nbformat": 4, 1256 | "nbformat_minor": 0 1257 | } 1258 | -------------------------------------------------------------------------------- /notebook/en/what_is_colaboratory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": { 7 | "colab_type": "text", 8 | "id": "view-in-github" 9 | }, 10 | "source": [ 11 | "\"Open" 12 | ] 13 | }, 14 | { 15 | "attachments": {}, 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "> Project page is [Keyboard as a Python Code](https://hackaday.io/project/188907-keyboard-as-a-python-code)\n" 20 | ] 21 | }, 22 | { 23 | "attachments": {}, 24 | "cell_type": "markdown", 25 | "metadata": { 26 | "id": "iBVgqeBuA9d9" 27 | }, 28 | "source": [ 29 | "# What is Google Colaboratory?\n", 30 | "\n", 31 | "Google Colaboratory is Python develop enviroment by Google.\n", 32 | "\n", 33 | "You can run python code without setting up an environment on your own computer and write document by markdown.\n", 34 | "\n", 35 | "In Google Colaboratory, an entire file is called a `notebook` and a chunk of code or documentation is called a `cell`, and if the `cell` type is code, clicking the `▷` icon will execute it.\n", 36 | "When execution is done, the result will be displayed below the cell.\n" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": { 43 | "id": "I952Ee82A9eA" 44 | }, 45 | "outputs": [], 46 | "source": [ 47 | "1 + 2\n" 48 | ] 49 | }, 50 | { 51 | "attachments": {}, 52 | "cell_type": "markdown", 53 | "metadata": { 54 | "id": "kFJTKn2vA9eB" 55 | }, 56 | "source": [ 57 | "It keeps state until it exits, like a Python interpreter, so it keeps libraries and variables imported from one cell to another. Therefore, the order of execution may be limited. Most of the time, the order of execution is from the top to the bottom." 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": { 64 | "id": "zQ-WBWT1A9eC" 65 | }, 66 | "outputs": [], 67 | "source": [ 68 | "num = 10\n" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": { 75 | "id": "1tpeXhRsA9eC" 76 | }, 77 | "outputs": [], 78 | "source": [ 79 | "print(num)\n", 80 | "num\n" 81 | ] 82 | }, 83 | { 84 | "attachments": {}, 85 | "cell_type": "markdown", 86 | "metadata": { 87 | "id": "KBtF5rEoA9eC" 88 | }, 89 | "source": [ 90 | "You can also access bash in a virtual environment running Python. \n", 91 | "This can be used to install libraries via pip, change the version of python itself, or clone a project from github.\n", 92 | "\n", 93 | "Colaboratory already has some commonly used libraries installed. Let's check them as well.\n" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": { 100 | "id": "prpb-nLaA9eC" 101 | }, 102 | "outputs": [], 103 | "source": [ 104 | "# If you start a line with \"!\", it becomes a command to bash\n", 105 | "!python --version\n", 106 | "!pip list" 107 | ] 108 | }, 109 | { 110 | "attachments": {}, 111 | "cell_type": "markdown", 112 | "metadata": { 113 | "id": "OtUUjHcNA9eD" 114 | }, 115 | "source": [ 116 | "# Let's try it out\n", 117 | "\n", 118 | "Let's quickly run a script to generate and save a simple image in python.\n", 119 | "\n", 120 | "Import the image processing library pillow.\n" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": { 127 | "id": "I0LWySJvA9eD" 128 | }, 129 | "outputs": [], 130 | "source": [ 131 | "from PIL import Image, ImageDraw\n" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": { 138 | "id": "t9SUodyRA9eE" 139 | }, 140 | "outputs": [], 141 | "source": [ 142 | "# Generate an image file with a rectangle\n", 143 | "size = (300, 200)\n", 144 | "img = Image.new(\"RGB\", size, (255, 255, 255))\n", 145 | "draw = ImageDraw.Draw(img)\n", 146 | "draw.rectangle((20, 20, 150, 150), fill=(255, 0, 0))\n", 147 | "\n", 148 | "# Display the generated image\n", 149 | "img" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": null, 155 | "metadata": { 156 | "id": "J22ChRcCA9eE" 157 | }, 158 | "outputs": [], 159 | "source": [ 160 | "# Add a circle\n", 161 | "draw.ellipse((50, 50, 200, 200), fill=(0, 255, 0))\n", 162 | "img" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": { 169 | "id": "JrTzWf-2A9eE" 170 | }, 171 | "outputs": [], 172 | "source": [ 173 | "# save the file\n", 174 | "img.save(\"test.png\")\n" 175 | ] 176 | }, 177 | { 178 | "attachments": {}, 179 | "cell_type": "markdown", 180 | "metadata": { 181 | "id": "OBHr1gy1A9eE" 182 | }, 183 | "source": [ 184 | "If you saved a file, it may disappear once you close the notebook because the environment is reset. Download the file beforehand.\n", 185 | "\n", 186 | "Saved files are shown in the file browser on the left side tab." 187 | ] 188 | } 189 | ], 190 | "metadata": { 191 | "colab": { 192 | "include_colab_link": true, 193 | "provenance": [] 194 | }, 195 | "kernelspec": { 196 | "display_name": ".venv", 197 | "language": "python", 198 | "name": "python3" 199 | }, 200 | "language_info": { 201 | "codemirror_mode": { 202 | "name": "ipython", 203 | "version": 3 204 | }, 205 | "file_extension": ".py", 206 | "mimetype": "text/x-python", 207 | "name": "python", 208 | "nbconvert_exporter": "python", 209 | "pygments_lexer": "ipython3", 210 | "version": "3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]" 211 | }, 212 | "orig_nbformat": 4, 213 | "vscode": { 214 | "interpreter": { 215 | "hash": "b68c839e49670d3aa77ea8bdd6f541fdcce81585b85d6e1d97674ad5f32db92f" 216 | } 217 | } 218 | }, 219 | "nbformat": 4, 220 | "nbformat_minor": 0 221 | } 222 | -------------------------------------------------------------------------------- /notebook/imgs/case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/case.jpg -------------------------------------------------------------------------------- /notebook/imgs/case_usb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/case_usb.jpg -------------------------------------------------------------------------------- /notebook/imgs/exported_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/exported_case.png -------------------------------------------------------------------------------- /notebook/imgs/kbd_python_preview_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/kbd_python_preview_bot.png -------------------------------------------------------------------------------- /notebook/imgs/kbd_python_preview_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/kbd_python_preview_top.png -------------------------------------------------------------------------------- /notebook/imgs/keyboard_made_by_python.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/keyboard_made_by_python.jpg -------------------------------------------------------------------------------- /notebook/imgs/kicad_gerber_to_pcbnew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/kicad_gerber_to_pcbnew.png -------------------------------------------------------------------------------- /notebook/imgs/kicad_gerberviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/kicad_gerberviewer.png -------------------------------------------------------------------------------- /notebook/imgs/kicad_pcbnew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/kicad_pcbnew.png -------------------------------------------------------------------------------- /notebook/imgs/pcb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/pcb.jpg -------------------------------------------------------------------------------- /notebook/imgs/slicer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/slicer.png -------------------------------------------------------------------------------- /notebook/imgs/soldered_pcb_bottom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/soldered_pcb_bottom.jpg -------------------------------------------------------------------------------- /notebook/imgs/soldered_pcb_top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsgw/keyboard-made-by-python/5eec6137b7ef8e4a4235d50819f6c2d74ccb4c6e/notebook/imgs/soldered_pcb_top.jpg -------------------------------------------------------------------------------- /notebook/jp/README.md: -------------------------------------------------------------------------------- 1 | ![top](../imgs/keyboard_made_by_python.jpg) 2 | 3 | # Pythonだけでキーボードを作る 4 | 5 | > これは[キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529)の20日目の記事です。 6 | > 昨日の記事は74thさんの[コンスルーピンヘッダの代わりを探して](https://74th.hateblo.jp/entry/2022/12/19/124809)でした。 7 | 8 | こんにちは。[hsgw](https://twitter.com/hsgw)です。 9 | 毎年、アドベントカレンダーの記事では役に立つtipsを投稿することにしています。 10 | 11 | 今年はkicadや3DCADを使わずにPythonだけでキーボードを設計する方法を紹介します。 12 | 13 | # TL;DR 14 | ※ 基板やケース、ファームウェアのデータは無保証です。間違いがあったら教えてください。 15 | 16 | - ファームウェアだけでなく、回路図(ネットリスト)や基板、ケースを全てPythonで設計する 17 | - 設計の流れを主に紹介する (細かいコードについてはドキュメントとソースを読んで下さい) 18 | - 設計したコードをjupyter notebook(google colaboratory)で実行できる形で解説する 19 | - 主に使用するライブラリは以下の通り 20 | 21 | | 目的 |ライブラリ| 22 | |------------|-----| 23 | |回路図 |[skidl](https://github.com/devbisme/skidl)| 24 | |基板 |[pcbflow](https://github.com/michaelgale/pcbflow) ([folkしたリポジトリ](https://github.com/hsgw/pcbflow/tree/fix_kicad))| 25 | |ケース |[cadquery](https://github.com/CadQuery/cadquery)| 26 | |ファームウェア|[KMK Firmware](https://github.com/KMKfw/kmk_firmware), [circuitpython](https://circuitpython.org/)| 27 | 28 | # なぜPythonでキーボードを作るのか 29 | [Python](https://www.python.org/)はプログラミング言語のひとつです。実用的なプログラムを簡単に読みやすく書くことを重視して作られています。 30 | 31 | もちろんそれだけでなく、ありとあらゆるライブラリによって拡張され、複雑な計算や画像認識、機械学習といったコンピュータらしい振る舞いだけでなく、ゲームでも音楽でも絵でもなんでも作れます。 32 | AIと名がつくものには大抵使われていますし、最近巷を騒がしている画像生成AIもPythonで動いています。 33 | 34 | 例外にもれず、回路図を書くためのライブラリや基板を配線するライブラリ、3Dモデルを作るライブラリもありますし、実際にマイコンを動かすのもPythonで書かれたライブラリです。 35 | 36 | Pythonに限らず`コードを書くこと`で`何かを設計する`利点は、設計が文字ベースの情報であることです。 37 | 特殊な事柄(回路図記号など)を意識せずにテキストエディタだけで理解・設計が可能です。 38 | 他の誰かと共同編集するときもgitの強力なバージョン管理や差分抽出が使えるのでスムーズです。 39 | 40 | そして、その設計のためのコードはプログラムなので複雑な計算や同じ値や部分の繰り返しをコンピュータにまかせましょう。間違いが少なくなって変更も容易です。 41 | 42 | # コードと解説 43 | 以下のリンクをクリックするとgoogle colaboratoryで実行できるコードと解説を開きます。 44 | 45 | ケース編は環境の都合で[binder](https://mybinder.org)を使用します。これも同じような実行環境です。 46 | 47 | - [google colaboratoryとは](https://colab.research.google.com/github/hsgw/keyboard-made-by-python/blob/main/notebook/jp/what_is_colaboratory.ipynb) 48 | - [基板を設計する](https://colab.research.google.com/github/hsgw/keyboard-made-by-python/blob/main/notebook/jp/pcb.ipynb) 49 | - [ケースを設計する](https://mybinder.org/v2/gh/hsgw/keyboard-made-by-python/HEAD?labpath=notebook%2Fjp%2Fcase.ipynb) ※ [binder](https://mybinder.org)で開きます。表示するのに時間がかかるかもしれません 50 | - [ファームウェアを書く](firmware.md) 51 | 52 | # 感想 53 | Pythonだけでハードウェアも含めて全て設計できました。 54 | 特にskidlでのネットリスト設計とcadqueryでの3Dモデリングは全てパラメトリックに出来る上、書き方もわかりやすく簡単なので今後も使い所は多そうです。 55 | pcbflowは開発途絶してるようで縛りが多いのとプレビューが難しいのでskidlのネットリストをkicadのpcbnewで読み込むワークフローをおすすめします。 56 | 57 | # 最後に 58 | 今年はLainのGBの発送をしたぐらいでドタバタした一年でした。半導体・部品不足も円安も収まりそうなので、来年はのんびりキーボード出来ればいいなと思っています。 59 | 良いお年を! 60 | 61 | // この記事はcasasagi+3Dプリントケースで書きました -------------------------------------------------------------------------------- /notebook/jp/case.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "> これは[キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529)の20日目の記事の一部です。 \n", 9 | "> トップページは[Pythonだけでキーボードを作る](https://5z6p.com/2022/12/21/ac2022/)です。" 10 | ] 11 | }, 12 | { 13 | "attachments": {}, 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "# ケースを作る\n", 18 | "\n", 19 | "![目標のケース](../imgs/exported_case.png)\n", 20 | "\n", 21 | "cadqueryを使って3Dモデリングし、3Dプリントできるケースを作ります。\n", 22 | "\n", 23 | "google colaboratoryではOSの都合上cadqueryのビューアーが使えないため、binderでノートブックを開いています。 \n", 24 | "binderはDockerを使ってOSや必要なライブラリを予め構成してjupyter notebookが使えます。 \n", 25 | "そのため、ライブラリなどのインストールは必要ありません。" 26 | ] 27 | }, 28 | { 29 | "attachments": {}, 30 | "cell_type": "markdown", 31 | "metadata": {}, 32 | "source": [ 33 | "# cadqueryとは\n", 34 | "[cadquery](https://github.com/CadQuery/cadquery)はpythonのコードを書いて3Dモデルを生成する環境です。\n", 35 | "ビューアーには[jupyter_cadquery](https://github.com/bernhard-42/jupyter-cadquery)を使用します。\n", 36 | "\n", 37 | "- cadquery https://github.com/CadQuery/cadquery\n", 38 | "- cadqueryのドキュメント https://cadquery-ja.readthedocs.io/ja/latest/\n", 39 | "- jupyter-cadquery https://github.com/bernhard-42/jupyter-cadquery\n", 40 | "\n", 41 | "環境を用意します。下のセルを実行してバージョン情報が表示されるビューアーが開くことを確認してください。\n", 42 | "\n", 43 | "セルを実行するにはセルをクリックして`Shift + Enter`か上部メニューの`▶`をクリックしてください" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "import cadquery as cq\n", 53 | "\n", 54 | "from jupyter_cadquery import (\n", 55 | " versions,\n", 56 | " show, PartGroup, Part, \n", 57 | " get_viewer, close_viewer, get_viewers, close_viewers, open_viewer, set_defaults, get_defaults, open_viewer,\n", 58 | " get_pick,\n", 59 | ")\n", 60 | "\n", 61 | "from jupyter_cadquery.replay import replay, enable_replay, disable_replay\n", 62 | "\n", 63 | "enable_replay(False)\n", 64 | "\n", 65 | "set_defaults(\n", 66 | " cad_width=640, \n", 67 | " height=480, \n", 68 | ")\n", 69 | "\n", 70 | "print()\n", 71 | "versions()\n", 72 | "\n", 73 | "cv = open_viewer(\"CadQuery\", anchor=\"right\")" 74 | ] 75 | }, 76 | { 77 | "attachments": {}, 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "## cadqueryを試してみる\n", 82 | "簡単な直方体を生成してビューアーに表示します。" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "# xy平面上から直方体を作ってエッジにフィレットをかける\n", 92 | "box = cq.Workplane('XY').box(1, 2, 3).edges().fillet(0.1)\n", 93 | "# デフォルトのビューアーに表示する\n", 94 | "show(box)" 95 | ] 96 | }, 97 | { 98 | "attachments": {}, 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "# ケースを設計する\n", 103 | "基板をネジで固定するトレイ型のケースを設計します。\n", 104 | "\n", 105 | "## おおまかな外形\n", 106 | "基板の形を元におおまかな外形を作ります。例に漏れず、定数を宣言して使いまわします。" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "# 基板外形\n", 116 | "PCB_WIDTH = 76.0\n", 117 | "PCB_HEIGHT = 57.0\n", 118 | "PCB_THICKNESS = 1.6\n", 119 | "\n", 120 | "# ケースと基板のZ方向のマージン\n", 121 | "CASE_MARGIN_TOP = 11.0\n", 122 | "CASE_MARGIN_BOTTOM = 3.5\n", 123 | "\n", 124 | "# ケースと基板のXY方向のマージン\n", 125 | "CASE_MARGIN_PCB = 0.5\n", 126 | "\n", 127 | "# ケースの厚み\n", 128 | "CASE_FRAME = 2.0\n", 129 | "CASE_BOTTOM = 3.0\n", 130 | "\n", 131 | "INNER_HEIGHT = CASE_MARGIN_TOP + PCB_THICKNESS + CASE_MARGIN_BOTTOM\n", 132 | "CASE_HEIGHT = INNER_HEIGHT + CASE_BOTTOM\n", 133 | "\n", 134 | "# XY平面を基準に四角形を書いて押し出す\n", 135 | "case = (\n", 136 | " cq.Workplane(\"XY\")\n", 137 | " .rect(\n", 138 | " PCB_WIDTH + (CASE_FRAME + CASE_MARGIN_PCB) * 2,\n", 139 | " PCB_HEIGHT + (CASE_FRAME + CASE_MARGIN_PCB) * 2,\n", 140 | " )\n", 141 | " .extrude(CASE_BOTTOM + INNER_HEIGHT)\n", 142 | " .edges(\"|Z\")\n", 143 | " .fillet(2)\n", 144 | " .edges(\"|X\")\n", 145 | " .chamfer(1)\n", 146 | ")\n", 147 | "\n", 148 | "show(case)" 149 | ] 150 | }, 151 | { 152 | "attachments": {}, 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "## ケースの内側を切り取る\n", 157 | "ケースの内側を切り取ってトレイの形にします。 \n", 158 | "基準の平面を現在のcaseの面から選択してはじめます" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "# caseの面からZ軸で一番上にあるものを基準とする\n", 168 | "# 四角形を書いて内側の高さ分切り取る\n", 169 | "case = (\n", 170 | " case.faces(\">Z\")\n", 171 | " .workplane()\n", 172 | " .rect(PCB_WIDTH + CASE_MARGIN_PCB * 2, PCB_HEIGHT + CASE_MARGIN_PCB * 2)\n", 173 | " .cutBlind(-INNER_HEIGHT)\n", 174 | ")\n", 175 | "\n", 176 | "show(case)" 177 | ] 178 | }, 179 | { 180 | "attachments": {}, 181 | "cell_type": "markdown", 182 | "metadata": {}, 183 | "source": [ 184 | "## ネジのボスと穴\n", 185 | "ネジでとめるためのボスと穴を作ります。 \n", 186 | "`tag(\"name\")`で基準面を保存して使いまわしています。 \n", 187 | "あらかじめネジ位置の座標の配列を用意して全ての箇所で同じ処理を繰り返します。" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "# ネジ位置の座標\n", 197 | "SCREW_POINTS = [(19, 9.5), (19, -9.5), (-19, -9.5), (0, 9.5)]\n", 198 | "\n", 199 | "# caseの面からz軸で一番上にある面(ぎりぎり残ったケースの縁部分)を選択して\n", 200 | "# INNER_HEIGHT分、下にオフセットした面を基準とする\n", 201 | "# 各座標に半径2.5mmの円柱を作る\n", 202 | "# 各座標に半径1.1mmの穴を開ける\n", 203 | "# 基準面から1mm下にオフセットした面を新たな基準とし、四角い穴を底面まで開ける (ナットを入れる穴)\n", 204 | "case = (\n", 205 | " case.faces(\">Z\")\n", 206 | " .workplane(offset=-INNER_HEIGHT)\n", 207 | " .tag(\"InnerBottom\")\n", 208 | " .pushPoints(SCREW_POINTS)\n", 209 | " .circle(2.5)\n", 210 | " .extrude(CASE_MARGIN_BOTTOM)\n", 211 | " .workplaneFromTagged(\"InnerBottom\")\n", 212 | " .pushPoints(SCREW_POINTS)\n", 213 | " .circle(1.1)\n", 214 | " .cutThruAll()\n", 215 | " .workplaneFromTagged(\"InnerBottom\")\n", 216 | " .workplane(offset=-1)\n", 217 | " .pushPoints(SCREW_POINTS)\n", 218 | " .rect(4.2, 4.8)\n", 219 | " .cutBlind(-2)\n", 220 | ")\n", 221 | "\n", 222 | "show(case)" 223 | ] 224 | }, 225 | { 226 | "attachments": {}, 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "## USBコネクタ周り\n", 231 | "USBコネクタの入る穴を開けます。 \n", 232 | "USBコネクタは背面に開ける必要があるので基準にする平面はY軸上で一番遠い面を基準にします。 \n", 233 | "前後で同じ距離の面がありますが、選択したところ背面になったのでそのままにしています。" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": null, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "# USBコネクタの穴位置・寸法\n", 243 | "USB_POS = (11.25, 1.2 + 3.15 / 2)\n", 244 | "USB_HOLE_SIZE = [9, 3.2, 7.5]\n", 245 | "USB_HOLE_MARGIN = 0.5\n", 246 | "USB_CONN_SIZE = (11, 8)\n", 247 | "\n", 248 | "# Y軸方向に一番遠い面を基準にする\n", 249 | "# USBコネクタの穴は内側まで深めに切り取ってUSBコネクタが当たらないようにする\n", 250 | "# USBケーブルのハウジングを避ける部分を切り取る\n", 251 | "case = (\n", 252 | " case.faces(\">Y\")\n", 253 | " .workplane(centerOption=\"CenterOfMass\")\n", 254 | " .center(\n", 255 | " PCB_WIDTH / 2 - USB_POS[0],\n", 256 | " CASE_HEIGHT / 2 - CASE_MARGIN_TOP - PCB_THICKNESS - USB_POS[1],\n", 257 | " )\n", 258 | " .tag(\"USBCutout\")\n", 259 | " .rect(USB_HOLE_SIZE[0] + USB_HOLE_MARGIN, USB_HOLE_SIZE[1] + USB_HOLE_MARGIN)\n", 260 | " .cutBlind(-(CASE_FRAME + USB_HOLE_SIZE[2] + 2))\n", 261 | " .workplaneFromTagged(\"USBCutout\")\n", 262 | " .rect(USB_CONN_SIZE[0], USB_CONN_SIZE[1])\n", 263 | " .cutBlind(-1)\n", 264 | ")\n", 265 | "\n", 266 | "show(case)" 267 | ] 268 | }, 269 | { 270 | "attachments": {}, 271 | "cell_type": "markdown", 272 | "metadata": {}, 273 | "source": [ 274 | "## OLEDの下のカバー\n", 275 | "基板とOLEDの間にはさむカバーを設計します。 \n", 276 | "別のオブジェクトとして作りますが、位置はあわせます。\n", 277 | "カバーは両面テープで固定するので特にネジなどは用意しません。" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": null, 283 | "metadata": {}, 284 | "outputs": [], 285 | "source": [ 286 | "OLED_COVER_WIDTH = 19\n", 287 | "OLED_COVER_HEIGHT = 19 * 2 - 3\n", 288 | "OLED_COVER_THICKNESS = 10\n", 289 | "\n", 290 | "# XY平面から基板の上にのる高さ分オフセットした面を基準にする\n", 291 | "oled_cover = (\n", 292 | " cq.Workplane(\"XY\")\n", 293 | " .workplane(offset=CASE_BOTTOM + CASE_MARGIN_BOTTOM + PCB_THICKNESS)\n", 294 | " .center(-PCB_WIDTH / 2, PCB_HEIGHT / 2)\n", 295 | " .tag(\"PCB_ORIGIN\")\n", 296 | " .center(OLED_COVER_WIDTH / 2, -OLED_COVER_HEIGHT / 2)\n", 297 | " .rect(OLED_COVER_WIDTH, OLED_COVER_HEIGHT)\n", 298 | " .extrude(OLED_COVER_THICKNESS)\n", 299 | " .faces(\"Z\")\n", 300 | " .tag(\"CASE_TOP\")\n", 301 | " .edges(\">Y or これは[キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529)の20日目の記事の一部です。 2 | > トップページは[Pythonだけでキーボードを作る](https://5z6p.com/2022/12/21/ac2022/)です。 3 | 4 | # ファームウェアを作る 5 | ファームウェアは組み込み向けPythonの[CircuitPython](https://circuitpython.org/)ベースで実装された[KMK Firmware](https://github.com/KMKfw/kmk_firmware)を使用します。 6 | - CircuitPython https://circuitpython.org/ 7 | - KMK Firmware https://github.com/KMKfw/kmk_firmware 8 | 9 | RP2040とCircuitPythonの環境なら認識されたドライブへ直接Pythonのコードを書き込むだけです。もちろん、保存したファイルを直接編集すればコードが書き換わります。 10 | 11 | ## KMK Firmwareをインストールする 12 | KMK Firmwareの[Getting start guide](https://github.com/KMKfw/kmk_firmware/blob/master/docs/en/Getting_Started.md)を参考にしてインストールします。 13 | 14 | - XIAO RP2040向けCircuitPythonの7.xをインストールします。(2022/12時点では7.3.3) 15 | https://circuitpython.org/board/seeeduino_xiao_rp2040/ 16 | 17 | ## 追加のライブラリをインストールする 18 | KMK FirmwareのOLED機能を利用するために必要なパッケージを導入します。 19 | 20 | - OLED機能のドキュメント 21 | https://github.com/KMKfw/kmk_firmware/blob/master/docs/en/peg_oled_display.md 22 | 23 | ドキュメントにある通り、bundleをダウンロードして`adafruit-circuitpython-bundle-7.x~/lib`内にある`adafruit_display_text`(ディレクトリごと)、`adafruit_displayio_ssd1306.mpy`を認識されたXIAOのドライブ内の`lib/`以下にコピーします。 24 | 25 | # ファームウェア 26 | KMK Fimwareでは`kb.py`と`main.py`が必要です。 27 | このキーボード用のファームウェアは[firmware/](../../firmware/)内にあります。 28 | 29 | XIAOのルートに予め`code.py`もしくは`main.py`がある場合は一旦削除します。 30 | `boot.py`や`libs/`などは削除しません。 31 | 32 | ## kb.py 33 | `kb.py`では、キーボードのハードウェア情報を定義します。 34 | 35 | ```python 36 | # CircuitPythonで定義されているboard情報 37 | # 最初にダウンロードしたCircuitPythonによって変わる 38 | # print(dir(board))で定義が確認できる 39 | import board 40 | 41 | from kmk.kmk_keyboard import KMKKeyboard as _KMKKeyboard 42 | from kmk.scanners import DiodeOrientation 43 | 44 | class KMKKeyboard(_KMKKeyboard): 45 | # マトリクスのピン情報 46 | row_pins = (board.D0, board.D5, board.D6) 47 | col_pins = ( 48 | board.D7, 49 | board.D2, 50 | board.D3, 51 | board.D4, 52 | ) 53 | # マトリクスのダイオードの方向 54 | diode_orientation = DiodeOrientation.COL2ROW 55 | 56 | # oledで使用するi2cのピン 57 | SCL = board.D10 58 | SDA = board.D8 59 | 60 | # マトリクスと実際のキー位置の紐付け 61 | # QMKのLAYOUTみたいなもの 62 | # fmt: off 63 | coord_mapping = [ 64 | 1, 2, 3, 65 | 5, 6, 7, 66 | 8, 9, 10, 11 67 | ] 68 | # fmt: on 69 | 70 | ``` 71 | 72 | ## main.py 73 | `main.py`では、キーボードのキーマップや振る舞いを実装します。 74 | また、OLEDに表示する画像をビットマップとして用意します。 75 | 76 | ```python 77 | import board 78 | 79 | # kb.pyをインポート 80 | from kb import KMKKeyboard 81 | 82 | # キーコード 83 | from kmk.keys import KC 84 | # レイヤー機能 85 | from kmk.modules.layers import Layers 86 | # OLEDエクステンション 87 | from kmk.extensions.peg_oled_Display import ( 88 | Oled, 89 | OledDisplayMode, 90 | OledReactionType, 91 | OledData, 92 | ) 93 | 94 | keyboard = KMKKeyboard() 95 | 96 | # レイヤー機能を有効化 97 | layers_ext = Layers() 98 | keyboard.modules.append(layers_ext) 99 | 100 | # キーボードの設定 101 | keyboard.tap_time = 250 102 | # デバッグ 103 | # Trueにするとシリアルポートへデバッグ情報を出力する 104 | keyboard.debug_enabled = False 105 | 106 | # OLEDの設定 107 | # 画像は白黒のビットマップとして用意する 108 | oled_ext = Oled( 109 | OledData(image={0: OledReactionType.STATIC, 1: ["oled_python.bmp"]}), 110 | toDisplay=OledDisplayMode.IMG, 111 | flip=True, 112 | ) 113 | keyboard.extensions.append(oled_ext) 114 | 115 | # キーマップで使うキーコード 116 | ____ = KC.TRNS 117 | 118 | L1_P0 = KC.LT(1, KC.P0) 119 | 120 | # キーマップ 121 | # fmt: off 122 | keyboard.keymap = [ 123 | # default layer 124 | [ 125 | KC.P7, KC.P8, KC.P9, 126 | KC.P4, KC.P5, KC.P6, 127 | L1_P0, KC.P1, KC.P2, KC.P3 128 | ], 129 | # Fn layer 130 | [ 131 | KC.ESC, KC.DEL, KC.BSPC, 132 | KC.COMM, KC.NO, KC.TAB, 133 | ____, KC.DOT, KC.SPC, KC.ENT 134 | ], 135 | ] 136 | # fmt: on 137 | 138 | if __name__ == "__main__": 139 | keyboard.go() 140 | 141 | ``` 142 | 143 | # XIAOに書き込む 144 | `kb.py`と`main.py`、画像ファイルをXIAOへ書き込んで動作確認をします。 145 | キーボードとして正しく認識されているか、OLEDに正しい画像が表示されているか確認しました。 146 | これで完成です! 147 | -------------------------------------------------------------------------------- /notebook/jp/pcb.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "colab_type": "text", 7 | "id": "view-in-github" 8 | }, 9 | "source": [ 10 | "\"Open" 11 | ] 12 | }, 13 | { 14 | "attachments": {}, 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "> これは[キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529)の20日目の記事の一部です。 \n", 19 | "> トップページは[Pythonだけでキーボードを作る](https://5z6p.com/2022/12/21/ac2022/)です。" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": { 25 | "id": "g6rr65y7TUEE" 26 | }, 27 | "source": [ 28 | "# 基板の設計\n", 29 | "\n", 30 | "\n", 31 | "\n", 32 | "\n", 33 | "このノートブックではキーボードの基板を設計し、最終的に基板を生産するためのデータ`ガーバーファイル`を得ることを目標とします。\n", 34 | "\n", 35 | "通常の工程で基板を作るにはまず`回路図エディタ`で`回路図`を書いて使用する部品とその結線情報をまとめて`ネットリスト`として出力し、`基板エディタ`を用いて実際に`基板`へ部品を配置・配線します。" 36 | ] 37 | }, 38 | { 39 | "attachments": {}, 40 | "cell_type": "markdown", 41 | "metadata": { 42 | "id": "RwgtrhL6DhoQ" 43 | }, 44 | "source": [ 45 | "## 記事を読むための注意\n", 46 | "このノートブックには2回実行すると正常に動作しないセルが含まれています。\n", 47 | "\n", 48 | "それを防止するために上のメニューから`ランタイム>全てのセルを実行`してから読み始めると良いかもしれません。実行完了には少し時間がかかります。" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": { 54 | "id": "Topn5GuhW6Z2" 55 | }, 56 | "source": [ 57 | "## リソースのダウンロード\n", 58 | "以下のセルを実行してkicadのライブラリや他のリソースが入ったリポジトリをクローンしておきます。" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": null, 64 | "metadata": { 65 | "colab": { 66 | "base_uri": "https://localhost:8080/" 67 | }, 68 | "id": "hEThVfBsW8RE", 69 | "outputId": "b4211474-89e8-4cf3-9779-fdbd6e895db2" 70 | }, 71 | "outputs": [], 72 | "source": [ 73 | "!git clone https://github.com/hsgw/keyboard-made-by-python/" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": { 79 | "id": "Ancu-CO6W_Y4" 80 | }, 81 | "source": [ 82 | "# skidlでネットリスト(回路図)を設計する\n", 83 | "Pythonを使った設計では回路図を書かずにコードで使用する部品と結線情報を定義してネットリストを出力します。\n", 84 | "\n", 85 | "[skidl](https://github.com/devbisme/skidl)というライブラリを用います。このライブラリは独自フォーマットの部品ライブラリだけでなく、kicadの部品ライブラリをインポートして使用します。また、ネットリストはkicadのPCBエディタと互換性があり直接kicadへ読み込んで回路図を書くこともできます。\n", 86 | "\n", 87 | "- skidl https://github.com/devbisme/skidl\n", 88 | "- skidlのドキュメント https://devbisme.github.io/skidl/" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": { 94 | "id": "-cW5lSiPYuUS" 95 | }, 96 | "source": [ 97 | "## skidlのインストールとインポート\n", 98 | "[skidl](https://github.com/devbisme/skidl)をインストールしてインポートします。" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "metadata": { 105 | "colab": { 106 | "base_uri": "https://localhost:8080/" 107 | }, 108 | "id": "Oy3vcMzUWq9g", 109 | "outputId": "aa8168c0-4184-4e8b-cd14-815035fc8882" 110 | }, 111 | "outputs": [], 112 | "source": [ 113 | "# インストールには時間がかかります\n", 114 | "!pip install skidl" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": { 121 | "colab": { 122 | "base_uri": "https://localhost:8080/" 123 | }, 124 | "id": "KMF3_yPHTF0L", 125 | "outputId": "e51fa261-a270-4d10-c6a9-b27fae81e2b9" 126 | }, 127 | "outputs": [], 128 | "source": [ 129 | "# kicadのデフォルトパスが環境変数に設定されていないのでWarningが出ます\n", 130 | "from skidl import *" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": { 136 | "id": "LTeD6nJwogVT" 137 | }, 138 | "source": [ 139 | "## 定数を宣言する\n", 140 | "何度も出てくる値を定数として宣言しておきます。この値を使い回すことで変更に強い設計になります。\n" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": { 147 | "id": "6jwqmkIjo5MH" 148 | }, 149 | "outputs": [], 150 | "source": [ 151 | "KEY_COUNT = 10\n", 152 | "COL_COUNT = 4\n", 153 | "ROW_COUNT = 3\n", 154 | "MATRIX_MAP = [\n", 155 | " (0,1),(0,2),(0,3), \n", 156 | " (1,1),(1,2),(1,3),\n", 157 | " (2,0),(2,1),(2,2),(2,3)\n", 158 | "]" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": { 164 | "id": "QWaPdvrNmmzk" 165 | }, 166 | "source": [ 167 | "## kicadライブラリの読み込みと部品の準備\n", 168 | "使用するkicadライブラリのパスを追加をします。\n", 169 | "\n", 170 | "kicadがインストールされている環境であれば通常のライブラリへのパスがデフォルトで追加されているはずですが、今回はkicadのインストールされていない環境で実行するために使う部品だけ別に用意しておきます。\n", 171 | "先程githubからcloneしてきた`keyboard-made-by-python/hardware/kicad_libs`に入っています。\n", 172 | "\n", 173 | "シンボルとフットプリントを結びつけて必要な個数だけ部品を用意します。" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": { 180 | "colab": { 181 | "base_uri": "https://localhost:8080/" 182 | }, 183 | "id": "EjCwQBClnF5q", 184 | "outputId": "0c0ad665-d336-4d2a-af5e-55f8886532ac" 185 | }, 186 | "outputs": [], 187 | "source": [ 188 | "# 使用するライブラリを登録する\n", 189 | "# シンボル・フットプリントどちらも別で登録する\n", 190 | "lib_search_paths[KICAD].append(\"keyboard-made-by-python/hardware/kicad_libs\")\n", 191 | "footprint_search_paths[KICAD].append(\"keyboard-made-by-python/hardware/kicad_libs/kicad.pretty\")\n", 192 | "\n", 193 | "# 同じ部品を何回も使うならシンボルとフットプリントを結びつけたテンプレートとして読み込んでおく\n", 194 | "diode = Part(\n", 195 | " \"kicad_symbols\", \"D_Small_ALT\", TEMPLATE, footprint=\"kicad:D_SOD123_hand\"\n", 196 | ")\n", 197 | "switch = Part(\n", 198 | " \"kicad_symbols\",\n", 199 | " \"SW_Push\",\n", 200 | " TEMPLATE,\n", 201 | " footprint=\"kicad:SW_Cherry_MX_1.00u_PCB\",\n", 202 | ")\n", 203 | "\n", 204 | "# 宣言した定数とテンプレートを使ってダイオードとスイッチを用意する\n", 205 | "# それぞれ KEY_COUNT 個用意して配列にいれておく\n", 206 | "diodes = diode(KEY_COUNT)\n", 207 | "switches = switch(KEY_COUNT)\n", 208 | "\n", 209 | "# ひとつしか使わない部品はそのまま読み込む\n", 210 | "xiao = Part(\"kicad_symbols\", \"xiao_rp2040\", footprint=\"kicad:xiao_rp2040\")\n", 211 | "oled = Part(\"kicad_symbols\", \"oled_i2c\", footprint=\"kicad:oled_i2c\")\n", 212 | "\n", 213 | "# printするとシンボルのPin情報が見えます\n", 214 | "print(diode, switch, xiao, oled)" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": { 220 | "id": "oG5xuNjqq6qg" 221 | }, 222 | "source": [ 223 | "## PinやNetを接続する\n", 224 | "用意した部品のピン同士を接続を入力して回路の構成を定義します。\n", 225 | "\n", 226 | "このときなるべく配線自体に`Net`名をつけるようにします。`Net`を基準に部品の`Pin`を繋いでいくことで同じところにつながるもの(電源やスイッチマトリクス)を整理して見やすいコードにします。また、kicadへネットリストをインポートしたときに表示されるので目印にもなります。\n", 227 | "\n", 228 | "今回の設計でも基板配線時に`ピン番号`を`Net`から読み出します。" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": { 235 | "colab": { 236 | "base_uri": "https://localhost:8080/" 237 | }, 238 | "id": "PI9UK581tf5H", 239 | "outputId": "3cb4724e-8401-4655-a2e2-66fb5cb5368f" 240 | }, 241 | "outputs": [], 242 | "source": [ 243 | "# Pin情報を表示しながら配線すると便利\n", 244 | "print(xiao)" 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": null, 250 | "metadata": { 251 | "colab": { 252 | "base_uri": "https://localhost:8080/" 253 | }, 254 | "id": "Ufo-bwPNtWzt", 255 | "outputId": "833c4292-eef8-46e5-c648-298eb0fe0c53" 256 | }, 257 | "outputs": [], 258 | "source": [ 259 | "# スイッチマトリクスのROW,COLのネットリストが入った配列を作る\n", 260 | "netRows = [Net(f\"ROW{i}\") for i in range(ROW_COUNT)]\n", 261 | "netCols = [Net(f\"COL{i}\") for i in range(COL_COUNT)]\n", 262 | "\n", 263 | "\n", 264 | "# ROWのNet -> スイッチの1ピン スイッチの2ピン -> ダイオードのカソード ダイオードのアノード -> COLのNet をまとめて宣言する\n", 265 | "# NetやPinは`&`で繋ぐと接続される\n", 266 | "# 部品のPinには part[\"pin name\"]でアクセス出来る\n", 267 | "# 部品のPinの添字を2つにするとinとoutになる\n", 268 | "# 例: sw[\"1\"]につながるNetかPin & sw[\"1 2\"] & sw[\"2\"]に繋がるNetかPin\n", 269 | "for sw, d, mapping in zip(switches, diodes, MATRIX_MAP):\n", 270 | " netRows[mapping[0]] & sw[\"1 2\"] & d[\"K A\"] & netCols[mapping[1]]\n", 271 | "\n", 272 | "# スイッチマトリクスとxiaoを接続する\n", 273 | "# NetにPinを`+`ことで接続する\n", 274 | "netCols[0] += xiao[8]\n", 275 | "netCols[1] += xiao[3]\n", 276 | "netCols[2] += xiao[4]\n", 277 | "netCols[3] += xiao[5]\n", 278 | "\n", 279 | "netRows[0] += xiao[1]\n", 280 | "netRows[1] += xiao[6]\n", 281 | "netRows[2] += xiao[7]\n", 282 | "\n", 283 | "# oledとxiaoも接続する\n", 284 | "# Net(\"3.3V\")みたいに直接Netを宣言して繋いでもOK\n", 285 | "Net(\"3.3V\") & oled[\"Vcc\"] & xiao[\"3.3V\"]\n", 286 | "Net(\"GND\") & oled[\"GND\"] & xiao[\"GND\"]\n", 287 | "Net(\"SDA\") & oled[\"SDA\"] & xiao[9]\n", 288 | "Net(\"SCL\") & oled[\"SCL\"] & xiao[11]\n", 289 | "\n", 290 | "# printすると接続されているPinが表示される\n", 291 | "print(netRows[0])" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": { 297 | "id": "ygS8U7MywJ8k" 298 | }, 299 | "source": [ 300 | "## ERCとネットリストの出力\n", 301 | "ERC(Electrical Rule Check・回路図のルールチェック)をかけて、ネットリストを出力します。\n", 302 | "これで回路図・ネットリストは完成です!" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": null, 308 | "metadata": { 309 | "colab": { 310 | "base_uri": "https://localhost:8080/", 311 | "height": 330 312 | }, 313 | "id": "7UCaNTPgwJVV", 314 | "outputId": "209f3e2a-ff1e-4806-d9b4-13bd61d0f927" 315 | }, 316 | "outputs": [], 317 | "source": [ 318 | "# 他のセルを複数回実行しているとエラーが出たり回路が複数個になったりするかもしれません\n", 319 | "# そのときは`メニュー`の`ランタイム -> ランタイムを再起動`して最初からやりなおすか`再起動してすべてのセルを実行`してください\n", 320 | "# 未結線のWarningが出ますが問題ありません\n", 321 | "ERC()\n", 322 | "generate_netlist(file_=\"keyboard.net\")" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": { 328 | "id": "5URJ6fd7xzse" 329 | }, 330 | "source": [ 331 | "出力されたネットリストは今記事では使用しませんが、kicadのpcbnewのネットリスト読み込みからインポートしてそのまま基板を作ることも出来ます。\n", 332 | "\n", 333 | "![pcbnewへネットリストをインポートした](https://github.com/hsgw/keyboard-made-by-python/blob/main/notebook/imgs/kicad_pcbnew.png?raw=1)" 334 | ] 335 | }, 336 | { 337 | "cell_type": "markdown", 338 | "metadata": { 339 | "id": "rtU9DM-AD2hu" 340 | }, 341 | "source": [ 342 | "# pcbflowで基板を設計する\n", 343 | "[pcbflow](https://github.com/michaelgale/pcbflow)を使って、skidlで設計したフットプリントとピンの接続情報を読み込み、基板上に実際に部品を配置し配線を繋いでいきます。随時、画像ファイルでプレビューをしたり、最終的には製造に必要なガーバーファイル一式を出力します。\n", 344 | "\n", 345 | "配線は昔ながらの[Turtle graphics](https://docs.python.org/ja/3/library/turtle.html)風の表記で行います。\n", 346 | "\n", 347 | "pcbflowは開発中(?)のようで意図しない動作や不具合があったため修正と追加をしました。今記事ではその[folk](https://github.com/hsgw/pcbflow/tree/fix_kicad)を使用します。\n", 348 | "\n", 349 | "- pcbflow https://github.com/michaelgale/pcbflow\n", 350 | "- pcvflowのドキュメントはREADME.mdにあります\n", 351 | "- folkして修正したpcbflow https://github.com/hsgw/pcbflow/tree/fix_kicad" 352 | ] 353 | }, 354 | { 355 | "cell_type": "markdown", 356 | "metadata": { 357 | "id": "mXU5TZOeFBGI" 358 | }, 359 | "source": [ 360 | "## pcbflowのダウンロードとインストール\n", 361 | "以下のセルを実行してインストールします。\n", 362 | "インポートしてエラーがないことを確認します。" 363 | ] 364 | }, 365 | { 366 | "cell_type": "code", 367 | "execution_count": null, 368 | "metadata": { 369 | "colab": { 370 | "base_uri": "https://localhost:8080/" 371 | }, 372 | "id": "D4V_SArjWQXs", 373 | "outputId": "1d0507f1-bea5-4027-fa2c-08d44f90cbb0" 374 | }, 375 | "outputs": [], 376 | "source": [ 377 | "# インストールに時間がかかります\n", 378 | "!pip install git+https://github.com/hsgw/pcbflow/@fix_kicad" 379 | ] 380 | }, 381 | { 382 | "cell_type": "code", 383 | "execution_count": null, 384 | "metadata": { 385 | "id": "zZH-ygq3FlJu" 386 | }, 387 | "outputs": [], 388 | "source": [ 389 | "from pcbflow import *" 390 | ] 391 | }, 392 | { 393 | "cell_type": "markdown", 394 | "metadata": { 395 | "id": "qkWfPZF2I37R" 396 | }, 397 | "source": [ 398 | "## 定数を宣言する\n", 399 | "ネットリストと同じようによく使う値を定数として宣言しておきます。 \n", 400 | "座標のY軸が反転していて違和感があったので変換するための関数も宣言しています。" 401 | ] 402 | }, 403 | { 404 | "cell_type": "code", 405 | "execution_count": null, 406 | "metadata": { 407 | "id": "a031kS7yJEtJ" 408 | }, 409 | "outputs": [], 410 | "source": [ 411 | "BOARD_WIDTH = 76.0\n", 412 | "BOARD_HEIGHT = 57.0\n", 413 | "\n", 414 | "KEY_PITCH = 19.0\n", 415 | "\n", 416 | "SCREW_HOLE = 2.2\n", 417 | "\n", 418 | "LAYER_TOP = \"GTL\"\n", 419 | "LAYER_BOTTOM = \"GBL\"\n", 420 | "\n", 421 | "# Y軸の座標を反転させる関数\n", 422 | "def pos(x, y):\n", 423 | " return (x, BOARD_HEIGHT - y)" 424 | ] 425 | }, 426 | { 427 | "cell_type": "markdown", 428 | "metadata": { 429 | "id": "K2o9ziq3N2Bb" 430 | }, 431 | "source": [ 432 | "## 基板と回路の変数を宣言する\n", 433 | "基板と回路にアクセスするための変数を宣言しておきます。\n", 434 | "基板はpcbflowのもの、回路はskidlのものです。\n", 435 | "\n", 436 | "`board`の宣言時に外形の大きさを指定し、外形線を追加します。" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": null, 442 | "metadata": { 443 | "id": "8bzraF-yOaIH" 444 | }, 445 | "outputs": [], 446 | "source": [ 447 | "# skidlで作った回路情報\n", 448 | "circuit = builtins.default_circuit\n", 449 | "# pcbflowの基板\n", 450 | "board = Board((BOARD_WIDTH, BOARD_HEIGHT))\n", 451 | "board.add_outline()" 452 | ] 453 | }, 454 | { 455 | "cell_type": "markdown", 456 | "metadata": { 457 | "id": "9nWBUCJJNdgE" 458 | }, 459 | "source": [ 460 | "## デザインルールを設定する\n", 461 | "ドリルの大きさや配線の太さなど、基板のデザインルールを設定します。\n", 462 | "\n", 463 | "この設定を基本として基板を配線していきます。" 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": null, 469 | "metadata": { 470 | "id": "PdPPua_3Nuoi" 471 | }, 472 | "outputs": [], 473 | "source": [ 474 | "board.drc.trace_width = 0.5 # 配線の太さ\n", 475 | "board.drc.via_drill = 0.6 # ビアのドリル径\n", 476 | "board.drc.via_annular_ring = 0.4 # ビアのパッド径\n", 477 | "board.drc.clearance = 0.4 # 配線のクリアランス" 478 | ] 479 | }, 480 | { 481 | "cell_type": "markdown", 482 | "metadata": { 483 | "id": "NFHOh_4xaSMc" 484 | }, 485 | "source": [ 486 | "## 固定用の穴をあける\n", 487 | "スイッチの間に基板を固定するための穴を作ります。" 488 | ] 489 | }, 490 | { 491 | "cell_type": "code", 492 | "execution_count": null, 493 | "metadata": { 494 | "id": "1AY3bcKpaa6M" 495 | }, 496 | "outputs": [], 497 | "source": [ 498 | "board.add_hole(pos(KEY_PITCH * 2, KEY_PITCH), SCREW_HOLE)\n", 499 | "board.add_hole(pos(KEY_PITCH * 3, KEY_PITCH), SCREW_HOLE)\n", 500 | "board.add_hole(pos(KEY_PITCH * 3, KEY_PITCH * 2), SCREW_HOLE)\n", 501 | "board.add_hole(pos(KEY_PITCH, KEY_PITCH * 2), SCREW_HOLE)" 502 | ] 503 | }, 504 | { 505 | "cell_type": "markdown", 506 | "metadata": { 507 | "id": "gxtMIg5IcLKb" 508 | }, 509 | "source": [ 510 | "### 画像ファイルを出力してプレビューしてみる\n", 511 | "ここで一度画像ファイルを出力して基板上に正しく穴があいているか確認してみます。" 512 | ] 513 | }, 514 | { 515 | "cell_type": "code", 516 | "execution_count": null, 517 | "metadata": { 518 | "colab": { 519 | "base_uri": "https://localhost:8080/", 520 | "height": 1000 521 | }, 522 | "id": "22HqPC8IcsFh", 523 | "outputId": "3c5cb1e1-9994-4dbc-9208-43b08752bbcb" 524 | }, 525 | "outputs": [], 526 | "source": [ 527 | "from IPython.display import Image\n", 528 | "\n", 529 | "# 現在のboardをpngファイルとして書き出す\n", 530 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 531 | "\n", 532 | "# 表示\n", 533 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 534 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 535 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 536 | ] 537 | }, 538 | { 539 | "cell_type": "markdown", 540 | "metadata": { 541 | "id": "t2mUZjAfY31k" 542 | }, 543 | "source": [ 544 | "## 部品に位置情報を追加する\n", 545 | "skidlの部品それぞれに基板上の位置・回転・実装面の情報を追加していきます。\n", 546 | "\n", 547 | "今記事ではいきなり位置を指定していますが、基板に配置してはプレビューを繰り返しながら決めていきました。" 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": null, 553 | "metadata": { 554 | "id": "R4cUuHhZZPm_" 555 | }, 556 | "outputs": [], 557 | "source": [ 558 | "# sw, d, xiao, oledはskidlのところで宣言した変数です\n", 559 | "\n", 560 | "# スイッチとダイオードはブロックにまとめて位置を指定する\n", 561 | "# ブロックごとにスイッチとダイオードの位置関係は同じ\n", 562 | "for sw, d, mapping in zip(switches, diodes, MATRIX_MAP):\n", 563 | " sw.pos = pos(\n", 564 | " mapping[1] * KEY_PITCH + KEY_PITCH / 2,\n", 565 | " mapping[0] * KEY_PITCH + KEY_PITCH / 2,\n", 566 | " )\n", 567 | " sw.side = \"top\"\n", 568 | " sw.rotate = 0\n", 569 | " d.pos = (sw.pos[0] + 6, sw.pos[1] + 5)\n", 570 | " d.side = \"bottom\"\n", 571 | " d.rotate = 0\n", 572 | "\n", 573 | "xiao.pos = pos(11.25, 11.5)\n", 574 | "xiao.side = \"bottom\"\n", 575 | "xiao.rotate = 0\n", 576 | "\n", 577 | "oled.pos = pos(9.5, 36)\n", 578 | "oled.side = \"top\"\n", 579 | "oled.rotate = 270" 580 | ] 581 | }, 582 | { 583 | "cell_type": "markdown", 584 | "metadata": { 585 | "id": "jQy4fcGbfAfd" 586 | }, 587 | "source": [ 588 | "## 基板に部品を配置する\n", 589 | "追加した位置情報を元にskidlの部品をpcbflowの基板へ変換しながら配置していきます。" 590 | ] 591 | }, 592 | { 593 | "cell_type": "code", 594 | "execution_count": null, 595 | "metadata": { 596 | "id": "MBdn1GA4e_ea" 597 | }, 598 | "outputs": [], 599 | "source": [ 600 | "for part in circuit.parts:\n", 601 | " try:\n", 602 | " # skidlの部品を読み込んでpcbflowのboardに配置していく\n", 603 | " SkiPart(board.DC(part.pos).right(part.rotate), part, side=part.side)\n", 604 | " except AttributeError:\n", 605 | " print(f\"{part.ref} has no pos or side\")\n", 606 | " continue" 607 | ] 608 | }, 609 | { 610 | "cell_type": "markdown", 611 | "metadata": { 612 | "id": "376guVBxgOOm" 613 | }, 614 | "source": [ 615 | "### プレビュー" 616 | ] 617 | }, 618 | { 619 | "cell_type": "code", 620 | "execution_count": null, 621 | "metadata": { 622 | "colab": { 623 | "base_uri": "https://localhost:8080/", 624 | "height": 1000 625 | }, 626 | "id": "pMku38a9gIMl", 627 | "outputId": "e09a38c7-d36a-4ed8-88b7-e3031accccbb" 628 | }, 629 | "outputs": [], 630 | "source": [ 631 | "from IPython.display import Image\n", 632 | "\n", 633 | "# 現在のboardをpngファイルとして書き出す\n", 634 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 635 | "\n", 636 | "# 表示\n", 637 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 638 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 639 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 640 | ] 641 | }, 642 | { 643 | "cell_type": "markdown", 644 | "metadata": { 645 | "id": "HIvuWJ6_sxcM" 646 | }, 647 | "source": [ 648 | "## 配線:xiao-スイッチ(ROW)\n", 649 | "xiaoからスイッチのROWへの配線をします。 \n", 650 | "Turtle記法を使ってxiaoから配線を引き出してスイッチの`Pad`に接続します。\n", 651 | "\n", 652 | "skidlの`Pin`とpcbflowの`pads`は同じもの、部品のピンを指しています。" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": null, 658 | "metadata": { 659 | "colab": { 660 | "base_uri": "https://localhost:8080/" 661 | }, 662 | "id": "QYuy9Np7sx9M", 663 | "outputId": "ed24ff82-e77b-46f8-fba6-2d8cdfdee9fe" 664 | }, 665 | "outputs": [], 666 | "source": [ 667 | "xiaoRef = \"U1\"\n", 668 | "\n", 669 | "# skidlのNetから名前で検索してPadの番号を返す関数\n", 670 | "# skidlのPin番号は1から、pcbflowのPad番号は0から始まる\n", 671 | "def get_pin_number_from_net(netLabel, ref):\n", 672 | " net = Net.get(netLabel)\n", 673 | " return list((int(x.num) - 1 for x in net.pins if x.ref == ref))[0]\n", 674 | "\n", 675 | "# skidlのNetから名前で検索して接続されているxiaoのPadを返す関数\n", 676 | "# kicadライブラリの都合でPad番号がずれているのを修正して返す\n", 677 | "# 最後のnewpath()は初期値に変な値が入っているのを初期化している\n", 678 | "def xiao_pads(net: str):\n", 679 | " return (\n", 680 | " board.get_part(xiaoRef)\n", 681 | " .pads[get_pin_number_from_net(net, xiaoRef) + 14]\n", 682 | " .newpath()\n", 683 | " )\n", 684 | "\n", 685 | "# Net:ROWに繋がったxiaoとスイッチのピンを繋ぐ\n", 686 | "# ROWに繋がったxiaoのPad→レイヤーを指定→Turtle記法で配線→SWのパッドまで軸を合わせて配線\n", 687 | "xiao_pads(\"ROW0\").set_layer(LAYER_TOP).w(\"r 45\").align_meet(\n", 688 | " board.get_part(\"SW1\").pads[0], \"x\"\n", 689 | ")\n", 690 | "xiao_pads(\"ROW1\").set_layer(LAYER_TOP).w(\"r 45\").align_meet(\n", 691 | " board.get_part(\"SW4\").pads[0], \"y\"\n", 692 | ")\n", 693 | "xiao_pads(\"ROW2\").set_layer(LAYER_TOP).w(\"l 180 f 12 r 45\").align_meet(\n", 694 | " board.get_part(\"SW8\").pads[0], \"y\"\n", 695 | ")" 696 | ] 697 | }, 698 | { 699 | "cell_type": "markdown", 700 | "metadata": { 701 | "id": "fSKc2qDnu_c3" 702 | }, 703 | "source": [ 704 | "### プレビュー" 705 | ] 706 | }, 707 | { 708 | "cell_type": "code", 709 | "execution_count": null, 710 | "metadata": { 711 | "colab": { 712 | "base_uri": "https://localhost:8080/", 713 | "height": 965 714 | }, 715 | "id": "bA690BhQu_c8", 716 | "outputId": "dae75eb2-5ee9-47a2-8a80-598494b4f07e" 717 | }, 718 | "outputs": [], 719 | "source": [ 720 | "from IPython.display import Image\n", 721 | "\n", 722 | "# 現在のboardをpngファイルとして書き出す\n", 723 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 724 | "\n", 725 | "# 表示\n", 726 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 727 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 728 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 729 | ] 730 | }, 731 | { 732 | "cell_type": "markdown", 733 | "metadata": { 734 | "id": "7g0j_Biziy3k" 735 | }, 736 | "source": [ 737 | "## 配線:ダイオード-スイッチ・スイッチ-スイッチ(ROW)・ダイオード-ダイオード(COL)\n", 738 | "接続されているピンの位置関係が同じものであれば同じように接続できるので、ループを回して配線します。\n", 739 | "\n", 740 | "COLの配線では途中でビアを作ります。ビアの情報を保存しておいて後ほどxiaoと接続します。" 741 | ] 742 | }, 743 | { 744 | "cell_type": "code", 745 | "execution_count": null, 746 | "metadata": { 747 | "id": "tfu1rTPnjlSW" 748 | }, 749 | "outputs": [], 750 | "source": [ 751 | "# スイッチとダイオードはブロックとして同じように並べたので同じように配線できる\n", 752 | "# REF Noでアクセスしたいので1始まり\n", 753 | "for i in range(1, KEY_COUNT + 1):\n", 754 | " sw = board.get_part(f\"SW{i}\")\n", 755 | " d = board.get_part(f\"D{i}\")\n", 756 | " # 配線をはじめるpadを指定→レイヤーを指定→Turtle記法で配線→パッドと軸を合わせて接続\n", 757 | " d.pads[0].set_layer(LAYER_BOTTOM).w(\"f 1 r 45\").align_meet(sw.pads[1], \"x\")\n", 758 | "\n", 759 | "# スイッチマトリクスのROWに繋がっているスイッチのPinは直線で並んでいるので同じように配線できる\n", 760 | "# ROWによってスイッチの個数が違うので注意\n", 761 | "for i in range(3):\n", 762 | " # ROWのNet情報からSWに繋がっているもののRef Noを配列にする\n", 763 | " refs = list(x.ref for x in netRows[i].pins if x.ref.startswith(\"SW\"))\n", 764 | " for j in range(len(refs) - 1):\n", 765 | " # 配線を始めるpadを指定→新しいパスを作る→レイヤーの指定→Turtle記法で配線→指定したpadへ直線で接続\n", 766 | " board.get_part(refs[j]).pads[0].newpath().set_layer(LAYER_TOP).w(\n", 767 | " \"r 90 f 2 l 45 f 1 r 45 f 2.5 r 45 f1\"\n", 768 | " ).meet(board.get_part(refs[j + 1]).pads[0])\n", 769 | "\n", 770 | "# ビアの情報を保存しておく配列\n", 771 | "# xiaoと接続するときに使う\n", 772 | "viaCol = [0] * 4\n", 773 | "\n", 774 | "# スイッチマトリクスのCOL1-3に繋がるダイオードは直線に並んでいるので同じように配線できる\n", 775 | "# 途中でビアを挟む\n", 776 | "for i in range(1, 4):\n", 777 | " d1 = board.get_part(f\"D{i}\")\n", 778 | " d2 = board.get_part(f\"D{i+3}\")\n", 779 | " d3 = board.get_part(f\"D{i+7}\")\n", 780 | " # ビアの情報を保持しておくためにダイオード-ビアを先に配線する\n", 781 | " via = (\n", 782 | " d1.pads[1]\n", 783 | " .set_layer(LAYER_BOTTOM)\n", 784 | " .w(\"l 180 f 1.25 r 45 f 2 l 45 f 6.5 l 45\")\n", 785 | " .align(d2.pads[1], \"y\")\n", 786 | " .wire()\n", 787 | " .via()\n", 788 | " )\n", 789 | " viaCol[i] = via\n", 790 | " # ビアから次のダイオードのPadまで配線する\n", 791 | " via.set_layer(LAYER_BOTTOM).meet(d2.pads[1])\n", 792 | " d2.pads[1].set_layer(LAYER_BOTTOM).w(\n", 793 | " \"l 180 f 1 r 45 f 2 l 45 f 6.5 l 45\"\n", 794 | " ).align_meet(d3.pads[1], \"y\")" 795 | ] 796 | }, 797 | { 798 | "cell_type": "markdown", 799 | "metadata": { 800 | "id": "RHUfHju2lnf4" 801 | }, 802 | "source": [ 803 | "### プレビュー" 804 | ] 805 | }, 806 | { 807 | "cell_type": "code", 808 | "execution_count": null, 809 | "metadata": { 810 | "colab": { 811 | "base_uri": "https://localhost:8080/", 812 | "height": 965 813 | }, 814 | "id": "TdpxBEHelnf9", 815 | "outputId": "4505fad8-49ab-4c5d-b885-1b856bfea16c" 816 | }, 817 | "outputs": [], 818 | "source": [ 819 | "from IPython.display import Image\n", 820 | "\n", 821 | "# 現在のboardをpngファイルとして書き出す\n", 822 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 823 | "\n", 824 | "# 表示\n", 825 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 826 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 827 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 828 | ] 829 | }, 830 | { 831 | "cell_type": "markdown", 832 | "metadata": { 833 | "id": "4sNQELnWwx5h" 834 | }, 835 | "source": [ 836 | "## 配線:ビア-xiao(COL)\n", 837 | "保存しておいたビアからxiaoへ配線します。" 838 | ] 839 | }, 840 | { 841 | "cell_type": "code", 842 | "execution_count": null, 843 | "metadata": { 844 | "colab": { 845 | "base_uri": "https://localhost:8080/" 846 | }, 847 | "id": "uQ1xlQk8wx5m", 848 | "outputId": "66bcf5b0-6c5b-4774-fff5-11b4a61d82e8" 849 | }, 850 | "outputs": [], 851 | "source": [ 852 | "# ビアからxiaoのpadへ配線する\n", 853 | "viaCol[1].set_layer(LAYER_TOP).w(\"l 45 f 11 l 45\").align_meet(\n", 854 | " xiao_pads(\"COL1\"), \"x\"\n", 855 | ")\n", 856 | "viaCol[2].set_layer(LAYER_TOP).w(\"f 2 l 45 f 29.5 l 45\").align_meet(\n", 857 | " xiao_pads(\"COL2\"), \"x\"\n", 858 | ")\n", 859 | "viaCol[3].set_layer(LAYER_TOP).w(\"f 4 l 45 f 48.5 l 45\").align_meet(\n", 860 | " xiao_pads(\"COL3\"), \"x\"\n", 861 | ")" 862 | ] 863 | }, 864 | { 865 | "cell_type": "markdown", 866 | "metadata": { 867 | "id": "BZp2rf73xjPt" 868 | }, 869 | "source": [ 870 | "### プレビュー" 871 | ] 872 | }, 873 | { 874 | "cell_type": "code", 875 | "execution_count": null, 876 | "metadata": { 877 | "colab": { 878 | "base_uri": "https://localhost:8080/", 879 | "height": 965 880 | }, 881 | "id": "-eCOnReyxjPt", 882 | "outputId": "ab969902-942c-4924-c529-4064fd769043" 883 | }, 884 | "outputs": [], 885 | "source": [ 886 | "from IPython.display import Image\n", 887 | "\n", 888 | "# 現在のboardをpngファイルとして書き出す\n", 889 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 890 | "\n", 891 | "# 表示\n", 892 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 893 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 894 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 895 | ] 896 | }, 897 | { 898 | "cell_type": "markdown", 899 | "metadata": { 900 | "id": "ViFc_c0oxjPo" 901 | }, 902 | "source": [ 903 | "## 配線:xiao-OLED\n", 904 | "xiaoからOLEDへ配線します。\n", 905 | "\n", 906 | "Netの名前を列挙しておいてskidlのNet情報から接続するPadの番号を調べます。" 907 | ] 908 | }, 909 | { 910 | "cell_type": "code", 911 | "execution_count": null, 912 | "metadata": { 913 | "id": "pEkSdb7YxjPs" 914 | }, 915 | "outputs": [], 916 | "source": [ 917 | "oledRef = \"DISP1\"\n", 918 | "\n", 919 | "# ビアからxiaoのpadへ配線する\n", 920 | "for net in [\"GND\", \"3.3V\", \"SCL\", \"SDA\"]:\n", 921 | " oledPin = board.get_part(oledRef).pads[get_pin_number_from_net(net, oledRef)]\n", 922 | " xiao_pads(net).set_layer(LAYER_TOP).left(135).align_meet(oledPin, \"y\")" 923 | ] 924 | }, 925 | { 926 | "cell_type": "markdown", 927 | "metadata": { 928 | "id": "Xa0VwbDJwx5m" 929 | }, 930 | "source": [ 931 | "## 配線の完成\n", 932 | "これで全ての配線が終わりました!" 933 | ] 934 | }, 935 | { 936 | "cell_type": "code", 937 | "execution_count": null, 938 | "metadata": { 939 | "colab": { 940 | "base_uri": "https://localhost:8080/", 941 | "height": 965 942 | }, 943 | "id": "_z-12LbDwx5m", 944 | "outputId": "31258366-12f5-4a81-fa95-ff16facfab80" 945 | }, 946 | "outputs": [], 947 | "source": [ 948 | "from IPython.display import Image\n", 949 | "\n", 950 | "# 現在のboardをpngファイルとして書き出す\n", 951 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 952 | "\n", 953 | "# 表示\n", 954 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 955 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 956 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 957 | ] 958 | }, 959 | { 960 | "cell_type": "markdown", 961 | "metadata": { 962 | "id": "KduW_1h4OLdU" 963 | }, 964 | "source": [ 965 | "## 基板上に絵を配置する\n", 966 | "基板のシルクレイヤーにロゴなどの画像を配置します。\n", 967 | "使用する画像データは冒頭のリソース内にあります。" 968 | ] 969 | }, 970 | { 971 | "cell_type": "code", 972 | "execution_count": null, 973 | "metadata": { 974 | "id": "Ygvq-EqzOxDH" 975 | }, 976 | "outputs": [], 977 | "source": [ 978 | "DIR_IMGS = \"keyboard-made-by-python/\"\n", 979 | "\n", 980 | "board.add_bitmap(\n", 981 | " (47.5, 28.5), \n", 982 | " DIR_IMGS + \"hardware/imgs/python-logo.png\",\n", 983 | " side=\"top\",\n", 984 | " scale=0.85\n", 985 | ")\n", 986 | "\n", 987 | "board.add_bitmap(\n", 988 | " (11.25, 45),\n", 989 | " DIR_IMGS + \"hardware/imgs/QR.png\",\n", 990 | " side=\"bottom\",\n", 991 | " scale=0.75,\n", 992 | ")\n", 993 | "\n", 994 | "board.add_bitmap(\n", 995 | " (11, 28.5),\n", 996 | " DIR_IMGS + \"hardware/imgs/logo-python-powered-w-logo.png\",\n", 997 | " side=\"bottom\",\n", 998 | " layer=\"GBS\",\n", 999 | " scale=0.7,\n", 1000 | ")\n", 1001 | "board.add_bitmap(\n", 1002 | " (11, 28.5),\n", 1003 | " DIR_IMGS + \"hardware/imgs/logo-python-powered-w-logoBG.png\",\n", 1004 | " side=\"bottom\",\n", 1005 | " layer=\"GBL\",\n", 1006 | " scale=0.7,\n", 1007 | ")\n", 1008 | "board.add_bitmap(\n", 1009 | " (11, 28.5),\n", 1010 | " DIR_IMGS + \"hardware/imgs/logo-python-powered-w-text.png\",\n", 1011 | " side=\"bottom\",\n", 1012 | " scale=0.7,\n", 1013 | ")\n", 1014 | "\n", 1015 | "board.add_bitmap(\n", 1016 | " (47.5, 22),\n", 1017 | " DIR_IMGS + \"hardware/imgs/dm9-logo.png\",\n", 1018 | " side=\"bottom\",\n", 1019 | " scale=0.8,\n", 1020 | ")\n", 1021 | "\n", 1022 | "board.add_bitmap(\n", 1023 | " (66.5, 22),\n", 1024 | " DIR_IMGS + \"hardware/imgs/hsgw-logo.png\",\n", 1025 | " side=\"bottom\",\n", 1026 | " scale=0.4,\n", 1027 | ")" 1028 | ] 1029 | }, 1030 | { 1031 | "cell_type": "markdown", 1032 | "metadata": { 1033 | "id": "32qY1ujHQRuA" 1034 | }, 1035 | "source": [ 1036 | "### プレビュー" 1037 | ] 1038 | }, 1039 | { 1040 | "cell_type": "code", 1041 | "execution_count": null, 1042 | "metadata": { 1043 | "colab": { 1044 | "base_uri": "https://localhost:8080/", 1045 | "height": 1000 1046 | }, 1047 | "id": "x6OEVJKUQO1W", 1048 | "outputId": "01b64bbd-db89-4d8a-f7b6-c1349efc29d4" 1049 | }, 1050 | "outputs": [], 1051 | "source": [ 1052 | "from IPython.display import Image\n", 1053 | "\n", 1054 | "# 現在のboardをpngファイルとして書き出す\n", 1055 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 1056 | "\n", 1057 | "# 表示\n", 1058 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 1059 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 1060 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 1061 | ] 1062 | }, 1063 | { 1064 | "cell_type": "markdown", 1065 | "metadata": { 1066 | "id": "HQ_stk2iQmiy" 1067 | }, 1068 | "source": [ 1069 | "## 基板上にテキストを配置する\n", 1070 | "基板のシルクレイヤーにテキストを配置します。\n", 1071 | "\n", 1072 | "JLCPCBに発注するので注文番号の入る位置を指定しておきます。" 1073 | ] 1074 | }, 1075 | { 1076 | "cell_type": "code", 1077 | "execution_count": null, 1078 | "metadata": { 1079 | "id": "1Lf-MAn1Qmiy" 1080 | }, 1081 | "outputs": [], 1082 | "source": [ 1083 | "board.add_text(\n", 1084 | " (47.5, 41.5),\n", 1085 | " \"https://github.com/hsgw/keyboard_made_by_python\",\n", 1086 | " scale=1.25,\n", 1087 | " side=\"bottom\",\n", 1088 | ")\n", 1089 | "\n", 1090 | "board.add_text(\n", 1091 | " (BOARD_WIDTH - 39, 5),\n", 1092 | " \"KEYBOARD MADE BY PYTHON\",\n", 1093 | " scale=2,\n", 1094 | " side=\"bottom\",\n", 1095 | " justify=\"left\",\n", 1096 | ")\n", 1097 | "board.add_text(\n", 1098 | " (BOARD_WIDTH - 44, 4.75),\n", 1099 | " \"Rev.1\",\n", 1100 | " scale=1.25,\n", 1101 | " side=\"bottom\",\n", 1102 | " justify=\"left\",\n", 1103 | ")\n", 1104 | "board.add_text(\n", 1105 | " (BOARD_WIDTH - 47.5, 2.25),\n", 1106 | " \"(c) 2022, Takuya Urakawa / Dm9Records / 5z6p.com\",\n", 1107 | " scale=1.25,\n", 1108 | " side=\"bottom\",\n", 1109 | " justify=\"left\",\n", 1110 | ")\n", 1111 | "\n", 1112 | "# MAGIC WORD for JLCPCB\n", 1113 | "board.add_text(\n", 1114 | " (11.5, 55),\n", 1115 | " \"JLCJLCJLCJLC\",\n", 1116 | " scale=1.1,\n", 1117 | " side=\"bottom\",\n", 1118 | ")" 1119 | ] 1120 | }, 1121 | { 1122 | "cell_type": "markdown", 1123 | "metadata": { 1124 | "id": "3FdBYSKrQmiz" 1125 | }, 1126 | "source": [ 1127 | "### プレビュー" 1128 | ] 1129 | }, 1130 | { 1131 | "cell_type": "code", 1132 | "execution_count": null, 1133 | "metadata": { 1134 | "colab": { 1135 | "base_uri": "https://localhost:8080/", 1136 | "height": 1000 1137 | }, 1138 | "id": "8lkcYwWVQmiz", 1139 | "outputId": "3c3bb898-f533-4105-f32b-46901d75f7fc" 1140 | }, 1141 | "outputs": [], 1142 | "source": [ 1143 | "from IPython.display import Image\n", 1144 | "\n", 1145 | "# 現在のboardをpngファイルとして書き出す\n", 1146 | "board.save_png(\"kbd_python\", subdir=\"pcb_png\")\n", 1147 | "\n", 1148 | "# 表示\n", 1149 | "Image(\"pcb_png/kbd_python_preview_all.png\")\n", 1150 | "# Image(\"pcb_png/kbd_python_preview_top.png\")\n", 1151 | "# Image(\"pcb_png/kbd_python_preview_bot.png\")" 1152 | ] 1153 | }, 1154 | { 1155 | "cell_type": "markdown", 1156 | "metadata": { 1157 | "id": "dvHvS6UHRLil" 1158 | }, 1159 | "source": [ 1160 | "# 基板の完成とガーバーファイルの出力\n", 1161 | "最後に製造用ガーバーファイルを出力して完成です。" 1162 | ] 1163 | }, 1164 | { 1165 | "cell_type": "code", 1166 | "execution_count": null, 1167 | "metadata": { 1168 | "colab": { 1169 | "base_uri": "https://localhost:8080/" 1170 | }, 1171 | "id": "KIt69DLaRIgA", 1172 | "outputId": "6d7c135a-49d8-4036-d2b3-84f697309b49" 1173 | }, 1174 | "outputs": [], 1175 | "source": [ 1176 | "board.save_gerbers(\"kbd_python\", subdir=\"pcb_gerber\")" 1177 | ] 1178 | }, 1179 | { 1180 | "attachments": {}, 1181 | "cell_type": "markdown", 1182 | "metadata": { 1183 | "id": "TjFdKPM9Sont" 1184 | }, 1185 | "source": [ 1186 | "ガーバーファイルの確認にはkicadのガーバービュアーを使用しました。\n", 1187 | "\n", 1188 | "![kicadのガーバービュアー](../imgs/kicad_gerberviewer.png)\n", 1189 | "\n", 1190 | "また、kicadのガーバービュアーからpcbnewのデータへ変換できます。発注時には一度pcbnewへ読み込んでシルクのデータを整えたガーバーファイルを再度出力しました。\n", 1191 | "\n", 1192 | "![pcbnewへ変換した基板](../imgs/kicad_gerber_to_pcbnew.png)" 1193 | ] 1194 | }, 1195 | { 1196 | "attachments": {}, 1197 | "cell_type": "markdown", 1198 | "metadata": {}, 1199 | "source": [ 1200 | "# 基板を発注する\n", 1201 | "\n", 1202 | "![届いた基板](../imgs/pcb.jpg)\n", 1203 | "\n", 1204 | "ガーバーファイルをまとめてJLCPCBに発注しました。5枚で$8程度、2週間弱で到着しました。\n", 1205 | "\n", 1206 | "# 部品のはんだづけ\n", 1207 | "\n", 1208 | "![はんだづけした基板](../imgs/soldered_pcb_top.jpg)\n", 1209 | "![はんだづけした基板](../imgs/soldered_pcb_bottom.jpg)\n", 1210 | "\n", 1211 | "用意しておいた部品をはんだづけしました。これで基板の設計工程は完成です。" 1212 | ] 1213 | } 1214 | ], 1215 | "metadata": { 1216 | "colab": { 1217 | "include_colab_link": true, 1218 | "provenance": [] 1219 | }, 1220 | "kernelspec": { 1221 | "display_name": ".venv", 1222 | "language": "python", 1223 | "name": "python3" 1224 | }, 1225 | "language_info": { 1226 | "codemirror_mode": { 1227 | "name": "ipython", 1228 | "version": 3 1229 | }, 1230 | "file_extension": ".py", 1231 | "mimetype": "text/x-python", 1232 | "name": "python", 1233 | "nbconvert_exporter": "python", 1234 | "pygments_lexer": "ipython3", 1235 | "version": "3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]" 1236 | }, 1237 | "vscode": { 1238 | "interpreter": { 1239 | "hash": "b68c839e49670d3aa77ea8bdd6f541fdcce81585b85d6e1d97674ad5f32db92f" 1240 | } 1241 | } 1242 | }, 1243 | "nbformat": 4, 1244 | "nbformat_minor": 0 1245 | } 1246 | -------------------------------------------------------------------------------- /notebook/jp/what_is_colaboratory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "colab_type": "text", 7 | "id": "view-in-github" 8 | }, 9 | "source": [ 10 | "\"Open" 11 | ] 12 | }, 13 | { 14 | "attachments": {}, 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "> これは[キーボード #1 Advent Calendar 2022](https://adventar.org/calendars/7529)の20日目の記事の一部です。 \n", 19 | "> トップページは[Pythonだけでキーボードを作る](https://5z6p.com/2022/12/21/ac2022/)です。" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": { 25 | "id": "iBVgqeBuA9d9" 26 | }, 27 | "source": [ 28 | "# Google Colaboratory とは?\n", 29 | "\n", 30 | "Google Colaboratory は Google が提供する Python の実行環境です。\n", 31 | "\n", 32 | "自分のコンピュータに環境を用意せずに、Python を動かしたりマークダウンによるドキュメントの記述が出来ます。\n", 33 | "\n", 34 | "Google Colaboratory では、ファイル全体のことを`ノートブック`、コードやドキュメントの一塊のことを`セル`と呼び、`セル`の種類がコードなら`▷`アイコンをクリックすると実行されます。\n", 35 | "\n", 36 | "実行すると結果がセルの下に表示されます。\n" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": { 43 | "id": "I952Ee82A9eA" 44 | }, 45 | "outputs": [], 46 | "source": [ 47 | "1 + 2\n" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": { 53 | "id": "kFJTKn2vA9eB" 54 | }, 55 | "source": [ 56 | "Python インタプリタのように終了するまで状態を保持するため、セル間で import したライブラリや変数を引き継いでいます。そのため、実行順序に縛りがあるかもしれません。大抵の場合は上から順番に実行していきます。\n" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": { 63 | "id": "zQ-WBWT1A9eC" 64 | }, 65 | "outputs": [], 66 | "source": [ 67 | "num = 10\n" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": { 74 | "id": "1tpeXhRsA9eC" 75 | }, 76 | "outputs": [], 77 | "source": [ 78 | "# printしても変数を直接書いても表示される\n", 79 | "print(num)\n", 80 | "num\n" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": { 86 | "id": "KBtF5rEoA9eC" 87 | }, 88 | "source": [ 89 | "Python が動いている仮想環境の bash にアクセスすることも出来ます。 \n", 90 | "これを使って pip 経由で用意されていてないライブラリをインストールしたり、python そのもののバージョンを変更したり、github からプロジェクトをクローンします。\n", 91 | "\n", 92 | "Colaboratory には前もってよく使われるライブラリがプリインストールされています。それも確認してみましょう。\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "id": "prpb-nLaA9eC" 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "# !を文頭につけるとbashへのコマンドになります\n", 104 | "!python --version\n", 105 | "!pip list" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": { 111 | "id": "OtUUjHcNA9eD" 112 | }, 113 | "source": [ 114 | "# 実際に使ってみる\n", 115 | "\n", 116 | "早速、python で簡単な画像を生成して保存するスクリプトを実行してみましょう。\n", 117 | "\n", 118 | "画像処理ライブラリの pillow をインポートします。\n" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": { 125 | "id": "I0LWySJvA9eD" 126 | }, 127 | "outputs": [], 128 | "source": [ 129 | "from PIL import Image, ImageDraw\n" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "metadata": { 136 | "id": "t9SUodyRA9eE" 137 | }, 138 | "outputs": [], 139 | "source": [ 140 | "# 四角が書かれた画像ファイルを生成する\n", 141 | "size = (300, 200)\n", 142 | "img = Image.new(\"RGB\", size, (255, 255, 255))\n", 143 | "draw = ImageDraw.Draw(img)\n", 144 | "draw.rectangle((20, 20, 150, 150), fill=(255, 0, 0))\n", 145 | "\n", 146 | "# 変数の名前だけを入力すると画像をセルの実行結果として表示できる\n", 147 | "img" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": { 154 | "id": "J22ChRcCA9eE" 155 | }, 156 | "outputs": [], 157 | "source": [ 158 | "# 丸を追加する\n", 159 | "draw.ellipse((50, 50, 200, 200), fill=(0, 255, 0))\n", 160 | "img" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "metadata": { 167 | "id": "JrTzWf-2A9eE" 168 | }, 169 | "outputs": [], 170 | "source": [ 171 | "# 画像を保存する\n", 172 | "img.save(\"test.png\")\n" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": { 178 | "id": "OBHr1gy1A9eE" 179 | }, 180 | "source": [ 181 | "ファイルを保存した場合、一度ノートブックを閉じてしまうと環境がリセットされるため、消えてしまうかもしれません。あらかじめダウンロードしておきましょう。\n", 182 | "\n", 183 | "保存した一時ファイルは左側のタブにあるファイルブラウザから見えます。\n" 184 | ] 185 | } 186 | ], 187 | "metadata": { 188 | "colab": { 189 | "include_colab_link": true, 190 | "provenance": [] 191 | }, 192 | "kernelspec": { 193 | "display_name": ".venv", 194 | "language": "python", 195 | "name": "python3" 196 | }, 197 | "language_info": { 198 | "codemirror_mode": { 199 | "name": "ipython", 200 | "version": 3 201 | }, 202 | "file_extension": ".py", 203 | "mimetype": "text/x-python", 204 | "name": "python", 205 | "nbconvert_exporter": "python", 206 | "pygments_lexer": "ipython3", 207 | "version": "3.8.10 (default, Jun 22 2022, 20:18:18) \n[GCC 9.4.0]" 208 | }, 209 | "orig_nbformat": 4, 210 | "vscode": { 211 | "interpreter": { 212 | "hash": "b68c839e49670d3aa77ea8bdd6f541fdcce81585b85d6e1d97674ad5f32db92f" 213 | } 214 | } 215 | }, 216 | "nbformat": 4, 217 | "nbformat_minor": 0 218 | } 219 | --------------------------------------------------------------------------------