10 | #
11 | # LICENSE : MIT
12 | #
13 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
14 | # documentation files (the "Software"), to deal in the Software without restriction, including without limitation
15 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
16 | # to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17 | #
18 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of
19 | # the Software.
20 | #
21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
22 | # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26 | #
27 |
28 | import os
29 | import sys
30 | import time
31 | import re
32 | import qrcode
33 | import hashlib
34 | import requests
35 | import json
36 | import traceback
37 | from PIL import Image
38 | from PIL import ImageFont
39 | from PIL import ImageDraw
40 |
41 | sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + '/../lib'))
42 |
43 | from nafuda import Nafuda
44 |
45 | if "IMG_DIR" in os.environ:
46 | IMG_DIR = os.environ["IMG_DIR"]
47 | else:
48 | if os.path.isdir('/mnt/virtual_sd/img'):
49 | IMG_DIR = '/mnt/virtual_sd/img'
50 | else:
51 | # virtual sdがない時(開発時など)用
52 | IMG_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + '/img')
53 |
54 | CLOUD_JSON_CACHE_PATH = IMG_DIR + "/cloud.json"
55 | CLOUD_QR_CODE_FILE_NAME = "__CONVERT_TO_QR__.png"
56 | CLOUD_QR_CODE_FILE_PATH = IMG_DIR + "/" + CLOUD_QR_CODE_FILE_NAME
57 |
58 | def main():
59 | load_settings_from_cloud()
60 |
61 | # load image file list
62 | file_list = []
63 | for file in os.listdir(IMG_DIR):
64 | # get (png|jpg|jpeg|gif) files. and skip dot files.
65 | if re.search('^[^\.].*\.(png|jpg|jpeg|gif)', file, re.IGNORECASE):
66 | file_list.append(file)
67 |
68 | if len(file_list) == 0:
69 | print('no image, exit.')
70 | sys.exit(0)
71 |
72 | file_list.sort()
73 |
74 | print(file_list)
75 |
76 | nafuda = Nafuda()
77 |
78 | data = local_settings()
79 |
80 | while True:
81 | for file in file_list:
82 |
83 | # QRコード置換用画像が来た
84 | if file == CLOUD_QR_CODE_FILE_NAME:
85 | # CLOUD_BASE_URLがなければ、QRコードを表示しない
86 | if "CLOUD_BASE_URL" in data and data['CLOUD_BASE_URL'] != "":
87 | # QRコードを合成して表示
88 | base_image = Image.open(CLOUD_QR_CODE_FILE_PATH)
89 | qr_img = get_control_url_qrcode_img()
90 | base_image.paste(qr_img, (10, 10))
91 | nafuda.draw_image_buffer(base_image, orientation=90)
92 | if "PSEUDO_EPD_MODE" in os.environ:
93 | # guard for img bomb.
94 | time.sleep(3)
95 |
96 | continue
97 |
98 | try:
99 | nafuda.draw_image_file(IMG_DIR + '/' + file, 90)
100 |
101 | except OSError:
102 | # maybe, the file is not correct image file.
103 | print("load image fail: " + file)
104 |
105 | if "PSEUDO_EPD_MODE" in os.environ:
106 | # guard for img bomb.
107 | time.sleep(3)
108 |
109 | # 一枚しか画像がなければ、スライドショーする意味がないので終了
110 | if len(file_list) == 1:
111 | exit(0)
112 |
113 |
114 | def local_settings():
115 | setting_json = "settings.json"
116 |
117 | if not os.path.exists(setting_json):
118 | raise LoadLocalSettingsError
119 |
120 | with open(setting_json, "r") as sj:
121 | data = json.loads(sj.read())
122 |
123 | return data
124 |
125 |
126 | def load_settings_from_cloud():
127 | data = local_settings()
128 |
129 | # 設定がなければ、実行しない
130 | if "CLOUD_BASE_URL" not in data or data['CLOUD_BASE_URL'] == "":
131 | print("no CLOUD_BASE_URL")
132 | return False
133 |
134 | # QRコードに置換する画像がなければ、クラウド機能は不要と判断して実行しない
135 | if not os.path.isfile(CLOUD_QR_CODE_FILE_PATH):
136 | return False
137 |
138 | # https://server/{SHA2}/json settings
139 | # https://server/{SHA2}/ UI
140 | # https://server/bc2018/{SHA2}/[0-9].(png|jpg|gif...) imgs
141 | print("try get data from cloud")
142 | try:
143 | # 設定JSONをサーバーから取得試行
144 | r = requests.get(get_control_url() + "json")
145 | if r.status_code != 200:
146 | return "json not found"
147 |
148 | json_str = r.text
149 |
150 | data = json.loads(json_str)
151 | img_list = data['list']
152 | if not isinstance(img_list, list):
153 | return "maybe invalid json"
154 |
155 | if len(img_list) == 0:
156 | # 空の場合はなにもしない
157 | return "empty list"
158 |
159 | # 過去のJSONがあれば、更新があるか確認
160 | if os.path.isfile(CLOUD_JSON_CACHE_PATH):
161 | with open(CLOUD_JSON_CACHE_PATH, "r") as jc:
162 | cached_json = jc.read()
163 | if cached_json == json_str:
164 | return "json not updated"
165 |
166 | # rwで再マウント
167 | if os.path.isfile("/usr/bin/mount_vsd_rw"):
168 | if os.system('/usr/bin/mount_vsd_rw') != 0:
169 | return "mount_vsd_rw fail."
170 |
171 | # clean up img dir
172 | file_list_to_rm = os.listdir(IMG_DIR)
173 | for f in file_list_to_rm:
174 | p = IMG_DIR + "/" + f
175 | if os.path.isfile(p):
176 | if re.search('^[^\.].*\.(png|jpg|jpeg|gif)', p, re.IGNORECASE):
177 | if f != CLOUD_QR_CODE_FILE_NAME:
178 | os.remove(p)
179 |
180 | # 画像をDLして保存
181 | id = 1
182 | for img in img_list:
183 | root, ext = os.path.splitext(img)
184 | get_and_save_file(get_img_url_base() + "/" + img, IMG_DIR + "/" + str(id) + ext)
185 | id = id + 1
186 |
187 | # save json
188 | with open(CLOUD_JSON_CACHE_PATH, "w") as jc:
189 | jc.write(json_str)
190 |
191 | # roで再マウント
192 | if os.path.isfile("/usr/bin/mount_vsd_ro"):
193 | if os.system('/usr/bin/mount_vsd_ro') != 0:
194 | return "mount_vsd_ro fail."
195 |
196 | return True
197 |
198 | except:
199 | # 止まられると困る!
200 | traceback.print_exc()
201 | return False
202 |
203 |
204 | def get_nafuda_id():
205 | h_path = '/mnt/virtual_sd/default_hostname.txt'
206 | p_path = '/mnt/virtual_sd/default_passwd.txt'
207 |
208 | if not os.path.isfile(h_path) or not os.path.isfile(p_path):
209 | raise CouldNotGenerateNafudaIdError
210 |
211 | try:
212 | hostname = open(h_path).read(128).encode('UTF-8')
213 | passwd = open(p_path).read(128).encode('UTF-8')
214 |
215 | hash = hashlib.sha256(hostname + passwd).hexdigest()
216 | # print(hash)
217 |
218 | except OSError:
219 | return False
220 |
221 | return hash
222 |
223 |
224 | def get_img_url_base():
225 | data = local_settings()
226 | return data['CLOUD_BASE_URL'] + "/bc2018/" + get_nafuda_id() + "/"
227 |
228 |
229 | def get_control_url():
230 | data = local_settings()
231 | return data['CLOUD_BASE_URL'] + get_nafuda_id() + "/"
232 |
233 |
234 | def get_control_url_qrcode_img():
235 | qr = qrcode.QRCode(
236 | version=1,
237 | error_correction=qrcode.constants.ERROR_CORRECT_L,
238 | box_size=6,
239 | border=4,
240 | )
241 |
242 | try:
243 | qr.add_data(get_control_url())
244 | qr.make(fit=True)
245 | # print(qr.size)
246 | return qr.make_image(fill_color="black", back_color="white")
247 |
248 | except CouldNotGenerateNafudaIdError:
249 | # qrコード生成に失敗。エラーメッセージの画像を作って返す
250 | return generate_sorry_image("QRコードの生成に失敗:\n初期化してください")
251 |
252 |
253 | def generate_sorry_image(error_msg):
254 | if "EPD_FONT_PATH" in os.environ:
255 | font_path = os.environ['EPD_FONT_PATH']
256 | else:
257 | font_path = '/usr/share/fonts/truetype/vlgothic/VL-Gothic-Regular.ttf'
258 |
259 | font_pt = 16
260 | font = ImageFont.truetype(font_path, font_pt)
261 | image = Image.new('1', (200, 200), 1) # 1: clear the frame
262 | draw = ImageDraw.Draw(image)
263 |
264 | draw.text(
265 | (0, 0),
266 | error_msg,
267 | font=font,
268 | fill=0)
269 |
270 | return image
271 |
272 |
273 | def get_and_save_file(url, file_path):
274 | r = requests.get(url, stream=True)
275 | with open(file_path, 'wb') as f:
276 | for chunk in r.iter_content(chunk_size=1024):
277 | if chunk:
278 | f.write(chunk)
279 | f.flush()
280 |
281 |
282 | class CouldNotGenerateNafudaIdError(Exception):
283 | """ファイル不足などで名札IDが生成できなかった場合のエラー"""
284 |
285 |
286 | class LoadLocalSettingsError(Exception):
287 | """settings.jsonをロードできなかった場合のエラー"""
288 |
289 |
290 | if __name__ == '__main__':
291 | main()
292 |
--------------------------------------------------------------------------------
/simple_nafuda/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "CLOUD_BASE_URL": ""
3 | }
--------------------------------------------------------------------------------
/simple_nafuda/simple-nafuda.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=simple nafuda
3 |
4 | [Service]
5 | WorkingDirectory=/home/pi/electronic_badge_2018/simple_nafuda/
6 | ExecStart=/usr/bin/python3 main.py
7 | ExecStopPost=/home/pi/electronic_badge_2018/simple_nafuda/exec_stop_post.sh
8 | Restart=no
9 | Type=simple
10 | User=pi
11 |
12 | [Install]
13 | WantedBy=multi-user.target
14 |
15 | ## how to install
16 | # sudo cp simple-nafuda.service /etc/systemd/system/
17 | # sudo systemctl daemon-reload
18 | # sudo systemctl status simple-nafuda
19 | # (check output
20 | # sudo systemctl start simple-nafuda
21 | #
--------------------------------------------------------------------------------
/simple_nafuda_server/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | public/bc2018/*
3 |
--------------------------------------------------------------------------------
/simple_nafuda_server/README.md:
--------------------------------------------------------------------------------
1 | simple nafuda ota update server
2 | ===================
3 |
4 | - 「`simple-nafuda`アプリをスマホとかでも更新できるようにしたい!」
5 | - 「えっ(時間ないんですけど…)…作ります…」
6 |
7 | TBD
8 |
9 | ## require
10 |
11 | - php
12 | - php-gd
13 | - etc.
14 |
--------------------------------------------------------------------------------
/simple_nafuda_server/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "nikic/fast-route": "^1.3",
4 | "twig/twig": "^2.5",
5 | "intervention/image": "^2.4"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/simple_nafuda_server/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "e39604e1833283c20752cf4f49b2e4a4",
8 | "packages": [
9 | {
10 | "name": "guzzlehttp/psr7",
11 | "version": "1.4.2",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/guzzle/psr7.git",
15 | "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
20 | "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "php": ">=5.4.0",
25 | "psr/http-message": "~1.0"
26 | },
27 | "provide": {
28 | "psr/http-message-implementation": "1.0"
29 | },
30 | "require-dev": {
31 | "phpunit/phpunit": "~4.0"
32 | },
33 | "type": "library",
34 | "extra": {
35 | "branch-alias": {
36 | "dev-master": "1.4-dev"
37 | }
38 | },
39 | "autoload": {
40 | "psr-4": {
41 | "GuzzleHttp\\Psr7\\": "src/"
42 | },
43 | "files": [
44 | "src/functions_include.php"
45 | ]
46 | },
47 | "notification-url": "https://packagist.org/downloads/",
48 | "license": [
49 | "MIT"
50 | ],
51 | "authors": [
52 | {
53 | "name": "Michael Dowling",
54 | "email": "mtdowling@gmail.com",
55 | "homepage": "https://github.com/mtdowling"
56 | },
57 | {
58 | "name": "Tobias Schultze",
59 | "homepage": "https://github.com/Tobion"
60 | }
61 | ],
62 | "description": "PSR-7 message implementation that also provides common utility methods",
63 | "keywords": [
64 | "http",
65 | "message",
66 | "request",
67 | "response",
68 | "stream",
69 | "uri",
70 | "url"
71 | ],
72 | "time": "2017-03-20T17:10:46+00:00"
73 | },
74 | {
75 | "name": "intervention/image",
76 | "version": "2.4.2",
77 | "source": {
78 | "type": "git",
79 | "url": "https://github.com/Intervention/image.git",
80 | "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb"
81 | },
82 | "dist": {
83 | "type": "zip",
84 | "url": "https://api.github.com/repos/Intervention/image/zipball/e82d274f786e3d4b866a59b173f42e716f0783eb",
85 | "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb",
86 | "shasum": ""
87 | },
88 | "require": {
89 | "ext-fileinfo": "*",
90 | "guzzlehttp/psr7": "~1.1",
91 | "php": ">=5.4.0"
92 | },
93 | "require-dev": {
94 | "mockery/mockery": "~0.9.2",
95 | "phpunit/phpunit": "^4.8 || ^5.7"
96 | },
97 | "suggest": {
98 | "ext-gd": "to use GD library based image processing.",
99 | "ext-imagick": "to use Imagick based image processing.",
100 | "intervention/imagecache": "Caching extension for the Intervention Image library"
101 | },
102 | "type": "library",
103 | "extra": {
104 | "branch-alias": {
105 | "dev-master": "2.4-dev"
106 | },
107 | "laravel": {
108 | "providers": [
109 | "Intervention\\Image\\ImageServiceProvider"
110 | ],
111 | "aliases": {
112 | "Image": "Intervention\\Image\\Facades\\Image"
113 | }
114 | }
115 | },
116 | "autoload": {
117 | "psr-4": {
118 | "Intervention\\Image\\": "src/Intervention/Image"
119 | }
120 | },
121 | "notification-url": "https://packagist.org/downloads/",
122 | "license": [
123 | "MIT"
124 | ],
125 | "authors": [
126 | {
127 | "name": "Oliver Vogel",
128 | "email": "oliver@olivervogel.com",
129 | "homepage": "http://olivervogel.com/"
130 | }
131 | ],
132 | "description": "Image handling and manipulation library with support for Laravel integration",
133 | "homepage": "http://image.intervention.io/",
134 | "keywords": [
135 | "gd",
136 | "image",
137 | "imagick",
138 | "laravel",
139 | "thumbnail",
140 | "watermark"
141 | ],
142 | "time": "2018-05-29T14:19:03+00:00"
143 | },
144 | {
145 | "name": "nikic/fast-route",
146 | "version": "v1.3.0",
147 | "source": {
148 | "type": "git",
149 | "url": "https://github.com/nikic/FastRoute.git",
150 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812"
151 | },
152 | "dist": {
153 | "type": "zip",
154 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
155 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812",
156 | "shasum": ""
157 | },
158 | "require": {
159 | "php": ">=5.4.0"
160 | },
161 | "require-dev": {
162 | "phpunit/phpunit": "^4.8.35|~5.7"
163 | },
164 | "type": "library",
165 | "autoload": {
166 | "psr-4": {
167 | "FastRoute\\": "src/"
168 | },
169 | "files": [
170 | "src/functions.php"
171 | ]
172 | },
173 | "notification-url": "https://packagist.org/downloads/",
174 | "license": [
175 | "BSD-3-Clause"
176 | ],
177 | "authors": [
178 | {
179 | "name": "Nikita Popov",
180 | "email": "nikic@php.net"
181 | }
182 | ],
183 | "description": "Fast request router for PHP",
184 | "keywords": [
185 | "router",
186 | "routing"
187 | ],
188 | "time": "2018-02-13T20:26:39+00:00"
189 | },
190 | {
191 | "name": "psr/http-message",
192 | "version": "1.0.1",
193 | "source": {
194 | "type": "git",
195 | "url": "https://github.com/php-fig/http-message.git",
196 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
197 | },
198 | "dist": {
199 | "type": "zip",
200 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
201 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
202 | "shasum": ""
203 | },
204 | "require": {
205 | "php": ">=5.3.0"
206 | },
207 | "type": "library",
208 | "extra": {
209 | "branch-alias": {
210 | "dev-master": "1.0.x-dev"
211 | }
212 | },
213 | "autoload": {
214 | "psr-4": {
215 | "Psr\\Http\\Message\\": "src/"
216 | }
217 | },
218 | "notification-url": "https://packagist.org/downloads/",
219 | "license": [
220 | "MIT"
221 | ],
222 | "authors": [
223 | {
224 | "name": "PHP-FIG",
225 | "homepage": "http://www.php-fig.org/"
226 | }
227 | ],
228 | "description": "Common interface for HTTP messages",
229 | "homepage": "https://github.com/php-fig/http-message",
230 | "keywords": [
231 | "http",
232 | "http-message",
233 | "psr",
234 | "psr-7",
235 | "request",
236 | "response"
237 | ],
238 | "time": "2016-08-06T14:39:51+00:00"
239 | },
240 | {
241 | "name": "symfony/polyfill-ctype",
242 | "version": "v1.9.0",
243 | "source": {
244 | "type": "git",
245 | "url": "https://github.com/symfony/polyfill-ctype.git",
246 | "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
247 | },
248 | "dist": {
249 | "type": "zip",
250 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
251 | "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
252 | "shasum": ""
253 | },
254 | "require": {
255 | "php": ">=5.3.3"
256 | },
257 | "suggest": {
258 | "ext-ctype": "For best performance"
259 | },
260 | "type": "library",
261 | "extra": {
262 | "branch-alias": {
263 | "dev-master": "1.9-dev"
264 | }
265 | },
266 | "autoload": {
267 | "psr-4": {
268 | "Symfony\\Polyfill\\Ctype\\": ""
269 | },
270 | "files": [
271 | "bootstrap.php"
272 | ]
273 | },
274 | "notification-url": "https://packagist.org/downloads/",
275 | "license": [
276 | "MIT"
277 | ],
278 | "authors": [
279 | {
280 | "name": "Symfony Community",
281 | "homepage": "https://symfony.com/contributors"
282 | },
283 | {
284 | "name": "Gert de Pagter",
285 | "email": "BackEndTea@gmail.com"
286 | }
287 | ],
288 | "description": "Symfony polyfill for ctype functions",
289 | "homepage": "https://symfony.com",
290 | "keywords": [
291 | "compatibility",
292 | "ctype",
293 | "polyfill",
294 | "portable"
295 | ],
296 | "time": "2018-08-06T14:22:27+00:00"
297 | },
298 | {
299 | "name": "symfony/polyfill-mbstring",
300 | "version": "v1.9.0",
301 | "source": {
302 | "type": "git",
303 | "url": "https://github.com/symfony/polyfill-mbstring.git",
304 | "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8"
305 | },
306 | "dist": {
307 | "type": "zip",
308 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8",
309 | "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8",
310 | "shasum": ""
311 | },
312 | "require": {
313 | "php": ">=5.3.3"
314 | },
315 | "suggest": {
316 | "ext-mbstring": "For best performance"
317 | },
318 | "type": "library",
319 | "extra": {
320 | "branch-alias": {
321 | "dev-master": "1.9-dev"
322 | }
323 | },
324 | "autoload": {
325 | "psr-4": {
326 | "Symfony\\Polyfill\\Mbstring\\": ""
327 | },
328 | "files": [
329 | "bootstrap.php"
330 | ]
331 | },
332 | "notification-url": "https://packagist.org/downloads/",
333 | "license": [
334 | "MIT"
335 | ],
336 | "authors": [
337 | {
338 | "name": "Nicolas Grekas",
339 | "email": "p@tchwork.com"
340 | },
341 | {
342 | "name": "Symfony Community",
343 | "homepage": "https://symfony.com/contributors"
344 | }
345 | ],
346 | "description": "Symfony polyfill for the Mbstring extension",
347 | "homepage": "https://symfony.com",
348 | "keywords": [
349 | "compatibility",
350 | "mbstring",
351 | "polyfill",
352 | "portable",
353 | "shim"
354 | ],
355 | "time": "2018-08-06T14:22:27+00:00"
356 | },
357 | {
358 | "name": "twig/twig",
359 | "version": "v2.5.0",
360 | "source": {
361 | "type": "git",
362 | "url": "https://github.com/twigphp/Twig.git",
363 | "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323"
364 | },
365 | "dist": {
366 | "type": "zip",
367 | "url": "https://api.github.com/repos/twigphp/Twig/zipball/6a5f676b77a90823c2d4eaf76137b771adf31323",
368 | "reference": "6a5f676b77a90823c2d4eaf76137b771adf31323",
369 | "shasum": ""
370 | },
371 | "require": {
372 | "php": "^7.0",
373 | "symfony/polyfill-ctype": "^1.8",
374 | "symfony/polyfill-mbstring": "~1.0"
375 | },
376 | "require-dev": {
377 | "psr/container": "^1.0",
378 | "symfony/debug": "^2.7",
379 | "symfony/phpunit-bridge": "^3.3"
380 | },
381 | "type": "library",
382 | "extra": {
383 | "branch-alias": {
384 | "dev-master": "2.5-dev"
385 | }
386 | },
387 | "autoload": {
388 | "psr-0": {
389 | "Twig_": "lib/"
390 | },
391 | "psr-4": {
392 | "Twig\\": "src/"
393 | }
394 | },
395 | "notification-url": "https://packagist.org/downloads/",
396 | "license": [
397 | "BSD-3-Clause"
398 | ],
399 | "authors": [
400 | {
401 | "name": "Fabien Potencier",
402 | "email": "fabien@symfony.com",
403 | "homepage": "http://fabien.potencier.org",
404 | "role": "Lead Developer"
405 | },
406 | {
407 | "name": "Armin Ronacher",
408 | "email": "armin.ronacher@active-4.com",
409 | "role": "Project Founder"
410 | },
411 | {
412 | "name": "Twig Team",
413 | "homepage": "https://twig.symfony.com/contributors",
414 | "role": "Contributors"
415 | }
416 | ],
417 | "description": "Twig, the flexible, fast, and secure template language for PHP",
418 | "homepage": "https://twig.symfony.com",
419 | "keywords": [
420 | "templating"
421 | ],
422 | "time": "2018-07-13T07:18:09+00:00"
423 | }
424 | ],
425 | "packages-dev": [],
426 | "aliases": [],
427 | "minimum-stability": "stable",
428 | "stability-flags": [],
429 | "prefer-stable": false,
430 | "prefer-lowest": false,
431 | "platform": [],
432 | "platform-dev": []
433 | }
434 |
--------------------------------------------------------------------------------
/simple_nafuda_server/public/.htaccess:
--------------------------------------------------------------------------------
1 | RewriteEngine On
2 | RewriteCond %{REQUEST_FILENAME} !-f
3 | RewriteRule ^ index.php [QSA,L]
4 |
5 | # sudo a2enmod headers
6 | RequestHeader unset If-Modified-Since
7 | Header set Cache-Control no-store
--------------------------------------------------------------------------------
/simple_nafuda_server/public/bc2018/image_store_dir:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/builderscon/electronic_badge_2018/0999c28f7f7c635e9f3a809f723c237b3856138d/simple_nafuda_server/public/bc2018/image_store_dir
--------------------------------------------------------------------------------
/simple_nafuda_server/public/form.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | electronic badge - builderscon tokyo 2018
7 |
13 |
14 |
15 |
16 | 電子名札 画像設定ツール
17 |
18 | こちらの画面で画像をアップロードした後、「ネットワークがある状態で」電子名札のrebootが必要です。
19 | (起動時の情報表示に、wlan0などでIPが振られえいることを確認してください)
20 | reboot後、nafuda slideshow前にDownloadが行われます。
21 |
22 |
23 | Max file size は 5MB です。アップロードに失敗する場合は、resizeしてください
24 |
25 |
26 |
27 | QRコードは他人にスキャンされないようにご注意ください(わざとであれば問題はありません)
28 |
29 |
30 | 一覧
31 |
32 | $exists) { $num = $i;?>
33 |
34 |
35 |
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 電子名札の情報やドキュメントなどが記載された、公式のGithubはこちらからどうぞ
52 | github/builderscon/electronic_badge_2018
53 |
54 |
55 |
56 | 初期に登録されていた注意書きや、メルカリ様ロゴなどは以下からDLできます
57 | IMG
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/simple_nafuda_server/public/index.php:
--------------------------------------------------------------------------------
1 | addRoute('GET', '/{uid}/', 'form');
9 | $r->addRoute('GET', '/{uid}/json', 'json');
10 | $r->addRoute('POST', '/{uid}/upload/{num}', 'upload');
11 | $r->addRoute('POST', '/{uid}/delete/{num}', 'delete');
12 | };
13 | $dispatcher = FastRoute\cachedDispatcher($handlers, [
14 | 'cacheFile' => __DIR__ . '/../route.cache',
15 | 'cacheDisabled' => true
16 | ]);
17 | $uri = $_SERVER['REQUEST_URI'];
18 | $method = $_SERVER['REQUEST_METHOD'];
19 | $routeInfo = $dispatcher->dispatch($method, $uri);
20 | switch ($routeInfo[0]) {
21 | case FastRoute\Dispatcher::NOT_FOUND:
22 | echo "NotFound";
23 | break;
24 | case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
25 | $allowedMethods = $routeInfo[1];
26 | echo "Forbidden";
27 | break;
28 | case FastRoute\Dispatcher::FOUND:
29 | $handler = $routeInfo[1];
30 | $vars = $routeInfo[2];
31 | echo $handler($vars);
32 | break;
33 | }
34 |
35 | function form($args)
36 | {
37 | $s = generate_something($args);
38 |
39 | $list = exists_img_list($s);
40 | include 'form.php';
41 | }
42 |
43 | function exists_img_list($s)
44 | {
45 | if (!file_exists($s['base_img_dir'])) {
46 | mkdir($s['$base_img_dir']);
47 | }
48 |
49 | $list = [];
50 | for ($i = 1; $i < 10; $i++) {
51 | if (file_exists("{$s['base_img_dir']}/{$i}.png")) {
52 | $list[$i] = true;
53 | } else {
54 | $list[$i] = false;
55 | }
56 | }
57 | return $list;
58 | }
59 |
60 | function json($args)
61 | {
62 | $s = generate_something($args);
63 | $list = [];
64 | foreach (exists_img_list($s) as $k => $exists) {
65 | if ($exists) {
66 | $list[] = "{$k}.png";
67 | }
68 | }
69 |
70 | header('content-type: application/json');
71 | echo json_encode([
72 | "last_update" => time(), # fix me!!!
73 | "list" => $list
74 | ], JSON_PRETTY_PRINT);
75 |
76 | }
77 |
78 | function upload($args)
79 | {
80 | if (!isset($_FILES['image'])) {
81 | redirect($s['your_form_url']);
82 | }
83 |
84 | $tmp = $_FILES['image']['tmp_name'];
85 | if (!is_uploaded_file($tmp)) {
86 | throw new \InvalidArgumentException("not upload file");
87 | }
88 |
89 | if (filesize($tmp) > 1024 * 1024 * 5) {
90 | throw new \InvalidArgumentException("too large. 5MB is max.");
91 | }
92 |
93 | $img_size = getimagesize($tmp);
94 | if ($img_size[0] > 5000 || $img_size[1] > 5000) {
95 | throw new \InvalidArgumentException("too big. 5000px is max.");
96 | }
97 |
98 | $imgmanager = new \Intervention\Image\ImageManager();
99 |
100 | $image = $imgmanager
101 | ->make($tmp)
102 | ->orientate()// なんか…色がおかしくなる…
103 | ->widen(300, function ($constraint) {
104 | $constraint->upsize();
105 | })
106 | ->heighten(400, function ($constraint) {
107 | $constraint->upsize();
108 | });
109 |
110 | $s = generate_something($args);
111 |
112 | if (!file_exists($s['base_img_dir'])) {
113 | mkdir($s['base_img_dir']);
114 | }
115 |
116 | $image->save("{$s['base_img_dir']}/{$s['num']}.png", 60);
117 |
118 | redirect($s['your_form_url']);
119 | }
120 |
121 | function delete($args)
122 | {
123 | $s = generate_something($args);
124 | $file_path = "{$s['base_img_dir']}/{$s['num']}.png";
125 | if (file_exists($file_path)) {
126 | unlink($file_path);
127 | }
128 | redirect($s['your_form_url']);
129 | }
130 |
131 | #
132 | ###
133 | #
134 |
135 | function generate_something($args)
136 | {
137 | $rtn = [];
138 |
139 | $uid = $args['uid'];
140 | if (strlen($uid) != 64) {
141 | throw new \InvalidArgumentException('invalid nafuda id (len)' . strlen($uid));
142 | }
143 | if (preg_match('/[^A-Za-z[0-9]]/u', $uid)) {
144 | throw new \InvalidArgumentException('invalid nafuda id (char)');
145 | }
146 | $rtn['uid'] = $uid;
147 |
148 | $rtn['base_img_dir'] = IMG_DIR . '/' . $uid;
149 | $rtn['base_img_url'] = '/' . TG . '/' . $uid;
150 |
151 | $rtn['your_form_url'] = $uid . "/";
152 |
153 | if (isset($args['num'])) {
154 | $num = (int)$args['num'];
155 | if ($num > 10 || 1 > $num) {
156 | throw new \InvalidArgumentException("invalid num");
157 | }
158 | $rtn['num'] = $num;
159 | }
160 |
161 | return $rtn;
162 | }
163 |
164 | function redirect($url)
165 | {
166 | header("Location: /{$url}");
167 | exit;
168 | }
169 |
170 | function e($str, $rtn = false)
171 | {
172 | if ($rtn) {
173 | return htmlspecialchars($str, ENT_QUOTES);
174 | } else {
175 | echo htmlspecialchars($str, ENT_QUOTES);
176 | return;
177 | }
178 | }
179 |
180 |
--------------------------------------------------------------------------------
/simple_sample/README.md:
--------------------------------------------------------------------------------
1 | simple sample
2 | =============
3 |
4 | 一番簡単なHello worldサンプル。
5 |
6 | 「ログインせずに」NAFUDAドライブの中身だけでハックする!
7 |
8 | # 試し方
9 |
10 | - このディレクトリの`startup.sh`をNAFUDAドライブのトップに設置する
11 | - 名札を再起動する
12 | - 「hello nafuda world!」
13 |
14 |
15 | # 概要説明
16 |
17 | systemdに登録された`nafuda-bootup.service`(`electronic_badge_2018/bootup/bootup.sh`)は、最後にNAFUDAドライブの直下にstartup.shがあればそれを起動します。
18 |
19 | > ※ その場合、simple-nafudaは起動されません。
20 |
21 | この`starup.sh`は無限ループで名札内に保存されている `show_txt`や`show_img`コマンドを用いて画像を表示しています。
22 |
23 | このシェルスクリプトを改変することで、簡単なものならばログインなしにプログラムが可能です。
24 |
25 | > ※ エラー原因を特定するなどを考えるとなかなか難しいですが…
26 |
27 | > ※ `show_txt`や`show_img`の使い方はGitHubのレポジトリのそれぞれをみてください。
28 |
29 | > ※ この`startup.sh`はroot権限で起動されます
30 |
31 |
32 | # たとえば他の例
33 |
34 | ```
35 | # nictの現在時刻取得jsonをたたいて表示してみるとか
36 | curl http://ntp-a1.nict.go.jp/cgi-bin/json | show_txt -
37 |
38 | # 画像をDLして表示するとか
39 | # ※ /mnt/virtual_sdは初期ではread onlyでマウントしているため、
40 | # 書き込みが必要であれば/tmpを指定してください。
41 | curl 'https://builderscon.io/static/images/mrbeacon-001.png' > /tmp/bcon.png
42 | show_img /tmp/bcon.png
43 | # (もしご自身でmount_vsd_rwを用いて再マウントすればその限りではありませんが、
44 | # busyだと失敗するので、やはりログインできるようになってからが良いでしょう)
45 | ```
46 |
47 | > ※ `show_img`の実体は `/home/pi/electronic_badge_2018/show_img/show_img.py`
48 |
49 | > ※ `show_txt`の実体は `/home/pi/electronic_badge_2018/show_txt/show_txt.py`
50 |
51 | # おまけ
52 |
53 | ## 各インタプリタへのパス
54 |
55 | ```
56 | /usr/bin/python3
57 | ※ /usr/bin/python は python2なので推奨はしません
58 | /usr/bin/perl
59 | ```
60 |
61 | ## 「とにかくなにか実行して結果が知りたい」
62 |
63 | `show_txt`をつかえば、epaperに表示することが可能です。
64 |
65 | ```bash
66 | #!/bin/bash
67 | ls -al ~/ 2>&1 | show_txt -
68 | # 2>&1 をいれないと、エラー出力がすてられます
69 | ```
70 |
71 | > ※ 大変なので、ログインしたほうが良いとおもいます!
72 |
73 |
74 | ## git cloneやpullできるのでは?
75 |
76 | やってやれないことはありません。
77 |
78 | ```bash
79 | export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
80 | git -C /home/pi clone https://github.com/builderscon/electronic_badge_2018.git 2>&1 | show_txt -
81 | git -C /home/pi pull 2>&1 | show_txt -
82 | ```
83 |
84 | > ※ コンフリクトしていたときに解消が難しいので、おすすめはできませんが…
85 |
86 | ## 「あれやこれやができるのでは?」
87 |
88 | root権限で、TTYが不要で、CLIで実行できることなら、大体できるとおもいます。
89 |
90 | ## 「危険では?!」
91 |
92 | microSDを抜いてマウントすればなんでもできますし、NAFUDAドライブの中にはデフォルトパスワードもみえるので、電子名札はそういう思想です。
93 |
94 | 電子名札には機密情報をいれないようにしましょう。入れる場合は自己責任。
95 |
96 | ## Do you want PHP?
97 |
98 | phpがお好き?そう、PHPもいれられますね。以下のstartup.shを作成して再起動すればインストールできるよ!
99 |
100 | ```bash
101 | #!/bin/bash
102 | echo "php installing now..." | show_txt -
103 | # とても時間かかります!
104 | sudo apt -y install php 2>&1 | show_txt -
105 | LOG=`which php`
106 | LOG="${LOG} `php -v`"
107 | echo "${LOG}"
108 | echo "finished. ${LOG}" | show_txt -
109 | ```
110 |
111 |
112 | あとは、たとえば以下みたいなコードをかけば表示もできます!
113 |
114 | ```php
115 | ※ NAFUDAドライブは読み込み専用でマウントされているので、一時ファイルを作成するときは /tmp/ に作成してください。
123 |
124 | > ※ もしご自身の自己責任でmount_vsd_rw.shを用いて再マウントすればその限りではありません。
125 |
--------------------------------------------------------------------------------
/simple_sample/startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 無限ループ
4 | while :
5 | do
6 |
7 | # テキストを標準入力から受け取って、電子ペーパーに表示
8 | echo "hello NAFUDA world!" | show_txt -
9 |
10 | # テキストをファイルから読み込んで、電子ペーパーに表示
11 | show_txt /mnt/virtual_sd/startup.sh
12 |
13 | # 画像をファイルから読み込んで、電子ペーパーに表示
14 | show_img /mnt/virtual_sd/img/bcon-logo.png
15 |
16 | # コマンド結果を電子ペーパーに表示
17 | ls -al ~/ 2>&1 | show_txt -
18 |
19 | done
20 |
21 |
--------------------------------------------------------------------------------
/weather/README.md:
--------------------------------------------------------------------------------
1 | # weather news sample
2 |
3 | 天気を表示するサンプルです。
4 |
5 | Show today's weather(in japanese).
6 |
7 | ## usage
8 |
9 | ```
10 | $ python3 main.py
11 | ```
12 |
13 | ## info
14 |
15 | Livedoorの天気 WebAPIを利用しています。
16 |
--------------------------------------------------------------------------------
/weather/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # draw today weather
3 |
4 | # for development.
5 | # $ export PSEUDO_EPD_MODE=1
6 | # $ python show.py your_image.png
7 | #
8 | # if you want to use your fav font
9 | # $ export EPD_FONT_PATH="/System/Library/Fonts/ヒラギノ角ゴシック W0.ttc"
10 | # $ python main.py
11 | #
12 | # Copyright (C) Aug 4 2018, Junichi Ishida
13 | #
14 | # LICENSE : MIT
15 | #
16 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
17 | # documentation files (the "Software"), to deal in the Software without restriction, including without limitation
18 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
19 | # to permit persons to whom the Software is furnished to do so, subject to the following conditions:
20 | #
21 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of
22 | # the Software.
23 | #
24 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
25 | # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 | #
30 |
31 | import os
32 | import sys
33 | from PIL import Image
34 | from PIL import ImageDraw
35 | from PIL import ImageFont
36 | import weather
37 |
38 | sys.path.append(os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + '/../lib'))
39 |
40 | # for PC development
41 | if "PSEUDO_EPD_MODE" in os.environ:
42 | import epd4in2_mock as epd4in2
43 | else:
44 | import epd4in2
45 |
46 | EPD_WIDTH = 400
47 | EPD_HEIGHT = 300
48 |
49 |
50 | def main():
51 | epd = epd4in2.EPD()
52 | epd.init()
53 |
54 | weather_client = weather.Weather()
55 |
56 | # 130010 is tokyo
57 | weather_data = weather_client.get_usable_array(130010)
58 |
59 | # For simplicity, the arguments are explicit numerical coordinates
60 | image = Image.new('1', (EPD_WIDTH, EPD_HEIGHT), 1) # 1: clear the frame
61 |
62 | if "EPD_FONT_PATH" in os.environ:
63 | font_path = os.environ['EPD_FONT_PATH']
64 | else:
65 | font_path = '/usr/share/fonts/truetype/vlgothic/VL-Gothic-Regular.ttf'
66 |
67 | draw_fit_text_to_image(
68 | image,
69 | font_path,
70 | weather_data['telop'],
71 | # 縦半分の高さで、y軸0始点
72 | (EPD_WIDTH, EPD_HEIGHT / 2),
73 | (0, 0))
74 |
75 | temperture_text = "最高気温" + weather_data['max_temperature'] + "度"
76 |
77 | draw_fit_text_to_image(
78 | image,
79 | font_path,
80 | temperture_text,
81 | # 縦半分の高さで、y軸中央を始点
82 | (EPD_WIDTH, EPD_HEIGHT / 2),
83 | (0, EPD_HEIGHT / 2))
84 |
85 | epd.display_frame(epd.get_frame_buffer(image))
86 |
87 |
88 | def draw_fit_text_to_image(image, font_path, text, window_size, window_offset):
89 | font_pt = get_fit_font_pt(font_path, text, window_size[0], window_size[1])
90 |
91 | font = ImageFont.truetype(font_path, font_pt)
92 |
93 | draw = ImageDraw.Draw(image)
94 | offset = get_offset_for_centering(window_size, font.getsize(text))
95 |
96 | draw.text(
97 | (offset[0] + window_offset[0], offset[1] + window_offset[1]),
98 | text,
99 | font=font,
100 | fill=0)
101 |
102 |
103 | def get_fit_font_pt(font_path, text, canvas_w, canvas_h):
104 | pt = 0
105 | max_pt = 256
106 |
107 | while True:
108 | # too bulldoze method. but e-paper is more than slow.
109 | pt = pt + 1
110 | if max_pt < pt:
111 | break
112 |
113 | font = ImageFont.truetype(font_path, pt)
114 | font_w, font_h = font.getsize(text)
115 |
116 | if font_w > canvas_w:
117 | break
118 | if font_h > canvas_h:
119 | break
120 |
121 | return pt - 1
122 |
123 |
124 | def get_offset_for_centering(canvas_size, img_size):
125 | x = 0
126 | y = 0
127 | # print(canvas_size)
128 | # print(canvas_size[0])
129 | # print(img_size)
130 | # print(img_size[0])
131 |
132 | if canvas_size[0] > img_size[0]:
133 | x = int((canvas_size[0] - img_size[0]) / 2)
134 | if canvas_size[1] > img_size[1]:
135 | y = int((canvas_size[1] - img_size[1]) / 2)
136 |
137 | return x, y
138 |
139 |
140 | if __name__ == '__main__':
141 | main()
142 |
--------------------------------------------------------------------------------
/weather/weather.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) Aug 4 2018, Junichi Ishida
3 | #
4 | # LICENSE : MIT
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
7 | # documentation files (the "Software"), to deal in the Software without restriction, including without limitation
8 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
9 | # to permit persons to whom the Software is furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of
12 | # the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
15 | # THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 | #
20 |
21 | import urllib.request
22 | import json
23 | from pprint import pprint
24 |
25 |
26 | class Weather:
27 |
28 | @staticmethod
29 | def get_data(city_id=130010):
30 | # Livedoor Weather Web Service
31 | # http://weather.livedoor.com/weather_hacks/webservice
32 | url = 'http://weather.livedoor.com/forecast/webservice/json/v1'
33 | params = {
34 | 'city': city_id,
35 | }
36 |
37 | req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
38 | with urllib.request.urlopen(req) as res:
39 | body = res.read()
40 |
41 | return json.loads(body.decode('utf-8'))
42 |
43 | @staticmethod
44 | def get_usable_array(city_id):
45 | data = Weather.get_data(city_id)
46 | # pprint(data['forecasts'])
47 |
48 | telop = data['forecasts'][0]['telop']
49 |
50 | if data['forecasts'][0]['temperature']['max'] is None:
51 | max_temperture = "データ無し"
52 | else:
53 | max_temperture = data['forecasts'][0]['temperature']['max']['celsius']
54 |
55 | return {
56 | 'telop': telop,
57 | 'max_temperature': max_temperture
58 | }
59 |
60 |
61 | if __name__ == '__main__':
62 | pprint(Weather.get_usable_array(130010))
63 |
--------------------------------------------------------------------------------