New scanners added: Chrome + Smart Phone with JS library

This commit is contained in:
Dirk Jahnke 2022-05-30 11:46:11 +02:00
parent 068d13261b
commit d0ae1092d9
5 changed files with 497 additions and 104 deletions

View File

@ -0,0 +1,226 @@
{% extends "base.html" %}
{% load static %}
{% block additional_css %}
<link rel="preload" as="style" onload="this.rel='stylesheet';this.onload=null" href="https://unpkg.com/normalize.css@8.0.0/normalize.css">
<link rel="preload" as="style" onload="this.rel='stylesheet';this.onload=null" href="https://unpkg.com/milligram@1.3.0/dist/milligram.min.css">
{% endblock %}
{% block title %}Smartphone Scanner{% endblock %}
{% block content %}
<div class="container" >
<div class="row-mb8" id="video-button-content">
<a class="button" id="startButton">Start</a>
<a class="button-outline" id="resetButton">Reset</a>
</div>
<div class="row" id="video-content">
<div class="col-6">
<video id="video" width="450" height="320" style="border: 1px solid gray"></video>
</div>
<div class="col-2" id="sourceSelectPanel" style="display:none">
<label for="sourceSelect">Change video source:</label>
<select id="sourceSelect" style="max-width:400px">
</select>
</div>
<div class="col-2">
<label>Result:</label>
<pre><code id="result"></code></pre>
<ul id="barcode-list"></ul>
</div>
</div>
</div>
<script type="text/javascript">
function beep() {
var audio = new Audio("{% static 'sound/beep.mp3' %}");
audio.volume = 0.1
console.log("Audio play {% static 'sound/beep.mp3' %}")
audio.play();
}
// check compatibility
if (!('BarcodeDetector' in window)) {
console.log('Barcode Detector is not supported by this browser.');
} else {
console.log('Barcode Detector supported!');
// create new detector
// var barcodeDetector = new BarcodeDetector({formats: ['code_128', 'code_39', 'code_93', 'codabar', 'data_matrix', 'ean_13', 'ean_8', 'qr_code', 'upc_a', 'upc_e']});
var barcodeDetector = new BarcodeDetector({formats: ['code_128', 'ean_13']});
console.log('Supported code formats:');
BarcodeDetector.getSupportedFormats().then(supportedFormats => {
supportedFormats.forEach(format => console.log(format));
});
console.log('end-of-format-list');
}
async function detect() {
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: {facingMode: 'environment'}
});
const video = document.getElementById('video');
// const video = document.createElement('video');
let itemsFound = [];
video.srcObject = mediaStream;
video.autoplay = true;
const list = document.getElementById('barcode-list');
// list.before(video);
function render() {
barcodeDetector
.detect(video)
.then(barcodes => {
barcodes.forEach(barcode => {
if (!itemsFound.includes(barcode.rawValue)) {
itemsFound.push(barcode.rawValue);
const li = document.createElement('li');
li.innerHTML = barcode.rawValue;
list.appendChild(li);
console.log('Got scan ' + barcode.rawValue);
}
});
// beep();
})
.catch(console.error);
}
(function renderLoop() {
requestAnimationFrame(renderLoop);
render();
})();
}
window.addEventListener('load', function () {
detect();
document.getElementById('resetButton').addEventListener('click', () => {
document.getElementById('result').textContent = '';
codeReader.reset();
console.log('Reset.')
});
});
</script>
<hr />
<div class="row mb-3">
<div class="col-8">
<div class="input-group">
<input class="form-control" type="text" maxlength="40" id="container_id" placeholder="Container ID">
<input class="form-control" type="text" maxlength="40" id="asset_id" placeholder="Asset ID">
</div>
</div>
<div class="col-2">
<div class="input-group">
<input class="btn btn-primary" id="book-in-button" type="button" value="+Book">
<input class="btn btn-primary" id="book-out-button" type="button" value="-Book">
</div>
</div>
<div class="col-8">
<div class="input-group">
<textarea class="form-control" readonly="readonly" rows="4" id="container_details"></textarea>
<textarea class="form-control" readonly="readonly" rows="4" id="asset_details"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="input-group">
<textarea class="form-control mb-2" id="chat-log" cols="100" rows="10"></textarea>
<input class="btn btn-secondary" id="clear-chat-area" type="button" value="Clear">
</div>
</div>
<script>
var containerIsValid = false;
var assetIsValid = false;
async function getContainerInfo(containerId) {
document.querySelector('#container_id').value = containerId;
url = '/api/containers?named_id=' + containerId;
document.querySelector('#container_details').value = 'Trying ' + url;
const response = await fetch(url);
const container = await response.json();
if (container.count >= 1) {
document.querySelector('#container_details').value = container.results[0].description;
containerIsValid = true;
} else {
document.querySelector('#container_details').value = document.querySelector('#container_details').value + "\nERROR: No result\n";
console.log(response);
containerIsValid = false;
}
}
async function getAssetInfo(assetId) {
document.querySelector('#asset_id').value = assetId;
url = '/api/assets?named_id=' + assetId;
document.querySelector('#asset_details').value = 'Trying ' + url;
const response = await fetch(url);
const asset = await response.json();
if (asset.count >= 1) {
document.querySelector('#asset_details').value = asset.results[0].description;
assetIsValid = true;
} else {
document.querySelector('#asset_details').value = document.querySelector('#asset_details').value + "\nERROR: No result\n";
console.log(response);
assetIsValid = false;
}
}
function useScannedText(msg) {
// const data = JSON.parse(e.data);
// msg = String(data.message);
document.querySelector('#chat-log').value += (msg + '\n');
if (msg.startsWith('C-')) {
getContainerInfo(data.message);
// document.querySelector('#container_id').value = msg;
} else if (msg.startsWith('A-')) {
getAssetInfo(data.message);
// document.querySelector('#asset_id').value = msg;
} else {
assetId = 'A-' + msg;
document.querySelector('#asset_id').value = assetId;
getAssetInfo(assetId);
}
};
document.querySelector('#clear-chat-area').onclick = function(e) {
document.querySelector('#chat-log').value = "cleared\n";
}
document.querySelector('#book-in-button').onclick = function(e) {
if (!containerIsValid) {
msg = 'Cannot book-in as container is invalid';
console.log(msg);
document.querySelector('#chat-log').value += ('ERROR: ' + msg + '\n');
return;
}
if (!assetIsValid) {
msg = 'Cannot book-in as asset is invalid';
console.log(msg);
document.querySelector('#chat-log').value += ('ERROR: ' + msg + '\n');
return;
}
}
document.querySelector('#book-out-button').onclick = function(e) {
if (!containerIsValid) {
msg = 'Cannot book-out as container is invalid';
console.log(msg);
document.querySelector('#chat-log').value += ('ERROR: ' + msg + '\n');
return;
}
if (!assetIsValid) {
msg = 'Cannot book-out as asset is invalid';
console.log(msg);
document.querySelector('#chat-log').value += ('ERROR: ' + msg + '\n');
return;
}
}
</script>
{% endblock %}

