├── code ├── cron │ ├── wc-send-log │ └── wc-get-update ├── aws_credentials ├── website │ ├── favicon.ico │ ├── README.md │ ├── body.html │ └── script.js ├── wordclock │ ├── username.py │ ├── __init__.py │ ├── calibrate.py │ ├── send_log.py │ ├── configdefs.py │ ├── twords.py │ ├── tsense.py │ ├── tneo.py │ ├── magnetometer.py │ └── get_update.py ├── wc.service ├── setup.py ├── authorized_keys ├── README.md ├── dnsmasq.conf ├── config │ ├── config_dm.py │ ├── config_ts.py │ ├── config_mb.py │ ├── config_ea.py │ ├── config_dp.py │ └── config_ht.py └── makefile ├── docs ├── manual.docx ├── CartonLabels.pdf ├── ManualLabel.pdf ├── CartonLabels.avery ├── ManualLabel.avery ├── README.md └── steps.md ├── carbide3d ├── Bracket.c2d ├── BackPanel.c2d ├── CenterJig.c2d ├── CornerJig.c2d ├── BandBottom.c2d ├── CornerJigPreMerge.c2d ├── LightSensorWell.c2d ├── BracketBottomRight.nc ├── BracketTop.nc ├── README.md ├── LightSensorWell.nc ├── BrackBottomLeft.nc └── CenterJig.nc ├── face ├── ClockFaceAE.ai ├── ClockFaceAE.jpg ├── ClockFaceDM.ai ├── ClockFaceDM.jpg ├── ClockFaceDP.ai ├── ClockFaceDP.jpg ├── ClockFaceHT.ai ├── ClockFaceHT.jpg ├── ClockFaceMB.ai ├── ClockFaceTS.ai ├── ClockFaceTS.jpg ├── gridDM.txt ├── gridDP.txt ├── gridEA.txt ├── gridHT.txt ├── gridMB.txt ├── gridTS.txt ├── README.md ├── checkgrid └── genwords ├── pcbs ├── top │ ├── fp-lib-table │ ├── top.pro │ ├── top.kicad_pcb-bak │ └── top.kicad_pcb ├── bottom │ ├── fp-lib-table │ └── bottom.pro ├── backlight │ ├── fp-lib-table │ ├── backlight.pro │ └── backlight.kicad_pcb ├── README.md └── neopixels.pretty │ └── strip.kicad_mod ├── .gitignore ├── pi-pinouts.md ├── aws-s3-policy-example.json ├── LICENSE └── README.md /code/cron/wc-send-log: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | send-log 3 | -------------------------------------------------------------------------------- /code/cron/wc-get-update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | get-update 3 | -------------------------------------------------------------------------------- /code/aws_credentials: -------------------------------------------------------------------------------- 1 | [default] 2 | aws_access_key_id = 3 | aws_secret_access_key = 4 | -------------------------------------------------------------------------------- /docs/manual.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/docs/manual.docx -------------------------------------------------------------------------------- /carbide3d/Bracket.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/Bracket.c2d -------------------------------------------------------------------------------- /docs/CartonLabels.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/docs/CartonLabels.pdf -------------------------------------------------------------------------------- /docs/ManualLabel.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/docs/ManualLabel.pdf -------------------------------------------------------------------------------- /face/ClockFaceAE.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceAE.ai -------------------------------------------------------------------------------- /face/ClockFaceAE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceAE.jpg -------------------------------------------------------------------------------- /face/ClockFaceDM.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceDM.ai -------------------------------------------------------------------------------- /face/ClockFaceDM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceDM.jpg -------------------------------------------------------------------------------- /face/ClockFaceDP.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceDP.ai -------------------------------------------------------------------------------- /face/ClockFaceDP.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceDP.jpg -------------------------------------------------------------------------------- /face/ClockFaceHT.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceHT.ai -------------------------------------------------------------------------------- /face/ClockFaceHT.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceHT.jpg -------------------------------------------------------------------------------- /face/ClockFaceMB.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceMB.ai -------------------------------------------------------------------------------- /face/ClockFaceTS.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceTS.ai -------------------------------------------------------------------------------- /face/ClockFaceTS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/face/ClockFaceTS.jpg -------------------------------------------------------------------------------- /carbide3d/BackPanel.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/BackPanel.c2d -------------------------------------------------------------------------------- /carbide3d/CenterJig.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/CenterJig.c2d -------------------------------------------------------------------------------- /carbide3d/CornerJig.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/CornerJig.c2d -------------------------------------------------------------------------------- /docs/CartonLabels.avery: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/docs/CartonLabels.avery -------------------------------------------------------------------------------- /docs/ManualLabel.avery: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/docs/ManualLabel.avery -------------------------------------------------------------------------------- /carbide3d/BandBottom.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/BandBottom.c2d -------------------------------------------------------------------------------- /code/website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/code/website/favicon.ico -------------------------------------------------------------------------------- /carbide3d/CornerJigPreMerge.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/CornerJigPreMerge.c2d -------------------------------------------------------------------------------- /carbide3d/LightSensorWell.c2d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marksidell/wordclock/HEAD/carbide3d/LightSensorWell.c2d -------------------------------------------------------------------------------- /pcbs/top/fp-lib-table: -------------------------------------------------------------------------------- 1 | (fp_lib_table 2 | (lib (name neopixels)(type KiCad)(uri ${KIPRJMOD}/../neopixels.pretty)(options "")(descr "")) 3 | ) 4 | -------------------------------------------------------------------------------- /pcbs/bottom/fp-lib-table: -------------------------------------------------------------------------------- 1 | (fp_lib_table 2 | (lib (name neopixels)(type KiCad)(uri ${KIPRJMOD}/../neopixels.pretty)(options "")(descr "")) 3 | ) 4 | -------------------------------------------------------------------------------- /pcbs/backlight/fp-lib-table: -------------------------------------------------------------------------------- 1 | (fp_lib_table 2 | (lib (name neopixels)(type KiCad)(uri ${KIPRJMOD}/../neopixels.pretty)(options "")(descr "")) 3 | ) 4 | -------------------------------------------------------------------------------- /face/gridDM.txt: -------------------------------------------------------------------------------- 1 | ITWISTHALFOR 2 | MASHAPPYETEN 3 | TWENTYFIVEAR 4 | QUARTEREPAST 5 | ORBIRTHDAYTO 6 | EONETWOTHREE 7 | LFOURFIVESIX 8 | EIGHTDOUGOAL 9 | VMARGUERITEN 10 | ESEVENTWELVE 11 | NINEOKOCLOCK 12 | SMTWTFS01234 13 | -------------------------------------------------------------------------------- /face/gridDP.txt: -------------------------------------------------------------------------------- 1 | ITWISTHALFOR 2 | MASHAPPYETEN 3 | TWENTYFIVEAR 4 | QUARTEREPAST 5 | ORBIRTHDAYTO 6 | EONETWOTHREE 7 | LFOURFIVESIX 8 | EIGHTDOUGTEN 9 | VPATYLUCASAT 10 | ESEVENTWELVE 11 | NINEOKOCLOCK 12 | SMTWTFS01234 13 | -------------------------------------------------------------------------------- /face/gridEA.txt: -------------------------------------------------------------------------------- 1 | ITWISTHALFOR 2 | MASHAPPYETEN 3 | TWENTYFIVEAR 4 | QUARTEREPAST 5 | ORBIRTHDAYTO 6 | EFIVEFOURSIX 7 | LAUGGIELLEON 8 | ELELIAHEIGHT 9 | VETWONETHREE 10 | ESEVENTWELVE 11 | NINEOKOCLOCK 12 | SMTWTFS01234 13 | -------------------------------------------------------------------------------- /face/gridHT.txt: -------------------------------------------------------------------------------- 1 | ITWISTHALFOR 2 | MASHAPPYETEN 3 | TWENTYFIVEAR 4 | QUARTEREPAST 5 | ORBIRTHDAYTO 6 | EONETWOTHREE 7 | LFOURFIVESIX 8 | EIGHTHANSOAR 9 | VETOMOMITTEN 10 | ESEVENTWELVE 11 | NINEOKOCLOCK 12 | SMTWTFS01234 13 | -------------------------------------------------------------------------------- /face/gridMB.txt: -------------------------------------------------------------------------------- 1 | ITWISTHALFOR 2 | MASHAPPYETEN 3 | TWENTYFIVEAR 4 | QUARTEREPAST 5 | ORBIRTHDAYTO 6 | WONETWOTHREE 7 | SIXIFIVEFOUR 8 | ABUZZCAPNTEN 9 | SEVENOTWELVE 10 | ELEVENDEIGHT 11 | NINEOKOCLOCK 12 | SMTWTFS01234 13 | -------------------------------------------------------------------------------- /face/gridTS.txt: -------------------------------------------------------------------------------- 1 | ITWISTHALFOR 2 | MASHAPPYETEN 3 | TWENTYFIVEAR 4 | QUARTEREPAST 5 | ORBIRTHDAYTO 6 | WONETWOTHREE 7 | SIXIFIVEFOUR 8 | ATOMSANDYTEN 9 | SEVENOTWELVE 10 | ELEVENDEIGHT 11 | NINEOKOCLOCK 12 | SMTWTFS01234 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | code/wordclock.egg-info 3 | code/wordclock/__pycache__ 4 | code/wordclock/config.py 5 | code/wordclock/timestamp.txt 6 | code/wordclock/s3config.py 7 | fp-info-cache 8 | ~syncthing* 9 | docs/~$manual.docx 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /code/wordclock/username.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Get our AWS username 3 | ''' 4 | 5 | import boto3 6 | 7 | 8 | def get_username(): 9 | ''' Return our AWS username 10 | ''' 11 | return boto3.client('sts').get_caller_identity()['Arn'].split('/')[1] 12 | -------------------------------------------------------------------------------- /code/wc.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WordClock Service 3 | After=multi-user.target 4 | 5 | [Service] 6 | Type=idle 7 | ExecStart=/usr/local/bin/wc --daemon 8 | WorkingDirectory=/var/wordclock 9 | Restart=always 10 | RestartSec=10 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /code/wordclock/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The wordclock package 3 | ''' 4 | 5 | __version__ = '2' 6 | __author__ = 'Mark Sidell' 7 | __author_email__ = 'mark@sidell.org' 8 | __url__ = 'https://github.com/marksidell/wordclock' 9 | __license__ = '(c) 2021 Mark Sidell' 10 | __description__ = 'The Word Clock' 11 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Printed Material 2 | 3 | ### Files 4 | 5 | | File | Description | 6 | |------|-------------| 7 | | steps.md | How to build the clock. | 8 | | manual.docx | The user manual. | 9 | | CartonLabels.* | Design and PDF files for the carton lables. | 10 | | ManualLabels.* | Design and PDF files for the manual envelope label. | 11 | 12 | 13 | -------------------------------------------------------------------------------- /code/website/README.md: -------------------------------------------------------------------------------- 1 | ## Web Site Files 2 | 3 | This directory contains source files for the clock's web site. 4 | 5 | | File | Description | 6 | |------|-------------| 7 | | `body.html` | The web page HTML file. | 8 | | `favicon.ico` | The browser favorite icon. Downloaded from [icons8.com](https://icons8.com/icons/set/clock). | 9 | | `script.js` | The web page JavaScript code. | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /face/README.md: -------------------------------------------------------------------------------- 1 | ## The Clock Face Designs 2 | 3 | To date, I've built three clocks, each with custom birthday greetings. 4 | 5 | | File | Description | 6 | |------|-------------| 7 | | `*.ai` | The Adobe Illustrator design files. | 8 | | `*.jpg` | JPG images generated from the designs. | 9 | | `*.txt` | The character grids from which the designs are constructed. | 10 | | `checkgrid` | A Python program to check a character grid for column rule violations. | 11 | 12 | To run the `checkgrid` program, type: 13 | 14 | ``` 15 | python checkgrid 16 | ``` 17 | -------------------------------------------------------------------------------- /code/wordclock/calibrate.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Calibrate the compass 3 | ''' 4 | 5 | import time 6 | import board 7 | import busio 8 | from wordclock import magnetometer 9 | 10 | 11 | def main(): 12 | ''' do it 13 | ''' 14 | i2c = busio.I2C(board.SCL, board.SDA) 15 | compass = magnetometer.Magnetometer(i2c, verbose=True) 16 | 17 | try: 18 | while True: 19 | result = compass.update(do_calibration=True, verbose=True) 20 | print(result.orientation, int(result.angle)) 21 | time.sleep(1) 22 | 23 | except KeyboardInterrupt: 24 | print('Done!') 25 | -------------------------------------------------------------------------------- /pcbs/README.md: -------------------------------------------------------------------------------- 1 | ## The Printed Circuit Board Designs 2 | 3 | This directory contains the KiCad PCB designs, each in its own subdirectory. 4 | 5 | | File | Description | 6 | |------|-------------| 7 | | `backlight` | The small PCB used to connect power to the backlight neopixel strip. | 8 | | `bottom` | The "main" PCB that is attached to the bottom of the back panel. It connects power to and routes data between the face neopixels. | 9 | | `top` | The PCB that is attached to the top of the back panel. It routes data between the face neopixels. | 10 | | `neopixels.pretty` | A library containg the footprint for the neopixel solder pads used in the PCB designs. | 11 | -------------------------------------------------------------------------------- /pi-pinouts.md: -------------------------------------------------------------------------------- 1 | ## Raspberry Pi Pinouts 2 | 3 | Sorry, this file is what passes for a schematic. 4 | 5 | ### I2C connections to the sensors (via a QWIIC cable) 6 | 7 | | Pin | Description | 8 | |-----|-------------| 9 | | 1 | 3.3V power (red) | 10 | | 3 | SDA (blue) | 11 | | 5 | SCL (yellow) | 12 | | 9 | GND (black) | 13 | 14 | ### Logic level converter connections (for driving the neopixels) 15 | 16 | | Pin | Description | 17 | |-----|-------------| 18 | | 2 | 5V HV | 19 | | 17 | 3.3V LV | 20 | | 12 | LV D-out (GPIO 18) | 21 | 22 | ### Mode control button 23 | 24 | | Pin | Description | 25 | |-----|-------------| 26 | | 14 | GND | 27 | | 16 | D-in (GPIO 23) | 28 | 29 | ### Power 30 | 31 | | Pin | Description | 32 | |-----|-------------| 33 | | 4 | 5V | 34 | | 6 | GND 35 | -------------------------------------------------------------------------------- /code/wordclock/send_log.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Upload the wc systemd service log to S3. 3 | ''' 4 | 5 | import subprocess 6 | import datetime 7 | import boto3 8 | from wordclock.s3config import S3_BUCKET 9 | from wordclock.username import get_username 10 | 11 | 12 | def main(): 13 | ''' do it 14 | ''' 15 | try: 16 | result = subprocess.check_output( 17 | ['journalctl', '-u', 'wc', '-n', '100'], stderr=subprocess.STDOUT).decode() 18 | 19 | except subprocess.CalledProcessError as err: #pylint: disable=broad-except 20 | result = 'Exception: {}\n{}'.format(err.returncode, err.output.decode()) 21 | 22 | boto3.client('s3').put_object( 23 | Bucket=S3_BUCKET, 24 | Key='logs/{}/log-{}.txt'.format(get_username(), datetime.datetime.utcnow().isoformat()), 25 | Body=result.encode()) 26 | -------------------------------------------------------------------------------- /code/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import wordclock 3 | 4 | setup( 5 | name='wordclock', 6 | version=wordclock.__version__, 7 | author=wordclock.__author__, 8 | author_email=wordclock.__author_email__, 9 | url=wordclock.__url__, 10 | license=wordclock.__license__, 11 | description=wordclock.__description__, 12 | 13 | packages=['wordclock'], 14 | entry_points={ 15 | "console_scripts": [ 16 | "calibrate=wordclock.calibrate:main", 17 | "tneo=wordclock.tneo:main", 18 | "tsense=wordclock.tsense:main", 19 | "twords=wordclock.twords:main", 20 | "wc=wordclock.wc:main", 21 | "send-log=wordclock.send_log:main", 22 | "get-update=wordclock.get_update:main", 23 | ] 24 | }, 25 | install_requires=[] 26 | ) 27 | -------------------------------------------------------------------------------- /aws-s3-policy-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version":"2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "AllowListingOfClockUpdateFolder", 6 | "Action": ["s3:ListBucket"], 7 | "Effect": "Allow", 8 | "Resource": ["arn:aws:s3:::"], 9 | "Condition":{"StringLike":{"s3:prefix": 10 | [ 11 | "update/${aws:username}/*", 12 | "update/${aws:username}" 13 | ] 14 | } 15 | } 16 | }, 17 | { 18 | "Sid": "AllowUpdateRetrieval", 19 | "Action":["s3:GetObject"], 20 | "Effect":"Allow", 21 | "Resource": ["arn:aws:s3:::/update/${aws:username}/*"] 22 | }, 23 | { 24 | "Sid": "AllowLogWrites", 25 | "Action":["s3:PutObject"], 26 | "Effect":"Allow", 27 | "Resource": ["arn:aws:s3:::/logs/${aws:username}/*"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /face/checkgrid: -------------------------------------------------------------------------------- 1 | ''' 2 | Read a text file consisting of a letter grid and print 3 | all cases where a sequence of two rows have the same 4 | letter in any columns. 5 | ''' 6 | import sys 7 | 8 | def main(): 9 | ''' do it 10 | ''' 11 | grid = [line.strip() for line in open(sys.argv[1], 'r')] 12 | dim = len(grid) 13 | 14 | if any(len(row) != dim for row in grid): 15 | print('ERROR: The grid is not square') 16 | sys.exit(1) 17 | 18 | for row in range(dim-1): 19 | row_a = grid[row] 20 | row_b = grid[row+1] 21 | collisions = [row_a[col] == row_b[col] for col in range(dim)] 22 | 23 | if any(collisions): 24 | print(' ' + ''.join('|' if collision else ' ' for collision in collisions)) 25 | print('{:>2} {}'.format(row, row_a)) 26 | print('{:>2} {}'.format(row+1, row_b)) 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /code/authorized_keys: -------------------------------------------------------------------------------- 1 | # Public keys for SSH login to the pi account. 2 | # You'll want to replace these with your own keys. 3 | 4 | # me 5 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGVdclnPyA3n1FaXwH9V/ju+bZ/Phspod7HpW/y9Z7kbHGelrsbKLXuVho63iVX0E6OmYnmverU3AWag+qRL0gOJACQLela3U/3JlOH636VYehVlhRcnev1auGCZ4jvFiNfL2e5ZBWYbmGD3Z53gFg2C9yrzg+Eedpx5SjjPoa00pEQ/B4c79KoFYqHoUzOJrM8eGeLABdHFelZkndnDkTLSkmaBfwkFOqScxPWAf/fGSS0fLC3ndrKFlVBPpAiEDROrvAm1TXuvMh58lxcfq24SSw9rAtZ5XuAjirqKUWCo4Tv+HimQEUJETG5lTDIybzhtKJ5njMZhr/RBWcorxP 6 | 7 | # A key I generated just for logging into the word clock, which I can share with the clock owners. 8 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUWJBjzuW6GbpS+Dq54uSLJdAghNIWH/GsaNL8hoIrfYg/lrdmy+bMz5aiIwNTaYR1E9oYKXEOG3RzETTe3izdD1wFkvD22G/z888WNk/DlaYRsy/pMuBH++z/hFMpgoROSxrkfNOCfwfOW2zRRjGwZ1HIqvFPBl5Jx7Zlcsdts5qbw6m6cyfPHGASqrpW+b0piti1raa66AJzVVVB1kAjKyTuTvCEWTwCDjcHW6+NXkHoXFgO94hRa94qRYY/8VInVZjwBdlmaHp+i28RgWRY4AIlfOtRpV9pwgPE5OyxncnwxSAOFbksQDPbe2YfOGOi6GRowbsXwQ3qJD95/L1Z 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark Sidell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pcbs/neopixels.pretty/strip.kicad_mod: -------------------------------------------------------------------------------- 1 | (module strip (layer F.Cu) (tedit 6059F17A) 2 | (attr smd) 3 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 4 | (effects (font (size 1 1) (thickness 0.15))) 5 | ) 6 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 7 | (effects (font (size 1 1) (thickness 0.15))) 8 | ) 9 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 10 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 11 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 12 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 13 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 14 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 15 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 16 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 17 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 18 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 19 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 20 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 21 | ) 22 | -------------------------------------------------------------------------------- /carbide3d/BracketBottomRight.nc: -------------------------------------------------------------------------------- 1 | (Design File: C..Users.msidell.VIASAT.s.carbide.WordClockBracket.c2d) 2 | (stockMin:0.00mm, 0.00mm, -13.50mm) 3 | (stockMax:419.10mm, 50.80mm, 0.00mm) 4 | (STOCK/BLOCK,419.10, 50.80, 13.50,0.00, 0.00, 13.50) 5 | G90 6 | G21 7 | (Move to safe Z to avoid workholding) 8 | G53G0Z-5.000 9 | (socket.-.Pocket) 10 | M05 11 | (TOOL/MILL,3.17, 0.00, 0.00, 0.00) 12 | M6T102 13 | M03S10000 14 | (PREPOSITION FOR RAPID PLUNGE) 15 | G0X-0.03Y24.91 16 | Z12.70 17 | G1Z-1.27F203.2 18 | Y25.90F508.0 19 | X23.13 20 | Y24.91 21 | X-0.03 22 | X-1.61Y23.32 23 | Y27.48 24 | X24.71 25 | Y23.32 26 | X-1.61 27 | Z12.70 28 | G0X-0.03Y24.91 29 | Z-0.64 30 | G1Z-2.54F203.2 31 | Y25.90F508.0 32 | X23.13 33 | Y24.91 34 | X-0.03 35 | X-1.61Y23.32 36 | Y27.48 37 | X24.71 38 | Y23.32 39 | X-1.61 40 | Z12.70 41 | G0X-0.03Y24.91 42 | Z-1.91 43 | G1Z-3.81F203.2 44 | Y25.90F508.0 45 | X23.13 46 | Y24.91 47 | X-0.03 48 | X-1.61Y23.32 49 | Y27.48 50 | X24.71 51 | Y23.32 52 | X-1.61 53 | Z12.70 54 | G0X-0.03Y24.91 55 | Z-3.17 56 | G1Z-5.08F203.2 57 | Y25.90F508.0 58 | X23.13 59 | Y24.91 60 | X-0.03 61 | X-1.61Y23.32 62 | Y27.48 63 | X24.71 64 | Y23.32 65 | X-1.61 66 | Z12.70 67 | G0X-0.03Y24.91 68 | Z-4.44 69 | G1Z-6.35F203.2 70 | Y25.90F508.0 71 | X23.13 72 | Y24.91 73 | X-0.03 74 | X-1.61Y23.32 75 | Y27.48 76 | X24.71 77 | Y23.32 78 | X-1.61 79 | Z12.70 80 | M02 81 | -------------------------------------------------------------------------------- /carbide3d/BracketTop.nc: -------------------------------------------------------------------------------- 1 | (Design File: C:/Users/msidell.VIASAT/s/carbide/WordClockBracket.c2d) 2 | (stockMin:0.00mm, 0.00mm, -13.50mm) 3 | (stockMax:419.10mm, 50.80mm, 0.00mm) 4 | (STOCK/BLOCK,419.10, 50.80, 13.50,0.00, 0.00, 13.50) 5 | G90 6 | G21 7 | (Move to safe Z to avoid workholding) 8 | G53G0Z-5.000 9 | (slot) 10 | M05 11 | (TOOL/MILL,3.17, 0.00, 0.00, 0.00) 12 | M6T102 13 | M03S10000 14 | (PREPOSITION FOR RAPID PLUNGE) 15 | G0X109.59Y-0.00 16 | Z12.70 17 | G1Z-1.27F203.2 18 | Y50.80F508.0 19 | X110.41 20 | Y-0.00 21 | X109.59 22 | Z-2.54F203.2 23 | Y50.80F508.0 24 | X110.41 25 | Y-0.00 26 | X109.59 27 | Z-3.00F203.2 28 | Y50.80F508.0 29 | X110.41 30 | Y-0.00 31 | X109.59 32 | Z12.70 33 | (socket) 34 | M03S10000 35 | G0X-0.03Y24.91 36 | G1Z-1.27F203.2 37 | Y25.90F508.0 38 | X23.13 39 | Y24.91 40 | X-0.03 41 | X-1.61Y23.32 42 | Y27.48 43 | X24.71 44 | Y23.32 45 | X-1.61 46 | Z12.70 47 | G0X-0.03Y24.91 48 | G1Z-2.54F203.2 49 | Y25.90F508.0 50 | X23.13 51 | Y24.91 52 | X-0.03 53 | X-1.61Y23.32 54 | Y27.48 55 | X24.71 56 | Y23.32 57 | X-1.61 58 | Z12.70 59 | G0X-0.03Y24.91 60 | G1Z-3.81F203.2 61 | Y25.90F508.0 62 | X23.13 63 | Y24.91 64 | X-0.03 65 | X-1.61Y23.32 66 | Y27.48 67 | X24.71 68 | Y23.32 69 | X-1.61 70 | Z12.70 71 | G0X-0.03Y24.91 72 | G1Z-5.08F203.2 73 | Y25.90F508.0 74 | X23.13 75 | Y24.91 76 | X-0.03 77 | X-1.61Y23.32 78 | Y27.48 79 | X24.71 80 | Y23.32 81 | X-1.61 82 | Z12.70 83 | G0X-0.03Y24.91 84 | G1Z-6.35F203.2 85 | Y25.90F508.0 86 | X23.13 87 | Y24.91 88 | X-0.03 89 | X-1.61Y23.32 90 | Y27.48 91 | X24.71 92 | Y23.32 93 | X-1.61 94 | Z12.70 95 | M02 96 | -------------------------------------------------------------------------------- /carbide3d/README.md: -------------------------------------------------------------------------------- 1 | ## The Carbide 3D Designs 2 | 3 | This directory contains all of the word clock Carbide 3D milling 4 | designs (`*.c2d`) and GCode generated from the designs (`*.nc` files). 5 | 6 | Except as noted: 7 | 8 | - There is a .c2d and .nc file for each design. 9 | - The origin for all designs is the lower-left corner of the piece. 10 | 11 | | File | Description | 12 | |------|-------------| 13 | | `BackPanel`| The MDF back panel. | 14 | | `BandBottom` | The bottom acrylic band. The design assumes the band is placed in a jig for milling, with the jig origin 24 mm to the left of and below the band lower left corner. The offset is the height of the band, allowing another piece of pre-cut band acrylic to be used to construct the jig. | 15 | | `Bracket.c2d` | Both the top and bottom brackets in one design. The design is for half of the bracket, and assumes the bracket is milled in two operations, one for each side. | 16 | | `BrackTop.nc` | The gcode for the top bracket. (The top bracket is symmetric.) | 17 | | `BrackBottomLeft.nc` | The gcode for the left side of the bottom bracket. | 18 | | `BrackTopRight.nc` | The gcode for the right side of the bottom bracket. | 19 | | `CenterJig` | The center jig is used to position the brackets. This design is for the notch on the bottom of the jig to accomodate the power cord that loops through the bottom bracket. | 20 | | `CornerJig` | The jig for milling the rounded corners on the back panel. | 21 | | `CornerJigPreMerge.c2d` | The corner jig before the elements that form the curve have been joined. Handy if you want to tweak the design. | 22 | | `LightSensorWell` | The well cut into the clock face for the light sensor. | 23 | -------------------------------------------------------------------------------- /carbide3d/LightSensorWell.nc: -------------------------------------------------------------------------------- 1 | (Design File: C..Users.msidell.VIASAT.s.wordclock.carbide3d.LightSensorWell.c2d) 2 | (stockMin:0.00mm, 0.00mm, -3.20mm) 3 | (stockMax:250.00mm, 40.00mm, 0.00mm) 4 | (STOCK/BLOCK,250.00, 40.00, 3.20,0.00, 0.00, 3.20) 5 | G90 6 | G21 7 | (Move to safe Z to avoid workholding) 8 | G53G0Z-5.000 9 | (well.-.Pocket) 10 | M05 11 | (TOOL/MILL,2.00, 0.00, 0.00, 0.00) 12 | M6T282 13 | M03S10000 14 | (PREPOSITION FOR RAPID PLUNGE) 15 | G0X226.19Y21.20 16 | Z12.70 17 | G1Z-0.51F304.8 18 | Y21.85F762.0 19 | X231.00 20 | Y21.20 21 | X226.19 22 | X225.20Y20.20 23 | Y22.85 24 | X232.00 25 | Y20.20 26 | X225.20 27 | X224.20Y19.20 28 | Y23.85 29 | X233.00 30 | Y19.20 31 | X224.20 32 | X223.20Y18.20 33 | Y24.85 34 | X234.00 35 | Y18.20 36 | X223.20 37 | X222.20Y17.20 38 | Y25.85 39 | X235.00 40 | Y17.20 41 | X222.20 42 | Z12.70 43 | G0X226.19Y21.20 44 | Z-0.25 45 | G1Z-1.02F304.8 46 | Y21.85F762.0 47 | X231.00 48 | Y21.20 49 | X226.19 50 | X225.20Y20.20 51 | Y22.85 52 | X232.00 53 | Y20.20 54 | X225.20 55 | X224.20Y19.20 56 | Y23.85 57 | X233.00 58 | Y19.20 59 | X224.20 60 | X223.20Y18.20 61 | Y24.85 62 | X234.00 63 | Y18.20 64 | X223.20 65 | X222.20Y17.20 66 | Y25.85 67 | X235.00 68 | Y17.20 69 | X222.20 70 | Z12.70 71 | G0X226.19Y21.20 72 | Z-0.74 73 | G1Z-1.50F304.8 74 | Y21.85F762.0 75 | X231.00 76 | Y21.20 77 | X226.19 78 | X225.20Y20.20 79 | Y22.85 80 | X232.00 81 | Y20.20 82 | X225.20 83 | X224.20Y19.20 84 | Y23.85 85 | X233.00 86 | Y19.20 87 | X224.20 88 | X223.20Y18.20 89 | Y24.85 90 | X234.00 91 | Y18.20 92 | X223.20 93 | X222.20Y17.20 94 | Y25.85 95 | X235.00 96 | Y17.20 97 | X222.20 98 | Z12.70 99 | M02 100 | -------------------------------------------------------------------------------- /code/wordclock/configdefs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Namedtuples used in the config files 3 | ''' 4 | 5 | from collections import namedtuple 6 | 7 | ALL_WORDS = [] 8 | 9 | class Word: 10 | ''' Defines a word on the clock face 11 | ''' 12 | #pylint: disable=too-few-public-methods 13 | 14 | def __init__(self, text, y, x, vertical=False): 15 | ''' 16 | text: The word text, used to calculate its length 17 | y: # The starting row 18 | x: # The startin column 19 | vertical: # True if the word is vertical 20 | after: # A list of the words that appear after this word in the gripd 21 | poems: # A list of poems starting with this word 22 | ''' 23 | self.text = text 24 | self.y = y #pylint: disable=invalid-name 25 | self.x = x #pylint: disable=invalid-name 26 | self.vertical = vertical 27 | self.after = None 28 | self.poems = None 29 | 30 | ALL_WORDS.append(self) 31 | 32 | 33 | # Defines a birthday 34 | # 35 | BirthDate = namedtuple( 36 | 'BirthDate', 37 | [ 38 | 'month', # The month (1..12) 39 | 'day', # The month day (1..) 40 | ]) 41 | 42 | # Used to cause the clock to display the birthday message on a 43 | # special range of days. I used this when I ended up delivering the 44 | # clock late, and wanted my not to wait a year to see the greeting. 45 | # 46 | SpecialBirthday = namedtuple( 47 | 'SpecialBirthday', 48 | [ 49 | 'name', # The config variable for the name to display 50 | 'begin', # A datetime.Date() specifying the start day 51 | 'end', # A datetime.Date() specifying the day after the end day 52 | ]) 53 | -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | ## The Source Code 2 | 3 | ### Directories 4 | 5 | See the `README` file in each directory for more information. 6 | 7 | | Directory | Description | 8 | |------|-------------| 9 | | config | Software config files, used to customize each clock. You'll have to write a config file for your particular clock. | 10 | | cron | cron scripts for uploading logs and downloading updates. | 11 | | website | Source files for the clock's web page. | 12 | | wordclock | The word clock software, as a Python package. | 13 | 14 | ### Files 15 | 16 | | File | Description | 17 | |------|-------------| 18 | | authorized_keys | SSH public keys for logging into the Raspberry Pi. You'll have to change these, of course. | 19 | | dnsmasq.conf | A tweaked version of `/etc/dnsmasq.conf`, for configuring the Pi's DHCP service. | 20 | | makefile | For installing the software. | 21 | | setup.py | The Python package installation script. | 22 | | wc.service | The systemd service config file. | 23 | 24 | 25 | -------------------------------------------------------------------------------- /code/dnsmasq.conf: -------------------------------------------------------------------------------- 1 | dhcp-mac=set:client_is_a_pi,B8:27:EB:*:*:* 2 | dhcp-mac=set:client_is_a_pi,DC:A6:32:*:*:* 3 | dhcp-reply-delay=tag:client_is_a_pi,2 4 | 5 | # Never forward plain names (without a dot or domain part) 6 | domain-needed 7 | no-dhcp-interface=eth0,wlan0 8 | listen-address=10.0.0.1 9 | listen-address=127.0.0.1 10 | bind-interfaces 11 | 12 | #==========[ NAMESERVER ]==========# 13 | 14 | no-hosts # Do not read /etc/hosts 15 | # Read additional hosts-file (not only /etc/hosts) to add entries into DNS 16 | #addn-hosts=/etc/dnsmasq_static_hosts.conf 17 | expand-hosts 18 | domain=local 19 | local=/local/10.0.0.1 20 | 21 | # Host & PTR & Reverse 22 | host-record=wordclock.local,10.0.0.1 23 | server=/0.0.10.in-addr.arpa/10.0.0.1 24 | ptr-record=1.0.0.10.in-addr.arpa,workdclock.local 25 | 26 | #==========[ DHCP ]==========# 27 | 28 | dhcp-range=10.0.0.50,10.0.0.100.199,255.255.255.0,12h 29 | dhcp-option=option:router,10.0.0.1 30 | #dhcp-option=option:ntp-server,10.3.141.1 31 | 32 | dhcp-option=vendor:MSFT,2,1i # Tell MS Windows to release a lease on shutdown 33 | dhcp-option=44,10.0.0.1 # set netbios-over-TCP/IP aka WINS 34 | dhcp-option=45,10.0.0.1 # netbios datagram distribution server 35 | dhcp-option=46,8 # netbios node type 36 | dhcp-option=252,"\n" # REQUIRED to get win7 to behave 37 | dhcp-option=160,http://10.0.0.1/index.html # RFC 7710 38 | 39 | # Include another configuration options 40 | #conf-file=/etc/dnsmasq.captiveportal.conf 41 | 42 | # DHCPv4 Should be set when dnsmasq is definitely the only DHCP server on a network 43 | #dhcp-authoritative 44 | # DHCPv4 Server will attempt to ensure that an address is not in use before allocating it to a host 45 | #no-ping 46 | 47 | #==========[ etc ]==========# 48 | 49 | log-facility=/var/log/syslog 50 | #log-queries 51 | #log-dhcp 52 | log-async 53 | local-ttl=2 54 | local-service 55 | localise-queries 56 | -------------------------------------------------------------------------------- /code/wordclock/twords.py: -------------------------------------------------------------------------------- 1 | ''' Display all words 2 | ''' 3 | 4 | import time 5 | import argparse 6 | import board 7 | import neopixel 8 | 9 | from wordclock import config #pylint: disable=configdefs 10 | from wordclock import configdefs 11 | 12 | PIN_PIXELS = board.D18 13 | DIM = 12 14 | N_PIXELS = DIM*DIM + 24 * 4 15 | 16 | COLOR_OFF = (0, 0, 0) 17 | COLOR_ON = (255, 255, 255) 18 | 19 | PIXEL_MAP = [ 20 | [(DIM-1-x) * DIM + (DIM-1-y if x % 2 else y) for y in range(DIM)] 21 | for x in range(DIM) 22 | ] 23 | 24 | 25 | def main(): 26 | ''' do it 27 | ''' 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument('--auto', '-a', action='store_true') 30 | args = parser.parse_args() 31 | 32 | all_words_in_grid_order = sorted(configdefs.ALL_WORDS, key=lambda x: (x.y, x.x)) 33 | pixels = neopixel.NeoPixel(PIN_PIXELS, N_PIXELS, auto_write=False) 34 | 35 | def set_pixel(index): 36 | ''' Set a pixel 37 | ''' 38 | pixels[index] = COLOR_ON 39 | 40 | def set_word(word): 41 | ''' Set a word pixels 42 | ''' 43 | if word.vertical: 44 | for y_i in range(word.y, word.y + len(word.text)): 45 | set_pixel(PIXEL_MAP[word.x][y_i]) 46 | else: 47 | for x_i in range(word.x, word.x + len(word.text)): 48 | set_pixel(PIXEL_MAP[x_i][word.y]) 49 | 50 | try: 51 | while True: 52 | for word in all_words_in_grid_order: 53 | pixels.fill(COLOR_OFF) 54 | set_word(word) 55 | pixels.show() 56 | prompt = f'{word.text} {word.y} {word.x}' 57 | 58 | if args.auto: 59 | print(prompt) 60 | time.sleep(1) 61 | else: 62 | input(prompt + ': ') 63 | 64 | except KeyboardInterrupt: #pylint: disable=bare-except 65 | pass 66 | -------------------------------------------------------------------------------- /code/wordclock/tsense.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Test sensors 3 | ''' 4 | 5 | import time 6 | import board 7 | import busio 8 | import adafruit_mlx90393 9 | import qwiic_adxl313 10 | import adafruit_veml7700 11 | 12 | 13 | def main(): 14 | ''' do it 15 | ''' 16 | try: 17 | i2c = busio.I2C(board.SCL, board.SDA) 18 | magnetometer = None 19 | accelerometer = None 20 | light_sensor = None 21 | 22 | while True: 23 | if magnetometer is None: 24 | try: 25 | magnetometer = adafruit_mlx90393.MLX90393( 26 | i2c, gain=adafruit_mlx90393.GAIN_5X) 27 | print('Initialized magnetometer') 28 | except Exception as err: #pylint: disable=broad-except 29 | print('Failed to init the magnetometer:', str(err)) 30 | 31 | if accelerometer is None: 32 | try: 33 | accelerometer = qwiic_adxl313.QwiicAdxl313() 34 | accelerometer.measureModeOn() 35 | print('Initialized accelerometer') 36 | except Exception as err: #pylint: disable=broad-except 37 | print('Failed to init the accelerometer:', str(err)) 38 | time.sleep(1) 39 | 40 | if light_sensor is None: 41 | try: 42 | light_sensor = adafruit_veml7700.VEML7700(i2c) 43 | print('Initialized light sensor') 44 | 45 | except Exception as err: #pylint: disable=broad-except 46 | print('Failed to init light sensor:', str(err)) 47 | 48 | if magnetometer is not None: 49 | print('compass;', magnetometer.magnetic) 50 | 51 | if accelerometer is not None: 52 | if accelerometer.dataReady(): 53 | accelerometer.readAccel() 54 | 55 | print('accel: ', accelerometer.x, accelerometer.y, accelerometer.z) 56 | 57 | if light_sensor is not None: 58 | print('light: ', light_sensor.light) 59 | 60 | time.sleep(1) 61 | 62 | except KeyboardInterrupt: #pylint: disable=bare-except 63 | pass 64 | -------------------------------------------------------------------------------- /code/config/config_dm.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration for the Doug & Marguerite clock 3 | ''' 4 | 5 | import datetime 6 | from wordclock.configdefs import Word, BirthDate, SpecialBirthday 7 | 8 | VERSION_2 = False 9 | 10 | CLOCK_NAME = 'Holmgren-Rogers' 11 | LAT = 45.45918 12 | LON = -122.72911 13 | 14 | IT = Word('it', 0, 0, False) 15 | TWIST = Word('twist', 0, 1, False) 16 | IS = Word('is', 0, 3, False) 17 | T_HALF = Word('half', 0, 6, False) 18 | FOR = Word('for', 0, 9, False) 19 | 20 | MASH = Word('mash', 1, 0, False) 21 | AS0 = Word('as', 1, 1, False) 22 | ASH = Word('ash', 1, 1, False) 23 | HAPPY = Word('happy', 1, 3, False) 24 | YET = Word('yet', 1, 7, False) 25 | T_TEN = Word('ten', 1, 9, False) 26 | 27 | T_TWENTY = Word('twenty', 2, 0, False) 28 | T_FIVE = Word('five', 2, 6, False) 29 | T_TWENTYFIVE = Word('twentyfive', 2, 0, False) 30 | EAR = Word('ear', 2, 9, False) 31 | 32 | T_QUARTER = Word('quarter', 3, 0, False) 33 | REPAST = Word('repast', 3, 6, False) 34 | PAST = Word('past', 3, 8, False) 35 | AS1 = Word('as', 3, 9, False) 36 | 37 | ORB = Word('orb', 4, 0, False) 38 | BIRTH = Word('birth', 4, 2, False) 39 | BIRTHDAY = Word('birthday', 4, 2, False) 40 | DAY = Word('day', 4, 7, False) 41 | TO = Word('to', 4, 10, False) 42 | 43 | H_ONE = Word('one', 5, 1, False) 44 | NET = Word('net', 5, 2, False) 45 | H_TWO = Word('two', 5, 4, False) 46 | H_THREE = Word('three', 5, 7, False) 47 | 48 | H_FOUR = Word('four', 6, 1, False) 49 | OUR = Word('ou4', 6, 2, False) 50 | H_FIVE = Word('five', 6, 5, False) 51 | H_SIX = Word('six', 6, 9, False) 52 | 53 | H_EIGHT = Word('eight', 7, 0, False) 54 | DOUG = Word('doug', 7, 5, False) 55 | GO = Word('go', 7, 8, False) 56 | GOAL = Word('goal', 7, 8, False) 57 | 58 | MARGUERITE = Word('marguerite', 8, 1, False) 59 | IT2 = Word('it', 8, 8, False) 60 | H_TEN = Word('ten', 8, 9, False) 61 | 62 | H_SEVEN = Word('seven', 9, 1, False) 63 | H_TWELVE = Word('twelve', 9, 6, False) 64 | 65 | H_ELEVEN = Word('eleven', 5, 0, True) 66 | 67 | H_NINE = Word('nine', 10, 0, False) 68 | NEO = Word('neo', 10, 2, False) 69 | OK = Word('ok', 10, 4, False) 70 | OCLOCK = Word('oclock', 10, 6, False) 71 | CLOCK = Word('clock', 10, 7, False) 72 | LOCK = Word('lock', 10, 8, False) 73 | 74 | BIRTHDAYS = { 75 | BirthDate(2, 20): DOUG, 76 | BirthDate(9, 17): MARGUERITE, 77 | } 78 | 79 | SPECIAL_BIRTHDAY = SpecialBirthday( 80 | DOUG, 81 | datetime.date(2021, 3, 11), 82 | datetime.date(2021, 3, 17)) 83 | -------------------------------------------------------------------------------- /code/config/config_ts.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration for the Tom & Sandy clock 3 | ''' 4 | 5 | import datetime 6 | from wordclock.configdefs import Word, BirthDate, SpecialBirthday 7 | 8 | VERSION_2 = False 9 | 10 | CLOCK_NAME = 'Tom & Sandy' 11 | LAT = 38.00426 12 | LON = -122.28954 13 | 14 | IT = Word('it', 0, 0, False) 15 | TWIST = Word('twist', 0, 1, False) 16 | IS = Word('is', 0, 3, False) 17 | T_HALF = Word('half', 0, 6, False) 18 | FOR = Word('for', 0, 9, False) 19 | 20 | MASH = Word('mash', 1, 0, False) 21 | AS0 = Word('as', 1, 1, False) 22 | ASH = Word('ash', 1, 1, False) 23 | HAPPY = Word('happy', 1, 3, False) 24 | YET = Word('yet', 1, 7, False) 25 | T_TEN = Word('ten', 1, 9, False) 26 | 27 | T_TWENTY = Word('twenty', 2, 0, False) 28 | T_FIVE = Word('five', 2, 6, False) 29 | T_TWENTYFIVE = Word('twentyfive', 2, 0, False) 30 | EAR = Word('ear', 2, 9, False) 31 | 32 | T_QUARTER = Word('quarter', 3, 0, False) 33 | REPAST = Word('repast', 3, 6, False) 34 | PAST = Word('past', 3, 8, False) 35 | AS1 = Word('as', 3, 9, False) 36 | 37 | ORB = Word('orb', 4, 0, False) 38 | BIRTH = Word('birth', 4, 2, False) 39 | BIRTHDAY = Word('birthday', 4, 2, False) 40 | DAY = Word('day', 4, 7, False) 41 | TO = Word('to', 4, 10, False) 42 | 43 | WON = Word('won', 5, 0, False) 44 | H_ONE = Word('one', 5, 1, False) 45 | NET = Word('net', 5, 2, False) 46 | H_TWO = Word('two', 5, 4, False) 47 | H_THREE = Word('three', 5, 7, False) 48 | 49 | H_SIX = Word('six', 6, 0, False) 50 | IF = Word('if', 6, 3, False) 51 | H_FIVE = Word('five', 6, 4, False) 52 | H_FOUR = Word('four', 6, 8, False) 53 | OUR = Word('our', 6, 9, False) 54 | 55 | AT = Word('at', 7, 0, False) 56 | ATOM = Word('atom', 7, 0, False) 57 | TOM = Word('tom', 7, 1, False) 58 | SANDY = Word('sandy', 7, 4, False) 59 | SAND = Word('sand', 7, 4, False) 60 | H_TEN = Word('ten', 7, 9, False) 61 | 62 | H_SEVEN = Word('seven', 8, 0, False) 63 | NOT = Word('not', 8, 4, False) 64 | H_TWELVE = Word('twelve', 8, 6, False) 65 | 66 | H_ELEVEN = Word('eleven', 9, 0, False) 67 | END = Word('end', 9, 4, False) 68 | H_EIGHT = Word('eight', 9, 7, False) 69 | 70 | H_NINE = Word('nine', 10, 0, False) 71 | NEO = Word('neo', 10, 2, False) 72 | OK = Word('ok', 10, 4, False) 73 | OCLOCK = Word('oclock', 10, 6, False) 74 | CLOCK = Word('clock', 10, 7, False) 75 | LOCK = Word('lock', 10, 8, False) 76 | 77 | BIRTHDAYS = { 78 | BirthDate(3, 24): TOM, 79 | BirthDate(1, 28): SANDY, 80 | } 81 | 82 | SPECIAL_BIRTHDAY = SpecialBirthday( 83 | TOM, 84 | datetime.date(2021, 3, 24), 85 | datetime.date(2021, 3, 27)) 86 | -------------------------------------------------------------------------------- /code/wordclock/tneo.py: -------------------------------------------------------------------------------- 1 | ''' test neopixels 2 | ''' 3 | import argparse 4 | import time 5 | import board 6 | import neopixel 7 | 8 | DIM = 12 9 | BORDER_BEGIN = DIM*DIM 10 | BORDER_DIM_V1 = DIM*4 11 | BORDER_DIM_V2 = DIM*4*2 12 | 13 | 14 | def parse_args(args, border_dim): 15 | ''' parse the args 16 | ''' 17 | do_shift = False 18 | 19 | if args.all: 20 | begin = 0 21 | end = begin + DIM 22 | elif args.strip is not None: 23 | if args.strip >= DIM: 24 | begin = BORDER_BEGIN 25 | end = begin + border_dim 26 | else: 27 | begin = args.strip * DIM 28 | end = begin + DIM 29 | elif args.begin is not None: 30 | begin = args.begin 31 | end = args.end if args.end else begin + DIM 32 | else: 33 | begin = end = 0 34 | do_shift = True 35 | 36 | return (do_shift, begin, end) 37 | 38 | 39 | def main(): 40 | ''' do it 41 | ''' 42 | parser = argparse.ArgumentParser() 43 | parser.add_argument('--begin', '-b', type=int) 44 | parser.add_argument('--end', '-e', type=int) 45 | parser.add_argument('--strip', '-s', type=int) 46 | parser.add_argument('--all', '-a', action='store_true') 47 | parser.add_argument('--v1', action='store_true') 48 | 49 | args = parser.parse_args() 50 | 51 | border_dim = BORDER_DIM_V1 if args.v1 else BORDER_DIM_V2 52 | n_pixels = BORDER_BEGIN + border_dim 53 | 54 | do_shift, begin, end = parse_args(args, border_dim) 55 | print(begin, end, do_shift) 56 | 57 | pixels = neopixel.NeoPixel(board.D18, n_pixels, auto_write=False) 58 | pixels.fill((0, 0, 0)) 59 | 60 | try: 61 | while True: 62 | if do_shift: 63 | for i in range(0, n_pixels): 64 | pixels[i] = (255, 255, 255) 65 | pixels.show() 66 | time.sleep(4 / n_pixels) 67 | pixels[i] = (0, 0, 0) 68 | continue 69 | 70 | for color in [(0, 0, 0), (255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 255)]: 71 | for i in range(begin, end): 72 | pixels[i] = color 73 | 74 | pixels.show() 75 | time.sleep(0.5) 76 | 77 | if args.all: 78 | begin = 0 if begin >= BORDER_BEGIN else begin + DIM 79 | end = begin + (border_dim if begin == BORDER_BEGIN else DIM) 80 | pixels.fill((0, 0, 0)) 81 | 82 | except KeyboardInterrupt: #pylint: disable=bare-except 83 | pass 84 | -------------------------------------------------------------------------------- /carbide3d/BrackBottomLeft.nc: -------------------------------------------------------------------------------- 1 | (Design File: C..Users.msidell.VIASAT.s.carbide.WordClockBracket.c2d) 2 | (stockMin:0.00mm, 0.00mm, -13.50mm) 3 | (stockMax:419.10mm, 50.80mm, 0.00mm) 4 | (STOCK/BLOCK,419.10, 50.80, 13.50,0.00, 0.00, 13.50) 5 | G90 6 | G21 7 | (Move to safe Z to avoid workholding) 8 | G53G0Z-5.000 9 | (socket.-.Pocket) 10 | M05 11 | (TOOL/MILL,3.17, 0.00, 0.00, 0.00) 12 | M6T102 13 | M03S10000 14 | (PREPOSITION FOR RAPID PLUNGE) 15 | G0X-0.03Y24.91 16 | Z12.70 17 | G1Z-1.27F203.2 18 | Y25.90F508.0 19 | X23.13 20 | Y24.91 21 | X-0.03 22 | X-1.61Y23.32 23 | Y27.48 24 | X24.71 25 | Y23.32 26 | X-1.61 27 | Z12.70 28 | G0X-0.03Y24.91 29 | Z-0.64 30 | G1Z-2.54F203.2 31 | Y25.90F508.0 32 | X23.13 33 | Y24.91 34 | X-0.03 35 | X-1.61Y23.32 36 | Y27.48 37 | X24.71 38 | Y23.32 39 | X-1.61 40 | Z12.70 41 | G0X-0.03Y24.91 42 | Z-1.91 43 | G1Z-3.81F203.2 44 | Y25.90F508.0 45 | X23.13 46 | Y24.91 47 | X-0.03 48 | X-1.61Y23.32 49 | Y27.48 50 | X24.71 51 | Y23.32 52 | X-1.61 53 | Z12.70 54 | G0X-0.03Y24.91 55 | Z-3.17 56 | G1Z-5.08F203.2 57 | Y25.90F508.0 58 | X23.13 59 | Y24.91 60 | X-0.03 61 | X-1.61Y23.32 62 | Y27.48 63 | X24.71 64 | Y23.32 65 | X-1.61 66 | Z12.70 67 | G0X-0.03Y24.91 68 | Z-4.44 69 | G1Z-6.35F203.2 70 | Y25.90F508.0 71 | X23.13 72 | Y24.91 73 | X-0.03 74 | X-1.61Y23.32 75 | Y27.48 76 | X24.71 77 | Y23.32 78 | X-1.61 79 | Z12.70 80 | (power.slots.-.Pocket) 81 | M03S10000 82 | G0X175.14Y-0.00 83 | G1Z-1.27F203.2 84 | Y50.80F508.0 85 | X177.96 86 | Y-0.00 87 | X175.14 88 | Z12.70 89 | G0 90 | Z-0.64 91 | G1Z-2.54F203.2 92 | Y50.80F508.0 93 | X177.96 94 | Y-0.00 95 | X175.14 96 | Z12.70 97 | G0 98 | Z-1.91 99 | G1Z-3.81F203.2 100 | Y50.80F508.0 101 | X177.96 102 | Y-0.00 103 | X175.14 104 | Z12.70 105 | G0 106 | Z-3.17 107 | G1Z-5.08F203.2 108 | Y50.80F508.0 109 | X177.96 110 | Y-0.00 111 | X175.14 112 | Z12.70 113 | G0 114 | Z-4.09 115 | G1Z-6.00F203.2 116 | Y50.80F508.0 117 | X177.96 118 | Y-0.00 119 | X175.14 120 | Z12.70 121 | G0X208.14 122 | G1Z-1.27F203.2 123 | Y50.80F508.0 124 | X210.96 125 | Y-0.00 126 | X208.14 127 | Z12.70 128 | G0 129 | Z-0.64 130 | G1Z-2.54F203.2 131 | Y50.80F508.0 132 | X210.96 133 | Y-0.00 134 | X208.14 135 | Z12.70 136 | G0 137 | Z-1.91 138 | G1Z-3.81F203.2 139 | Y50.80F508.0 140 | X210.96 141 | Y-0.00 142 | X208.14 143 | Z12.70 144 | G0 145 | Z-3.17 146 | G1Z-5.08F203.2 147 | Y50.80F508.0 148 | X210.96 149 | Y-0.00 150 | X208.14 151 | Z12.70 152 | G0 153 | Z-4.09 154 | G1Z-6.00F203.2 155 | Y50.80F508.0 156 | X210.96 157 | Y-0.00 158 | X208.14 159 | Z12.70 160 | M02 161 | -------------------------------------------------------------------------------- /code/config/config_mb.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration for the Mark & Beth clock 3 | ''' 4 | 5 | from wordclock.configdefs import Word, BirthDate 6 | 7 | VERSION_2 = True 8 | 9 | CLOCK_NAME = "Buzz & Cap'n" 10 | LAT = 35.90823 11 | LON = -79.04082 12 | 13 | IT = Word('it', 0, 0, False) 14 | TWIST = Word('twist', 0, 1, False) 15 | IS = Word('is', 0, 3, False) 16 | T_HALF = Word('half', 0, 6, False) 17 | FOR = Word('for', 0, 9, False) 18 | 19 | MASH = Word('mash', 1, 0, False) 20 | AS0 = Word('as', 1, 1, False) 21 | ASH = Word('ash', 1, 1, False) 22 | HAPPY = Word('happy', 1, 3, False) 23 | YET = Word('yet', 1, 7, False) 24 | T_TEN = Word('ten', 1, 9, False) 25 | 26 | T_TWENTY = Word('twenty', 2, 0, False) 27 | T_FIVE = Word('five', 2, 6, False) 28 | T_TWENTYFIVE = Word('twentyfive', 2, 0, False) 29 | EAR = Word('ear', 2, 9, False) 30 | 31 | T_QUARTER = Word('quarter', 3, 0, False) 32 | REPAST = Word('repast', 3, 6, False) 33 | PAST = Word('past', 3, 8, False) 34 | AS1 = Word('as', 3, 9, False) 35 | 36 | ORB = Word('orb', 4, 0, False) 37 | BIRTH = Word('birth', 4, 2, False) 38 | BIRTHDAY = Word('birthday', 4, 2, False) 39 | DAY = Word('day', 4, 7, False) 40 | TO = Word('to', 4, 10, False) 41 | 42 | WON = Word('won', 5, 0, False) 43 | H_ONE = Word('one', 5, 1, False) 44 | NET = Word('net', 5, 2, False) 45 | H_TWO = Word('two', 5, 4, False) 46 | H_THREE = Word('three', 5, 7, False) 47 | 48 | H_SIX = Word('six', 6, 0, False) 49 | IF = Word('if', 6, 3, False) 50 | H_FIVE = Word('five', 6, 4, False) 51 | H_FOUR = Word('four', 6, 8, False) 52 | OUR = Word('our', 6, 9, False) 53 | 54 | ABUZZ = Word('abuzz', 7, 0, False) 55 | BUZZ = Word('buzz', 7, 1, False) 56 | CAP = Word('cap', 7, 5, False) 57 | CAPN = Word('capn', 7, 5, False) 58 | H_TEN = Word('ten', 7, 9, False) 59 | 60 | H_SEVEN = Word('seven', 8, 0, False) 61 | NOT = Word('not', 8, 4, False) 62 | H_TWELVE = Word('twelve', 8, 6, False) 63 | 64 | H_ELEVEN = Word('eleven', 9, 0, False) 65 | END = Word('end', 9, 4, False) 66 | H_EIGHT = Word('eight', 9, 7, False) 67 | 68 | H_NINE = Word('nine', 10, 0, False) 69 | NEO = Word('neo', 10, 2, False) 70 | OK = Word('ok', 10, 4, False) 71 | OCLOCK = Word('oclock', 10, 6, False) 72 | CLOCK = Word('clock', 10, 7, False) 73 | LOCK = Word('lock', 10, 8, False) 74 | 75 | SEA = Word('sea', 1, 2, True) 76 | VEN = Word('ven', 8, 2, True) 77 | EVE = Word('eve', 8, 3, True) 78 | SAT = Word('sat', 0, 4, True) 79 | NEO2 = Word('neo', 8, 4, True) 80 | YET2 = Word('yet', 2, 5, True) 81 | CONK = Word('conk', 7, 5, True) 82 | VAT = Word('vat', 6, 6, True) 83 | DO = Word('do', 9, 6, True) 84 | WE = Word('we', 8, 7, True) 85 | PA = Word('pa', 3, 8, True) 86 | TEA = Word('tea', 1, 9, True) 87 | ROT = Word('rot', 5, 9, True) 88 | GO = Word('go', 9, 9, True) 89 | EAST = Word('east', 1, 10, True) 90 | TOE = Word('toe', 3, 11, True) 91 | NET2 = Word('net', 7, 11, True) 92 | 93 | A0 = Word('a', 0, 7, False) 94 | A1 = Word('a', 1, 1, False) 95 | A2 = Word('a', 1, 4, False) 96 | A3 = Word('a', 2, 10, False) 97 | A4 = Word('a', 3, 2, False) 98 | A5 = Word('a', 3, 9, False) 99 | A6 = Word('a', 4, 8, False) 100 | A7 = Word('a', 7, 0, False) 101 | A8 = Word('a', 7, 6, False) 102 | 103 | 104 | BIRTHDAYS = { 105 | BirthDate(3, 21): CAPN, 106 | BirthDate(9, 8): BUZZ, 107 | } 108 | 109 | SPECIAL_BIRTHDAY = None 110 | -------------------------------------------------------------------------------- /carbide3d/CenterJig.nc: -------------------------------------------------------------------------------- 1 | (Design File: C..Users.msidell.VIASAT.s.carbide.CenterJig.c2d) 2 | (stockMin:0.00mm, 0.00mm, -13.00mm) 3 | (stockMax:330.20mm, 127.00mm, 0.00mm) 4 | (STOCK/BLOCK,330.20, 127.00, 13.00,0.00, 0.00, 13.00) 5 | G90 6 | G21 7 | (Move to safe Z to avoid workholding) 8 | G53G0Z-5.000 9 | (notch.-.Pocket) 10 | M05 11 | (TOOL/MILL,6.35, 0.00, 0.00, 0.00) 12 | M6T201 13 | M03S10000 14 | (PREPOSITION FOR RAPID PLUNGE) 15 | G0X161.35Y2.35 16 | Z12.70 17 | G1Z-0.69F254.0 18 | Y8.65F1524.0 19 | X203.65 20 | Y2.35 21 | X161.35 22 | X158.18Y-0.82 23 | Y11.82 24 | X206.82 25 | Y-0.82 26 | X158.18 27 | Z12.70 28 | G0X161.35Y2.35 29 | Z-0.34 30 | G1Z-1.37F254.0 31 | Y8.65F1524.0 32 | X203.65 33 | Y2.35 34 | X161.35 35 | X158.18Y-0.82 36 | Y11.82 37 | X206.82 38 | Y-0.82 39 | X158.18 40 | Z12.70 41 | G0X161.35Y2.35 42 | Z-1.03 43 | G1Z-2.06F254.0 44 | Y8.65F1524.0 45 | X203.65 46 | Y2.35 47 | X161.35 48 | X158.18Y-0.82 49 | Y11.82 50 | X206.82 51 | Y-0.82 52 | X158.18 53 | Z12.70 54 | G0X161.35Y2.35 55 | Z-1.72 56 | G1Z-2.74F254.0 57 | Y8.65F1524.0 58 | X203.65 59 | Y2.35 60 | X161.35 61 | X158.18Y-0.82 62 | Y11.82 63 | X206.82 64 | Y-0.82 65 | X158.18 66 | Z12.70 67 | G0X161.35Y2.35 68 | Z-2.40 69 | G1Z-3.43F254.0 70 | Y8.65F1524.0 71 | X203.65 72 | Y2.35 73 | X161.35 74 | X158.18Y-0.82 75 | Y11.82 76 | X206.82 77 | Y-0.82 78 | X158.18 79 | Z12.70 80 | G0X161.35Y2.35 81 | Z-3.09 82 | G1Z-4.12F254.0 83 | Y8.65F1524.0 84 | X203.65 85 | Y2.35 86 | X161.35 87 | X158.18Y-0.82 88 | Y11.82 89 | X206.82 90 | Y-0.82 91 | X158.18 92 | Z12.70 93 | G0X161.35Y2.35 94 | Z-3.77 95 | G1Z-4.80F254.0 96 | Y8.65F1524.0 97 | X203.65 98 | Y2.35 99 | X161.35 100 | X158.18Y-0.82 101 | Y11.82 102 | X206.82 103 | Y-0.82 104 | X158.18 105 | Z12.70 106 | G0X161.35Y2.35 107 | Z-4.46 108 | G1Z-5.49F254.0 109 | Y8.65F1524.0 110 | X203.65 111 | Y2.35 112 | X161.35 113 | X158.18Y-0.82 114 | Y11.82 115 | X206.82 116 | Y-0.82 117 | X158.18 118 | Z12.70 119 | G0X161.35Y2.35 120 | Z-5.15 121 | G1Z-6.17F254.0 122 | Y8.65F1524.0 123 | X203.65 124 | Y2.35 125 | X161.35 126 | X158.18Y-0.82 127 | Y11.82 128 | X206.82 129 | Y-0.82 130 | X158.18 131 | Z12.70 132 | G0X161.35Y2.35 133 | Z-5.83 134 | G1Z-6.86F254.0 135 | Y8.65F1524.0 136 | X203.65 137 | Y2.35 138 | X161.35 139 | X158.18Y-0.82 140 | Y11.82 141 | X206.82 142 | Y-0.82 143 | X158.18 144 | Z12.70 145 | G0X161.35Y2.35 146 | Z-6.52 147 | G1Z-7.55F254.0 148 | Y8.65F1524.0 149 | X203.65 150 | Y2.35 151 | X161.35 152 | X158.18Y-0.82 153 | Y11.82 154 | X206.82 155 | Y-0.82 156 | X158.18 157 | Z12.70 158 | G0X161.35Y2.35 159 | Z-7.20 160 | G1Z-8.23F254.0 161 | Y8.65F1524.0 162 | X203.65 163 | Y2.35 164 | X161.35 165 | X158.18Y-0.82 166 | Y11.82 167 | X206.82 168 | Y-0.82 169 | X158.18 170 | Z12.70 171 | G0X161.35Y2.35 172 | Z-7.89 173 | G1Z-8.92F254.0 174 | Y8.65F1524.0 175 | X203.65 176 | Y2.35 177 | X161.35 178 | X158.18Y-0.82 179 | Y11.82 180 | X206.82 181 | Y-0.82 182 | X158.18 183 | Z12.70 184 | G0X161.35Y2.35 185 | Z-8.58 186 | G1Z-9.60F254.0 187 | Y8.65F1524.0 188 | X203.65 189 | Y2.35 190 | X161.35 191 | X158.18Y-0.82 192 | Y11.82 193 | X206.82 194 | Y-0.82 195 | X158.18 196 | Z12.70 197 | G0X161.35Y2.35 198 | Z-9.26 199 | G1Z-10.29F254.0 200 | Y8.65F1524.0 201 | X203.65 202 | Y2.35 203 | X161.35 204 | X158.18Y-0.82 205 | Y11.82 206 | X206.82 207 | Y-0.82 208 | X158.18 209 | Z12.70 210 | G0X161.35Y2.35 211 | Z-9.95 212 | G1Z-10.98F254.0 213 | Y8.65F1524.0 214 | X203.65 215 | Y2.35 216 | X161.35 217 | X158.18Y-0.82 218 | Y11.82 219 | X206.82 220 | Y-0.82 221 | X158.18 222 | Z12.70 223 | G0X161.35Y2.35 224 | Z-10.63 225 | G1Z-11.66F254.0 226 | Y8.65F1524.0 227 | X203.65 228 | Y2.35 229 | X161.35 230 | X158.18Y-0.82 231 | Y11.82 232 | X206.82 233 | Y-0.82 234 | X158.18 235 | Z12.70 236 | G0X161.35Y2.35 237 | Z-11.32 238 | G1Z-12.35F254.0 239 | Y8.65F1524.0 240 | X203.65 241 | Y2.35 242 | X161.35 243 | X158.18Y-0.82 244 | Y11.82 245 | X206.82 246 | Y-0.82 247 | X158.18 248 | Z12.70 249 | G0X161.35Y2.35 250 | Z-11.97 251 | G1Z-13.00F254.0 252 | Y8.65F1524.0 253 | X203.65 254 | Y2.35 255 | X161.35 256 | X158.18Y-0.82 257 | Y11.82 258 | X206.82 259 | Y-0.82 260 | X158.18 261 | Z12.70 262 | M02 263 | -------------------------------------------------------------------------------- /face/genwords: -------------------------------------------------------------------------------- 1 | ''' 2 | Read a text file consisting of a letter grid and print 3 | the words. 4 | ''' 5 | import sys 6 | from argparse import ArgumentParser 7 | from collections import defaultdict 8 | import enchant 9 | 10 | DELTAS = [ 11 | 'five', 'ten', 'quarter', 'twenty', 'twentyfive', 'half' 12 | ] 13 | 14 | HOURS = [ 15 | 'one', 'two', 'three', 'four', 'five', 'six', 16 | 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 17 | ] 18 | 19 | SPECIAL_WORDS = [ 20 | 'twentyfive', 'oclock'] 21 | 22 | BAD_WORDS = set([ 23 | 'st', 24 | 'al', 25 | 'fo', 26 | 'mas', 27 | 'sh', 28 | 'pp', 29 | 'et', 30 | 'en', 31 | 'en', 32 | 'iv', 33 | 'ea', 34 | 'ar', 35 | 'qu', 36 | 'ar', 37 | 'rt', 38 | 'rte', 39 | 'ter', 40 | 'er', 41 | 'st', 42 | 'bi', 43 | 'rt', 44 | 'hd', 45 | 'ne', 46 | 'et', 47 | 'wo', 48 | 'hr', 49 | 're', 50 | 'fo', 51 | 'iv', 52 | 'es', 53 | 'ix', 54 | 'ht', 55 | 'gt', 56 | 'en', 57 | 'ca', 58 | 'es', 59 | 'en', 60 | 'el', 61 | 'lv', 62 | 'ne', 63 | 'cl', 64 | 'ck', 65 | 'mt', 66 | 'el', 67 | 'elev', 68 | 'lev', 69 | 'en', 70 | 'ps', 71 | 'psi', 72 | 'ea', 73 | 'ab', 74 | 'ae', 75 | 'en', 76 | 'nr', 77 | 'ht', 78 | 'att', 79 | 'tr', 80 | 'rt', 81 | 'tr', 82 | 'rt', 83 | 'et', 84 | 'hp', 85 | 'pf', 86 | 'fr', 87 | 'oi', 88 | 'ut', 89 | 'ed', 90 | 'cw', 91 | 'pah', 92 | 'ae', 93 | 'el', 94 | 'ft', 95 | 'ea', 96 | 'yr', 97 | 'yrs', 98 | 'rs', 99 | 'st', 100 | 'ts', 101 | 'nr', 102 | 'rt', 103 | 're', 104 | 'st', 105 | 'ea', 106 | 'av', 107 | ]) 108 | 109 | HOURS_ROW = 5 110 | 111 | 112 | def add_word(all_words, names, my_words, row, col, vertical, word): 113 | ''' Maybe add a word 114 | ''' 115 | if (len(word) > 1 and word not in BAD_WORDS and ( 116 | word in SPECIAL_WORDS or 117 | word in names or 118 | all_words.check(word)) 119 | or word == 'a' and not vertical): 120 | 121 | word_u = word.upper() 122 | index = my_words[word] 123 | my_words[word] += 1 124 | 125 | if word in DELTAS and row < HOURS_ROW: 126 | name = f'T_{word_u}' 127 | elif word in HOURS and row >= HOURS_ROW: 128 | name = f'H_{word_u}' 129 | else: 130 | name = word_u if index == 0 else f'{word_u}_{index}' 131 | 132 | print(f'{name} = Word(\'{word}\', {row}, {col}, {vertical})') 133 | 134 | 135 | def main(): 136 | ''' do it 137 | ''' 138 | parser = ArgumentParser() 139 | parser.add_argument('grid') 140 | parser.add_argument('--names', nargs='+', required=True) 141 | args = parser.parse_args() 142 | 143 | grid = [line.strip().lower() for line in open(args.grid, 'r')] 144 | dim = len(grid) 145 | 146 | if any(len(row) != dim for row in grid): 147 | print('ERROR: The grid is not square') 148 | sys.exit(1) 149 | 150 | names = [name.lower() for name in args.names] 151 | all_words = enchant.Dict('en_US') 152 | my_words = defaultdict(int) 153 | 154 | for row in range(dim - 1): 155 | for col in range(dim): 156 | for j in range(col+1, dim+1): 157 | add_word( 158 | all_words, names, my_words, row, col, False, grid[row][col:j]) 159 | 160 | print('') 161 | 162 | for col in range(dim): 163 | for row in range(dim - 1): 164 | for j in range(row+1, dim): 165 | add_word( 166 | all_words, names, my_words, row, col, True, 167 | ''.join([grid[i][col] for i in range(row, j)])) 168 | 169 | print('') 170 | 171 | 172 | if __name__ == "__main__": 173 | main() 174 | -------------------------------------------------------------------------------- /pcbs/backlight/backlight.pro: -------------------------------------------------------------------------------- 1 | update=3/23/2021 6:55:18 PM 2 | version=1 3 | last_client=kicad 4 | [general] 5 | version=1 6 | RootSch= 7 | BoardNm= 8 | [cvpcb] 9 | version=1 10 | NetIExt=net 11 | [eeschema] 12 | version=1 13 | LibDir= 14 | [eeschema/libraries] 15 | [pcbnew] 16 | version=1 17 | PageLayoutDescrFile= 18 | LastNetListRead= 19 | CopperLayerCount=2 20 | BoardThickness=1.6 21 | AllowMicroVias=0 22 | AllowBlindVias=0 23 | RequireCourtyardDefinitions=0 24 | ProhibitOverlappingCourtyards=1 25 | MinTrackWidth=0.2 26 | MinViaDiameter=0.4 27 | MinViaDrill=0.3 28 | MinMicroViaDiameter=0.2 29 | MinMicroViaDrill=0.09999999999999999 30 | MinHoleToHole=0.25 31 | TrackWidth1=0.25 32 | TrackWidth2=1 33 | ViaDiameter1=0.8 34 | ViaDrill1=0.4 35 | dPairWidth1=0.2 36 | dPairGap1=0.25 37 | dPairViaGap1=0.25 38 | SilkLineWidth=0.12 39 | SilkTextSizeV=1 40 | SilkTextSizeH=1 41 | SilkTextSizeThickness=0.15 42 | SilkTextItalic=0 43 | SilkTextUpright=1 44 | CopperLineWidth=0.2 45 | CopperTextSizeV=1.5 46 | CopperTextSizeH=1.5 47 | CopperTextThickness=0.3 48 | CopperTextItalic=0 49 | CopperTextUpright=1 50 | EdgeCutLineWidth=0.05 51 | CourtyardLineWidth=0.05 52 | OthersLineWidth=0.15 53 | OthersTextSizeV=1 54 | OthersTextSizeH=1 55 | OthersTextSizeThickness=0.15 56 | OthersTextItalic=0 57 | OthersTextUpright=1 58 | SolderMaskClearance=0.0508 59 | SolderMaskMinWidth=0 60 | SolderPasteClearance=0 61 | SolderPasteRatio=-0 62 | [pcbnew/Layer.F.Cu] 63 | Name=F.Cu 64 | Type=0 65 | Enabled=1 66 | [pcbnew/Layer.In1.Cu] 67 | Name=In1.Cu 68 | Type=0 69 | Enabled=0 70 | [pcbnew/Layer.In2.Cu] 71 | Name=In2.Cu 72 | Type=0 73 | Enabled=0 74 | [pcbnew/Layer.In3.Cu] 75 | Name=In3.Cu 76 | Type=0 77 | Enabled=0 78 | [pcbnew/Layer.In4.Cu] 79 | Name=In4.Cu 80 | Type=0 81 | Enabled=0 82 | [pcbnew/Layer.In5.Cu] 83 | Name=In5.Cu 84 | Type=0 85 | Enabled=0 86 | [pcbnew/Layer.In6.Cu] 87 | Name=In6.Cu 88 | Type=0 89 | Enabled=0 90 | [pcbnew/Layer.In7.Cu] 91 | Name=In7.Cu 92 | Type=0 93 | Enabled=0 94 | [pcbnew/Layer.In8.Cu] 95 | Name=In8.Cu 96 | Type=0 97 | Enabled=0 98 | [pcbnew/Layer.In9.Cu] 99 | Name=In9.Cu 100 | Type=0 101 | Enabled=0 102 | [pcbnew/Layer.In10.Cu] 103 | Name=In10.Cu 104 | Type=0 105 | Enabled=0 106 | [pcbnew/Layer.In11.Cu] 107 | Name=In11.Cu 108 | Type=0 109 | Enabled=0 110 | [pcbnew/Layer.In12.Cu] 111 | Name=In12.Cu 112 | Type=0 113 | Enabled=0 114 | [pcbnew/Layer.In13.Cu] 115 | Name=In13.Cu 116 | Type=0 117 | Enabled=0 118 | [pcbnew/Layer.In14.Cu] 119 | Name=In14.Cu 120 | Type=0 121 | Enabled=0 122 | [pcbnew/Layer.In15.Cu] 123 | Name=In15.Cu 124 | Type=0 125 | Enabled=0 126 | [pcbnew/Layer.In16.Cu] 127 | Name=In16.Cu 128 | Type=0 129 | Enabled=0 130 | [pcbnew/Layer.In17.Cu] 131 | Name=In17.Cu 132 | Type=0 133 | Enabled=0 134 | [pcbnew/Layer.In18.Cu] 135 | Name=In18.Cu 136 | Type=0 137 | Enabled=0 138 | [pcbnew/Layer.In19.Cu] 139 | Name=In19.Cu 140 | Type=0 141 | Enabled=0 142 | [pcbnew/Layer.In20.Cu] 143 | Name=In20.Cu 144 | Type=0 145 | Enabled=0 146 | [pcbnew/Layer.In21.Cu] 147 | Name=In21.Cu 148 | Type=0 149 | Enabled=0 150 | [pcbnew/Layer.In22.Cu] 151 | Name=In22.Cu 152 | Type=0 153 | Enabled=0 154 | [pcbnew/Layer.In23.Cu] 155 | Name=In23.Cu 156 | Type=0 157 | Enabled=0 158 | [pcbnew/Layer.In24.Cu] 159 | Name=In24.Cu 160 | Type=0 161 | Enabled=0 162 | [pcbnew/Layer.In25.Cu] 163 | Name=In25.Cu 164 | Type=0 165 | Enabled=0 166 | [pcbnew/Layer.In26.Cu] 167 | Name=In26.Cu 168 | Type=0 169 | Enabled=0 170 | [pcbnew/Layer.In27.Cu] 171 | Name=In27.Cu 172 | Type=0 173 | Enabled=0 174 | [pcbnew/Layer.In28.Cu] 175 | Name=In28.Cu 176 | Type=0 177 | Enabled=0 178 | [pcbnew/Layer.In29.Cu] 179 | Name=In29.Cu 180 | Type=0 181 | Enabled=0 182 | [pcbnew/Layer.In30.Cu] 183 | Name=In30.Cu 184 | Type=0 185 | Enabled=0 186 | [pcbnew/Layer.B.Cu] 187 | Name=B.Cu 188 | Type=0 189 | Enabled=1 190 | [pcbnew/Layer.B.Adhes] 191 | Enabled=1 192 | [pcbnew/Layer.F.Adhes] 193 | Enabled=1 194 | [pcbnew/Layer.B.Paste] 195 | Enabled=1 196 | [pcbnew/Layer.F.Paste] 197 | Enabled=1 198 | [pcbnew/Layer.B.SilkS] 199 | Enabled=1 200 | [pcbnew/Layer.F.SilkS] 201 | Enabled=1 202 | [pcbnew/Layer.B.Mask] 203 | Enabled=1 204 | [pcbnew/Layer.F.Mask] 205 | Enabled=1 206 | [pcbnew/Layer.Dwgs.User] 207 | Enabled=1 208 | [pcbnew/Layer.Cmts.User] 209 | Enabled=1 210 | [pcbnew/Layer.Eco1.User] 211 | Enabled=1 212 | [pcbnew/Layer.Eco2.User] 213 | Enabled=1 214 | [pcbnew/Layer.Edge.Cuts] 215 | Enabled=1 216 | [pcbnew/Layer.Margin] 217 | Enabled=1 218 | [pcbnew/Layer.B.CrtYd] 219 | Enabled=1 220 | [pcbnew/Layer.F.CrtYd] 221 | Enabled=1 222 | [pcbnew/Layer.B.Fab] 223 | Enabled=1 224 | [pcbnew/Layer.F.Fab] 225 | Enabled=1 226 | [pcbnew/Layer.Rescue] 227 | Enabled=0 228 | [pcbnew/Netclasses] 229 | [pcbnew/Netclasses/Default] 230 | Name=Default 231 | Clearance=0.2 232 | TrackWidth=0.25 233 | ViaDiameter=0.8 234 | ViaDrill=0.4 235 | uViaDiameter=0.3 236 | uViaDrill=0.1 237 | dPairWidth=0.2 238 | dPairGap=0.25 239 | dPairViaGap=0.25 240 | -------------------------------------------------------------------------------- /pcbs/top/top.pro: -------------------------------------------------------------------------------- 1 | update=3/23/2021 10:49:01 AM 2 | version=1 3 | last_client=kicad 4 | [general] 5 | version=1 6 | RootSch= 7 | BoardNm= 8 | [cvpcb] 9 | version=1 10 | NetIExt=net 11 | [eeschema] 12 | version=1 13 | LibDir= 14 | [eeschema/libraries] 15 | [pcbnew] 16 | version=1 17 | PageLayoutDescrFile= 18 | LastNetListRead= 19 | CopperLayerCount=2 20 | BoardThickness=1.6 21 | AllowMicroVias=0 22 | AllowBlindVias=0 23 | RequireCourtyardDefinitions=0 24 | ProhibitOverlappingCourtyards=1 25 | MinTrackWidth=0.2 26 | MinViaDiameter=0.4 27 | MinViaDrill=0.3 28 | MinMicroViaDiameter=0.2 29 | MinMicroViaDrill=0.09999999999999999 30 | MinHoleToHole=0.25 31 | TrackWidth1=0.25 32 | TrackWidth2=1 33 | TrackWidth3=2 34 | ViaDiameter1=0.8 35 | ViaDrill1=0.4 36 | dPairWidth1=0.2 37 | dPairGap1=0.25 38 | dPairViaGap1=0.25 39 | SilkLineWidth=0.12 40 | SilkTextSizeV=1 41 | SilkTextSizeH=1 42 | SilkTextSizeThickness=0.15 43 | SilkTextItalic=0 44 | SilkTextUpright=1 45 | CopperLineWidth=0.2 46 | CopperTextSizeV=1.5 47 | CopperTextSizeH=1.5 48 | CopperTextThickness=0.3 49 | CopperTextItalic=0 50 | CopperTextUpright=1 51 | EdgeCutLineWidth=0.05 52 | CourtyardLineWidth=0.05 53 | OthersLineWidth=0.15 54 | OthersTextSizeV=1 55 | OthersTextSizeH=1 56 | OthersTextSizeThickness=0.15 57 | OthersTextItalic=0 58 | OthersTextUpright=1 59 | SolderMaskClearance=0.0508 60 | SolderMaskMinWidth=0 61 | SolderPasteClearance=0 62 | SolderPasteRatio=-0 63 | [pcbnew/Layer.F.Cu] 64 | Name=F.Cu 65 | Type=0 66 | Enabled=1 67 | [pcbnew/Layer.In1.Cu] 68 | Name=In1.Cu 69 | Type=0 70 | Enabled=0 71 | [pcbnew/Layer.In2.Cu] 72 | Name=In2.Cu 73 | Type=0 74 | Enabled=0 75 | [pcbnew/Layer.In3.Cu] 76 | Name=In3.Cu 77 | Type=0 78 | Enabled=0 79 | [pcbnew/Layer.In4.Cu] 80 | Name=In4.Cu 81 | Type=0 82 | Enabled=0 83 | [pcbnew/Layer.In5.Cu] 84 | Name=In5.Cu 85 | Type=0 86 | Enabled=0 87 | [pcbnew/Layer.In6.Cu] 88 | Name=In6.Cu 89 | Type=0 90 | Enabled=0 91 | [pcbnew/Layer.In7.Cu] 92 | Name=In7.Cu 93 | Type=0 94 | Enabled=0 95 | [pcbnew/Layer.In8.Cu] 96 | Name=In8.Cu 97 | Type=0 98 | Enabled=0 99 | [pcbnew/Layer.In9.Cu] 100 | Name=In9.Cu 101 | Type=0 102 | Enabled=0 103 | [pcbnew/Layer.In10.Cu] 104 | Name=In10.Cu 105 | Type=0 106 | Enabled=0 107 | [pcbnew/Layer.In11.Cu] 108 | Name=In11.Cu 109 | Type=0 110 | Enabled=0 111 | [pcbnew/Layer.In12.Cu] 112 | Name=In12.Cu 113 | Type=0 114 | Enabled=0 115 | [pcbnew/Layer.In13.Cu] 116 | Name=In13.Cu 117 | Type=0 118 | Enabled=0 119 | [pcbnew/Layer.In14.Cu] 120 | Name=In14.Cu 121 | Type=0 122 | Enabled=0 123 | [pcbnew/Layer.In15.Cu] 124 | Name=In15.Cu 125 | Type=0 126 | Enabled=0 127 | [pcbnew/Layer.In16.Cu] 128 | Name=In16.Cu 129 | Type=0 130 | Enabled=0 131 | [pcbnew/Layer.In17.Cu] 132 | Name=In17.Cu 133 | Type=0 134 | Enabled=0 135 | [pcbnew/Layer.In18.Cu] 136 | Name=In18.Cu 137 | Type=0 138 | Enabled=0 139 | [pcbnew/Layer.In19.Cu] 140 | Name=In19.Cu 141 | Type=0 142 | Enabled=0 143 | [pcbnew/Layer.In20.Cu] 144 | Name=In20.Cu 145 | Type=0 146 | Enabled=0 147 | [pcbnew/Layer.In21.Cu] 148 | Name=In21.Cu 149 | Type=0 150 | Enabled=0 151 | [pcbnew/Layer.In22.Cu] 152 | Name=In22.Cu 153 | Type=0 154 | Enabled=0 155 | [pcbnew/Layer.In23.Cu] 156 | Name=In23.Cu 157 | Type=0 158 | Enabled=0 159 | [pcbnew/Layer.In24.Cu] 160 | Name=In24.Cu 161 | Type=0 162 | Enabled=0 163 | [pcbnew/Layer.In25.Cu] 164 | Name=In25.Cu 165 | Type=0 166 | Enabled=0 167 | [pcbnew/Layer.In26.Cu] 168 | Name=In26.Cu 169 | Type=0 170 | Enabled=0 171 | [pcbnew/Layer.In27.Cu] 172 | Name=In27.Cu 173 | Type=0 174 | Enabled=0 175 | [pcbnew/Layer.In28.Cu] 176 | Name=In28.Cu 177 | Type=0 178 | Enabled=0 179 | [pcbnew/Layer.In29.Cu] 180 | Name=In29.Cu 181 | Type=0 182 | Enabled=0 183 | [pcbnew/Layer.In30.Cu] 184 | Name=In30.Cu 185 | Type=0 186 | Enabled=0 187 | [pcbnew/Layer.B.Cu] 188 | Name=B.Cu 189 | Type=0 190 | Enabled=1 191 | [pcbnew/Layer.B.Adhes] 192 | Enabled=1 193 | [pcbnew/Layer.F.Adhes] 194 | Enabled=1 195 | [pcbnew/Layer.B.Paste] 196 | Enabled=1 197 | [pcbnew/Layer.F.Paste] 198 | Enabled=1 199 | [pcbnew/Layer.B.SilkS] 200 | Enabled=1 201 | [pcbnew/Layer.F.SilkS] 202 | Enabled=1 203 | [pcbnew/Layer.B.Mask] 204 | Enabled=1 205 | [pcbnew/Layer.F.Mask] 206 | Enabled=1 207 | [pcbnew/Layer.Dwgs.User] 208 | Enabled=1 209 | [pcbnew/Layer.Cmts.User] 210 | Enabled=1 211 | [pcbnew/Layer.Eco1.User] 212 | Enabled=1 213 | [pcbnew/Layer.Eco2.User] 214 | Enabled=1 215 | [pcbnew/Layer.Edge.Cuts] 216 | Enabled=1 217 | [pcbnew/Layer.Margin] 218 | Enabled=1 219 | [pcbnew/Layer.B.CrtYd] 220 | Enabled=1 221 | [pcbnew/Layer.F.CrtYd] 222 | Enabled=1 223 | [pcbnew/Layer.B.Fab] 224 | Enabled=1 225 | [pcbnew/Layer.F.Fab] 226 | Enabled=1 227 | [pcbnew/Layer.Rescue] 228 | Enabled=0 229 | [pcbnew/Netclasses] 230 | [pcbnew/Netclasses/Default] 231 | Name=Default 232 | Clearance=0.2 233 | TrackWidth=0.25 234 | ViaDiameter=0.8 235 | ViaDrill=0.4 236 | uViaDiameter=0.3 237 | uViaDrill=0.1 238 | dPairWidth=0.2 239 | dPairGap=0.25 240 | dPairViaGap=0.25 241 | -------------------------------------------------------------------------------- /pcbs/bottom/bottom.pro: -------------------------------------------------------------------------------- 1 | update=3/23/2021 10:49:01 AM 2 | version=1 3 | last_client=kicad 4 | [general] 5 | version=1 6 | RootSch= 7 | BoardNm= 8 | [cvpcb] 9 | version=1 10 | NetIExt=net 11 | [eeschema] 12 | version=1 13 | LibDir= 14 | [eeschema/libraries] 15 | [pcbnew] 16 | version=1 17 | PageLayoutDescrFile= 18 | LastNetListRead= 19 | CopperLayerCount=2 20 | BoardThickness=1.6 21 | AllowMicroVias=0 22 | AllowBlindVias=0 23 | RequireCourtyardDefinitions=0 24 | ProhibitOverlappingCourtyards=1 25 | MinTrackWidth=0.2 26 | MinViaDiameter=0.4 27 | MinViaDrill=0.3 28 | MinMicroViaDiameter=0.2 29 | MinMicroViaDrill=0.09999999999999999 30 | MinHoleToHole=0.25 31 | TrackWidth1=0.25 32 | TrackWidth2=1 33 | TrackWidth3=2 34 | ViaDiameter1=0.8 35 | ViaDrill1=0.4 36 | dPairWidth1=0.2 37 | dPairGap1=0.25 38 | dPairViaGap1=0.25 39 | SilkLineWidth=0.12 40 | SilkTextSizeV=1 41 | SilkTextSizeH=1 42 | SilkTextSizeThickness=0.15 43 | SilkTextItalic=0 44 | SilkTextUpright=1 45 | CopperLineWidth=0.2 46 | CopperTextSizeV=1.5 47 | CopperTextSizeH=1.5 48 | CopperTextThickness=0.3 49 | CopperTextItalic=0 50 | CopperTextUpright=1 51 | EdgeCutLineWidth=0.05 52 | CourtyardLineWidth=0.05 53 | OthersLineWidth=0.15 54 | OthersTextSizeV=1 55 | OthersTextSizeH=1 56 | OthersTextSizeThickness=0.15 57 | OthersTextItalic=0 58 | OthersTextUpright=1 59 | SolderMaskClearance=0.0508 60 | SolderMaskMinWidth=0 61 | SolderPasteClearance=0 62 | SolderPasteRatio=-0 63 | [pcbnew/Layer.F.Cu] 64 | Name=F.Cu 65 | Type=0 66 | Enabled=1 67 | [pcbnew/Layer.In1.Cu] 68 | Name=In1.Cu 69 | Type=0 70 | Enabled=0 71 | [pcbnew/Layer.In2.Cu] 72 | Name=In2.Cu 73 | Type=0 74 | Enabled=0 75 | [pcbnew/Layer.In3.Cu] 76 | Name=In3.Cu 77 | Type=0 78 | Enabled=0 79 | [pcbnew/Layer.In4.Cu] 80 | Name=In4.Cu 81 | Type=0 82 | Enabled=0 83 | [pcbnew/Layer.In5.Cu] 84 | Name=In5.Cu 85 | Type=0 86 | Enabled=0 87 | [pcbnew/Layer.In6.Cu] 88 | Name=In6.Cu 89 | Type=0 90 | Enabled=0 91 | [pcbnew/Layer.In7.Cu] 92 | Name=In7.Cu 93 | Type=0 94 | Enabled=0 95 | [pcbnew/Layer.In8.Cu] 96 | Name=In8.Cu 97 | Type=0 98 | Enabled=0 99 | [pcbnew/Layer.In9.Cu] 100 | Name=In9.Cu 101 | Type=0 102 | Enabled=0 103 | [pcbnew/Layer.In10.Cu] 104 | Name=In10.Cu 105 | Type=0 106 | Enabled=0 107 | [pcbnew/Layer.In11.Cu] 108 | Name=In11.Cu 109 | Type=0 110 | Enabled=0 111 | [pcbnew/Layer.In12.Cu] 112 | Name=In12.Cu 113 | Type=0 114 | Enabled=0 115 | [pcbnew/Layer.In13.Cu] 116 | Name=In13.Cu 117 | Type=0 118 | Enabled=0 119 | [pcbnew/Layer.In14.Cu] 120 | Name=In14.Cu 121 | Type=0 122 | Enabled=0 123 | [pcbnew/Layer.In15.Cu] 124 | Name=In15.Cu 125 | Type=0 126 | Enabled=0 127 | [pcbnew/Layer.In16.Cu] 128 | Name=In16.Cu 129 | Type=0 130 | Enabled=0 131 | [pcbnew/Layer.In17.Cu] 132 | Name=In17.Cu 133 | Type=0 134 | Enabled=0 135 | [pcbnew/Layer.In18.Cu] 136 | Name=In18.Cu 137 | Type=0 138 | Enabled=0 139 | [pcbnew/Layer.In19.Cu] 140 | Name=In19.Cu 141 | Type=0 142 | Enabled=0 143 | [pcbnew/Layer.In20.Cu] 144 | Name=In20.Cu 145 | Type=0 146 | Enabled=0 147 | [pcbnew/Layer.In21.Cu] 148 | Name=In21.Cu 149 | Type=0 150 | Enabled=0 151 | [pcbnew/Layer.In22.Cu] 152 | Name=In22.Cu 153 | Type=0 154 | Enabled=0 155 | [pcbnew/Layer.In23.Cu] 156 | Name=In23.Cu 157 | Type=0 158 | Enabled=0 159 | [pcbnew/Layer.In24.Cu] 160 | Name=In24.Cu 161 | Type=0 162 | Enabled=0 163 | [pcbnew/Layer.In25.Cu] 164 | Name=In25.Cu 165 | Type=0 166 | Enabled=0 167 | [pcbnew/Layer.In26.Cu] 168 | Name=In26.Cu 169 | Type=0 170 | Enabled=0 171 | [pcbnew/Layer.In27.Cu] 172 | Name=In27.Cu 173 | Type=0 174 | Enabled=0 175 | [pcbnew/Layer.In28.Cu] 176 | Name=In28.Cu 177 | Type=0 178 | Enabled=0 179 | [pcbnew/Layer.In29.Cu] 180 | Name=In29.Cu 181 | Type=0 182 | Enabled=0 183 | [pcbnew/Layer.In30.Cu] 184 | Name=In30.Cu 185 | Type=0 186 | Enabled=0 187 | [pcbnew/Layer.B.Cu] 188 | Name=B.Cu 189 | Type=0 190 | Enabled=1 191 | [pcbnew/Layer.B.Adhes] 192 | Enabled=1 193 | [pcbnew/Layer.F.Adhes] 194 | Enabled=1 195 | [pcbnew/Layer.B.Paste] 196 | Enabled=1 197 | [pcbnew/Layer.F.Paste] 198 | Enabled=1 199 | [pcbnew/Layer.B.SilkS] 200 | Enabled=1 201 | [pcbnew/Layer.F.SilkS] 202 | Enabled=1 203 | [pcbnew/Layer.B.Mask] 204 | Enabled=1 205 | [pcbnew/Layer.F.Mask] 206 | Enabled=1 207 | [pcbnew/Layer.Dwgs.User] 208 | Enabled=1 209 | [pcbnew/Layer.Cmts.User] 210 | Enabled=1 211 | [pcbnew/Layer.Eco1.User] 212 | Enabled=1 213 | [pcbnew/Layer.Eco2.User] 214 | Enabled=1 215 | [pcbnew/Layer.Edge.Cuts] 216 | Enabled=1 217 | [pcbnew/Layer.Margin] 218 | Enabled=1 219 | [pcbnew/Layer.B.CrtYd] 220 | Enabled=1 221 | [pcbnew/Layer.F.CrtYd] 222 | Enabled=1 223 | [pcbnew/Layer.B.Fab] 224 | Enabled=1 225 | [pcbnew/Layer.F.Fab] 226 | Enabled=1 227 | [pcbnew/Layer.Rescue] 228 | Enabled=0 229 | [pcbnew/Netclasses] 230 | [pcbnew/Netclasses/Default] 231 | Name=Default 232 | Clearance=0.2 233 | TrackWidth=0.25 234 | ViaDiameter=0.8 235 | ViaDrill=0.4 236 | uViaDiameter=0.3 237 | uViaDrill=0.1 238 | dPairWidth=0.2 239 | dPairGap=0.25 240 | dPairViaGap=0.25 241 | -------------------------------------------------------------------------------- /code/config/config_ea.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration for the Elle & Ale clock 3 | ''' 4 | 5 | import datetime 6 | from wordclock.configdefs import Word, BirthDate, SpecialBirthday 7 | 8 | VERSION_2 = True 9 | 10 | CLOCK_NAME = "Elle & Ale" 11 | 12 | LAT = 28.610940839823634 13 | LON = -81.33362929069895 14 | 15 | IT = Word('it', 0, 0, False) 16 | TWIST = Word('twist', 0, 1, False) 17 | WIST = Word('wist', 0, 2, False) 18 | IS = Word('is', 0, 3, False) 19 | T_HALF = Word('half', 0, 6, False) 20 | A = Word('a', 0, 7, False) 21 | FOR = Word('for', 0, 9, False) 22 | OR = Word('or', 0, 10, False) 23 | 24 | MA = Word('ma', 1, 0, False) 25 | MASH = Word('mash', 1, 0, False) 26 | A_1 = Word('a', 1, 1, False) 27 | AS = Word('as', 1, 1, False) 28 | ASH = Word('ash', 1, 1, False) 29 | HA_1 = Word('ha', 1, 3, False) 30 | HAP = Word('hap', 1, 3, False) 31 | HAPPY = Word('happy', 1, 3, False) 32 | A_2 = Word('a', 1, 4, False) 33 | APP = Word('app', 1, 4, False) 34 | PYE = Word('pye', 1, 6, False) 35 | YE = Word('ye', 1, 7, False) 36 | YET = Word('yet', 1, 7, False) 37 | T_TEN = Word('ten', 1, 9, False) 38 | 39 | T_TWENTY = Word('twenty', 2, 0, False) 40 | T_TWENTYFIVE = Word('twentyfive', 2, 0, False) 41 | WE = Word('we', 2, 1, False) 42 | WEN = Word('wen', 2, 1, False) 43 | WENT = Word('went', 2, 1, False) 44 | T_FIVE = Word('five', 2, 6, False) 45 | EAR = Word('ear', 2, 9, False) 46 | A_3 = Word('a', 2, 10, False) 47 | 48 | QUA = Word('qua', 3, 0, False) 49 | QUART = Word('quart', 3, 0, False) 50 | T_QUARTER = Word('quarter', 3, 0, False) 51 | A_4 = Word('a', 3, 2, False) 52 | ART = Word('art', 3, 2, False) 53 | ERE = Word('ere', 3, 5, False) 54 | REP = Word('rep', 3, 6, False) 55 | REPAST = Word('repast', 3, 6, False) 56 | PA = Word('pa', 3, 8, False) 57 | PAS = Word('pas', 3, 8, False) 58 | PAST = Word('past', 3, 8, False) 59 | A_5 = Word('a', 3, 9, False) 60 | AS_1 = Word('as', 3, 9, False) 61 | 62 | OR_1 = Word('or', 4, 0, False) 63 | ORB = Word('orb', 4, 0, False) 64 | BIRTH = Word('birth', 4, 2, False) 65 | BIRTHDAY = Word('birthday', 4, 2, False) 66 | DAY = Word('day', 4, 7, False) 67 | A_6 = Word('a', 4, 8, False) 68 | TO = Word('to', 4, 10, False) 69 | 70 | H_FIVE = Word('five', 5, 1, False) 71 | FOU = Word('fou', 5, 5, False) 72 | H_FOUR = Word('four', 5, 5, False) 73 | OUR = Word('our', 5, 6, False) 74 | OURS = Word('ours', 5, 6, False) 75 | H_SIX = Word('six', 5, 9, False) 76 | 77 | A_7 = Word('a', 6, 1, False) 78 | AUGGIE = Word('auggie', 6, 1, False) 79 | ELL = Word('ell', 6, 6, False) 80 | ELLE = Word('elle', 6, 6, False) 81 | EON = Word('eon', 6, 9, False) 82 | ON = Word('on', 6, 10, False) 83 | 84 | ELIAH = Word('eliah', 7, 2, False) 85 | A_8 = Word('a', 7, 5, False) 86 | AH = Word('ah', 7, 5, False) 87 | HE = Word('he', 7, 6, False) 88 | HEIGHT = Word('height', 7, 6, False) 89 | H_EIGHT = Word('eight', 7, 7, False) 90 | 91 | VET = Word('vet', 8, 0, False) 92 | H_TWO = Word('two', 8, 2, False) 93 | WON = Word('won', 8, 3, False) 94 | ON_1 = Word('on', 8, 4, False) 95 | H_ONE = Word('one', 8, 4, False) 96 | NET = Word('net', 8, 5, False) 97 | H_THREE = Word('three', 8, 7, False) 98 | 99 | H_SEVEN = Word('seven', 9, 1, False) 100 | EVE = Word('eve', 9, 2, False) 101 | EVEN = Word('even', 9, 2, False) 102 | EVENT = Word('event', 9, 2, False) 103 | VENT = Word('vent', 9, 3, False) 104 | H_TWELVE = Word('twelve', 9, 6, False) 105 | WE_1 = Word('we', 9, 7, False) 106 | 107 | H_NINE = Word('nine', 10, 0, False) 108 | IN = Word('in', 10, 1, False) 109 | OCLOCK = Word('oclock', 10, 6, False) 110 | CLOCK = Word('clock', 10, 7, False) 111 | LO = Word('lo', 10, 8, False) 112 | LOCK = Word('lock', 10, 8, False) 113 | 114 | H_ELEVEN = Word('eleven', 5, 0, True) 115 | EVE_1 = Word('eve', 7, 0, True) 116 | EVEN_1 = Word('even', 7, 0, True) 117 | 118 | TAW = Word('taw', 0, 1, True) 119 | AW = Word('aw', 1, 1, True) 120 | ALE = Word('ale', 6, 1, True) 121 | 122 | SEA = Word('sea', 1, 2, True) 123 | H_TEN = Word('ten', 8, 2, True) 124 | 125 | SAT = Word('sat', 0, 4, True) 126 | AT = Word('at', 1, 4, True) 127 | 128 | PYE_1 = Word('pye', 1, 5, True) 129 | YE_1 = Word('ye', 2, 5, True) 130 | YET_1 = Word('yet', 2, 5, True) 131 | AN = Word('an', 7, 5, True) 132 | ANN = Word('ann', 7, 5, True) 133 | 134 | RHO = Word('rho', 3, 6, True) 135 | HOE = Word('hoe', 4, 6, True) 136 | HE_1 = Word('he', 7, 6, True) 137 | TO_1 = Word('to', 9, 6, True) 138 | 139 | LET = Word('let', 6, 7, True) 140 | 141 | PAR = Word('par', 3, 8, True) 142 | HE_2 = Word('he', 8, 8, True) 143 | 144 | TEA = Word('tea', 1, 9, True) 145 | LO_1 = Word('lo', 9, 9, True) 146 | 147 | EAST = Word('east', 1, 10, True) 148 | AS_2 = Word('as', 2, 10, True) 149 | OH = Word('oh', 6, 10, True) 150 | HE_3 = Word('he', 7, 10, True) 151 | 152 | TO_2 = Word('to', 3, 11, True) 153 | OX = Word('ox', 4, 11, True) 154 | TEE = Word('tee', 7, 11, True) 155 | EEK = Word('eek', 8, 11, True) 156 | 157 | BIRTHDAYS = { 158 | BirthDate(8, 25): ELLE, 159 | BirthDate(5, 13): ALE, 160 | BirthDate(6, 5): ELIAH, 161 | BirthDate(8, 3): AUGGIE, 162 | } 163 | 164 | SPECIAL_BIRTHDAY = SpecialBirthday( 165 | ELLE, 166 | datetime.date(2021, 9, 12), 167 | datetime.date(2021, 9, 19)) 168 | -------------------------------------------------------------------------------- /code/config/config_dp.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration for the Doug & Paty clock 3 | ''' 4 | 5 | from wordclock.configdefs import Word, BirthDate 6 | 7 | VERSION_2 = True 8 | 9 | CLOCK_NAME = 'Doug & Paty' 10 | 11 | LAT = 38.9865696556791 12 | LON = -77.01922719335109 13 | 14 | IT = Word('it', 0, 0, False) 15 | TWIST = Word('twist', 0, 1, False) 16 | WIST = Word('wist', 0, 2, False) 17 | IS = Word('is', 0, 3, False) 18 | HA = Word('ha', 0, 6, False) 19 | T_HALF = Word('half', 0, 6, False) 20 | A = Word('a', 0, 7, False) 21 | FOR = Word('for', 0, 9, False) 22 | OR = Word('or', 0, 10, False) 23 | 24 | MA = Word('ma', 1, 0, False) 25 | MASH = Word('mash', 1, 0, False) 26 | A_1 = Word('a', 1, 1, False) 27 | AS = Word('as', 1, 1, False) 28 | ASH = Word('ash', 1, 1, False) 29 | HA_1 = Word('ha', 1, 3, False) 30 | HAP = Word('hap', 1, 3, False) 31 | HAPPY = Word('happy', 1, 3, False) 32 | A_2 = Word('a', 1, 4, False) 33 | APP = Word('app', 1, 4, False) 34 | PYE = Word('pye', 1, 6, False) 35 | YE = Word('ye', 1, 7, False) 36 | YET = Word('yet', 1, 7, False) 37 | T_TEN = Word('ten', 1, 9, False) 38 | 39 | T_TWENTY = Word('twenty', 2, 0, False) 40 | T_TWENTYFIVE = Word('twentyfive', 2, 0, False) 41 | WE = Word('we', 2, 1, False) 42 | WEN = Word('wen', 2, 1, False) 43 | WENT = Word('went', 2, 1, False) 44 | T_FIVE = Word('five', 2, 6, False) 45 | EAR = Word('ear', 2, 9, False) 46 | A_3 = Word('a', 2, 10, False) 47 | 48 | QUA = Word('qua', 3, 0, False) 49 | QUART = Word('quart', 3, 0, False) 50 | T_QUARTER = Word('quarter', 3, 0, False) 51 | A_4 = Word('a', 3, 2, False) 52 | ART = Word('art', 3, 2, False) 53 | ERE = Word('ere', 3, 5, False) 54 | REP = Word('rep', 3, 6, False) 55 | REPAST = Word('repast', 3, 6, False) 56 | PA = Word('pa', 3, 8, False) 57 | PAS = Word('pas', 3, 8, False) 58 | PAST = Word('past', 3, 8, False) 59 | A_5 = Word('a', 3, 9, False) 60 | AS_1 = Word('as', 3, 9, False) 61 | 62 | OR_1 = Word('or', 4, 0, False) 63 | ORB = Word('orb', 4, 0, False) 64 | BIRTH = Word('birth', 4, 2, False) 65 | BIRTHDAY = Word('birthday', 4, 2, False) 66 | DAY = Word('day', 4, 7, False) 67 | A_6 = Word('a', 4, 8, False) 68 | TO = Word('to', 4, 10, False) 69 | 70 | EON = Word('eon', 5, 0, False) 71 | ON = Word('on', 5, 1, False) 72 | H_ONE = Word('one', 5, 1, False) 73 | NET = Word('net', 5, 2, False) 74 | H_TWO = Word('two', 5, 4, False) 75 | WOT = Word('wot', 5, 5, False) 76 | H_THREE = Word('three', 5, 7, False) 77 | 78 | FOU = Word('fou', 6, 1, False) 79 | H_FOUR = Word('four', 6, 1, False) 80 | OUR = Word('our', 6, 2, False) 81 | H_FIVE = Word('five', 6, 5, False) 82 | SI = Word('si', 6, 9, False) 83 | H_SIX = Word('six', 6, 9, False) 84 | 85 | H_EIGHT = Word('eight', 7, 0, False) 86 | DO = Word('do', 7, 5, False) 87 | DOUG = Word('doug', 7, 5, False) 88 | H_TEN = Word('ten', 7, 9, False) 89 | 90 | PA_1 = Word('pa', 8, 1, False) 91 | PAT = Word('pat', 8, 1, False) 92 | PATY = Word('paty', 8, 1, False) 93 | A_7 = Word('a', 8, 2, False) 94 | AT = Word('at', 8, 2, False) 95 | LUCAS = Word('lucas', 8, 5, False) 96 | CASA = Word('casa', 8, 7, False) 97 | A_8 = Word('a', 8, 8, False) 98 | AS_2 = Word('as', 8, 8, False) 99 | SAT = Word('sat', 8, 9, False) 100 | A_9 = Word('a', 8, 10, False) 101 | AT_1 = Word('at', 8, 10, False) 102 | 103 | H_SEVEN = Word('seven', 9, 1, False) 104 | EVE = Word('eve', 9, 2, False) 105 | EVEN = Word('even', 9, 2, False) 106 | EVENT = Word('event', 9, 2, False) 107 | VENT = Word('vent', 9, 3, False) 108 | H_TWELVE = Word('twelve', 9, 6, False) 109 | WE_1 = Word('we', 9, 7, False) 110 | 111 | H_NINE = Word('nine', 10, 0, False) 112 | IN = Word('in', 10, 1, False) 113 | OCLOCK = Word('oclock', 10, 6, False) 114 | CLOCK = Word('clock', 10, 7, False) 115 | LO = Word('lo', 10, 8, False) 116 | LOCK = Word('lock', 10, 8, False) 117 | 118 | H_ELEVEN = Word('eleven', 5, 0, True) 119 | EVE_1 = Word('eve', 7, 0, True) 120 | EVEN_1 = Word('even', 7, 0, True) 121 | 122 | TAW = Word('taw', 0, 1, True) 123 | AW = Word('aw', 1, 1, True) 124 | OF = Word('of', 5, 1, True) 125 | SI_1 = Word('si', 9, 1, True) 126 | 127 | SEA = Word('sea', 1, 2, True) 128 | NO = Word('no', 5, 2, True) 129 | NOG = Word('nog', 5, 2, True) 130 | 131 | UH = Word('uh', 6, 3, True) 132 | 133 | SAT_1 = Word('sat', 0, 4, True) 134 | AT_2 = Word('at', 1, 4, True) 135 | YE_1 = Word('ye', 8, 4, True) 136 | 137 | PYE_1 = Word('pye', 1, 5, True) 138 | YE_2 = Word('ye', 2, 5, True) 139 | YET_1 = Word('yet', 2, 5, True) 140 | 141 | RHO = Word('rho', 3, 6, True) 142 | HO = Word('ho', 4, 6, True) 143 | OUT = Word('out', 7, 6, True) 144 | TO_1 = Word('to', 9, 6, True) 145 | 146 | PA_2 = Word('pa', 3, 8, True) 147 | AH = Word('ah', 4, 8, True) 148 | HE = Word('he', 5, 8, True) 149 | 150 | TEA = Word('tea', 1, 9, True) 151 | LO_1 = Word('lo', 9, 9, True) 152 | 153 | EAST = Word('east', 1, 10, True) 154 | AS_3 = Word('as', 2, 10, True) 155 | 156 | TO_2 = Word('to', 3, 11, True) 157 | TOE = Word('toe', 3, 11, True) 158 | 159 | 160 | BIRTHDAYS = { 161 | BirthDate(1, 31): DOUG, 162 | BirthDate(4, 22): PATY, 163 | BirthDate(4, 1): LUCAS, 164 | } 165 | 166 | SPECIAL_BIRTHDAY = None 167 | -------------------------------------------------------------------------------- /code/makefile: -------------------------------------------------------------------------------- 1 | SHELL = /usr/bin/bash 2 | 3 | # The base filename of the clock-specific configuration 4 | CONFIG = mb 5 | 6 | # The name of the AWS S3 bucket for logging and updates. 7 | # Undefine this to disable the cron jobs 8 | S3_BUCKET = mfs-wordclock 9 | 10 | APT_PACKAGES = emacs hostapd dnsmasq 11 | 12 | PYTHON_PACKAGES = RPI.GPIO adafruit-blinka rpi_ws281x adafruit-circuitpython-neopixel \ 13 | adafruit-circuitpython-veml7700 adafruit-circuitpython-mlx90393 sparkfun-qwiic-adxl313 \ 14 | pylint aiohttp timezonefinder pytz astral boto3 geomag wpasupplicantconf 15 | 16 | VARS_DIR = /var/wordclock 17 | UPDATES_DIR = $(VARS_DIR)/updates 18 | WEB_DIR = $(VARS_DIR)/website 19 | WEB_STATIC_DIR = $(WEB_DIR)/static 20 | 21 | WEB_STATIC_FILES = favicon.ico script.js jquery.min.js w3.css 22 | WEB_TARGET_FILES = $(WEB_DIR)/body.html $(addprefix $(WEB_STATIC_DIR)/,$(WEB_STATIC_FILES)) 23 | 24 | PACKAGE = wordclock 25 | CONFIG_SRC_FILE = config/config_$(CONFIG).py 26 | CONFIG_FILE = $(PACKAGE)/config.py 27 | S3_CONFIG_FILE = $(PACKAGE)/s3config.py 28 | 29 | TIMESTAMP_FILE = $(PACKAGE)/timestamp.txt 30 | 31 | AUTHORIZED_KEYS_FILE = /home/pi/.ssh/authorized_keys 32 | 33 | SERVICE = wc 34 | SERVICE_SRC_FILE = $(SERVICE).service 35 | SERVICE_FILE = /etc/systemd/system/$(SERVICE_SRC_FILE) 36 | 37 | ifdef S3_BUCKET 38 | CRON_FILES = /etc/cron.daily/wc-send-log /etc/cron.hourly/wc-get-update 39 | endif 40 | 41 | HOTSPOT = /usr/local/sbin/hotspot 42 | 43 | 44 | # Install the word clock software 45 | # 46 | .PHONY: all 47 | all: $(TIMESTAMP_FILE) $(SERVICE_FILE) $(WEB_TARGET_FILES) $(CRON_FILES) $(UPDATES_DIR) 48 | 49 | # Install all prerequisite packages and do other necessary configuration 50 | # 51 | .PHONY: setup 52 | setup: python3 packages hostname /etc/dnsmasq.conf config_hotspot /root/.aws/credentials 53 | 54 | # For debugging: (re)install the software and restart the systemd wc service 55 | # 56 | .PHONY: reload 57 | reload: all 58 | systemctl restart $(SERVICE) 59 | 60 | # Start the systemd wc service 61 | # 62 | .PHONY: start 63 | start: 64 | systemctl start $(SERVICE) 65 | 66 | # Stop the systemd wc service 67 | # 68 | .PHONY: stop 69 | stop: 70 | systemctl stop $(SERVICE) 71 | 72 | .PHONY: clean 73 | clean: 74 | -rm $(TIMESTAMP_FILE) $(CONFIG_FILE) $(S3_CONFIG_FILE) 75 | 76 | # Configure the sshd daemon. This assumes you've modified the authorized_keys file to 77 | # permit login with your own key pair. 78 | # - Disable password login 79 | # - Disable root login 80 | # 81 | .PHONY: config_sshd 82 | config_sshd: $(AUTHORIZED_KEYS_FILE) 83 | sed '/^#PasswordAuth/s/^#PasswordAuth.*/PasswordAuthentication no/' --in-place /etc/ssh/sshd_config 84 | sed '/^#PermitRootLogin/s/^#PermitRootLogin.*/PermitRootLogin no/' --in-place /etc/ssh/sshd_config 85 | systemctl restart sshd 86 | 87 | .PHONY: python3 88 | python3: 89 | if [[ ! "$$(python --version 2>&1)" =~ "Python 3".* ]]; then \ 90 | pip3 install --upgrade setuptools; \ 91 | update-alternatives --install /usr/bin/python python $$(which python2) 1; \ 92 | update-alternatives --install /usr/bin/python python $$(which python2) 1; \ 93 | fi 94 | 95 | .PHONY: packages 96 | packages: 97 | apt -y install $(APT_PACKAGES) 98 | pip3 install $(PYTHON_PACKAGES) 99 | 100 | .PHONY: hostname 101 | hostname: 102 | if ! hostname | grep wordclock; then hostname wordclock; fi 103 | if ! grep wordclock /etc/hostname; then echo wordclock > /etc/hostname; fi 104 | sed '/^127.0.1.1.*raspberrypi/s/127.*/127.0.1.1\twordclock/' --in-place /etc/hosts 105 | 106 | $(HOTSPOT): 107 | cd /usr/local/sbin; \ 108 | wget https://raw.githubusercontent.com/rudiratlos/hotspot/master/hotspot; \ 109 | chmod +x hotspot 110 | 111 | .PHONY: config_hotspot 112 | config_hotspot: $(HOTSPOT) 113 | sed '/^ap_net/s/10.3.141/10.0.0/' --in-place $(HOTSPOT) 114 | hotspot modpar self aptaddinstlist "" 115 | hotspot setup 116 | hotspot modpar hostapd ssid wordclock 117 | hotspot modpar hostapd wpa_passphrase wordclock 118 | hotspot modpar hostapd country_code US 119 | hotspot modpar crda REGDOMAIN US 120 | 121 | /root/.aws/credentials: 122 | mkdir -p /root/.aws 123 | chmod 750 /root/.aws 124 | cp aws_credentials /root/.aws/credentials 125 | chmod 600 /root/.aws/credentials 126 | 127 | /etc/dnsmasq.conf: dnsmasq.conf 128 | cp -f $^ $@ 129 | 130 | AUTHORIZED_KEYS_FILE: authorized_keys 131 | cp -f $^ $@ 132 | 133 | $(TIMESTAMP_FILE): $(PACKAGE)/*.py $(CONFIG_FILE) $(S3_CONFIG_FILE) 134 | pip3 install -e . 135 | echo date > $@ 136 | 137 | $(VARS_DIR): 138 | mkdir -p $@ 139 | 140 | $(UPDATES_DIR): 141 | mkdir -p $@ 142 | 143 | $(CONFIG_FILE): $(CONFIG_SRC_FILE) 144 | cp -f $^ $@ 145 | 146 | $(S3_CONFIG_FILE): 147 | echo -e "S3_BUCKET = \"$(S3_BUCKET)\"" > $@ 148 | 149 | $(WEB_DIR)/body.html: website/body.html 150 | mkdir -p $(WEB_DIR) 151 | cp -f $^ $@ 152 | 153 | $(WEB_STATIC_DIR)/jquery.min.js: 154 | mkdir -p $(WEB_STATIC_DIR) 155 | cd $(WEB_STATIC_DIR); wget https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js 156 | 157 | $(WEB_STATIC_DIR)/w3.css: 158 | mkdir -p $(WEB_STATIC_DIR) 159 | cd $(WEB_STATIC_DIR); wget https://www.w3schools.com/w3css/4/w3.css 160 | 161 | $(WEB_STATIC_DIR)/favicon.ico: website/favicon.ico 162 | mkdir -p $(WEB_STATIC_DIR) 163 | cp -f $^ $@ 164 | 165 | $(WEB_STATIC_DIR)/script.js: website/script.js 166 | mkdir -p $(WEB_STATIC_DIR) 167 | cp -f $^ $@ 168 | 169 | $(SERVICE_FILE): $(SERVICE_SRC_FILE) 170 | cp -f $^ $@ 171 | systemctl enable $(SERVICE) 172 | 173 | /etc/cron.daily/wc-send-log: cron/wc-send-log 174 | cp -f $^ $@ 175 | chmod 755 $@ 176 | 177 | /etc/cron.hourly/wc-get-update: cron/wc-get-update 178 | cp -f $^ $@ 179 | chmod 755 $@ 180 | -------------------------------------------------------------------------------- /code/config/config_ht.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration for the Hans & Tomomi clock 3 | ''' 4 | 5 | from wordclock.configdefs import Word, BirthDate 6 | 7 | VERSION_2 = True 8 | 9 | CLOCK_NAME = 'Hans & Tomomi' 10 | 11 | LAT = 37.38770336913209 12 | LON = -122.08460007293597 13 | 14 | IT = Word('it', 0, 0, False) 15 | TWIST = Word('twist', 0, 1, False) 16 | WIST = Word('wist', 0, 2, False) 17 | IS = Word('is', 0, 3, False) 18 | HA = Word('ha', 0, 6, False) 19 | T_HALF = Word('half', 0, 6, False) 20 | A = Word('a', 0, 7, False) 21 | FOR = Word('for', 0, 9, False) 22 | OR = Word('or', 0, 10, False) 23 | 24 | MA = Word('ma', 1, 0, False) 25 | MASH = Word('mash', 1, 0, False) 26 | A_1 = Word('a', 1, 1, False) 27 | AS = Word('as', 1, 1, False) 28 | ASH = Word('ash', 1, 1, False) 29 | HA_1 = Word('ha', 1, 3, False) 30 | HAP = Word('hap', 1, 3, False) 31 | HAPPY = Word('happy', 1, 3, False) 32 | A_2 = Word('a', 1, 4, False) 33 | APP = Word('app', 1, 4, False) 34 | PYE = Word('pye', 1, 6, False) 35 | YE = Word('ye', 1, 7, False) 36 | YET = Word('yet', 1, 7, False) 37 | T_TEN = Word('ten', 1, 9, False) 38 | 39 | T_TWENTY = Word('twenty', 2, 0, False) 40 | T_TWENTYFIVE = Word('twentyfive', 2, 0, False) 41 | WE = Word('we', 2, 1, False) 42 | WEN = Word('wen', 2, 1, False) 43 | WENT = Word('went', 2, 1, False) 44 | T_FIVE = Word('five', 2, 6, False) 45 | EAR = Word('ear', 2, 9, False) 46 | A_3 = Word('a', 2, 10, False) 47 | 48 | QUA = Word('qua', 3, 0, False) 49 | QUART = Word('quart', 3, 0, False) 50 | T_QUARTER = Word('quarter', 3, 0, False) 51 | A_4 = Word('a', 3, 2, False) 52 | ART = Word('art', 3, 2, False) 53 | ERE = Word('ere', 3, 5, False) 54 | REP = Word('rep', 3, 6, False) 55 | REPAST = Word('repast', 3, 6, False) 56 | PA = Word('pa', 3, 8, False) 57 | PAS = Word('pas', 3, 8, False) 58 | PAST = Word('past', 3, 8, False) 59 | A_5 = Word('a', 3, 9, False) 60 | AS_1 = Word('as', 3, 9, False) 61 | 62 | OR_1 = Word('or', 4, 0, False) 63 | ORB = Word('orb', 4, 0, False) 64 | BIRTH = Word('birth', 4, 2, False) 65 | BIRTHDAY = Word('birthday', 4, 2, False) 66 | DAY = Word('day', 4, 7, False) 67 | A_6 = Word('a', 4, 8, False) 68 | TO = Word('to', 4, 10, False) 69 | 70 | EON = Word('eon', 5, 0, False) 71 | ON = Word('on', 5, 1, False) 72 | H_ONE = Word('one', 5, 1, False) 73 | NE = Word('ne', 5, 2, False) 74 | NET = Word('net', 5, 2, False) 75 | H_TWO = Word('two', 5, 4, False) 76 | WOT = Word('wot', 5, 5, False) 77 | H_THREE = Word('three', 5, 7, False) 78 | 79 | FOU = Word('fou', 6, 1, False) 80 | H_FOUR = Word('four', 6, 1, False) 81 | OUR = Word('our', 6, 2, False) 82 | H_FIVE = Word('five', 6, 5, False) 83 | H_SIX = Word('six', 6, 9, False) 84 | 85 | H_EIGHT = Word('eight', 7, 0, False) 86 | EIGHTH = Word('eighth', 7, 0, False) 87 | THAN = Word('than', 7, 4, False) 88 | HA_2 = Word('ha', 7, 5, False) 89 | HANS = Word('hans', 7, 5, False) 90 | A_7 = Word('a', 7, 6, False) 91 | AN = Word('an', 7, 6, False) 92 | SO = Word('so', 7, 8, False) 93 | SOAR = Word('soar', 7, 8, False) 94 | OAR = Word('oar', 7, 9, False) 95 | A_8 = Word('a', 7, 10, False) 96 | 97 | VET = Word('vet', 8, 0, False) 98 | VETO = Word('veto', 8, 0, False) 99 | TO_1 = Word('to', 8, 2, False) 100 | TOM = Word('tom', 8, 2, False) 101 | TOMOMI = Word('tomomi', 8, 2, False) 102 | MOM = Word('mom', 8, 4, False) 103 | OMIT = Word('omit', 8, 5, False) 104 | MITT = Word('mitt', 8, 6, False) 105 | MITTEN = Word('mitten', 8, 6, False) 106 | IT_1 = Word('it', 8, 7, False) 107 | H_TEN = Word('ten', 8, 9, False) 108 | 109 | H_SEVEN = Word('seven', 9, 1, False) 110 | EVE = Word('eve', 9, 2, False) 111 | EVEN = Word('even', 9, 2, False) 112 | EVENT = Word('event', 9, 2, False) 113 | VENT = Word('vent', 9, 3, False) 114 | H_TWELVE = Word('twelve', 9, 6, False) 115 | WE_1 = Word('we', 9, 7, False) 116 | 117 | H_NINE = Word('nine', 10, 0, False) 118 | IN = Word('in', 10, 1, False) 119 | OCLOCK = Word('oclock', 10, 6, False) 120 | CLOCK = Word('clock', 10, 7, False) 121 | LO = Word('lo', 10, 8, False) 122 | LOCK = Word('lock', 10, 8, False) 123 | 124 | H_ELEVEN = Word('eleven', 5, 0, True) 125 | EVE_1 = Word('eve', 7, 0, True) 126 | EVEN_1 = Word('even', 7, 0, True) 127 | 128 | TAW = Word('taw', 0, 1, True) 129 | AW = Word('aw', 1, 1, True) 130 | OF = Word('of', 5, 1, True) 131 | FIE = Word('fie', 6, 1, True) 132 | 133 | SEA = Word('sea', 1, 2, True) 134 | NO = Word('no', 5, 2, True) 135 | NOG = Word('nog', 5, 2, True) 136 | H_TEN = Word('ten', 8, 2, True) 137 | 138 | UH = Word('uh', 6, 3, True) 139 | HO = Word('ho', 7, 3, True) 140 | HOVE = Word('hove', 7, 3, True) 141 | 142 | SAT = Word('sat', 0, 4, True) 143 | AT = Word('at', 1, 4, True) 144 | ME = Word('me', 8, 4, True) 145 | 146 | PYE_1 = Word('pye', 1, 5, True) 147 | YE_1 = Word('ye', 2, 5, True) 148 | YET_1 = Word('yet', 2, 5, True) 149 | HO_1 = Word('ho', 7, 5, True) 150 | HONK = Word('honk', 7, 5, True) 151 | ON_1 = Word('on', 8, 5, True) 152 | 153 | RHO = Word('rho', 3, 6, True) 154 | HO_2 = Word('ho', 4, 6, True) 155 | AM = Word('am', 7, 6, True) 156 | TO_2 = Word('to', 9, 6, True) 157 | 158 | 159 | PA_1 = Word('pa', 3, 8, True) 160 | PAH = Word('pah', 3, 8, True) 161 | AH = Word('ah', 4, 8, True) 162 | HE = Word('he', 5, 8, True) 163 | HEST = Word('hest', 5, 8, True) 164 | 165 | TEA = Word('tea', 1, 9, True) 166 | SO_1 = Word('so', 6, 9, True) 167 | SOT = Word('sot', 6, 9, True) 168 | LO_1 = Word('lo', 9, 9, True) 169 | 170 | EAST = Word('east', 1, 10, True) 171 | AS_2 = Word('as', 2, 10, True) 172 | 173 | TO_3 = Word('to', 3, 11, True) 174 | TOE = Word('toe', 3, 11, True) 175 | NE_2 = Word('ne', 8, 11, True) 176 | 177 | BIRTHDAYS = { 178 | BirthDate(12, 18): HANS, 179 | BirthDate(12, 21): TOMOMI, 180 | } 181 | 182 | SPECIAL_BIRTHDAY = None 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Word Clock 2 | 3 | *View a 4 | 5 | video tour of the word clock at YouTube.* 6 | 7 | *Read all about the project on the 8 | 9 | wiki.* 10 | 11 | 12 | Bill of Materials 13 | 14 | ## Repo Contents 15 | 16 | ### Directories 17 | 18 | See the `README` file in each directory for more information. 19 | 20 | | Directory | Description | 21 | |------|-------------| 22 | | carbide3d | Carbide Create designs for various clock components. | 23 | | code | The code that needs to be copied to the Pi. | 24 | | docs | The manual and other printed material. | 25 | | face | Adobe Illustrator face designs for the clocks I've built. | 26 | | pcbs | KiCad printed circuit board designs. | 27 | 28 | ### Files 29 | 30 | | File | Description | 31 | |------|-------------| 32 | | aws-s3-policy-example.json | An example AWS IAM policy file for granting the word clock S3 permissions. | 33 | | manual.docx | The user manual. | 34 | | pi-pinouts.md | What passes for a schematic. | 35 | 36 | ## How It Works 37 | 38 | ### The Clock Program 39 | 40 | The clock program, `wc`, runs as a systemd service. It must run as root to be able to 41 | access the I2C hardware. The program uses the Python `asyncio` framework 42 | for orchestrating real-time processes. You can also run the program from the command line, 43 | with options to display verbose debugging messages. 44 | 45 | ### Calibrating the Compass 46 | 47 | Each clock's magnetometer must be calibrated. The result of the calibration is stored 48 | as a JSON file, `/var/wordclock/compass.json`. The word clock program reads the file 49 | on startup. To perform a calibration, start the `calibrate` program and then slowly 50 | rotate the magnetometer through all possible orientations. 51 | 52 | ### Phoning Home 53 | 54 | The clock can be configured to phone home periodically, to upload systemd logs for 55 | the clock service, and to download and install new software. The clock phones home 56 | simply by reading and writing an AWS S3 bucket. 57 | 58 | The clock uploads a log file daily, and saves it as `logs//log-.txt`, 59 | where `` is the IAM user name you'll create, as described below. 60 | 61 | The clock checks for updates hourly. See the documentation in `wordclock/get_update.py` 62 | for more information. 63 | 64 | Here's what you'll need to do to make the phone-home feature work: 65 | 66 | 1. Create an S3 bucket. 67 | 2. Create an IAM User for your clock and grant it permission to access the bucket. 68 | The file `aws-s3-policy-example.json` is an example IAM policy. 69 | 3. Create AWS API credentials for the user. Download the credential strings and store 70 | them on the Pi, in the `[default]` section of file `/root/.aws/credentials`. 71 | 72 | ### Test Programs 73 | 74 | The Python package includes several test programs: 75 | 76 | | Program | Description 77 | |---|---| 78 | | tneo | Send test patterns to the neopixels. | 79 | | tsense | Test and report values for the sensors: magnetometer, accelerometer, and light sensor. | 80 | | calibrate | Calibrate the magnetometer. | 81 | 82 | ## Setting up the Pi 83 | 84 | Notes to myself: 85 | 86 | - Add my own ssh public key to `/home/pi/.ssh/authorized_keys`. 87 | 88 | - Allow ssh agent forwarding when root by creating a sudoers file. 89 | This will allow me to use ssh to access github. 90 | 91 | ``` 92 | visudo -f /etc/sudoers.d/ssh_agent_forwarding 93 | ``` 94 | 95 | In VI add this line and save the file: 96 | 97 | ``` 98 | Defaults env_keep += "SSH_AUTH_SOCK" 99 | ``` 100 | 101 | - Add this line to `/root/.ssh.config` so that I can ssh to other servers when root: 102 | 103 | ``` 104 | User 105 | ``` 106 | 107 | - Add wordclock- AWS credentials to `/root/.aws/credentials`: 108 | 109 | ``` 110 | [default] 111 | aws_access_key_id = 112 | aws_secret_access_key = 113 | ``` 114 | 115 | 116 | ## Installation 117 | 118 | 1. Using `raspi-config`, enable `i2c`. 119 | 120 | 2. Get the contents of this repo onto the Pi. 121 | 122 | ``` 123 | cd /home/pi 124 | git clone git@github.com:marksidell/wordclock.git 125 | ``` 126 | 127 | 3. Create a custom `config-.py` file for your particular clock. The config files are stored 128 | in the `config` directory. 129 | 130 | 4. Do one-time setup. This assumes you've modified the authorized_keys file to 131 | permit login with your own key pair. 132 | 133 | ```bash 134 | cd /code 135 | sudo make config_sshd 136 | sudo make setup 137 | ``` 138 | 139 | 5. Install everything: 140 | 141 | ```bash 142 | cd /code 143 | sudo make all [CONFIG=] [S3_BUCKET=] 144 | ``` 145 | -------------------------------------------------------------------------------- /code/website/body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {title} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

