Allergen checking with the Arduino UNO Q
Check food allergens by scanning a barcode with the Arduino UNO Q and the Open Food Facts API
Devices & Components
1
USB-C to HDMI multiport adapter 4K, USB hub, PD pass through
1
Arduino® UNO™ Q 2GB
1
USB WebCam
1
USB Keyboard
Software & Tools
Arduino App Lab
Project description
Code
allergen_scanner_ui
markup
1<!-- 2SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA <http://www.arduino.cc> 3SPDX-License-Identifier: MPL-2.0 4--> 5<!DOCTYPE html> 6<html lang="en"> 7<head> 8 <meta charset="UTF-8"> 9 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 10 <title>Food Allergen Scanner</title> 11 <link rel="stylesheet" type="text/css" href="style.css"> 12 <style> 13 .allergen-tag { 14 display: inline-block; 15 padding: 3px 8px; 16 margin: 2px; 17 border-radius: 3px; 18 font-size: 0.85em; 19 font-weight: bold; 20 } 21 22 .allergen-warning { 23 background: #ffebee; 24 color: #c62828; 25 } 26 27 .allergen-trace { 28 background: #fff3cd; 29 color: #856404; 30 } 31 32 .allergen-safe { 33 background: #e8f5e9; 34 color: #2e7d32; 35 } 36 37 .allergen-not-found { 38 color: #999; 39 font-style: italic; 40 font-size: 0.9em; 41 } 42 43 .product-name { 44 font-weight: bold; 45 color: #00979D; 46 margin-top: 3px; 47 } 48 </style> 49</head> 50<body> 51 <div> 52 <div class="header"> 53 <h1 class="arduino-text">Food Allergen Scanner</h1> 54 <img class="arduino-logo" src="./img/logo.svg" alt="Arduino Logo"> 55 </div> 56 <div class="main-content"> 57 <div class="container"> 58 <div id="videoFeedContainer"> 59 <canvas id="videoCanvas" width="640" height="360"> 60 Your browser does not support the HTML5 canvas tag. 61 </canvas> 62 </div> 63 <!-- Camera Status Banner --> 64 <div id="cameraStatus" class="camera-status"> 65 <div class="search"> 66 <img class="icon" src="./img/lens.svg" alt="Lens"> 67 <span>Searching for QR or Barcodes</span> 68 </div> 69 <div class="search-small">Hold it in front of the cam</div> 70 </div> 71 <div id="scanInfo"></div> 72 <div id="rescan-button-container" style="display: none;"> 73 <button id="rescanButton" onclick="rescan()">Scan another</button> 74 </div> 75 </div> 76 <div class="container container-scans"> 77 <div class="recent-scans-title-container"> 78 <h2 class="recent-scans-title">Recent scans</h2> 79 <img id="delete-scan" class="delete-scan-logo" style="display:none;" src="./img/delete-scan.svg" alt="Delete Scan" onclick="clearRecentScans()"> 80 </div> 81 <ul id="recentScansList"></ul> 82 <div id="initialListError" class="error-message" style="display:none;"></div> 83 <div id="scanMessage" style="display:none;">only the last 5 scans are saved</div> 84 </div> 85 </div> 86 <div id="error-container" class="error-message" style="display: none;"></div> 87 </div> 88 <script src="libs/socket.io.min.js"></script> 89 <script> 90 const socket = io(); 91 const videoCanvas = document.getElementById('videoCanvas'); 92 const ctx = videoCanvas.getContext('2d'); 93 const scanInfo = document.getElementById('scanInfo'); 94 const cameraStatus = document.getElementById('cameraStatus'); 95 const rescanButtonContainer = document.getElementById('rescan-button-container'); 96 const recentScansList = document.getElementById('recentScansList'); 97 const errorContainer = document.getElementById('error-container'); 98 99 socket.on('frame_detected', (data) => { 100 const img = new Image(); 101 img.onload = () => { 102 ctx.drawImage(img, 0, 0, videoCanvas.width, videoCanvas.height); 103 }; 104 img.src = `data:${data.image_type};base64,${data.image}`; 105 }); 106 107 socket.on('code_detected', (data) => { 108 const img = new Image(); 109 img.onload = () => { 110 ctx.drawImage(img, 0, 0, videoCanvas.width, videoCanvas.height); 111 }; 112 img.src = `data:${data.image_type};base64,${data.image}`; 113 114 cameraStatus.style.display = 'none'; 115 scanInfo.innerHTML = `<h3>${data.type} detected:</h3><p>${data.content}</p>`; 116 scanInfo.style.display = 'block'; 117 rescanButtonContainer.style.display = 'block'; 118 119 loadRecentScans(); 120 }); 121 122 socket.on('error', (errorMsg) => { 123 errorContainer.textContent = `Error: ${errorMsg}`; 124 errorContainer.style.display = 'block'; 125 setTimeout(() => { 126 errorContainer.style.display = 'none'; 127 }, 5000); 128 }); 129 130 function rescan() { 131 socket.emit('reset_detection', {}); 132 scanInfo.innerHTML = ''; 133 scanInfo.style.display = 'none'; 134 rescanButtonContainer.style.display = 'none'; 135 cameraStatus.style.display = 'block'; 136 } 137 138 async function loadRecentScans() { 139 try { 140 const response = await fetch('/list_scans'); 141 const data = await response.json(); 142 143 if (data.scans && data.scans.length > 0) { 144 recentScansList.innerHTML = ''; 145 document.getElementById('delete-scan').style.display = 'block'; 146 document.getElementById('scanMessage').style.display = 'block'; 147 148 data.scans.forEach(scan => { 149 const li = document.createElement('li'); 150 const scanDate = new Date(scan.timestamp); 151 const formattedDate = scanDate.toLocaleString(); 152 153 let allergenLetters = ''; 154 let allergenHTML = ''; 155 156 if (scan.allergen_info) { 157 const info = scan.allergen_info; 158 159 if (!info.found) { 160 allergenHTML = `<div class="allergen-not-found">Product not in database</div>`; 161 } else { 162 // Collect first letters of allergens 163 let letters = []; 164 if (info.allergens && info.allergens.length > 0) { 165 info.allergens.forEach(allergen => { 166 letters.push(allergen.charAt(0).toUpperCase()); 167 }); 168 } 169 if (letters.length > 0) { 170 allergenLetters = ` (${letters.join(', ')})`; 171 } 172 173 allergenHTML = `<div class="product-name">${info.product_name}</div>`; 174 175 if (info.allergen_free && info.traces.length === 0) { 176 allergenHTML += '<span class="allergen-tag allergen-safe">✓ No allergens</span>'; 177 } else { 178 if (info.allergens && info.allergens.length > 0) { 179 info.allergens.forEach(allergen => { 180 allergenHTML += `<span class="allergen-tag allergen-warning">⚠️ ${allergen}</span>`; 181 }); 182 } 183 184 if (info.traces && info.traces.length > 0) { 185 info.traces.forEach(trace => { 186 allergenHTML += `<span class="allergen-tag allergen-trace">May contain ${trace}</span>`; 187 }); 188 } 189 } 190 } 191 } 192 193 li.innerHTML = ` 194 <strong>${scan.type}:</strong> ${scan.content}${allergenLetters}<br> 195 <small>${formattedDate}</small> 196 ${allergenHTML} 197 `; 198 recentScansList.appendChild(li); 199 }); 200 } 201 } catch (error) { 202 console.error('Error loading recent scans:', error); 203 } 204 } 205 206 function clearRecentScans() { 207 socket.emit('clear_scans', {}); 208 recentScansList.innerHTML = ''; 209 document.getElementById('delete-scan').style.display = 'none'; 210 document.getElementById('scanMessage').style.display = 'none'; 211 } 212 213 loadRecentScans(); 214 </script> 215</body> 216</html>
allergen_scanner
python
1# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA <http://www.arduino.cc> 2# 3# SPDX-License-Identifier: MPL-2.0 4from datetime import datetime, UTC 5import io 6import base64 7import requests 8from PIL.Image import Image 9from arduino.app_utils import * 10from arduino.app_peripherals.usb_camera import USBCamera 11from arduino.app_bricks.web_ui import WebUI 12from arduino.app_bricks.camera_code_detection import CameraCodeDetection, Detection, draw_bounding_box 13 14detected = False 15recent_scans = [] # Store recent scans in memory 16 17def check_open_food_facts(barcode): 18 """Query Open Food Facts API for product allergen information""" 19 try: 20 url = f"https://world.openfoodfacts.org/api/v0/product/{barcode}.json" 21 response = requests.get(url, timeout=5) 22 23 if response.status_code == 200: 24 data = response.json() 25 26 if data.get("status") == 1: 27 product = data.get("product", {}) 28 return extract_allergen_info(product) 29 else: 30 return { 31 "found": False, 32 "message": "Product not found in Open Food Facts database" 33 } 34 else: 35 return { 36 "found": False, 37 "message": f"API error: {response.status_code}" 38 } 39 40 except requests.exceptions.Timeout: 41 return { 42 "found": False, 43 "message": "Request timed out. Check internet connection." 44 } 45 except Exception as e: 46 return { 47 "found": False, 48 "message": f"Error: {str(e)}" 49 } 50 51def extract_allergen_info(product): 52 """Extract allergen information from product data""" 53 info = { 54 "found": True, 55 "product_name": product.get("product_name", "Unknown product"), 56 "brands": product.get("brands", ""), 57 "allergens": [], 58 "traces": [], 59 "allergen_free": True 60 } 61 62 # Check allergens field 63 allergens_tags = product.get("allergens_tags", []) 64 for tag in allergens_tags: 65 allergen = tag.replace("en:", "").replace("-", " ").title() 66 info["allergens"].append(allergen) 67 info["allergen_free"] = False 68 69 # Check traces field 70 traces_tags = product.get("traces_tags", []) 71 for tag in traces_tags: 72 trace = tag.replace("en:", "").replace("-", " ").title() 73 info["traces"].append(trace) 74 75 return info 76 77def on_code_detected(frame: Image, detection: Detection): 78 """Callback function that handles a detected code.""" 79 global detected, recent_scans 80 if detected: 81 return 82 83 frame = draw_bounding_box(frame, detection) 84 buffer = io.BytesIO() 85 frame.save(buffer, format="JPEG", quality=100) 86 b64_frame = base64.b64encode(buffer.getvalue()).decode("utf-8") 87 88 # Check if it's a barcode (CODE128, EAN13, etc.) and get allergen info 89 allergen_info = None 90 if detection.type != "QRCODE": # If it's not a QR code, it's a barcode 91 print(f"Checking allergens for barcode: {detection.content}") 92 allergen_info = check_open_food_facts(detection.content) 93 print(f"Allergen info: {allergen_info}") 94 95 entry = { 96 "content": detection.content, 97 "type": detection.type, 98 "timestamp": datetime.now(UTC).isoformat(), 99 "image": b64_frame, 100 "image_type": "image/jpeg", 101 "allergen_info": allergen_info 102 } 103 104 # Add to recent scans (keep only last 5) 105 recent_scans.insert(0, entry) 106 if len(recent_scans) > 5: 107 recent_scans = recent_scans[:5] 108 109 ui.send_message('code_detected', entry) 110 detected = True 111 112def on_frame(frame: Image): 113 """Callback function that processes each frame from the camera.""" 114 global detected 115 if detected: 116 return 117 118 buffer = io.BytesIO() 119 frame.save(buffer, format="JPEG", quality=100) 120 b64_frame = base64.b64encode(buffer.getvalue()).decode("utf-8") 121 122 entry = { 123 "timestamp": datetime.now(UTC).isoformat(), 124 "image": b64_frame, 125 "image_type": "image/jpeg", 126 } 127 ui.send_message('frame_detected', entry) 128 129def on_list_scans(): 130 """Callback function that lists the recent scans.""" 131 return {"scans": recent_scans} 132 133def on_clear_scans(_, __): 134 """Callback function to clear recent scans.""" 135 global recent_scans 136 recent_scans = [] 137 138def reset_detection(_, __): 139 """Callback function to reset the detection state.""" 140 global detected 141 detected = False 142 143def on_error(e: Exception): 144 """Callback function that handles exceptions from the detector.""" 145 ui.send_message('error', str(e)) 146 147camera = USBCamera(resolution=(640, 480), fps=5) 148detector = CameraCodeDetection(camera, detect_qr=True, detect_barcode=True) 149detector.on_detect(on_code_detected) 150detector.on_frame(on_frame) 151detector.on_error(on_error) 152 153ui = WebUI() 154ui.expose_api('GET', '/list_scans', on_list_scans) 155ui.on_message('reset_detection', reset_detection) 156ui.on_message('clear_scans', on_clear_scans) 157 158App.run()
Comments
Only logged in users can leave comments