···11-# Code Of Conduct
22-33-This Code of Conduct is based heavily on "instruments of good works" from chapter 4 of The Rule of St. Benedict (hereafter: "The Rule"). This Rule has proven its mettle in thousands of diverse communities for over 1,500 years, and has served as a baseline for many civil law codes since the time of Charlemagne.
44-55-The Rule is strict, and none are able to comply perfectly. Grace is readily granted for minor transgressions. All are encourage to follow this rule closely, as in so doing they may expect to live happier, healthier, and more productive lives. The entire Rule is good and wholesome, and yet we make no enforcement of the more introspective aspects.
66-77-We view The Rule as our promise to all users of this project of how the developers are expected to behave. This is a one-way promise, or covenant. In other words, the developers are saying: "We will treat you this way regardless of how you treat us." We ask you to join us in this covenant through your interactions with this project.
88-99-## 2. The Abbreviated Rule
1010-1111-1. Love the Lord God with all your heart, soul, mind, and strength.
1212-1. Love your neighbor as yourself.
1313-1. Do not steal or bear false witness.
1414-1. Honor all people and treat them with the respect you desire.
1515-1. Do not give way to anger or nurse grudges.
1616-1. Speak truthfully and reject all forms of deceit.
1717-1. Do not return evil for evil, and bear wrongs patiently. (that does not mean you must suffer abuse from others however)
1818-1. Be humble, avoid pride, jealousy, and arrogance.
1919-1. Respect the wisdom of those who are more experienced than you, recognizing that knowledge is earned through time and reflection.
2020-1. Generously share your understanding with those who are still learning, approaching teaching with compassion, patience, and genuine care.
2121-1. Settle disputes quickly and with care, ensuring that disagreements do not fester and grow beyond repair.
2222-1. Never despair of mercy.
2323-2424-## 3. Attribution
2525-2626-This code of conduct was adapted from [The rule of St. Benedict, as your Code of Conduct](https://github.com/saint-benedict/code-of-conduct) which in turn was adapted from [SQLite's Code of conduct](https://web.archive.org/web/20181024103452/https://sqlite.org/codeofconduct.html) and now [Code of Ethics](https://sqlite.org/codeofethics.html).
-8
CONTRIBUTING.md
···11-# Contributing.md
22-33-Hi there! I'm glad you decided to add something! I don't have a super strict contributing guideline but here are some things to keep in mind:
44-55-1. Please try to follow the existing style of code and documentation. I know this seems kinda obvious but its easy enough to forget when implementing a new feature!
66-2. Follow the CoC please 💖 It should be linked on this repo's home page but if not you can find it [here](https://github.com/taciturnaxolotl/carriage/blob/master/.github/CODE_OF_CONDUCT.md). The TLDR is be nice and don't be a jerk but then again so is the TLDR of most CoCs :)
77-3. Use conventional commits! I'm a big fan of the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard. It makes it easier to track changes and generate changelogs. I tend to use it at a pretty basic level (feat|chore|bug|docs: <message>) but feel free to get as fancy as you want within the standard ^-^
88-4. Last one. Once you open a PR ping me plz 😄 I tend to forget to check my repos for PRs so a ping will remind me it exists and i'll get to it much quicker! Alternatively you can open the PR and wait 4-6 business months 💀
+179
src/camera_server.py
···11+import RPi.GPIO as GPIO
22+import time
33+from picamera2 import Picamera2
44+from datetime import datetime
55+import os
66+import logging
77+import http.server
88+import socketserver
99+import threading
1010+1111+# Setup logging
1212+logger = logging.getLogger('camera_server')
1313+logger.setLevel(logging.INFO)
1414+formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
1515+file_handler = logging.FileHandler('/home/kierank/camera_server.log')
1616+file_handler.setFormatter(formatter)
1717+stream_handler = logging.StreamHandler()
1818+stream_handler.setFormatter(formatter)
1919+logger.addHandler(file_handler)
2020+logger.addHandler(stream_handler)
2121+2222+class Config:
2323+ BUTTON_PIN = 17
2424+ PHOTO_DIR = "/home/kierank/photos"
2525+ WEB_PORT = 80
2626+ PHOTO_RESOLUTION = (2592, 1944)
2727+ CAMERA_SETTLE_TIME = 1
2828+ DEBOUNCE_DELAY = 0.2
2929+ POLL_INTERVAL = 0.01
3030+3131+def validate_photo_dir():
3232+ if not os.path.isabs(Config.PHOTO_DIR):
3333+ raise ValueError("PHOTO_DIR must be an absolute path")
3434+ if not os.access(Config.PHOTO_DIR, os.W_OK):
3535+ raise PermissionError(f"No write access to {Config.PHOTO_DIR}")
3636+3737+# Ensure photo directory exists and is valid
3838+validate_photo_dir()
3939+os.makedirs(Config.PHOTO_DIR, exist_ok=True)
4040+4141+# Set up GPIO
4242+GPIO.setmode(GPIO.BCM)
4343+GPIO.setup(Config.BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
4444+4545+# Create a simple HTML gallery template - using triple quotes properly
4646+HTML_TEMPLATE = """<!DOCTYPE html>
4747+<html>
4848+<head>
4949+ <title>Inkpress: Gallery</title>
5050+ <meta name="viewport" content="width=device-width, initial-scale=1">
5151+ <style>
5252+ body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
5353+ h1 {{ text-align: center; }}
5454+ .gallery {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }}
5555+ .photo {{ border: 1px solid #ddd; padding: 5px; }}
5656+ .photo img {{ width: 100%; height: auto; }}
5757+ .photo a {{ display: block; text-align: center; margin-top: 5px; }}
5858+ button {{ display: block; margin: 10px auto; padding: 5px 10px; }}
5959+ </style>
6060+</head>
6161+<body>
6262+ <h1>Inkpress: Gallery</h1>
6363+ <button onclick="location.reload()">Refresh Gallery</button>
6464+ <div class="gallery">
6565+ {photo_items}
6666+ </div>
6767+</body>
6868+</html>
6969+"""
7070+7171+class PhotoHandler(http.server.SimpleHTTPRequestHandler):
7272+ def __init__(self, *args, **kwargs):
7373+ super().__init__(*args, directory=Config.PHOTO_DIR, **kwargs)
7474+7575+ def do_GET(self):
7676+ if self.path == '/':
7777+ self.send_response(200)
7878+ self.send_header('Content-type', 'text/html')
7979+ self.send_header('X-Content-Type-Options', 'nosniff')
8080+ self.send_header('X-Frame-Options', 'DENY')
8181+ self.send_header('X-XSS-Protection', '1; mode=block')
8282+ self.end_headers()
8383+8484+ # Generate photo gallery HTML
8585+ photo_items = ""
8686+ try:
8787+ files = sorted(os.listdir(Config.PHOTO_DIR), reverse=True)
8888+ for filename in files:
8989+ if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
9090+ timestamp = filename.replace('photo_', '').replace('.jpg', '')
9191+ photo_items += f"""
9292+ <div class="photo">
9393+ <img src="/{filename}" alt="{timestamp}">
9494+ <a href="/{filename}" download>Download</a>
9595+ </div>
9696+ """
9797+9898+ if not photo_items:
9999+ photo_items = "<p style='grid-column: 1/-1; text-align: center;'>No photos yet. Press the button to take a photo!</p>"
100100+ except Exception as e:
101101+ logger.error(f"Error generating gallery: {str(e)}")
102102+ photo_items = f"<p>Error loading photos: {str(e)}</p>"
103103+104104+ html = HTML_TEMPLATE.format(photo_items=photo_items)
105105+ self.wfile.write(html.encode())
106106+ else:
107107+ super().do_GET()
108108+109109+def take_photo():
110110+ """
111111+ Captures a photo using the Raspberry Pi camera.
112112+113113+ The photo is saved with a timestamp in the configured photo directory.
114114+ The camera is configured for still capture at the specified resolution.
115115+116116+ Raises:
117117+ IOError: If there's an error accessing the camera or saving the file
118118+ """
119119+ try:
120120+ with Picamera2() as picam2:
121121+ config = picam2.create_still_configuration(main={"size": Config.PHOTO_RESOLUTION})
122122+ picam2.configure(config)
123123+ picam2.start()
124124+ time.sleep(Config.CAMERA_SETTLE_TIME)
125125+126126+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
127127+ filename = f"{Config.PHOTO_DIR}/photo_{timestamp}.jpg"
128128+ logger.info(f"Taking photo: {filename}")
129129+130130+ picam2.capture_file(filename)
131131+ logger.info("Photo taken successfully")
132132+ except IOError as e:
133133+ logger.error(f"IO Error while taking photo: {str(e)}")
134134+ except Exception as e:
135135+ logger.error(f"Unexpected error while taking photo: {str(e)}")
136136+137137+def run_server():
138138+ try:
139139+ handler = PhotoHandler
140140+ with socketserver.TCPServer(("", Config.WEB_PORT), handler) as httpd:
141141+ logger.info(f"Web server started at port {Config.WEB_PORT}")
142142+ httpd.serve_forever()
143143+ except Exception as e:
144144+ logger.error(f"Server error: {str(e)}")
145145+146146+def main():
147147+ logger.info("Camera and web server starting")
148148+ server = None
149149+150150+ try:
151151+ server = socketserver.TCPServer(("", Config.WEB_PORT), PhotoHandler)
152152+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
153153+ server_thread.start()
154154+155155+ previous_state = GPIO.input(Config.BUTTON_PIN)
156156+ while True:
157157+ current_state = GPIO.input(Config.BUTTON_PIN)
158158+159159+ if current_state == False and previous_state == True:
160160+ logger.info("Button press detected")
161161+ take_photo()
162162+ time.sleep(Config.DEBOUNCE_DELAY)
163163+164164+ previous_state = current_state
165165+ time.sleep(Config.POLL_INTERVAL)
166166+167167+ except KeyboardInterrupt:
168168+ logger.info("Program stopped by user")
169169+ except Exception as e:
170170+ logger.error(f"Unexpected error: {str(e)}")
171171+ finally:
172172+ if server:
173173+ server.shutdown()
174174+ server.server_close()
175175+ GPIO.cleanup()
176176+ logger.info("GPIO cleaned up")
177177+178178+if __name__ == "__main__":
179179+ main()