{title}

13 |
14 |
15 |
16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
20 | 26 |
Web Site: http://
Ambient: 9943 lumens
Brightness: 99 %
Up: 
Compass:  ° (Sunrise )
34 |
35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 | 47 | 48 |
49 |
50 | 51 | 53 | 54 |
55 |
56 | 57 | 62 |
63 |
64 | 65 | 67 | 68 |
69 |
70 | 71 | 73 | 74 |
75 |
76 | 77 | 79 | 80 |
81 |
82 | 83 | 85 | 86 |
87 |
88 | 89 | 91 | 92 |
93 |
94 | 95 | 97 | 98 |
99 |
100 | 101 | 106 |
107 |
108 |
109 | 110 |
111 |
112 | Version 113 |
114 |
115 |
116 |
117 | × 119 | 120 |

Futz With Brightness

121 |
122 |
123 | 124 | 100 % 125 |
126 |
127 | 131 |
132 |
133 | 134 |
135 |
136 | 137 |
138 |
139 |
140 |
141 | 142 | 143 | -------------------------------------------------------------------------------- /pcbs/backlight/backlight.kicad_pcb: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 20171130) (host pcbnew "(5.1.9)-1") 2 | 3 | (general 4 | (thickness 1.6) 5 | (drawings 4) 6 | (tracks 5) 7 | (zones 0) 8 | (modules 2) 9 | (nets 1) 10 | ) 11 | 12 | (page A4) 13 | (layers 14 | (0 F.Cu signal) 15 | (31 B.Cu signal) 16 | (32 B.Adhes user) 17 | (33 F.Adhes user) 18 | (34 B.Paste user) 19 | (35 F.Paste user) 20 | (36 B.SilkS user) 21 | (37 F.SilkS user) 22 | (38 B.Mask user) 23 | (39 F.Mask user) 24 | (40 Dwgs.User user) 25 | (41 Cmts.User user) 26 | (42 Eco1.User user) 27 | (43 Eco2.User user) 28 | (44 Edge.Cuts user) 29 | (45 Margin user) 30 | (46 B.CrtYd user) 31 | (47 F.CrtYd user) 32 | (48 B.Fab user) 33 | (49 F.Fab user) 34 | ) 35 | 36 | (setup 37 | (last_trace_width 1) 38 | (user_trace_width 1) 39 | (trace_clearance 0.2) 40 | (zone_clearance 0.508) 41 | (zone_45_only no) 42 | (trace_min 0.2) 43 | (via_size 0.8) 44 | (via_drill 0.4) 45 | (via_min_size 0.4) 46 | (via_min_drill 0.3) 47 | (uvia_size 0.3) 48 | (uvia_drill 0.1) 49 | (uvias_allowed no) 50 | (uvia_min_size 0.2) 51 | (uvia_min_drill 0.1) 52 | (edge_width 0.05) 53 | (segment_width 0.2) 54 | (pcb_text_width 0.3) 55 | (pcb_text_size 1.5 1.5) 56 | (mod_edge_width 0.12) 57 | (mod_text_size 1 1) 58 | (mod_text_width 0.15) 59 | (pad_size 1.524 1.524) 60 | (pad_drill 0.762) 61 | (pad_to_mask_clearance 0.0508) 62 | (aux_axis_origin 0 0) 63 | (visible_elements FFFFFF7F) 64 | (pcbplotparams 65 | (layerselection 0x010fc_ffffffff) 66 | (usegerberextensions false) 67 | (usegerberattributes true) 68 | (usegerberadvancedattributes true) 69 | (creategerberjobfile true) 70 | (excludeedgelayer true) 71 | (linewidth 0.100000) 72 | (plotframeref false) 73 | (viasonmask false) 74 | (mode 1) 75 | (useauxorigin false) 76 | (hpglpennumber 1) 77 | (hpglpenspeed 20) 78 | (hpglpendiameter 15.000000) 79 | (psnegative false) 80 | (psa4output false) 81 | (plotreference true) 82 | (plotvalue true) 83 | (plotinvisibletext false) 84 | (padsonsilk false) 85 | (subtractmaskfromsilk false) 86 | (outputformat 1) 87 | (mirror false) 88 | (drillshape 1) 89 | (scaleselection 1) 90 | (outputdirectory "")) 91 | ) 92 | 93 | (net 0 "") 94 | 95 | (net_class Default "This is the default net class." 96 | (clearance 0.2) 97 | (trace_width 0.25) 98 | (via_dia 0.8) 99 | (via_drill 0.4) 100 | (uvia_dia 0.3) 101 | (uvia_drill 0.1) 102 | ) 103 | 104 | (module Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical (layer F.Cu) (tedit 59FED5CC) (tstamp 605A5F3A) 105 | (at 97.4 93.6 90) 106 | (descr "Through hole straight pin header, 1x03, 2.54mm pitch, single row") 107 | (tags "Through hole pin header THT 1x03 2.54mm single row") 108 | (fp_text reference REF** (at 0 -2.33 90) (layer F.SilkS) hide 109 | (effects (font (size 1 1) (thickness 0.15))) 110 | ) 111 | (fp_text value PinHeader_1x03_P2.54mm_Vertical (at 0 7.41 90) (layer F.Fab) hide 112 | (effects (font (size 1 1) (thickness 0.15))) 113 | ) 114 | (fp_text user %R (at 0 2.54) (layer F.Fab) hide 115 | (effects (font (size 1 1) (thickness 0.15))) 116 | ) 117 | (fp_line (start -0.635 -1.27) (end 1.27 -1.27) (layer F.Fab) (width 0.1)) 118 | (fp_line (start 1.27 -1.27) (end 1.27 6.35) (layer F.Fab) (width 0.1)) 119 | (fp_line (start 1.27 6.35) (end -1.27 6.35) (layer F.Fab) (width 0.1)) 120 | (fp_line (start -1.27 6.35) (end -1.27 -0.635) (layer F.Fab) (width 0.1)) 121 | (fp_line (start -1.27 -0.635) (end -0.635 -1.27) (layer F.Fab) (width 0.1)) 122 | (fp_line (start -1.33 6.41) (end 1.33 6.41) (layer F.SilkS) (width 0.12)) 123 | (fp_line (start -1.33 1.27) (end -1.33 6.41) (layer F.SilkS) (width 0.12)) 124 | (fp_line (start 1.33 1.27) (end 1.33 6.41) (layer F.SilkS) (width 0.12)) 125 | (fp_line (start -1.33 1.27) (end 1.33 1.27) (layer F.SilkS) (width 0.12)) 126 | (fp_line (start -1.33 0) (end -1.33 -1.33) (layer F.SilkS) (width 0.12)) 127 | (fp_line (start -1.33 -1.33) (end 0 -1.33) (layer F.SilkS) (width 0.12)) 128 | (fp_line (start -1.8 -1.8) (end -1.8 6.85) (layer F.CrtYd) (width 0.05)) 129 | (fp_line (start -1.8 6.85) (end 1.8 6.85) (layer F.CrtYd) (width 0.05)) 130 | (fp_line (start 1.8 6.85) (end 1.8 -1.8) (layer F.CrtYd) (width 0.05)) 131 | (fp_line (start 1.8 -1.8) (end -1.8 -1.8) (layer F.CrtYd) (width 0.05)) 132 | (pad 3 thru_hole oval (at 0 5.08 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)) 133 | (pad 2 thru_hole oval (at 0 2.54 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)) 134 | (pad 1 thru_hole rect (at 0 0 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)) 135 | (model ${KISYS3DMOD}/Connector_PinHeader_2.54mm.3dshapes/PinHeader_1x03_P2.54mm_Vertical.wrl 136 | (at (xyz 0 0 0)) 137 | (scale (xyz 1 1 1)) 138 | (rotate (xyz 0 0 0)) 139 | ) 140 | ) 141 | 142 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 605A5D05) 143 | (at 100 100) 144 | (attr smd) 145 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 146 | (effects (font (size 1 1) (thickness 0.15))) 147 | ) 148 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 149 | (effects (font (size 1 1) (thickness 0.15))) 150 | ) 151 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 152 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 153 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 154 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 155 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 156 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 157 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 158 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 159 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 160 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 161 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 162 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 163 | ) 164 | 165 | (gr_line (start 95 91) (end 95 104) (layer Edge.Cuts) (width 0.05) (tstamp 605A60EA)) 166 | (gr_line (start 105 91) (end 95 91) (layer Edge.Cuts) (width 0.05)) 167 | (gr_line (start 105 104) (end 105 91) (layer Edge.Cuts) (width 0.05)) 168 | (gr_line (start 95 104) (end 105 104) (layer Edge.Cuts) (width 0.05)) 169 | 170 | (segment (start 97.4 93.6) (end 97.4 100) (width 1) (layer F.Cu) (net 0)) 171 | (segment (start 99.94 99.94) (end 100 100) (width 1) (layer F.Cu) (net 0)) 172 | (segment (start 99.94 93.6) (end 99.94 99.94) (width 1) (layer F.Cu) (net 0)) 173 | (segment (start 102.48 99.88) (end 102.6 100) (width 1) (layer F.Cu) (net 0)) 174 | (segment (start 102.48 93.6) (end 102.48 99.88) (width 1) (layer F.Cu) (net 0)) 175 | 176 | ) 177 | -------------------------------------------------------------------------------- /code/wordclock/magnetometer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Manage the magnetometer 3 | ''' 4 | 5 | import os 6 | from collections import namedtuple, deque 7 | import json 8 | import math 9 | import adafruit_mlx90393 10 | import qwiic_adxl313 11 | import geomag 12 | 13 | COMPASS_SMOOTHING_DEQUE_LEN = 2 14 | 15 | RADIAN_TO_DEGREE = 180 / math.pi 16 | 17 | _COMPASS_FILE = '/var/wordclock/compass.json' 18 | 19 | _KEY_X = 'x' 20 | _KEY_Y = 'y' 21 | _KEY_Z = 'z' 22 | 23 | _XYZ = [_KEY_X, _KEY_Y, _KEY_Z] 24 | 25 | _COMPASS_VALUE_MIN = 'min' 26 | _COMPASS_VALUE_MAX = 'max' 27 | 28 | _COMPASS_DEFAULTS = { 29 | _KEY_X: {_COMPASS_VALUE_MIN: 100000, _COMPASS_VALUE_MAX: -100000}, 30 | _KEY_Y: {_COMPASS_VALUE_MIN: 100000, _COMPASS_VALUE_MAX: -100000}, 31 | _KEY_Z: {_COMPASS_VALUE_MIN: 100000, _COMPASS_VALUE_MAX: -100000}, 32 | } 33 | 34 | AccelCoord = namedtuple('AccelCoord', 'key positive') 35 | CompassAngle = namedtuple('CompassAngle', 'orientation opposite near offset') 36 | Orientation = namedtuple('Orientation', 'angle orientation') 37 | 38 | FACE_UP = 'face-up' 39 | BACK_UP = 'back-up' 40 | TOP_UP = 'top-op' 41 | RIGHT_UP = 'right-up' 42 | BOTTOM_UP = 'bottom-up' 43 | LEFT_UP = 'left-up' 44 | 45 | _ACCEL_COORD_TOP_UP = AccelCoord(_KEY_Y, False) 46 | 47 | _ORIENTATIONS = { 48 | AccelCoord(_KEY_X, True): CompassAngle(RIGHT_UP, _KEY_Y, _KEY_Z, -180), # 49 | AccelCoord(_KEY_X, False): CompassAngle(LEFT_UP, _KEY_Z, _KEY_Y, 90), # 50 | AccelCoord(_KEY_Y, True): CompassAngle(BOTTOM_UP, _KEY_Z, _KEY_X, 90), # 51 | _ACCEL_COORD_TOP_UP: CompassAngle(TOP_UP, _KEY_X, _KEY_Z, -180), # 52 | AccelCoord(_KEY_Z, True): CompassAngle(BACK_UP, _KEY_Y, _KEY_X, -90), # from top 53 | AccelCoord(_KEY_Z, False): CompassAngle(FACE_UP, _KEY_X, _KEY_Y, 0), # from top 54 | } 55 | 56 | XYZ = namedtuple('XYZ', 'x y z') 57 | 58 | class Magnetometer(): 59 | ''' Manage the magnetometer 60 | ''' 61 | #pylint: disable=too-few-public-methods 62 | 63 | def __init__(self, i2c, verbose): 64 | self._i2c = i2c 65 | self.verbose = verbose 66 | self._magnetometer = None 67 | self._accelerometer = None 68 | self._orientation = _ORIENTATIONS[_ACCEL_COORD_TOP_UP] 69 | self._angle_history = deque() 70 | 71 | self._settings = _COMPASS_DEFAULTS 72 | 73 | if os.path.isfile(_COMPASS_FILE): 74 | with open(_COMPASS_FILE, 'r') as fil: 75 | try: 76 | self._settings.update(json.loads(fil.read())) 77 | except: #pylint: disable=bare-except 78 | pass 79 | 80 | self._init_devices() 81 | 82 | def _init_devices(self): 83 | ''' Lazy-init the magnetometer and accelerometer 84 | ''' 85 | if self._magnetometer is None: 86 | try: 87 | self._magnetometer = adafruit_mlx90393.MLX90393( 88 | self._i2c, gain=adafruit_mlx90393.GAIN_5X) 89 | if self.verbose: 90 | print('Initialized magnetometer', flush=True) 91 | except Exception as err: #pylint: disable=broad-except 92 | print('Failed to init the magnetometer:', str(err), flush=True) 93 | 94 | if self._accelerometer is None: 95 | try: 96 | self._accelerometer = qwiic_adxl313.QwiicAdxl313() 97 | self._accelerometer.measureModeOn() 98 | if self.verbose: 99 | print('Initialized accelerometer', flush=True) 100 | except Exception as err: #pylint: disable=broad-except 101 | print('Failed to init the accelerometer:', str(err), flush=True) 102 | 103 | def update(self, do_calibration=False, verbose=False, lat=None, lon=None): 104 | ''' Update the compass position 105 | ''' 106 | angle = None 107 | self._init_devices() 108 | 109 | if self._accelerometer is not None: 110 | try: 111 | if self._accelerometer.dataReady(): 112 | self._accelerometer.readAccel() 113 | 114 | values = { 115 | abs(self._accelerometer.x): AccelCoord(_KEY_X, self._accelerometer.x >= 0), 116 | abs(self._accelerometer.y): AccelCoord(_KEY_Y, self._accelerometer.y >= 0), 117 | abs(self._accelerometer.z): AccelCoord(_KEY_Z, self._accelerometer.z >= 0), 118 | } 119 | 120 | max_abs = max(values.keys()) 121 | self._orientation = _ORIENTATIONS[values[max_abs]] 122 | 123 | if verbose: 124 | print( 125 | '{:06d}'.format(self._accelerometer.x), 126 | ' {: 06d}'.format(self._accelerometer.y), 127 | ' {: 06d}'.format(self._accelerometer.z), 128 | flush=True) 129 | 130 | print(self._orientation, max_abs) 131 | 132 | except Exception as err: #pylint: disable=broad-except 133 | self._accelerometer = None 134 | print('Failed to read the accelerometer:', str(err), flush=True) 135 | 136 | if self._magnetometer is not None: 137 | try: 138 | mag_coords = self._magnetometer.magnetic 139 | 140 | if do_calibration: 141 | self._calibrate(mag_coords, verbose) 142 | 143 | adjusted = { 144 | key: self._adjust(key, mag_coords[i]) 145 | for i, key in enumerate(_XYZ) 146 | } 147 | 148 | new_angle = math.atan2( 149 | adjusted[self._orientation.opposite], 150 | adjusted[self._orientation.near]) * RADIAN_TO_DEGREE 151 | 152 | declination = ( 153 | geomag.declination(dlat=lat, dlon=lon) 154 | if lat is not None and lon is not None 155 | else 0) 156 | 157 | new_angle = (new_angle + self._orientation.offset + declination + 360) % 360 158 | 159 | if verbose: 160 | print(int(new_angle), adjusted, declination, mag_coords, flush=True) 161 | 162 | self._angle_history.append(new_angle) 163 | 164 | if len(self._angle_history) > COMPASS_SMOOTHING_DEQUE_LEN: 165 | self._angle_history.popleft() 166 | 167 | angle = round(sum(self._angle_history) / len(self._angle_history)) 168 | 169 | except Exception as err: #pylint: disable=broad-except 170 | self._magnetometer = None 171 | print('Failed to read the magnetometer:', str(err), flush=True) 172 | 173 | return Orientation(angle, self._orientation.orientation) 174 | 175 | def _calibrate(self, mag_coords, verbose): 176 | ''' Update calibration 177 | ''' 178 | changed = [self._calibrate_coord(key, mag_coords[i]) for i, key in enumerate(_XYZ)] 179 | 180 | if any(changed): 181 | with open(_COMPASS_FILE, 'w') as fil: 182 | fil.write(json.dumps(self._settings)) 183 | 184 | if verbose: 185 | print('updated calibration', flush=True) 186 | 187 | def _calibrate_coord(self, key, value): 188 | ''' Calibrate one coordinate 189 | ''' 190 | changed = False 191 | coord_setting = self._settings[key] 192 | 193 | if value < coord_setting[_COMPASS_VALUE_MIN]: 194 | coord_setting[_COMPASS_VALUE_MIN] = value 195 | changed = True 196 | 197 | if value > coord_setting[_COMPASS_VALUE_MAX]: 198 | coord_setting[_COMPASS_VALUE_MAX] = value 199 | changed = True 200 | 201 | return changed 202 | 203 | def _adjust(self, key, value): 204 | ''' Adjust a compass coordinate value 205 | ''' 206 | coord_setting = self._settings[key] 207 | 208 | return ( 209 | value - 210 | (coord_setting[_COMPASS_VALUE_MAX] + coord_setting[_COMPASS_VALUE_MIN]) / 2) 211 | -------------------------------------------------------------------------------- /code/website/script.js: -------------------------------------------------------------------------------- 1 | var changed = false; 2 | var canSave = false; 3 | var ajaxError = false; 4 | var enablePolling = true; 5 | var okSettings = {}; 6 | var futzing = false; 7 | var futzBrightness = 100; 8 | var futzDisplay = "noon"; 9 | 10 | function setSaveMsg(msg, isError) { 11 | $("#save_msg").text(msg); 12 | 13 | var div = $("#save_msg_div"); 14 | 15 | div.attr("class", isError ? "w3-container w3-cell w3-cell-middle w3-red" : "w3-container w3-cell w3-cell-middle w3-green"); 16 | div.toggle(!!msg); 17 | } 18 | 19 | function doAjax(method, item, data, callback) { 20 | var params = { 21 | url: "/"+item, 22 | type: method, 23 | dataType: "json", 24 | success: function(result) { 25 | if(ajaxError) { 26 | ajaxError = false; 27 | setSaveMsg("", false); 28 | } 29 | callback && callback(result); 30 | }, 31 | error: function(jqXHR, textStatus, errorThrown) { 32 | ajaxError = true; 33 | setSaveMsg( 34 | jqXHR.status == 0 ? "The clock is not responding" : jqXHR.status.toString(), true); 35 | callback && callback(null); 36 | }, 37 | } 38 | 39 | if(data != null) { 40 | params["contentType"] = "application/json"; 41 | params["processData"] = false; 42 | params["data"] = JSON.stringify(data); 43 | } 44 | 45 | $.ajax(params); 46 | } 47 | 48 | function setValue(id, value) { 49 | $(id).val(value); 50 | } 51 | 52 | function changeSsid() { 53 | var value = $("#ssid").val(); 54 | newSettings["ssid"] = value; 55 | 56 | displayError( 57 | value.length == 0, 58 | "ssid", 59 | "The SSID may not be blank."); 60 | 61 | enableSave(); 62 | } 63 | 64 | function changePassword() { 65 | var value = $("#password").val(); 66 | newSettings["password"] = value; 67 | 68 | displayError( 69 | value.length != 0 && value.length < 8, 70 | "password", 71 | "The Password must be at least 8 characters."); 72 | 73 | enableSave(); 74 | } 75 | 76 | function changeLat() { 77 | checkInt("lat", "Latitude", -90, 90); 78 | } 79 | 80 | function changeLon() { 81 | checkInt("lon", "Longitude", -180, 180); 82 | } 83 | 84 | function changeMinLight() { 85 | checkRange("light", "Ambient Light", true, 0, 30000); 86 | } 87 | 88 | function changeMaxLight() { 89 | checkRange("light", "Ambient Light", false, 0, 30000); 90 | } 91 | 92 | function changeMinBrightness() { 93 | checkRange("brightness", "Brightness", true, 0, 100); 94 | } 95 | 96 | function changeMaxBrightness() { 97 | checkRange("brightness", "Brightness", false, 0, 100); 98 | } 99 | 100 | function changeSunrise() { 101 | newSettings["sunrise"] = $("#sunrise").val(); 102 | enableSave(); 103 | } 104 | 105 | function changePoems() { 106 | newSettings["poems"] = $("#poems").val(); 107 | enableSave(); 108 | } 109 | 110 | function changeDisplayMode() { 111 | doAjax("POST", "mode", {display_mode: $("#display_mode").val()}, null); 112 | } 113 | 114 | 115 | function checkInt(id, name, min, max) { 116 | var value = Number($("#"+id).val()); 117 | 118 | var ok = !displayError( 119 | isNaN(value) || value < min || value > max, 120 | id, 121 | name+" must be between "+min.toString()+" and "+max.toString()+"."); 122 | 123 | if(ok) { 124 | newSettings[id] = value; 125 | } 126 | 127 | enableSave(); 128 | return {value: value, ok: ok}; 129 | } 130 | 131 | function checkRange(id, name, isMin, min, max) { 132 | var minId = "min_"+id; 133 | var maxId = "max_"+id; 134 | minResult = checkInt(minId, "Min "+name, min, max); 135 | maxResult = checkInt(maxId, "Max "+name, min, max); 136 | 137 | if(minResult.ok && maxResult.ok) { 138 | displayError( 139 | minResult.value > maxResult.value, 140 | isMin ? minId : maxId, 141 | "Min "+name+" may not be greater than Max "+name+"."); 142 | 143 | enableSave(); 144 | } 145 | } 146 | 147 | function showElement(id, show) { 148 | $(id).toggle(show); 149 | } 150 | 151 | function displayError(isBad, id, msg) { 152 | if(isBad) { 153 | $("#errmsg_"+id).text(msg); 154 | } 155 | 156 | showElement("#err_"+id, isBad); 157 | okSettings[id] = !isBad; 158 | return isBad; 159 | } 160 | 161 | function enableSave() { 162 | var newCanSave = true; 163 | 164 | if(!isHotspot) { 165 | for (const [key, value] of Object.entries(okSettings)) { 166 | if(!value) { 167 | newCanSave = false; 168 | } 169 | } 170 | 171 | if(newCanSave) { 172 | newCanSave = false; 173 | 174 | for (const [key, value] of Object.entries(curSettings)) { 175 | if(value != newSettings[key]) { 176 | newCanSave = true; 177 | } 178 | } 179 | } 180 | 181 | if(!canSave && newCanSave) { 182 | setSaveMsg("", false); 183 | } 184 | } 185 | 186 | $("#save").prop("disabled", !newCanSave); 187 | 188 | canSave = newCanSave; 189 | } 190 | 191 | function save() { 192 | doAjax( 193 | "POST", "save", newSettings, 194 | function(results) { 195 | if(!!results) { 196 | curSettings = Object.assign({}, newSettings); 197 | isHotspot = false; 198 | enableSave(); 199 | setSaveMsg(results.msg, !results.ok); 200 | 201 | if(results.wifi_changed) { 202 | enablePolling = false; 203 | } 204 | } 205 | } 206 | ); 207 | } 208 | 209 | function getState() { 210 | if(enablePolling) { 211 | doAjax( 212 | "GET", "state", null, 213 | function(results) { 214 | if(!!results) { 215 | $("#cur_ambient").text(results.cur_ambient.toString()); 216 | $("#cur_brightness").text(results.cur_brightness.toString()); 217 | $("#cur_angle").text(results.cur_angle.toString()); 218 | setValue("#display_mode", results.display_mode); 219 | $("#sunrise_orientation").text(results.sunrise_orientation); 220 | 221 | var up; 222 | 223 | switch(results.cur_orientation) { 224 | case "face-up": 225 | up = "Face"; 226 | break; 227 | case "back-up": 228 | up = "Back"; 229 | break; 230 | case "right-up": 231 | up = "Right"; 232 | break; 233 | case "bottom-up": 234 | up = "Bottom"; 235 | break; 236 | case "left-up": 237 | up = "Left"; 238 | break; 239 | default: 240 | up = "Top"; 241 | break; 242 | } 243 | 244 | $("#cur_orientation").text(up); 245 | } 246 | 247 | setTimeout(getState, 1000); 248 | } 249 | ); 250 | } 251 | } 252 | 253 | function ping() { 254 | if(futzing) { 255 | doAjax( 256 | "GET", "ping", null, 257 | function(results) { 258 | if(!!results && !results.ok) { 259 | hideFutz(); 260 | } 261 | }); 262 | 263 | if(futzing) { 264 | setTimeout(ping, 1000); 265 | } 266 | } 267 | } 268 | 269 | function showFutz() { 270 | futzing = true; 271 | 272 | $("#brightness").val(futzBrightness.toString()); 273 | $("#futzdisplay").val(futzDisplay); 274 | 275 | futz(); 276 | ping(); 277 | 278 | document.getElementById('futz').style.display = "block"; 279 | } 280 | 281 | function hideFutz() { 282 | futzing = false; 283 | doAjax("POST", "futz", {}, null); 284 | document.getElementById('futz').style.display='none'; 285 | } 286 | 287 | 288 | function futz() { 289 | futzBrightness = Number($("#brightness").val()); 290 | $("#brightness_value").text(futzBrightness.toString()); 291 | futzDisplay = $("#futzdisplay").val(); 292 | 293 | doAjax("POST", "futz", {brightness: futzBrightness, display: futzDisplay}, null); 294 | } 295 | 296 | function setMinBrightness() { 297 | $("#min_brightness").val(futzBrightness); 298 | newSettings["min_brightness"] = futzBrightness; 299 | 300 | if(futzBrightness > Number($("#max_brightness").val())) { 301 | $("#max_brightness").val(futzBrightness); 302 | newSettings["max_brightness"] = futzBrightness; 303 | } 304 | 305 | enableSave(); 306 | } 307 | 308 | function setMaxBrightness() { 309 | $("#max_brightness").val(futzBrightness); 310 | newSettings["max_brightness"] = futzBrightness; 311 | 312 | if(futzBrightness < Number($("#min_brightness").val())) { 313 | $("#min_brightness").val(futzBrightness); 314 | newSettings["min_brightness"] = futzBrightness; 315 | } 316 | 317 | enableSave(); 318 | } 319 | 320 | 321 | $(document).ready( 322 | function() { 323 | $("#version").text(version); 324 | $("#server_ip").text(serverIp); 325 | 326 | for (const [key, value] of Object.entries(curSettings)) { 327 | if(key != "password") { 328 | $("#"+key).val(Number.isFinite(value) ? value.toString() : value); 329 | } 330 | 331 | okSettings[key] = true; 332 | } 333 | 334 | enableSave(); 335 | 336 | if(!!curSettings.ssid) { 337 | $("#password").attr("placeholder", "Leave this blank to retain the current password"); 338 | } 339 | 340 | getState(); 341 | } 342 | ); 343 | -------------------------------------------------------------------------------- /code/wordclock/get_update.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Check S3 for an update. If found download and run it. 3 | 4 | Updates are tar/gzip files. They may contain any content, 5 | but must contain a file named "update", which we execute. 6 | The S3 objects have names of the form: 7 | 8 | update//update-
.tgz 9 | 10 | The is the IAM username used by the clock to 11 | authenticate to AWS, and are of the form "wordclock-". 12 | 13 | We record which updates have been done, by creating local 14 | directories with names of the form: 15 | 16 | /var/wordclock/updates/
17 | 18 | where the timestamp is the timestamp portion of the filename 19 | downloaded from S3. We download a new update if its timestamp 20 | is greater than the newest recorded timestamp. 21 | 22 | The update process is: 23 | 24 | 1. List the objects in S3 and determine if there's a new update. 25 | Ignore object that are larger than 10MB. 26 | 27 | 2. Download the update tar object and save as a tmp file. 28 | 29 | 3. Verify that the hash of the file contents matches the S3 30 | object etag. Abandon any update that isn't valid. 31 | 32 | 4. Untar the file into "/var/wordclock/updates//files". 33 | 34 | 5. Execute the "update" script. 35 | 36 | 6. Delete the "files" subdirectory and all of its contents, 37 | leaving just the timestamp directory to record that the update 38 | was processed. 39 | 40 | 7. Upload a log recording the result to S3. 41 | ''' 42 | 43 | import subprocess 44 | import os 45 | import stat 46 | import re 47 | import tarfile 48 | import traceback 49 | import datetime 50 | import tempfile 51 | import shutil 52 | from argparse import ArgumentParser 53 | from hashlib import md5 54 | from collections import namedtuple 55 | import boto3 56 | from wordclock.s3config import S3_BUCKET 57 | from wordclock.username import get_username 58 | 59 | UPDATES_DIR = '/var/wordclock/updates' 60 | MAX_UPDATE_FILESIZE = 10000000 61 | MIN_OBJ_CHUNK_SIZE = 1<<20 62 | FILES_SUBDIR = 'files' 63 | SCRIPT = 'update' 64 | 65 | 66 | def parse_filename(item): 67 | ''' Parse an S3 object filename and return a corresponding tuple 68 | ''' 69 | size = item['Size'] 70 | 71 | if size > MAX_UPDATE_FILESIZE: 72 | return None 73 | 74 | fullname = item['Key'] 75 | basename = fullname.split('/')[-1] 76 | match = re.match(r'update-(?P\d{10})\.tgz$', basename) 77 | 78 | if not match: 79 | return None 80 | 81 | return namedtuple('FilenameParts', 'fullname basename timestamp etag size')( 82 | fullname, basename, match.group('timestamp'), item['ETag'], size) 83 | 84 | 85 | def download_update(s3_client, s3_filename): 86 | ''' Download an update file 87 | ''' 88 | name = None 89 | 90 | try: 91 | with tempfile.NamedTemporaryFile(delete=False) as fil: 92 | name = fil.name 93 | shutil.copyfileobj(s3_client.get_object(Bucket=S3_BUCKET, Key=s3_filename)['Body'], fil) 94 | 95 | except Exception as err: #pylint: disable=broad-except 96 | if name and os.path.isfile(name): 97 | os.remove(name) 98 | 99 | raise RuntimeError('Unable to download file: {}'.format(err)) 100 | 101 | return name 102 | 103 | 104 | def verify_file(tmp_tarfile, s3_file): 105 | ''' Verify that the hash of the contents of the downloaded file matches the S3 106 | object's etag. 107 | ''' 108 | etag = s3_file.etag[1:-1] # remove enclosing quotes 109 | etag_split = etag.split('-') # the format is either "" or "-" 110 | 111 | if len(etag_split) == 1: 112 | with open(tmp_tarfile, 'rb') as fil: 113 | if etag == md5(fil.read()).hexdigest(): 114 | return 115 | 116 | else: 117 | obj_hash = etag_split[0] 118 | n_chunks = int(etag_split[1]) 119 | 120 | for chunk_size in ( 121 | size for size in range(MIN_OBJ_CHUNK_SIZE, s3_file.size, MIN_OBJ_CHUNK_SIZE) 122 | if (s3_file.size + size - 1) // size == n_chunks): 123 | 124 | if obj_hash == chunked_hash(tmp_tarfile, chunk_size): 125 | return 126 | 127 | raise RuntimeError('File hash is not {}'.format(etag)) 128 | 129 | 130 | def chunked_hash(tmp_tarfile, chunk_size): 131 | ''' Calculate the etag hash of an S3 object for a given chunk size. 132 | ''' 133 | concatenated_chunk_hashes = bytes() 134 | 135 | with open(tmp_tarfile, 'rb') as fil: 136 | while True: 137 | chunk = fil.read(chunk_size) 138 | 139 | if not chunk: 140 | break 141 | 142 | concatenated_chunk_hashes += md5(chunk).digest() 143 | 144 | return md5(concatenated_chunk_hashes).hexdigest() 145 | 146 | 147 | def untar_file(tmp_tarfile, timestamp): 148 | ''' Untar the downloaded file and return the directory and script. 149 | ''' 150 | try: 151 | target_dir = os.path.join(UPDATES_DIR, timestamp, FILES_SUBDIR) 152 | os.makedirs(target_dir) 153 | 154 | with tarfile.open(name=tmp_tarfile, mode='r:gz') as fil: 155 | fil.extractall(path=target_dir) 156 | 157 | script = os.path.join(target_dir, SCRIPT) 158 | 159 | if not os.path.isfile(script): 160 | raise RuntimeError('The tar file contains no script') 161 | 162 | os.chmod(script, stat.S_IRWXU) 163 | return namedtuple('Target', 'dir script')(target_dir, script) 164 | 165 | except Exception as err: #pylint: disable=broad-except 166 | raise RuntimeError('Unable to untar file: {}'.format(err)) 167 | 168 | 169 | def do_update(script): 170 | ''' Execute an update script 171 | ''' 172 | try: 173 | return subprocess.check_output([script], stderr=subprocess.STDOUT).decode() 174 | 175 | except subprocess.CalledProcessError as err: #pylint: disable=broad-except 176 | raise RuntimeError('Script error: {}\n{}'.format(err.returncode, err.output.decode())) 177 | 178 | 179 | def parse_args(): 180 | ''' Parse command line args 181 | ''' 182 | parser = ArgumentParser( 183 | prog='get-update', 184 | description='Download and process any update') 185 | 186 | parser.add_argument( 187 | '--debug', 188 | action='store_true', 189 | default=False, 190 | help='Print verbose debugging statements') 191 | 192 | return parser.parse_args() 193 | 194 | 195 | def main(): 196 | ''' do it 197 | ''' 198 | args = parse_args() 199 | s3_client = boto3.client('s3') 200 | username = None 201 | result = None 202 | newest_update = None 203 | tmp_tarfile = None 204 | target = None 205 | 206 | try: 207 | username = get_username() 208 | 209 | avail_updates = { 210 | filename_parts.timestamp: filename_parts 211 | for filename_parts in ( 212 | parse_filename(item) 213 | for page in s3_client.get_paginator('list_objects_v2').paginate( 214 | Bucket=S3_BUCKET, 215 | Prefix='update/{}/'.format(username)) 216 | for item in page.get('Contents', [])) 217 | if filename_parts} 218 | 219 | dirname_re = re.compile(r'\d{10}$') 220 | 221 | have_update_timestamps = [ 222 | dirname for dirname in os.listdir(UPDATES_DIR) 223 | if os.path.isdir(os.path.join(UPDATES_DIR, dirname)) and dirname_re.match(dirname)] 224 | 225 | if avail_updates: 226 | newest_update = avail_updates[max(avail_updates.keys())] 227 | 228 | if not have_update_timestamps or newest_update.timestamp > max(have_update_timestamps): 229 | tmp_tarfile = download_update(s3_client, newest_update.fullname) 230 | verify_file(tmp_tarfile, newest_update) 231 | target = untar_file(tmp_tarfile, newest_update.timestamp) 232 | result = do_update(target.script) 233 | 234 | except Exception as err: #pylint: disable=broad-except 235 | result = 'Exception: {}\n{}'.format(err, traceback.format_exc()) 236 | 237 | finally: 238 | try: 239 | if tmp_tarfile and os.path.isfile(tmp_tarfile): 240 | os.remove(tmp_tarfile) 241 | 242 | if target and os.path.isdir(target.dir): 243 | shutil.rmtree(target.dir, ignore_errors=True) 244 | 245 | except Exception as err: #pylint: disable=broad-except 246 | print('Exception cleaning up: {}'.format(err)) 247 | 248 | if args.debug: 249 | print(username, newest_update, tmp_tarfile, target, result) 250 | 251 | if result and username: 252 | s3_client.put_object( 253 | Bucket=S3_BUCKET, 254 | Key='logs/{}/update-{}.txt'.format(username, datetime.datetime.utcnow().isoformat()), 255 | Body='{}\n{}'.format(newest_update.fullname if newest_update else 'unknown', result)) 256 | -------------------------------------------------------------------------------- /docs/steps.md: -------------------------------------------------------------------------------- 1 | ## Setting up the SD Card 2 | 3 | These are the minimal steps necessary to get the Pi to the point where 4 | you can ssh to it running headless. 5 | 6 | 1. Use the Raspberry Pi Imager (on Windows) to write the image to an 7 | SD card. 8 | 9 | 2. Mount the SD card on your Linux laptop. 10 | 11 | 4. Enable ssh: 12 | ``` 13 | touch /media/sidell/boot/ssh 14 | cp /s/wordclock/wpa_supplicant.conf /media/sidell/boot 15 | ``` 16 | 17 | 3. Copy the authorized_keys file: 18 | ``` 19 | mkdir /media/sidell/rootfs/home/pi/.ssh 20 | cp /s/wordclock/code/authorized_keys /media/sidell/rootfs/home/pi/.ssh 21 | chown 1000:1000 -R /media/sidell/rootfs/home/pi/.ssh 22 | chmod 700 /media/sidell/rootfs/home/pi/.ssh 23 | ``` 24 | 25 | ## Installing the Baseline Software 26 | 27 | 1. Unmount the SD card, put it in the pi. 28 | 29 | 2. Find the pi's IP address from dhcp and connect: 30 | ``` 31 | ssh pi@ 32 | sudo -s 33 | ``` 34 | 35 | 3. Enable I2C and disable the GUI console. 36 | 37 | 1. Run `raspi-config`. 38 | 2. Select *Interface Options*. 39 | 3. Enable I2C. 40 | 4. Select *System Options* 41 | 5. Select *Boot / Auto Login* 42 | 6. Select *Console Autologin*. Don't bother rebooting now. 43 | 4. Save and exit. 44 | 45 | 4. Create a file to enable ssh key forwarding when root, for logging 46 | into github: 47 | ``` 48 | visudo /etc/sudoers.d/ssh_agent_forwarding 49 | ``` 50 | 51 | Add this line and save the file: 52 | ``` 53 | Defaults env_keep += "SSH_AUTH_SOCK" 54 | ``` 55 | 56 | 5. Log out and back in, to enable ssh agent forwarding. 57 | 58 | 6. Install git: 59 | ``` 60 | apt-get update 61 | apt-get -y install git 62 | ``` 63 | 64 | 7. Clone the repo: 65 | ``` 66 | git clone git@github.com:marksidell/wordclock.git 67 | ``` 68 | 69 | 8. Do basic setup: 70 | ``` 71 | cd wordclock/code 72 | make setup 73 | make config_sshd 74 | ``` 75 | 76 | 9. Change the pi passwd. 77 | 78 | 10. Shutdown the pi. Remove the SD card and use Win32 Disk Imager to 79 | snapshot it to the `baseline.img` in \\ruxton\share\wordclock folder. 80 | 81 | ## Installing and Configuring the Software 82 | 83 | 4. Install AWS credentials: Edit `/root/.aws/credentials` and paste the credentials 84 | for this particular clock. 85 | 86 | 16. Install the software, where `` is the two-letter config name for this clock. 87 | ``` 88 | # This step is probably not necessary 89 | rm /vars/wordclock/params.json 90 | 91 | cd /home/pi/wordclock/code 92 | make CONFIG= 93 | ``` 94 | 95 | ## Building the Clock 96 | 97 | 1. Cut back panel. 0.5 inch MDF, 16 x 16 inches. 98 | 2. Cut 2 braces. 0.5 inch MDF, 16.5 x 2 inches. 99 | 3. Using straight edge aligned with back panel corners, find center. Then use square 100 | to draw orthognal lines through the center. These are used to align various items 101 | attached to the panel. 102 | 103 | 4. Mill the back panel. Place it on the MDF sacrificial base with the notch that allows 104 | it to overhang the mill's front edge. Blue-tape the panel to the base in the corners. 105 | Use small clamps to squeeze the front corners together while the glue sets. 106 | 107 | 5. Using the corner jig, route three back panel rounded corners. Omit the lower-right 108 | corner, viewed from the rear of the panel. 109 | 110 | 6. Mill the brackets. 111 | 112 | 7. Glue standoffs to the brackets. 113 | 114 | 7. Mill the bottom acrylic band, using the acrylic/MDF jig. 115 | 116 | 8. Cut and tin the short power cable. 10". Attach the powerpole connector. 117 | Solder the cable to the bottom PCB. 118 | 119 | 9. Cut the three-wire PCB jumper cable. 110 mm. Solder it to the PCB. 120 | 121 | 10. Glue the top and bottom PCBs to the back panel. Use four drops of medium-vicosity superglue, 122 | at points where there are no PCB through-holes. Secure with four small clamps. 123 | 124 | 10. Secure the three-wire jumper cable to the bottom PCB with a dab of hot glue. 125 | 126 | 10. Cut the four-wire level converter jumper cable. 155 mm. Solder it to the level converter. 127 | 128 | 11. Cut the two-wire pushbutton jumper cable. 110 mm. Solder it to the pushbutton, 129 | with heat shrink strain relief. 130 | 131 | 12. Solder 9x2 right-angle header to the pi. Header on TOP of pi. 132 | 133 | 13. Screw 5mm/2 standoffs to the pi, and 5mm/3 standoffs to the accelerometer, and magnetometer. 134 | Standoffs face UP. 135 | 136 | 13. Solder a qwiic cable to the light sensor. 90 mm (Use half of a 200 qwiic cable. Save 137 | the other half!) 138 | 139 | 14. Screw 10mm standoffs to the acrylic electronics panel. 140 | 141 | 14. Mount the pi and sensors to the acrylic plate. Hook everything up and test. 142 | See [Electronics Alignment](https://photos.app.goo.gl/rrW4gMBJYJBW6XC3A). 143 | 144 | 15. Calibrate the compass: 145 | ``` 146 | systemctl stop wc 147 | rm /var/wordclock/compass.json 148 | calibrate 149 | ``` 150 | 151 | While `calibrate` is running, rotate the electronics assembly through all orientations. 152 | 153 | 16. Cut the face neopixel strips. Lay them out in the correct up/down pattern. 154 | and tape them to an MDF board. Leftmost 155 | strip is *up*. 156 | 157 | 17. Tin the neopixels. Bottom: all three, top: center only. Test for shorts. 158 | 159 | 18. Tin the top and bottom PCBs. 160 | 161 | 19. Hot glue the neopixel strips to the back panel. Use the center line for alignment. 162 | 163 | 20. Solder the strips to the PCBs. Use the hold-down jig. 164 | Check all connections for shorts and for connectivity to the strips. 165 | 166 | 21. Hook up the pi and run `tneo --all` to test the face neopixels. 167 | 168 | 22. Solder wires to the border PCB and then solder the PCB to the neopixel strip. 169 | The PCB sits on top of the neopixels, facing down. The wires leave the *front* of the PCB. 170 | See [Border PCB](https://photos.app.goo.gl/wNSTMZ44nTu5heG16). 171 | 172 | 23. With the back panel clamped *face up*, glue on the border neopixels. Medium viscosity. 173 | See [Attaching border pixels](https://photos.app.goo.gl/Qw9Kop7GaBzFSAUHA). 174 | 175 | 24. Solder the border pixel wires to the bottom strip. 176 | See [Border pixel wiring](https://photos.app.goo.gl/Ag3d7LAccuLykpyG8). 177 | 178 | 25. Hook up the electronics and test all pixels. 179 | 180 | 17. Mount the brackets. See [Mounting brackets](https://photos.app.goo.gl/3VDmyGYpLevs3Q7WA). 181 | 182 | 1. Angle the back panel on the multifunction table so that holes 183 | are close enough to clamp the bracket jig. 184 | 185 | 2. Mark a center line on the top bracket. 186 | 187 | 3. Mark a line 101 mm from the left edge of the bottom bracket. 188 | 189 | 4. Draw lines along the edges of the brackets and Xs at the slot 190 | positions to indicate where to apply glue 191 | 192 | 5. Nail the brackets to the back panel. 193 | 194 | 18. Drill holes for the screw eyes and mount the eyes. 1/16" drill. 195 | Use blue tape on bit to mark 7/16" depth. 196 | 197 | 19. Attach the picture wire, with heat shrink in place. 198 | 199 | 20. Shrink the heat shrink. 200 | 201 | 21. Attach felt pads. 202 | 203 | 22. Mark hole positions on the side band drilling jig. 204 | 205 | 23. Drill holes in side bands. Place blue tape on left side of each band. 206 | 207 | 21. Glue the electronics to the back panel. 208 | 209 | 24. Attach powerpole connector to the power supply, a few inches from the 210 | supply to minimize power loss. White stripes are red/plus. 211 | 212 | 24. Attach powerpole connectors to the white power cable. 213 | 214 | 25. Mill the light sensor well: 215 | 216 | 1. Attach the sensor well jig to the milling machine. 217 | 218 | 2. Tape blue tape to over the sensor hole and in the corner where the mill alignment probe sits. 219 | 220 | 3. Clamp the face to the jig. 221 | 222 | 4. Mill the well. 223 | 224 | 25. Using a blue tape border, paint the light sensor mask. 3" wide, 1.25" from face edge. 225 | 226 | 26. hot-glue the light sensor to the face, with spots of glue on the bottom and side edges. 227 | 228 | 27. If the sensor is proud of the face, apply electrical tape to the sides. 229 | 230 | 28. Afix 3" gaffer's tape to the back panel under the picture hook wire. Use MDF 231 | straight edge and utility knife to cut clean edges. 232 | 233 | 29. Glue the acrylic band to the face. 234 | 235 | 1. Clamp band alignment jigs to the table, using face and under-face for alignment. 236 | Position the panel so that you can clamp the brackets down while cementing the 237 | bands. See [Band Clamping](https://photos.app.goo.gl/cj5uuTcT4vic3L4Q9). 238 | 239 | 2. Screw side acrylic bands to back panel brackets. 240 | 241 | 3. Use the fourth band to align the corner. 242 | 243 | 4. Place the back panel on the face and cement the first side band. 244 | 245 | 5. Rotate and cement the other side. 246 | 247 | 6. Cement the top and bottom bands. 248 | See [Top and Bottom Clamping](https://photos.app.goo.gl/YPZUVhCXy1rSnqiJ6). 249 | 250 | 30. Attach and connect the pushbutton. 251 | 252 | 25. Cut sides for shipping case: 253 | 254 | - 2 23.25" 255 | - 3 19" 256 | - 8 1" for standoffs 257 | 258 | 26. Screw the case together. Bottom try area is 2". 259 | 260 | 27. Cut cardboard sides. 261 | 262 | 28. Glue and staple back to frame. 263 | 264 | 29. Attach "OPEN THIS/OTHER SIDE" labels. 265 | 266 | 30. Place the clock face down in the case, on top of a layer of bubble wrap. 267 | 268 | 31. Tape standoffs: One at the end of each bracket, and one in each corner. 269 | 270 | 32. Cover the clock with another sheet of bubble wrap. 271 | 272 | 33. Tape the cover onto the case. 273 | 274 | 34. Tape the manual envelope and ziploc bag containing a picture hook to the case cover. 275 | 276 | 35. Ship it! 277 | -------------------------------------------------------------------------------- /pcbs/top/top.kicad_pcb-bak: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 20171130) (host pcbnew "(5.1.9)-1") 2 | 3 | (general 4 | (thickness 1.6) 5 | (drawings 4) 6 | (tracks 30) 7 | (zones 0) 8 | (modules 12) 9 | (nets 1) 10 | ) 11 | 12 | (page A4) 13 | (layers 14 | (0 F.Cu signal) 15 | (31 B.Cu signal) 16 | (32 B.Adhes user) 17 | (33 F.Adhes user) 18 | (34 B.Paste user) 19 | (35 F.Paste user) 20 | (36 B.SilkS user) 21 | (37 F.SilkS user) 22 | (38 B.Mask user) 23 | (39 F.Mask user) 24 | (40 Dwgs.User user) 25 | (41 Cmts.User user) 26 | (42 Eco1.User user) 27 | (43 Eco2.User user) 28 | (44 Edge.Cuts user) 29 | (45 Margin user) 30 | (46 B.CrtYd user) 31 | (47 F.CrtYd user) 32 | (48 B.Fab user) 33 | (49 F.Fab user) 34 | ) 35 | 36 | (setup 37 | (last_trace_width 0.25) 38 | (user_trace_width 1) 39 | (user_trace_width 2) 40 | (trace_clearance 0.2) 41 | (zone_clearance 0.508) 42 | (zone_45_only no) 43 | (trace_min 0.2) 44 | (via_size 0.8) 45 | (via_drill 0.4) 46 | (via_min_size 0.4) 47 | (via_min_drill 0.3) 48 | (uvia_size 0.3) 49 | (uvia_drill 0.1) 50 | (uvias_allowed no) 51 | (uvia_min_size 0.2) 52 | (uvia_min_drill 0.1) 53 | (edge_width 0.05) 54 | (segment_width 0.2) 55 | (pcb_text_width 0.3) 56 | (pcb_text_size 1.5 1.5) 57 | (mod_edge_width 0.12) 58 | (mod_text_size 1 1) 59 | (mod_text_width 0.15) 60 | (pad_size 1.524 1.524) 61 | (pad_drill 0.762) 62 | (pad_to_mask_clearance 0.0508) 63 | (aux_axis_origin 0 0) 64 | (grid_origin 14 24) 65 | (visible_elements 7FFFFFFF) 66 | (pcbplotparams 67 | (layerselection 0x010c0_ffffffff) 68 | (usegerberextensions false) 69 | (usegerberattributes true) 70 | (usegerberadvancedattributes true) 71 | (creategerberjobfile true) 72 | (excludeedgelayer true) 73 | (linewidth 0.100000) 74 | (plotframeref false) 75 | (viasonmask false) 76 | (mode 1) 77 | (useauxorigin false) 78 | (hpglpennumber 1) 79 | (hpglpenspeed 20) 80 | (hpglpendiameter 15.000000) 81 | (psnegative false) 82 | (psa4output false) 83 | (plotreference true) 84 | (plotvalue true) 85 | (plotinvisibletext false) 86 | (padsonsilk false) 87 | (subtractmaskfromsilk false) 88 | (outputformat 1) 89 | (mirror false) 90 | (drillshape 0) 91 | (scaleselection 1) 92 | (outputdirectory "")) 93 | ) 94 | 95 | (net 0 "") 96 | 97 | (net_class Default "This is the default net class." 98 | (clearance 0.2) 99 | (trace_width 0.25) 100 | (via_dia 0.8) 101 | (via_drill 0.4) 102 | (uvia_dia 0.3) 103 | (uvia_drill 0.1) 104 | ) 105 | 106 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 107 | (at 385.87 20) 108 | (attr smd) 109 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 110 | (effects (font (size 1 1) (thickness 0.15))) 111 | ) 112 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 113 | (effects (font (size 1 1) (thickness 0.15))) 114 | ) 115 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 116 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 117 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 118 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 119 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 120 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 121 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 122 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 123 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 124 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 125 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 126 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 127 | ) 128 | 129 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 130 | (at 352.65 20) 131 | (attr smd) 132 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 133 | (effects (font (size 1 1) (thickness 0.15))) 134 | ) 135 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 136 | (effects (font (size 1 1) (thickness 0.15))) 137 | ) 138 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 139 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 140 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 141 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 142 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 143 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 144 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 145 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 146 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 147 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 148 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 149 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 150 | ) 151 | 152 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 153 | (at 319.43 20) 154 | (attr smd) 155 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 156 | (effects (font (size 1 1) (thickness 0.15))) 157 | ) 158 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 159 | (effects (font (size 1 1) (thickness 0.15))) 160 | ) 161 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 162 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 163 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 164 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 165 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 166 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 167 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 168 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 169 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 170 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 171 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 172 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 173 | ) 174 | 175 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 176 | (at 286.21 20) 177 | (attr smd) 178 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 179 | (effects (font (size 1 1) (thickness 0.15))) 180 | ) 181 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 182 | (effects (font (size 1 1) (thickness 0.15))) 183 | ) 184 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 185 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 186 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 187 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 188 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 189 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 190 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 191 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 192 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 193 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 194 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 195 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 196 | ) 197 | 198 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 199 | (at 252.99 20) 200 | (attr smd) 201 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 202 | (effects (font (size 1 1) (thickness 0.15))) 203 | ) 204 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 205 | (effects (font (size 1 1) (thickness 0.15))) 206 | ) 207 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 208 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 209 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 210 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 211 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 212 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 213 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 214 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 215 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 216 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 217 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 218 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 219 | ) 220 | 221 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 222 | (at 219.77 20) 223 | (attr smd) 224 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 225 | (effects (font (size 1 1) (thickness 0.15))) 226 | ) 227 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 228 | (effects (font (size 1 1) (thickness 0.15))) 229 | ) 230 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 231 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 232 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 233 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 234 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 235 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 236 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 237 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 238 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 239 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 240 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 241 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 242 | ) 243 | 244 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 245 | (at 186.55 20) 246 | (attr smd) 247 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 248 | (effects (font (size 1 1) (thickness 0.15))) 249 | ) 250 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 251 | (effects (font (size 1 1) (thickness 0.15))) 252 | ) 253 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 254 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 255 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 256 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 257 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 258 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 259 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 260 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 261 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 262 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 263 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 264 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 265 | ) 266 | 267 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 268 | (at 153.33 20) 269 | (attr smd) 270 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 271 | (effects (font (size 1 1) (thickness 0.15))) 272 | ) 273 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 274 | (effects (font (size 1 1) (thickness 0.15))) 275 | ) 276 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 277 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 278 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 279 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 280 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 281 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 282 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 283 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 284 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 285 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 286 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 287 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 288 | ) 289 | 290 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 291 | (at 120.11 20) 292 | (attr smd) 293 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 294 | (effects (font (size 1 1) (thickness 0.15))) 295 | ) 296 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 297 | (effects (font (size 1 1) (thickness 0.15))) 298 | ) 299 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 300 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 301 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 302 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 303 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 304 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 305 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 306 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 307 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 308 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 309 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 310 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 311 | ) 312 | 313 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 314 | (at 86.89 20) 315 | (attr smd) 316 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 317 | (effects (font (size 1 1) (thickness 0.15))) 318 | ) 319 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 320 | (effects (font (size 1 1) (thickness 0.15))) 321 | ) 322 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 323 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 324 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 325 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 326 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 327 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 328 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 329 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 330 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 331 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 332 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 333 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 334 | ) 335 | 336 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 337 | (at 53.67 20) 338 | (attr smd) 339 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 340 | (effects (font (size 1 1) (thickness 0.15))) 341 | ) 342 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 343 | (effects (font (size 1 1) (thickness 0.15))) 344 | ) 345 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 346 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 347 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 348 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 349 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 350 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 351 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 352 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 353 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 354 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 355 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 356 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 357 | ) 358 | 359 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 360 | (at 19.95 20) 361 | (attr smd) 362 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 363 | (effects (font (size 1 1) (thickness 0.15))) 364 | ) 365 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 366 | (effects (font (size 1 1) (thickness 0.15))) 367 | ) 368 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 369 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 370 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 371 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 372 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 373 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 374 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 375 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 376 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 377 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 378 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 379 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 380 | ) 381 | 382 | (gr_line (start 392.5 24.5) (end 392.5 12) (layer Edge.Cuts) (width 0.05)) 383 | (gr_line (start 392.5 24.5) (end 13.5 24.5) (layer Edge.Cuts) (width 0.05) (tstamp 6059FAA8)) 384 | (gr_line (start 13.5 12) (end 392.5 12) (layer Edge.Cuts) (width 0.05)) 385 | (gr_line (start 13.5 24.5) (end 13.5 12) (layer Edge.Cuts) (width 0.05)) 386 | 387 | (segment (start 19.95 20) (end 19.95 15.55) (width 0.25) (layer F.Cu) (net 0)) 388 | (segment (start 19.95 15.55) (end 20 15.5) (width 0.25) (layer F.Cu) (net 0)) 389 | (segment (start 20 15.5) (end 53.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 390 | (segment (start 53.5 19.83) (end 53.67 20) (width 0.25) (layer F.Cu) (net 0)) 391 | (segment (start 53.5 15.5) (end 53.5 19.83) (width 0.25) (layer F.Cu) (net 0)) 392 | (segment (start 86.89 20) (end 86.89 15.61) (width 0.25) (layer F.Cu) (net 0)) 393 | (segment (start 86.89 15.61) (end 87 15.5) (width 0.25) (layer F.Cu) (net 0)) 394 | (segment (start 87 15.5) (end 120 15.5) (width 0.25) (layer F.Cu) (net 0)) 395 | (segment (start 120 19.89) (end 120.11 20) (width 0.25) (layer F.Cu) (net 0)) 396 | (segment (start 120 15.5) (end 120 19.89) (width 0.25) (layer F.Cu) (net 0)) 397 | (segment (start 153.33 20) (end 153.33 15.67) (width 0.25) (layer F.Cu) (net 0)) 398 | (segment (start 153.33 15.67) (end 153.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 399 | (segment (start 153.5 15.5) (end 186.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 400 | (segment (start 186.5 19.95) (end 186.55 20) (width 0.25) (layer F.Cu) (net 0)) 401 | (segment (start 186.5 15.5) (end 186.5 19.95) (width 0.25) (layer F.Cu) (net 0)) 402 | (segment (start 219.77 20) (end 219.77 15.73) (width 0.25) (layer F.Cu) (net 0)) 403 | (segment (start 219.77 15.73) (end 220 15.5) (width 0.25) (layer F.Cu) (net 0)) 404 | (segment (start 220 15.5) (end 253 15.5) (width 0.25) (layer F.Cu) (net 0)) 405 | (segment (start 253 19.99) (end 252.99 20) (width 0.25) (layer F.Cu) (net 0)) 406 | (segment (start 253 15.5) (end 253 19.99) (width 0.25) (layer F.Cu) (net 0)) 407 | (segment (start 286.21 20) (end 286.21 15.79) (width 0.25) (layer F.Cu) (net 0)) 408 | (segment (start 286.21 15.79) (end 286.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 409 | (segment (start 286.5 15.5) (end 319.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 410 | (segment (start 319.5 19.93) (end 319.43 20) (width 0.25) (layer F.Cu) (net 0)) 411 | (segment (start 319.5 15.5) (end 319.5 19.93) (width 0.25) (layer F.Cu) (net 0)) 412 | (segment (start 352.65 20) (end 352.65 15.65) (width 0.25) (layer F.Cu) (net 0)) 413 | (segment (start 352.65 15.65) (end 352.8 15.5) (width 0.25) (layer F.Cu) (net 0)) 414 | (segment (start 352.8 15.5) (end 386 15.5) (width 0.25) (layer F.Cu) (net 0)) 415 | (segment (start 386 19.87) (end 385.87 20) (width 0.25) (layer F.Cu) (net 0)) 416 | (segment (start 386 15.5) (end 386 19.87) (width 0.25) (layer F.Cu) (net 0)) 417 | 418 | ) 419 | -------------------------------------------------------------------------------- /pcbs/top/top.kicad_pcb: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 20171130) (host pcbnew "(5.1.9)-1") 2 | 3 | (general 4 | (thickness 1.6) 5 | (drawings 6) 6 | (tracks 30) 7 | (zones 0) 8 | (modules 12) 9 | (nets 1) 10 | ) 11 | 12 | (page A4) 13 | (layers 14 | (0 F.Cu signal) 15 | (31 B.Cu signal) 16 | (32 B.Adhes user) 17 | (33 F.Adhes user) 18 | (34 B.Paste user) 19 | (35 F.Paste user) 20 | (36 B.SilkS user) 21 | (37 F.SilkS user) 22 | (38 B.Mask user) 23 | (39 F.Mask user) 24 | (40 Dwgs.User user) 25 | (41 Cmts.User user) 26 | (42 Eco1.User user) 27 | (43 Eco2.User user) 28 | (44 Edge.Cuts user) 29 | (45 Margin user) 30 | (46 B.CrtYd user) 31 | (47 F.CrtYd user) 32 | (48 B.Fab user) 33 | (49 F.Fab user) 34 | ) 35 | 36 | (setup 37 | (last_trace_width 0.25) 38 | (user_trace_width 1) 39 | (user_trace_width 2) 40 | (trace_clearance 0.2) 41 | (zone_clearance 0.508) 42 | (zone_45_only no) 43 | (trace_min 0.2) 44 | (via_size 0.8) 45 | (via_drill 0.4) 46 | (via_min_size 0.4) 47 | (via_min_drill 0.3) 48 | (uvia_size 0.3) 49 | (uvia_drill 0.1) 50 | (uvias_allowed no) 51 | (uvia_min_size 0.2) 52 | (uvia_min_drill 0.1) 53 | (edge_width 0.05) 54 | (segment_width 0.2) 55 | (pcb_text_width 0.3) 56 | (pcb_text_size 1.5 1.5) 57 | (mod_edge_width 0.12) 58 | (mod_text_size 1 1) 59 | (mod_text_width 0.15) 60 | (pad_size 1.524 1.524) 61 | (pad_drill 0.762) 62 | (pad_to_mask_clearance 0.0508) 63 | (aux_axis_origin 0 0) 64 | (grid_origin 14 24) 65 | (visible_elements 7FFFFFFF) 66 | (pcbplotparams 67 | (layerselection 0x010c0_ffffffff) 68 | (usegerberextensions false) 69 | (usegerberattributes true) 70 | (usegerberadvancedattributes true) 71 | (creategerberjobfile true) 72 | (excludeedgelayer true) 73 | (linewidth 0.100000) 74 | (plotframeref false) 75 | (viasonmask false) 76 | (mode 1) 77 | (useauxorigin false) 78 | (hpglpennumber 1) 79 | (hpglpenspeed 20) 80 | (hpglpendiameter 15.000000) 81 | (psnegative false) 82 | (psa4output false) 83 | (plotreference true) 84 | (plotvalue true) 85 | (plotinvisibletext false) 86 | (padsonsilk false) 87 | (subtractmaskfromsilk false) 88 | (outputformat 1) 89 | (mirror false) 90 | (drillshape 0) 91 | (scaleselection 1) 92 | (outputdirectory "")) 93 | ) 94 | 95 | (net 0 "") 96 | 97 | (net_class Default "This is the default net class." 98 | (clearance 0.2) 99 | (trace_width 0.25) 100 | (via_dia 0.8) 101 | (via_drill 0.4) 102 | (uvia_dia 0.3) 103 | (uvia_drill 0.1) 104 | ) 105 | 106 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 107 | (at 385.87 20) 108 | (attr smd) 109 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 110 | (effects (font (size 1 1) (thickness 0.15))) 111 | ) 112 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 113 | (effects (font (size 1 1) (thickness 0.15))) 114 | ) 115 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 116 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 117 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 118 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 119 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 120 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 121 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 122 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 123 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 124 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 125 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 126 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 127 | ) 128 | 129 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 130 | (at 352.65 20) 131 | (attr smd) 132 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 133 | (effects (font (size 1 1) (thickness 0.15))) 134 | ) 135 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 136 | (effects (font (size 1 1) (thickness 0.15))) 137 | ) 138 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 139 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 140 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 141 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 142 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 143 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 144 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 145 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 146 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 147 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 148 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 149 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 150 | ) 151 | 152 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 153 | (at 319.43 20) 154 | (attr smd) 155 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 156 | (effects (font (size 1 1) (thickness 0.15))) 157 | ) 158 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 159 | (effects (font (size 1 1) (thickness 0.15))) 160 | ) 161 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 162 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 163 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 164 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 165 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 166 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 167 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 168 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 169 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 170 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 171 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 172 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 173 | ) 174 | 175 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 176 | (at 286.21 20) 177 | (attr smd) 178 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 179 | (effects (font (size 1 1) (thickness 0.15))) 180 | ) 181 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 182 | (effects (font (size 1 1) (thickness 0.15))) 183 | ) 184 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 185 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 186 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 187 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 188 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 189 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 190 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 191 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 192 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 193 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 194 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 195 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 196 | ) 197 | 198 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 199 | (at 252.99 20) 200 | (attr smd) 201 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 202 | (effects (font (size 1 1) (thickness 0.15))) 203 | ) 204 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 205 | (effects (font (size 1 1) (thickness 0.15))) 206 | ) 207 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 208 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 209 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 210 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 211 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 212 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 213 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 214 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 215 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 216 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 217 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 218 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 219 | ) 220 | 221 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 222 | (at 219.77 20) 223 | (attr smd) 224 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 225 | (effects (font (size 1 1) (thickness 0.15))) 226 | ) 227 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 228 | (effects (font (size 1 1) (thickness 0.15))) 229 | ) 230 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 231 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 232 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 233 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 234 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 235 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 236 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 237 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 238 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 239 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 240 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 241 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 242 | ) 243 | 244 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 245 | (at 186.55 20) 246 | (attr smd) 247 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 248 | (effects (font (size 1 1) (thickness 0.15))) 249 | ) 250 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 251 | (effects (font (size 1 1) (thickness 0.15))) 252 | ) 253 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 254 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 255 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 256 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 257 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 258 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 259 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 260 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 261 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 262 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 263 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 264 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 265 | ) 266 | 267 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 268 | (at 153.33 20) 269 | (attr smd) 270 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 271 | (effects (font (size 1 1) (thickness 0.15))) 272 | ) 273 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 274 | (effects (font (size 1 1) (thickness 0.15))) 275 | ) 276 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 277 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 278 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 279 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 280 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 281 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 282 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 283 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 284 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 285 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 286 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 287 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 288 | ) 289 | 290 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 291 | (at 120.11 20) 292 | (attr smd) 293 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 294 | (effects (font (size 1 1) (thickness 0.15))) 295 | ) 296 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 297 | (effects (font (size 1 1) (thickness 0.15))) 298 | ) 299 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 300 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 301 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 302 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 303 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 304 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 305 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 306 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 307 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 308 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 309 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 310 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 311 | ) 312 | 313 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 314 | (at 86.89 20) 315 | (attr smd) 316 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 317 | (effects (font (size 1 1) (thickness 0.15))) 318 | ) 319 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 320 | (effects (font (size 1 1) (thickness 0.15))) 321 | ) 322 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 323 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 324 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 325 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 326 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 327 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 328 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 329 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 330 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 331 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 332 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 333 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 334 | ) 335 | 336 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 337 | (at 53.67 20) 338 | (attr smd) 339 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 340 | (effects (font (size 1 1) (thickness 0.15))) 341 | ) 342 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 343 | (effects (font (size 1 1) (thickness 0.15))) 344 | ) 345 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 346 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 347 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 348 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 349 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 350 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 351 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 352 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 353 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 354 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 355 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 356 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 357 | ) 358 | 359 | (module neopixels:strip (layer F.Cu) (tedit 6059F17A) (tstamp 60625AC2) 360 | (at 19.95 20) 361 | (attr smd) 362 | (fp_text reference REF** (at 0 4) (layer F.SilkS) hide 363 | (effects (font (size 1 1) (thickness 0.15))) 364 | ) 365 | (fp_text value SOIC-3 (at 0 -4) (layer F.Fab) hide 366 | (effects (font (size 1 1) (thickness 0.15))) 367 | ) 368 | (fp_line (start -3.85 2.75) (end -3.85 -2.75) (layer F.CrtYd) (width 0.05)) 369 | (fp_line (start 3.85 2.75) (end -3.85 2.75) (layer F.CrtYd) (width 0.05)) 370 | (fp_line (start 3.85 -2.75) (end 3.85 2.75) (layer F.CrtYd) (width 0.05)) 371 | (fp_line (start -3.85 -2.75) (end 3.85 -2.75) (layer F.CrtYd) (width 0.05)) 372 | (fp_line (start 4.1 3) (end -3.3 3) (layer F.SilkS) (width 0.12)) 373 | (fp_line (start 4.1 -3) (end 4.1 3) (layer F.SilkS) (width 0.12)) 374 | (fp_line (start -4.1 -3) (end 4.1 -3) (layer F.SilkS) (width 0.12)) 375 | (fp_line (start -4.1 2.2) (end -4.1 -3) (layer F.SilkS) (width 0.12)) 376 | (fp_line (start -3.3 3) (end -4.1 2.2) (layer F.SilkS) (width 0.12)) 377 | (pad 1 smd rect (at -2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 378 | (pad 2 smd rect (at 0 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 379 | (pad 3 smd rect (at 2.6 0) (size 2 5) (layers F.Cu F.Paste F.Mask)) 380 | ) 381 | 382 | (gr_line (start 392.5 22.5) (end 386 24.5) (layer Edge.Cuts) (width 0.05) (tstamp 60B241D0)) 383 | (gr_line (start 20 24.5) (end 13.5 22.5) (layer Edge.Cuts) (width 0.05) (tstamp 60B2416D)) 384 | (gr_line (start 392.5 22.5) (end 392.5 12) (layer Edge.Cuts) (width 0.05)) 385 | (gr_line (start 386 24.5) (end 20 24.5) (layer Edge.Cuts) (width 0.05) (tstamp 6059FAA8)) 386 | (gr_line (start 13.5 12) (end 392.5 12) (layer Edge.Cuts) (width 0.05)) 387 | (gr_line (start 13.5 22.5) (end 13.5 12) (layer Edge.Cuts) (width 0.05)) 388 | 389 | (segment (start 19.95 20) (end 19.95 15.55) (width 0.25) (layer F.Cu) (net 0)) 390 | (segment (start 19.95 15.55) (end 20 15.5) (width 0.25) (layer F.Cu) (net 0)) 391 | (segment (start 20 15.5) (end 53.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 392 | (segment (start 53.5 19.83) (end 53.67 20) (width 0.25) (layer F.Cu) (net 0)) 393 | (segment (start 53.5 15.5) (end 53.5 19.83) (width 0.25) (layer F.Cu) (net 0)) 394 | (segment (start 86.89 20) (end 86.89 15.61) (width 0.25) (layer F.Cu) (net 0)) 395 | (segment (start 86.89 15.61) (end 87 15.5) (width 0.25) (layer F.Cu) (net 0)) 396 | (segment (start 87 15.5) (end 120 15.5) (width 0.25) (layer F.Cu) (net 0)) 397 | (segment (start 120 19.89) (end 120.11 20) (width 0.25) (layer F.Cu) (net 0)) 398 | (segment (start 120 15.5) (end 120 19.89) (width 0.25) (layer F.Cu) (net 0)) 399 | (segment (start 153.33 20) (end 153.33 15.67) (width 0.25) (layer F.Cu) (net 0)) 400 | (segment (start 153.33 15.67) (end 153.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 401 | (segment (start 153.5 15.5) (end 186.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 402 | (segment (start 186.5 19.95) (end 186.55 20) (width 0.25) (layer F.Cu) (net 0)) 403 | (segment (start 186.5 15.5) (end 186.5 19.95) (width 0.25) (layer F.Cu) (net 0)) 404 | (segment (start 219.77 20) (end 219.77 15.73) (width 0.25) (layer F.Cu) (net 0)) 405 | (segment (start 219.77 15.73) (end 220 15.5) (width 0.25) (layer F.Cu) (net 0)) 406 | (segment (start 220 15.5) (end 253 15.5) (width 0.25) (layer F.Cu) (net 0)) 407 | (segment (start 253 19.99) (end 252.99 20) (width 0.25) (layer F.Cu) (net 0)) 408 | (segment (start 253 15.5) (end 253 19.99) (width 0.25) (layer F.Cu) (net 0)) 409 | (segment (start 286.21 20) (end 286.21 15.79) (width 0.25) (layer F.Cu) (net 0)) 410 | (segment (start 286.21 15.79) (end 286.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 411 | (segment (start 286.5 15.5) (end 319.5 15.5) (width 0.25) (layer F.Cu) (net 0)) 412 | (segment (start 319.5 19.93) (end 319.43 20) (width 0.25) (layer F.Cu) (net 0)) 413 | (segment (start 319.5 15.5) (end 319.5 19.93) (width 0.25) (layer F.Cu) (net 0)) 414 | (segment (start 352.65 20) (end 352.65 15.65) (width 0.25) (layer F.Cu) (net 0)) 415 | (segment (start 352.65 15.65) (end 352.8 15.5) (width 0.25) (layer F.Cu) (net 0)) 416 | (segment (start 352.8 15.5) (end 386 15.5) (width 0.25) (layer F.Cu) (net 0)) 417 | (segment (start 386 19.87) (end 385.87 20) (width 0.25) (layer F.Cu) (net 0)) 418 | (segment (start 386 15.5) (end 386 19.87) (width 0.25) (layer F.Cu) (net 0)) 419 | 420 | ) 421 | --------------------------------------------------------------------------------