+6
-1
setup.sh
+6
-1
setup.sh
···
39
39
git pull
40
40
41
41
cp /home/ink/inky/src/camera_server.py /home/ink/ && chown ink:ink /home/ink/camera_server.py && chmod +x /home/ink/camera_server.py
42
+
cp /home/ink/inky/src/eink-4gray.png /home/ink/ && chown ink:ink /home/ink/eink-4gray.png
42
43
43
44
# Just restart the service since it's an update
44
45
echo "Restarting camera service..."
···
51
52
# Update system packages and install dependencies
52
53
echo "Updating package lists and installing dependencies..."
53
54
apt update
54
-
apt install -y python3-picamera2 python3-websockets python3-rpi.gpio git
55
+
apt install -y python3-picamera2 python3-websockets python3-rpi.gpio git imagemagick
55
56
56
57
# Create directory for storing photos
57
58
echo "Creating photos directory..."
···
67
68
cp /home/ink/inky/src/camera_server.py /home/ink/
68
69
chown ink:ink /home/ink/camera_server.py
69
70
chmod +x /home/ink/camera_server.py
71
+
72
+
# copy dither palate
73
+
cp /home/ink/inky/src/eink-4gray.png /home/ink/
74
+
chown ink:ink /home/ink/eink-4gray.png
70
75
71
76
# Copy and set up systemd service
72
77
echo "Setting up systemd service..."
+51
-30
src/camera_server.py
+51
-30
src/camera_server.py
···
10
10
import websockets
11
11
import asyncio
12
12
import json
13
-
from PIL import Image
14
13
15
14
# Setup logging
16
15
logger = logging.getLogger('camera_server')
···
28
27
PHOTO_DIR = "/home/ink/photos"
29
28
WEB_PORT = 80
30
29
WS_PORT = 8765
31
-
PHOTO_RESOLUTION = (2592, 1944)
30
+
PHOTO_RESOLUTION = (1280, 960)
32
31
CAMERA_SETTLE_TIME = 1
33
32
DEBOUNCE_DELAY = 0.2
34
33
POLL_INTERVAL = 0.01
···
61
60
body {{ font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }}
62
61
h1 {{ text-align: center; }}
63
62
.gallery {{ display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }}
64
-
.photo {{ border: 1px solid #ddd; padding: 5px; animation: fadeIn 0.1s; flex: 0 1 200px; }}
65
-
.photo img {{ width: 100%; height: auto; }}
63
+
.photo {{ border: 1px solid #ddd; padding: 5px; animation: fadeIn 0.1s; flex: 0 1 200px; position: relative; }}
64
+
.photo img {{ width: 100%; height: auto; transition: opacity 0.3s; }}
65
+
.photo .colored-img {{ position: absolute; top: 5px; left: 5px; opacity: 0; pointer-events: none; }}
66
+
.photo:hover .dithered-img {{ opacity: 0; }}
67
+
.photo:hover .colored-img {{ opacity: 1; }}
66
68
.photo .actions {{ text-align: center; margin-top: 5px; }}
67
69
.photo .actions a {{ margin: 0 5px; }}
68
70
@keyframes fadeIn {{ from {{ opacity: 0; }} to {{ opacity: 1; }} }}
···
105
107
noPhotosMsg.remove();
106
108
}}
107
109
108
-
const photoDiv = document.createElement('div');
109
-
photoDiv.className = 'photo';
110
-
photoDiv.id = `photo-${{filename}}`;
110
+
const originalFilename = filename.replace('dithered_', '');
111
+
const isDithered = filename.startsWith('dithered_');
111
112
112
-
photoDiv.innerHTML = `
113
-
<img src="/${{filename}}" alt="${{timestamp}}">
114
-
<div class="actions">
115
-
<a href="/${{filename}}" download>Download</a>
116
-
<a href="#" onclick="deletePhoto('${{filename}}'); return false;">Delete</a>
117
-
</div>
118
-
`;
113
+
if (isDithered) {{
114
+
const photoDiv = document.createElement('div');
115
+
photoDiv.className = 'photo';
116
+
photoDiv.id = `photo-${{filename}}`;
117
+
118
+
photoDiv.innerHTML = `
119
+
<img class="dithered-img" src="/${{filename}}" alt="${{timestamp}}">
120
+
<img class="colored-img" src="/${{originalFilename}}" alt="${{timestamp}}">
121
+
<div class="actions">
122
+
<a href="/${{originalFilename}}" download>Download Color</a>
123
+
<a href="/${{filename}}" download>Download Dithered</a>
124
+
<a href="#" onclick="deletePhoto('${{filename}}', '${{originalFilename}}'); return false;">Delete</a>
125
+
</div>
126
+
`;
119
127
120
-
gallery.insertBefore(photoDiv, gallery.firstChild);
128
+
gallery.insertBefore(photoDiv, gallery.firstChild);
129
+
}}
121
130
}}
122
131
123
132
function removePhoto(filename) {{
···
136
145
}}
137
146
}}
138
147
139
-
function deletePhoto(filename) {{
148
+
function deletePhoto(ditheredFilename, originalFilename) {{
140
149
if (confirm('Are you sure you want to delete this photo?')) {{
141
-
fetch('/delete/' + filename, {{
150
+
fetch('/delete/' + ditheredFilename, {{
142
151
method: 'POST'
143
152
}}).then(response => {{
144
153
if(response.ok) {{
145
-
removePhoto(filename);
154
+
return fetch('/delete/' + originalFilename, {{ method: 'POST' }});
155
+
}}
156
+
}}).then(response => {{
157
+
if(response.ok) {{
158
+
removePhoto(ditheredFilename);
146
159
}}
147
160
}});
148
161
}}
···
176
189
try:
177
190
files = sorted(os.listdir(Config.PHOTO_DIR), reverse=True)
178
191
for filename in files:
179
-
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
180
-
timestamp = filename.replace('photo_', '').replace('.jpg', '')
192
+
if filename.lower().endswith(('.jpg', '.jpeg', '.png')) and filename.startswith('dithered_'):
193
+
originalFilename = filename.replace('dithered_', 'photo_')
194
+
timestamp = filename.replace('dithered_', '').replace('.jpg', '')
181
195
photo_items += f"""
182
196
<div class="photo" id="photo-{filename}">
183
-
<img src="/{filename}" alt="{timestamp}">
197
+
<img class="dithered-img" src="/{filename}" alt="{timestamp}">
198
+
<img class="colored-img" src="/{originalFilename}" alt="{timestamp}">
184
199
<div class="actions">
185
-
<a href="/{filename}" download>Download</a>
186
-
<a href="#" onclick="deletePhoto('{filename}'); return false;">Delete</a>
200
+
<a href="/{originalFilename}" download>Download Color</a>
201
+
<a href="/{filename}" download>Download Dithered</a>
202
+
<a href="#" onclick="deletePhoto('{filename}', '{originalFilename}'); return false;">Delete</a>
187
203
</div>
188
204
</div>
189
205
"""
···
264
280
265
281
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
266
282
filename = f"photo_{timestamp}.jpg"
283
+
dithered_filename = f"dithered_{timestamp}.jpg"
267
284
filepath = os.path.join(Config.PHOTO_DIR, filename)
285
+
dithered_filepath = os.path.join(Config.PHOTO_DIR, dithered_filename)
268
286
logger.info(f"Taking photo: {filepath}")
269
287
270
288
picam2.capture_file(filepath)
271
289
logger.info("Photo taken successfully")
272
290
273
-
# Rotate the image using PIL
274
-
with Image.open(filepath) as img:
275
-
rotated_img = img.rotate(Config.ROTATION, expand=True)
276
-
rotated_img.save(filepath)
277
-
logger.info("Photo rotated successfully")
291
+
# Rotate the image using ImageMagick
292
+
os.system(f"magick {filepath} -rotate {Config.ROTATION} {filepath}")
293
+
logger.info("Photo rotated successfully")
294
+
295
+
# Create dithered version using ImageMagick
296
+
os.system(f"magick {filepath} -dither FloydSteinberg -define dither:diffusion-amount=100% -remap eink-4gray.png {dithered_filepath}")
297
+
logger.info("Dithered version created successfully")
278
298
279
-
# Notify websocket clients about new photo
299
+
# Notify websocket clients about both photos
280
300
asyncio.run(notify_clients('new_photo', {
281
-
'filename': filename,
301
+
'filename': dithered_filename,
282
302
'timestamp': timestamp
283
303
}))
304
+
284
305
except IOError as e:
285
306
logger.error(f"IO Error while taking photo: {str(e)}")
286
307
except Exception as e:
src/eink-2color.png
src/eink-2color.png
This is a binary file and will not be displayed.
src/eink-4gray.png
src/eink-4gray.png
This is a binary file and will not be displayed.