View File

@ -56,7 +56,8 @@
</div> </div>
<div class="row"> <div class="row">
<h1>Use your smartphone</h1> <h1>Use your smartphone</h1>
<p>Use your <a class="navbar-brand" href="{% url 'booker:smartphone_scanner' %}">smartphone</a> to scan.</p> <p>Use your <a href="{% url 'booker:smartphone_scanner' %}">smartphone</a> to scan.</p>
<p>Use your <a href="{% url 'booker:chrome_scanner' %}">chrome Barcode-Scanner to scan.</p>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,118 +2,223 @@
{% load static %} {% load static %}
{% block additional_css %} {% block additional_css %}
<link rel="preload" as="style" onload="this.rel='stylesheet';this.onload=null" href="https://unpkg.com/normalize.css@8.0.0/normalize.css">
<link rel="preload" as="style" onload="this.rel='stylesheet';this.onload=null" href="https://unpkg.com/milligram@1.3.0/dist/milligram.min.css">
{% endblock %} {% endblock %}
{% block title %}Smartphone Scanner{% endblock %} {% block title %}Smartphone Scanner{% endblock %}
{% block content %} {% block content %}
<div class="container" > <style>
#interactive canvas { position: absolute; left: 0; }
</style>
<div class="row-mb8" id="video-button-content"> <div class="container" >
<a class="button" id="startButton">Start</a> <div class="row" id="video-button-content">
<a class="button-outline" id="resetButton">Reset</a> <div class="col-1">
<a class="btn btn-primary" id="startButton">Start</a>
</div>
<div class="col-1">
<a class="btn btn-outline-primary" id="resetButton">Reset</a>
</div>
<div class="col-1">
<a class="btn btn-outline-primary" id="clearOverlayButton">Clear</a>
</div>
</div> </div>
<div class="row" id="video-content"> <div class="row" id="video-content">
<div class="col-6"> <div id="interactive" class="viewport col-12" style="position: relative;">
<video id="video" width="450" height="320" style="border: 1px solid gray"></video> <!-- QuaggaJS will populate this element -->
</div> </div>
<!--
<div class="col-2" id="sourceSelectPanel" style="display:none"> <div class="col-8">
<video id="video" autoplay="true" width="640" height="480" style="border: 1px solid gray"></video>
</div>-->
</div>
<div class="row" id="control-content">
<div class="col-6" id="sourceSelectPanel" style="display:none">
<div>
<label for="sourceSelect">Change video source:</label> <label for="sourceSelect">Change video source:</label>
<select id="sourceSelect" style="max-width:400px"> <select id="sourceSelect" style="max-width:400px"></select>
</select>
</div> </div>
</div>
<div class="col-2"> <div class="col-6" id="scan-result-panel" style="display:none">
<label>Result:</label> <div>
<label>Scan:</label>
<pre><code id="result"></code></pre> <pre><code id="result"></code></pre>
</div> </div>
</div> </div>
</div>
</div> </div>
<script type="text/javascript" src="https://unpkg.com/@zxing/library@latest"></script>
<script type="text/javascript"> <script type="text/javascript" src="{% static 'quagga/dist/quagga.js' %}"></script>
<script type="text/javascript">
function beep() { function beep() {
var audio = new Audio("{% static 'sound/beep.mp3' %}"); var audio = new Audio("{% static 'sound/beep.mp3' %}");
audio.volume = 0.1 audio.volume = 0.02;
console.log("Audio play {% static 'sound/beep.mp3' %}") console.log("Audio play {% static 'sound/beep.mp3' %}");
audio.play(); audio.play();
} }
function clearCanvas() {
canvas = document.querySelector('#interactive .drawingBuffer');
drawing = (canvas != null) ? canvas.getContext('2d') : null;
console.log('Clearing canvas, drawing=' + drawing + ', canvas=' + canvas);
if (drawing != null) {
drawing.clearRect(0, 0, canvas.width, canvas.height);
console.log('Clearing done');
}
}
console.log('detected HW concurrency: ' + self.navigator.hardwareConcurrency);
window.addEventListener('load', function () { window.addEventListener('load', function () {
let selectedDeviceId; let selectedDeviceId;
let lastScanResult = ''; let lastScanResult = '';
// const codeReader = new ZXing.BrowserBarcodeReader() let config = {
const codeReader = new ZXing.BrowserMultiFormatReader() inputStream: {
console.log('ZXing code reader initialized') name: "Live",
codeReader.getVideoInputDevices() type: "LiveStream",
.then((videoInputDevices) => { constraints: {
const sourceSelect = document.getElementById('sourceSelect') facingMode: "environment",
selectedDeviceId = videoInputDevices[0].deviceId },
if (videoInputDevices.length > 1) {
videoInputDevices.forEach((element) => {
const sourceOption = document.createElement('option')
sourceOption.text = element.label
sourceOption.value = element.deviceId
sourceSelect.appendChild(sourceOption)
})
sourceSelect.onchange = () => { target: document.getElementById('interactive'),
selectedDeviceId = sourceSelect.value; /*area: {
top: '0%',
bottom: '0%',
left: '0%',
right: '0%'
},*/
},
//numOfWorkers: 4,
locate: true,
debug: true,
frequency: 20,
decoder: {
readers: ["code_128_reader"],
debug: {
drawScanline: false,
drawBoundingBox: false,
showPattern: false,
showFrequency: false
},
multiple: false
},
locator: {
halfSample: false,
patchSize: "x-large", // x-small, small, medium, large, x-large
debug: {
/*showCanvas: true,*/
showPatches: false,
showFoundPatches: false,
showSkeleton: false,
showLabels: false,
showPatchLabels: false,
showRemainingPatchLabels: false,
boxFromPatches: {
showTransformed: false,
showTransformedBox: false,
showBB: true
} }
const sourceSelectPanel = document.getElementById('sourceSelectPanel')
sourceSelectPanel.style.display = 'block'
} }
}
};
document.getElementById('startButton').addEventListener('click', () => { function quagga_init() {
beep() Quagga.init(config, function(err) {
codeReader.decodeFromInputVideoDeviceContinuously(selectedDeviceId, 'video', (result) => { if (err) {
if (result && lastScanResult !== result.text) { console.log(err);
lastScanResult = result.text return;
console.log('Got scan (cont.): ' + result) }
document.getElementById('result').textContent = result.text console.log("Initialization finished. Ready to start");
beep() Quagga.start();
useScannedText(result.text) Quagga.onDetected(function(data) {
} else if (result) { code = data.codeResult.code;
console.log('... scan again') console.log('Got scan: ' + code);
if (code && lastScanResult !== code) {
lastScanResult = code;
console.log('Got scan (cont.): ' + lastScanResult);
document.querySelector('#result').textContent = lastScanResult;
// document.getElementById('result').textContent = lastScanResult
beep();
useScannedText(lastScanResult);
console.log(data);
canvas = document.querySelector('#interactive .drawingBuffer');
drawing = canvas.getContext('2d');
drawing.clearRect(0, 0, canvas.width, canvas.height);
drawing.beginPath();
drawing.moveTo(data.line[0].x, data.line[0].y);
drawing.lineTo(data.line[1].x, data.line[1].y);
drawing.lineWidth = 2;
drawing.strokeStyle = 'yellow';
drawing.closePath();
drawing.stroke();
setTimeout(clearCanvas, 3000);
} else if (code) {
console.log('... scan again');
} }
}); });
/* /*
codeReader.decodeOnceFromVideoDevice(selectedDeviceId, 'video').then((result) => { Quagga.onProcessed(function(data) {
if (lastScanResult !== result.text) { canvas = document.querySelector('#interactive .drawingBuffer');
lastScanResult = result.text if (canvas != null) {
console.log('Got scan: ' + result.text) context = canvas.getContext('2d');
document.getElementById('result').textContent = result.text if (context) {
useScannedText(result.text) context.clearRect(0, 0, canvas.width, canvas.height);
} else {
console.log('... scan again')
} }
}).catch((err) => { }
console.error(err) });*/
document.getElementById('result').textContent = err });
}) }
*/
console.log(`Started continous decode from camera with id ${selectedDeviceId}`) quagga_init();
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
console.log("enumerateDevices() not supported.");
return;
}
Quagga.CameraAccess.enumerateVideoDevices()
.then(function(devices) {
const sourceSelect = document.getElementById('sourceSelect');
selectedDeviceId = devices[0].deviceId; // default
if (devices.length > 1) {
devices.forEach(function(device) {
console.log(device.kind + ": " + device.label + " id=" + device.deviceId);
const sourceOption = document.createElement('option');
sourceOption.text = device.label;
sourceOption.value = device.deviceId;
sourceSelect.appendChild(sourceOption);
});
sourceSelect.onchange = () => {
selectedDeviceId = sourceSelect.value;
config.inputStream.constraints.deviceId = selectedDeviceId;
Quagga.stop();
quagga_init();
}
const sourceSelectPanel = document.getElementById('sourceSelectPanel');
sourceSelectPanel.style.display = 'block';
}
document.getElementById('startButton').addEventListener('click', () => {
beep();
Quagga.start();
console.log(`Started scan from camera with id ${selectedDeviceId}`)
}) })
document.getElementById('resetButton').addEventListener('click', () => { document.getElementById('resetButton').addEventListener('click', () => {
document.getElementById('result').textContent = ''; document.getElementById('result').textContent = '';
codeReader.reset(); Quagga.stop();
console.log('Reset.') console.log('Reset/stopped.')
}) })
}) document.getElementById('clearOverlayButton').addEventListener('click', () => {
.catch((err) => { clearCanvas();
console.error(err)
}) })
}) })
</script> .catch(function(err) {
console.log(err);
});
});
</script>
<hr /> <hr />
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8"> <div class="col-8">
@ -145,14 +250,22 @@
var containerIsValid = false; var containerIsValid = false;
var assetIsValid = false; var assetIsValid = false;
async function getObjectInfo(url) {
const response = await fetch(url);
const object_json = response.json();
return object_json;
}
async function getContainerInfo(containerId) { async function getContainerInfo(containerId) {
document.querySelector('#container_id').value = containerId; document.querySelector('#container_id').value = containerId;
url = '/api/containers?named_id=' + containerId; url = '/api/containers?named_id=' + containerId;
document.querySelector('#container_details').value = 'Trying ' + url; document.querySelector('#container_details').value = 'Trying ' + url;
const response = await fetch(url); const container = await getObjectInfo(url);
const container = await response.json();
if (container.count >= 1) { if (container.count >= 1) {
document.querySelector('#container_details').value = container.results[0].description; c = container.results[0];
console.log(c);
containerType = await getObjectInfo(c.container_type);
document.querySelector('#container_details').value = c.description + "\n" + c.named_id + ': ' + c.color + "\n" + containerType.description;
containerIsValid = true; containerIsValid = true;
} else { } else {
document.querySelector('#container_details').value = document.querySelector('#container_details').value + "\nERROR: No result\n"; document.querySelector('#container_details').value = document.querySelector('#container_details').value + "\nERROR: No result\n";
@ -178,15 +291,11 @@
} }
function useScannedText(msg) { function useScannedText(msg) {
// const data = JSON.parse(e.data);
// msg = String(data.message);
document.querySelector('#chat-log').value += (msg + '\n'); document.querySelector('#chat-log').value += (msg + '\n');
if (msg.startsWith('C-')) { if (msg.startsWith('C-')) {
getContainerInfo(data.message); getContainerInfo(msg);
// document.querySelector('#container_id').value = msg;
} else if (msg.startsWith('A-')) { } else if (msg.startsWith('A-')) {
getAssetInfo(data.message); getAssetInfo(msg);
// document.querySelector('#asset_id').value = msg;
} else { } else {
assetId = 'A-' + msg; assetId = 'A-' + msg;
document.querySelector('#asset_id').value = assetId; document.querySelector('#asset_id').value = assetId;

View File

@ -6,5 +6,7 @@ app_name = 'booker'
urlpatterns = [ urlpatterns = [
path('', views.IndexView.as_view(), name='index'), path('', views.IndexView.as_view(), name='index'),
path('smartphone_scanner/', views.SmartphoneScannerView.as_view(), name='smartphone_scanner'), path('smartphone_scanner/', views.SmartphoneScannerView.as_view(), name='smartphone_scanner'),
path('<str:scanner_id>/', views.scanner, name='scanner') path('chrome_scanner/', views.ChromeScannerView.as_view(), name='chrome_scanner'),
path('<str:scanner_id>/', views.scanner_view, name='scanner'),
path('webscanner/<str:scanner_id>/', views.web_scanner_view, name='web_scanner'),
] ]

View File

@ -1,6 +1,18 @@
from datetime import datetime
from channels.layers import get_channel_layer
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from gmqtt import Client
from rest_framework.utils import json
from homelog import settings
from homelog.system_user import get_system_user
from .models import Scanner from .models import Scanner
from django.http import HttpResponse
import logging
LOGGER = logging.getLogger(__name__)
class IndexView(ListView): class IndexView(ListView):
@ -8,10 +20,53 @@ class IndexView(ListView):
template_name = 'booker/index.html' template_name = 'booker/index.html'
def scanner(request, scanner_id): def scanner_view(request, scanner_id):
return render(request, 'booker/scanner.html', { return render(request, 'booker/scanner.html', {
'scanner_id': scanner_id 'scanner_id': scanner_id
}) })
class SmartphoneScannerView(TemplateView): class SmartphoneScannerView(TemplateView):
template_name = 'booker/smartphone_scanner.html' template_name = 'booker/smartphone_scanner.html'
class ChromeScannerView(TemplateView):
template_name = 'booker/chrome_scanner.html'
@csrf_exempt
def web_scanner_view(request, scanner_id):
def get_or_create_scanner(named_id):
scanner, created = Scanner.objects.get_or_create(named_id=named_id, defaults={
# 'lwt_topic': topic,
'description': 'WebAPI scanner',
'last_online_ts': datetime.now(),
'created_by': get_system_user(),
'changed_by': get_system_user()
})
if created:
LOGGER.debug(f"Created new scanner entry for {named_id}")
else:
LOGGER.debug(f"Updated scanner entry for {named_id}")
return scanner
print(f"Got {request.POST['scan']} from scanner {scanner_id}")
get_or_create_scanner(scanner_id)
topic = f"barcodescanner/{scanner_id}/scan"
'''
client = Client('homelog-webscanner')
client.set_auth_credentials(username=settings.MQTT_USER, password=settings.MQTT_PASSWORD)
client.connect(settings.MQTT_HOST, 1883, keepalive=60, version=settings.MQTT_VERSION)
client.publish(topic, request.POST['scan'])
client.disconnect()
# MqttConsumer().mqtt_publish({'topic': topic, 'payload': request.POST['scan'], 'retain': False})
'''
channel_layer = get_channel_layer()
channel_layer.group_send(
'mqtt', # scanner_id,
{"type": "distribute", "text": json.dumps({'message': request.POST['scan'], 'topic': topic})}
)
'''
'''
return HttpResponse(f"<html><body><p>OK, got {request.POST['scan']} from {scanner_id}</p></body></html>")