Initital commit
This commit is contained in:
commit
4d96d3ca60
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
FROM python:3.11-bullseye
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt-get update && apt-get install -y libglib2.0-dev usbutils
|
||||||
|
RUN pip3 install 'radiacode[examples]' --upgrade
|
||||||
|
RUN apt-get install -y libusb-1.0-0-dev
|
||||||
|
COPY . .
|
||||||
|
RUN pip3 install pyusb
|
||||||
|
CMD ["python3", "webserver.py"]
|
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
version: '3.7'
|
||||||
|
services:
|
||||||
|
radia:
|
||||||
|
environment:
|
||||||
|
- UDEV=1
|
||||||
|
devices:
|
||||||
|
- '/dev:/dev'
|
||||||
|
# restart: always
|
||||||
|
privileged: true
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
build: .
|
||||||
|
container_name: radia
|
||||||
|
volumes:
|
||||||
|
- /dev/bus/usb:/dev/bus/usb
|
||||||
|
- /run/udev/control:/run/udev/control
|
||||||
|
- ./:/app
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
labels:
|
||||||
|
caddy: kyiv.dead.guru
|
||||||
|
caddy.tls: "assada+ua@gmail.com"
|
||||||
|
caddy.reverse_proxy: "{{upstreams 8080}}"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
external: true
|
5
usb_test.py
Normal file
5
usb_test.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import usb.core
|
||||||
|
|
||||||
|
|
||||||
|
d = usb.core.find(find_all=1)
|
||||||
|
print(list(d))
|
146
webserver.html
Normal file
146
webserver.html
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Kyiv Radioactive!</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue-apexcharts"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
|
<style>
|
||||||
|
#app > div {
|
||||||
|
margin: 5px auto;
|
||||||
|
width: 80%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px #aaa dashed;
|
||||||
|
}
|
||||||
|
#app fieldset {
|
||||||
|
display: inline-block;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
неРадіоактивна ситуація в Києві.
|
||||||
|
</h1>
|
||||||
|
<h2 class="subtitle">В реальному часі.</h2>
|
||||||
|
<div id="app">
|
||||||
|
<div>
|
||||||
|
<apexchart type="bar" height="350" :options="spectrumChartOptions" :series="spectrum_series"></apexchart>
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<input type="checkbox" id="spectrum_x_accum" v-model="spectrum_accum">
|
||||||
|
<label for="spectrum_x_accum">Акумульований</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<input type="radio" id="spectrum_x_channel" v-bind:value="false" v-model="spectrum_energy">
|
||||||
|
<label for="spectrum_x_channel">Канал</label>
|
||||||
|
|
||||||
|
<input type="radio" id="spectrum_x_energy" v-bind:value="true" v-model="spectrum_energy">
|
||||||
|
<label for="spectrum_x_energy">Енергія</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<input type="radio" id="spectrum_linear" v-bind:value="false" v-model="spectrum_logarithmic">
|
||||||
|
<label for="spectrum_linear">Лінійне</label>
|
||||||
|
<input type="radio" id="spectrum_log" v-bind:value="true" v-model="spectrum_logarithmic">
|
||||||
|
<label for="spectrum_log">Логарифмічне</label>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<button @click="updateSpectrum">Оновити спектр</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<apexchart type="line" height="350" :options="ratesChartOptions" :series="rates_series"></apexchart>
|
||||||
|
<button @click="rates_autoupdate = !rates_autoupdate">Автооновлення: {{ rates_autoupdate ? "ВКЛ" : "ВИКЛ" }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
const common_options = {
|
||||||
|
chart: {
|
||||||
|
animations: {enabled: false},
|
||||||
|
zoom: {autoScaleYaxis: true},
|
||||||
|
},
|
||||||
|
tooltip: {intersect: false},
|
||||||
|
grid: {xaxis: {lines: {show: true}}},
|
||||||
|
dataLabels: {enabled: false},
|
||||||
|
};
|
||||||
|
var app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
components: {
|
||||||
|
apexchart: VueApexCharts,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
ws: null,
|
||||||
|
spectrum_duration: 0,
|
||||||
|
rates_autoupdate: true,
|
||||||
|
rates_series: [],
|
||||||
|
spectrum_accum: false,
|
||||||
|
spectrum_series: [],
|
||||||
|
spectrum_coef: [0, 0, 0],
|
||||||
|
spectrum_logarithmic: true,
|
||||||
|
spectrum_energy: true,
|
||||||
|
ratesChartOptions: {
|
||||||
|
...common_options,
|
||||||
|
title: {text: 'Активнсть подій (подій в секунду) і доза'},
|
||||||
|
xaxis: {type: 'datetime'},
|
||||||
|
yaxis: [
|
||||||
|
{seriesName: 'Подій', title: {text: 'ПНС'}, labels: {formatter:(v) => v.toFixed(2) + ' ПНС'}},
|
||||||
|
{seriesName: 'Доза', title: {text: 'мк3в/г'}, labels: {formatter:(v) => v.toFixed(4) + ' мк3в/г'}, opposite: true},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
spectrum_accum() {
|
||||||
|
this.updateSpectrum();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
spectrumChartOptions() {
|
||||||
|
const a0 = this.spectrum_coef[0], a1 = this.spectrum_coef[1], a2 = this.spectrum_coef[2];
|
||||||
|
const fmt = this.spectrum_energy ? ((c) => (a0 + a1*c + a2*c*c).toFixed(0)) : undefined;
|
||||||
|
const title = this.spectrum_energy ? 'кеВ' : 'канал';
|
||||||
|
return{
|
||||||
|
...common_options,
|
||||||
|
title: {text: `Спектр, ${this.spectrum_duration} секунд`},
|
||||||
|
xaxis: {type: 'numeric', title: {text: title}, tickAmount: 25, labels: {formatter:fmt}},
|
||||||
|
yaxis: {logarithmic: this.spectrum_logarithmic, decimalsInFloat: 0},
|
||||||
|
plotOptions: {bar: {columnWidth: '95%'}},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.ws = new WebSocket('wss://' + window.location.host + '/ws')
|
||||||
|
this.ws.onmessage = this.onmessage;
|
||||||
|
this.updateSpectrum();
|
||||||
|
},
|
||||||
|
beforeDestroy: function() {
|
||||||
|
this.ws.close();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onmessage(ev) {
|
||||||
|
if (!this.rates_autoupdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = JSON.parse(ev.data);
|
||||||
|
this.rates_series = d.series;
|
||||||
|
},
|
||||||
|
updateSpectrum() {
|
||||||
|
fetch(`/spectrum?accum=${this.spectrum_accum}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => (this.spectrum_duration=data.duration, this.spectrum_coef=data.coef, this.spectrum_series=data.series));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
113
webserver.py
Normal file
113
webserver.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from radiacode import CountRate, DoseRate, RadiaCode
|
||||||
|
|
||||||
|
|
||||||
|
print('LOL')
|
||||||
|
|
||||||
|
file = open(pathlib.Path(__file__).parent.absolute() / 'webserver.html', 'r')
|
||||||
|
contecnt = file.read()
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_index(request):
|
||||||
|
return web.Response(content_type='text/html', text=contecnt)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_ws(request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
request.app.ws_clients.append(ws)
|
||||||
|
async for _ in ws: # noqa: WPS328
|
||||||
|
pass
|
||||||
|
request.app.ws_clients.remove(ws)
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_spectrum(request):
|
||||||
|
cn = request.app.rc_conn
|
||||||
|
accum = request.query.get('accum') == 'true'
|
||||||
|
spectrum = cn.spectrum_accum() if accum else cn.spectrum()
|
||||||
|
# apexcharts can't handle 0 in logarithmic view
|
||||||
|
spectrum_data = [
|
||||||
|
(channel, cnt if cnt > 0 else 0.5)
|
||||||
|
for channel, cnt in enumerate(spectrum.counts)
|
||||||
|
]
|
||||||
|
print('Spectrum updated')
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
'coef': [spectrum.a0, spectrum.a1, spectrum.a2],
|
||||||
|
'duration': spectrum.duration.total_seconds(),
|
||||||
|
'series': [{'name': 'spectrum', 'data': spectrum_data}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def process(app):
|
||||||
|
max_history_size = 128
|
||||||
|
countrate_history, doserate_history = [], []
|
||||||
|
while True: # noqa: WPS457
|
||||||
|
databuf = app.rc_conn.data_buf()
|
||||||
|
for v in databuf:
|
||||||
|
if isinstance(v, CountRate):
|
||||||
|
countrate_history.append(v)
|
||||||
|
elif isinstance(v, DoseRate):
|
||||||
|
doserate_history.append(v)
|
||||||
|
|
||||||
|
countrate_history.sort(key=lambda x: x.dt)
|
||||||
|
countrate_history = countrate_history[-max_history_size:]
|
||||||
|
doserate_history.sort(key=lambda x: x.dt)
|
||||||
|
doserate_history = doserate_history[-max_history_size:]
|
||||||
|
jdata = json.dumps(
|
||||||
|
{
|
||||||
|
'series': [
|
||||||
|
{
|
||||||
|
'name': 'Події',
|
||||||
|
'data': [(int(1000 * x.dt.timestamp()), x.count_rate) for x in countrate_history],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Доза',
|
||||||
|
'data': [(int(1000 * x.dt.timestamp()), 10000 * x.dose_rate) for x in doserate_history],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
print(f'Rates updated, sending to {len(app.ws_clients)} connected clients')
|
||||||
|
await asyncio.gather(*[ws.send_str(jdata) for ws in app.ws_clients], asyncio.sleep(1.0))
|
||||||
|
|
||||||
|
|
||||||
|
async def on_startup(app):
|
||||||
|
asyncio.create_task(process(app))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--bluetooth-mac', type=str, required=False, help='bluetooth MAC address of radiascan device')
|
||||||
|
parser.add_argument('--listen-host', type=str, required=False, default='0.0.0.0', help='listen host for webserver')
|
||||||
|
parser.add_argument('--listen-port', type=int, required=False, default=8080, help='listen port for webserver')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
|
||||||
|
|
||||||
|
app.ws_clients = []
|
||||||
|
if args.bluetooth_mac:
|
||||||
|
print('will use Bluetooth connection')
|
||||||
|
app.rc_conn = RadiaCode(bluetooth_mac=args.bluetooth_mac)
|
||||||
|
else:
|
||||||
|
print('will use USB connection')
|
||||||
|
app.rc_conn = RadiaCode()
|
||||||
|
|
||||||
|
app.on_startup.append(on_startup)
|
||||||
|
app.add_routes(
|
||||||
|
[
|
||||||
|
web.get('/', handle_index),
|
||||||
|
web.get('/spectrum', handle_spectrum),
|
||||||
|
web.get('/ws', handle_ws),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
web.run_app(app, host=args.listen_host, port=args.listen_port)
|
Loading…
Reference in New Issue
Block a user