blob: 3e830b175521abc91ca9ac422d6b28d0a18d2ce9 [file] [log] [blame]
Eric7d897cb2022-11-28 19:20:32 +01001/*
2 * Copyright 2022 sysmocom - s.f.m.c. GmbH
3 *
4 * Author: Eric Wild <ewild@sysmocom.de>
5 *
6 * SPDX-License-Identifier: AGPL-3.0+
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the GNU Affero General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 * See the COPYING file in the main directory for details.
21 */
22
23#include <map>
24#include <libbladeRF.h>
25#include "radioDevice.h"
26#include "bladerf.h"
27#include "Threads.h"
28#include "Logger.h"
29
30#ifdef HAVE_CONFIG_H
31#include "config.h"
32#endif
33
34extern "C" {
35#include <osmocom/core/utils.h>
36#include <osmocom/gsm/gsm_utils.h>
37#include <osmocom/vty/cpu_sched_vty.h>
38}
39
40#define SAMPLE_BUF_SZ (1 << 20)
41
42#define B2XX_TIMING_4_4SPS 6.18462e-5
43
44#define CHKRET() \
45 { \
46 if (status != 0) \
47 LOGC(DDEV, ERROR) << bladerf_strerror(status); \
48 }
49
Ericc0f78a32023-05-12 13:00:14 +020050static const dev_map_t dev_param_map{
Eric7d897cb2022-11-28 19:20:32 +010051 { std::make_tuple(blade_dev_type::BLADE2, 4, 4), { 1, 26e6, GSMRATE, B2XX_TIMING_4_4SPS, "B200 4 SPS" } },
52};
53
Ericc0f78a32023-05-12 13:00:14 +020054static const power_map_t dev_band_nom_power_param_map{
Eric7d897cb2022-11-28 19:20:32 +010055 { std::make_tuple(blade_dev_type::BLADE2, GSM_BAND_850), { 89.75, 13.3, -7.5 } },
56 { std::make_tuple(blade_dev_type::BLADE2, GSM_BAND_900), { 89.75, 13.3, -7.5 } },
57 { std::make_tuple(blade_dev_type::BLADE2, GSM_BAND_1800), { 89.75, 7.5, -11.0 } },
58 { std::make_tuple(blade_dev_type::BLADE2, GSM_BAND_1900), { 89.75, 7.7, -11.0 } },
59};
60
61/* So far measurements done for B210 show really close to linear relationship
62 * between gain and real output power, so we simply adjust the measured offset
63 */
64static double TxGain2TxPower(const dev_band_desc &desc, double tx_gain_db)
65{
66 return desc.nom_out_tx_power - (desc.nom_uhd_tx_gain - tx_gain_db);
67}
68static double TxPower2TxGain(const dev_band_desc &desc, double tx_power_dbm)
69{
70 return desc.nom_uhd_tx_gain - (desc.nom_out_tx_power - tx_power_dbm);
71}
72
Eric19e134a2023-05-10 23:50:38 +020073blade_device::blade_device(InterfaceType iface, const struct trx_cfg *cfg)
Ericc0f78a32023-05-12 13:00:14 +020074 : RadioDevice(iface, cfg), band_manager(dev_band_nom_power_param_map, dev_param_map), dev(nullptr),
75 rx_gain_min(0.0), rx_gain_max(0.0), tx_spp(0), rx_spp(0), started(false), aligned(false), drop_cnt(0),
76 prev_ts(0), ts_initial(0), ts_offset(0), async_event_thrd(NULL)
Eric7d897cb2022-11-28 19:20:32 +010077{
78}
79
80blade_device::~blade_device()
81{
82 if (dev) {
83 bladerf_enable_module(dev, BLADERF_CHANNEL_RX(0), false);
84 bladerf_enable_module(dev, BLADERF_CHANNEL_TX(0), false);
85 }
86
87 stop();
88
89 for (size_t i = 0; i < rx_buffers.size(); i++)
90 delete rx_buffers[i];
91}
92
Eric7d897cb2022-11-28 19:20:32 +010093void blade_device::init_gains()
94{
95 double tx_gain_min, tx_gain_max;
96 int status;
97
98 const struct bladerf_range *r;
99 bladerf_get_gain_range(dev, BLADERF_RX, &r);
100
101 rx_gain_min = r->min;
102 rx_gain_max = r->max;
103 LOGC(DDEV, INFO) << "Supported Rx gain range [" << rx_gain_min << "; " << rx_gain_max << "]";
104
105 for (size_t i = 0; i < rx_gains.size(); i++) {
106 double gain = (rx_gain_min + rx_gain_max) / 2;
107 status = bladerf_set_gain_mode(dev, BLADERF_CHANNEL_RX(i), BLADERF_GAIN_MGC);
108 CHKRET()
109 bladerf_gain_mode m;
110 bladerf_get_gain_mode(dev, BLADERF_CHANNEL_RX(i), &m);
111 LOGC(DDEV, INFO) << (m == BLADERF_GAIN_MANUAL ? "gain manual" : "gain AUTO");
112
113 status = bladerf_set_gain(dev, BLADERF_CHANNEL_RX(i), 0);
114 CHKRET()
115 int actual_gain;
116 status = bladerf_get_gain(dev, BLADERF_CHANNEL_RX(i), &actual_gain);
117 CHKRET()
118 LOGC(DDEV, INFO) << "Default setting Rx gain for channel " << i << " to " << gain << " scale "
119 << r->scale << " actual " << actual_gain;
120 rx_gains[i] = actual_gain;
121
122 status = bladerf_set_gain(dev, BLADERF_CHANNEL_RX(i), 0);
123 CHKRET()
124 status = bladerf_get_gain(dev, BLADERF_CHANNEL_RX(i), &actual_gain);
125 CHKRET()
126 LOGC(DDEV, INFO) << "Default setting Rx gain for channel " << i << " to " << gain << " scale "
127 << r->scale << " actual " << actual_gain;
128 rx_gains[i] = actual_gain;
129 }
130
131 status = bladerf_get_gain_range(dev, BLADERF_TX, &r);
132 CHKRET()
133 tx_gain_min = r->min;
134 tx_gain_max = r->max;
135 LOGC(DDEV, INFO) << "Supported Tx gain range [" << tx_gain_min << "; " << tx_gain_max << "]";
136
137 for (size_t i = 0; i < tx_gains.size(); i++) {
138 double gain = (tx_gain_min + tx_gain_max) / 2;
139 status = bladerf_set_gain(dev, BLADERF_CHANNEL_TX(i), 30);
140 CHKRET()
141 int actual_gain;
142 status = bladerf_get_gain(dev, BLADERF_CHANNEL_TX(i), &actual_gain);
143 CHKRET()
144 LOGC(DDEV, INFO) << "Default setting Tx gain for channel " << i << " to " << gain << " scale "
145 << r->scale << " actual " << actual_gain;
146 tx_gains[i] = actual_gain;
147 }
148
149 return;
150}
151
152void blade_device::set_rates()
153{
154 struct bladerf_rational_rate rate = { 0, static_cast<uint64_t>((1625e3 * 4)), 6 }, actual;
155 auto status = bladerf_set_rational_sample_rate(dev, BLADERF_CHANNEL_RX(0), &rate, &actual);
156 CHKRET()
157 status = bladerf_set_rational_sample_rate(dev, BLADERF_CHANNEL_TX(0), &rate, &actual);
158 CHKRET()
159
160 tx_rate = rx_rate = (double)rate.num / (double)rate.den;
161
162 LOGC(DDEV, INFO) << "Rates set to" << tx_rate << " / " << rx_rate;
163
164 bladerf_set_bandwidth(dev, BLADERF_CHANNEL_RX(0), (bladerf_bandwidth)2e6, (bladerf_bandwidth *)NULL);
165 bladerf_set_bandwidth(dev, BLADERF_CHANNEL_TX(0), (bladerf_bandwidth)2e6, (bladerf_bandwidth *)NULL);
166
167 ts_offset = 60; // FIXME: actual blade offset, should equal b2xx
168}
169
170double blade_device::setRxGain(double db, size_t chan)
171{
172 if (chan >= rx_gains.size()) {
173 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
174 return 0.0f;
175 }
176
177 bladerf_set_gain(dev, BLADERF_CHANNEL_RX(chan), 30); //db);
178 int actual_gain;
179 bladerf_get_gain(dev, BLADERF_CHANNEL_RX(chan), &actual_gain);
180
181 rx_gains[chan] = actual_gain;
182
183 LOGC(DDEV, INFO) << "Set RX gain to " << rx_gains[chan] << "dB (asked for " << db << "dB)";
184
185 return rx_gains[chan];
186}
187
188double blade_device::getRxGain(size_t chan)
189{
190 if (chan >= rx_gains.size()) {
191 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
192 return 0.0f;
193 }
194
195 return rx_gains[chan];
196}
197
198double blade_device::rssiOffset(size_t chan)
199{
200 double rssiOffset;
201 dev_band_desc desc;
202
203 if (chan >= rx_gains.size()) {
204 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
205 return 0.0f;
206 }
207
208 get_dev_band_desc(desc);
209 rssiOffset = rx_gains[chan] + desc.rxgain2rssioffset_rel;
210 return rssiOffset;
211}
212
213double blade_device::setPowerAttenuation(int atten, size_t chan)
214{
215 double tx_power, db;
216 dev_band_desc desc;
217
218 if (chan >= tx_gains.size()) {
219 LOGC(DDEV, ALERT) << "Requested non-existent channel" << chan;
220 return 0.0f;
221 }
222
223 get_dev_band_desc(desc);
224 tx_power = desc.nom_out_tx_power - atten;
225 db = TxPower2TxGain(desc, tx_power);
226
227 bladerf_set_gain(dev, BLADERF_CHANNEL_TX(chan), 30);
228 int actual_gain;
229 bladerf_get_gain(dev, BLADERF_CHANNEL_RX(chan), &actual_gain);
230
231 tx_gains[chan] = actual_gain;
232
233 LOGC(DDEV, INFO)
234 << "Set TX gain to " << tx_gains[chan] << "dB, ~" << TxGain2TxPower(desc, tx_gains[chan]) << " dBm "
235 << "(asked for " << db << " dB, ~" << tx_power << " dBm)";
236
237 return desc.nom_out_tx_power - TxGain2TxPower(desc, tx_gains[chan]);
238}
239double blade_device::getPowerAttenuation(size_t chan)
240{
241 dev_band_desc desc;
242 if (chan >= tx_gains.size()) {
243 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
244 return 0.0f;
245 }
246
247 get_dev_band_desc(desc);
248 return desc.nom_out_tx_power - TxGain2TxPower(desc, tx_gains[chan]);
249}
250
251int blade_device::getNominalTxPower(size_t chan)
252{
253 dev_band_desc desc;
254 get_dev_band_desc(desc);
255
256 return desc.nom_out_tx_power;
257}
258
Eric19e134a2023-05-10 23:50:38 +0200259int blade_device::open()
Eric7d897cb2022-11-28 19:20:32 +0100260{
261 bladerf_log_set_verbosity(BLADERF_LOG_LEVEL_VERBOSE);
262 bladerf_set_usb_reset_on_open(true);
Eric19e134a2023-05-10 23:50:38 +0200263 auto success = bladerf_open(&dev, cfg->dev_args);
Eric7d897cb2022-11-28 19:20:32 +0100264 if (success != 0) {
265 struct bladerf_devinfo *info;
266 auto num_devs = bladerf_get_device_list(&info);
Eric19e134a2023-05-10 23:50:38 +0200267 LOGC(DDEV, ALERT) << "No bladerf devices found with identifier '" << cfg->dev_args << "'";
Eric7d897cb2022-11-28 19:20:32 +0100268 if (num_devs) {
269 for (int i = 0; i < num_devs; i++)
270 LOGC(DDEV, ALERT) << "Found device:" << info[i].product << " serial " << info[i].serial;
271 }
272
273 return -1;
274 }
275 if (strcmp("bladerf2", bladerf_get_board_name(dev))) {
276 LOGC(DDEV, ALERT) << "Only BladeRF2 supported! found:" << bladerf_get_board_name(dev);
277 return -1;
278 }
279
280 dev_type = blade_dev_type::BLADE2;
281 tx_window = TX_WINDOW_FIXED;
282
283 struct bladerf_devinfo info;
284 bladerf_get_devinfo(dev, &info);
285 LOGC(DDEV, INFO) << "Using discovered bladerf device " << info.serial;
286
287 tx_freqs.resize(chans);
288 rx_freqs.resize(chans);
289 tx_gains.resize(chans);
290 rx_gains.resize(chans);
291 rx_buffers.resize(chans);
292
Eric19e134a2023-05-10 23:50:38 +0200293 switch (cfg->clock_ref) {
Eric7d897cb2022-11-28 19:20:32 +0100294 case REF_INTERNAL:
295 case REF_EXTERNAL:
296 break;
297 default:
298 LOGC(DDEV, ALERT) << "Invalid reference type";
299 return -1;
300 }
301
Eric19e134a2023-05-10 23:50:38 +0200302 if (cfg->clock_ref == REF_EXTERNAL) {
Eric7d897cb2022-11-28 19:20:32 +0100303 bool is_locked;
304 int status = bladerf_set_pll_enable(dev, true);
305 CHKRET()
306 status = bladerf_set_pll_refclk(dev, 10000000);
307 CHKRET()
308 for (int i = 0; i < 20; i++) {
309 usleep(50 * 1000);
310 status = bladerf_get_pll_lock_state(dev, &is_locked);
311 CHKRET()
312 if (is_locked)
313 break;
314 }
315 if (!is_locked) {
316 LOGC(DDEV, ALERT) << "unable to lock refclk!";
317 return -1;
318 }
319 }
320
Eric19e134a2023-05-10 23:50:38 +0200321 LOGC(DDEV, INFO)
322 << "Selected clock source is " << ((cfg->clock_ref == REF_INTERNAL) ? "internal" : "external 10Mhz");
Eric7d897cb2022-11-28 19:20:32 +0100323
324 set_rates();
325
326 /*
327 1ts = 3/5200s
328 1024*2 = small gap(~180us) every 9.23ms = every 16 ts? -> every 2 frames
329 1024*1 = large gap(~627us) every 9.23ms = every 16 ts? -> every 2 frames
330
331 rif convertbuffer = 625*4 = 2500 -> 4 ts
332 rif rxtxbuf = 4 * segment(625*4) = 10000 -> 16 ts
333 */
334 const unsigned int num_buffers = 256;
335 const unsigned int buffer_size = 1024 * 4; /* Must be a multiple of 1024 */
336 const unsigned int num_transfers = 32;
337 const unsigned int timeout_ms = 3500;
338
339 bladerf_sync_config(dev, BLADERF_RX_X1, BLADERF_FORMAT_SC16_Q11_META, num_buffers, buffer_size, num_transfers,
340 timeout_ms);
341
342 bladerf_sync_config(dev, BLADERF_TX_X1, BLADERF_FORMAT_SC16_Q11_META, num_buffers, buffer_size, num_transfers,
343 timeout_ms);
344
345 /* Number of samples per over-the-wire packet */
346 tx_spp = rx_spp = buffer_size;
347
348 size_t buf_len = SAMPLE_BUF_SZ / sizeof(uint32_t);
349 for (size_t i = 0; i < rx_buffers.size(); i++)
350 rx_buffers[i] = new smpl_buf(buf_len);
351
352 pkt_bufs = std::vector<std::vector<short> >(chans, std::vector<short>(2 * rx_spp));
353 for (size_t i = 0; i < pkt_bufs.size(); i++)
354 pkt_ptrs.push_back(&pkt_bufs[i].front());
355
356 init_gains();
357
358 return NORMAL;
359}
360
361bool blade_device::restart()
362{
363 /* Allow 100 ms delay to align multi-channel streams */
364 double delay = 0.2;
365 int status;
366
367 status = bladerf_enable_module(dev, BLADERF_CHANNEL_RX(0), true);
368 CHKRET()
369 status = bladerf_enable_module(dev, BLADERF_CHANNEL_TX(0), true);
370 CHKRET()
371
372 bladerf_timestamp now;
373 status = bladerf_get_timestamp(dev, BLADERF_RX, &now);
374 ts_initial = now + rx_rate * delay;
375 LOGC(DDEV, INFO) << "Initial timestamp " << ts_initial << std::endl;
376
377 return true;
378}
379
380bool blade_device::start()
381{
382 LOGC(DDEV, INFO) << "Starting USRP...";
383
384 if (started) {
385 LOGC(DDEV, ERROR) << "Device already started";
386 return false;
387 }
388
389 if (!restart())
390 return false;
391
392 started = true;
393 return true;
394}
395
396bool blade_device::stop()
397{
398 if (!started)
399 return false;
400
401 /* reset internal buffer timestamps */
402 for (size_t i = 0; i < rx_buffers.size(); i++)
403 rx_buffers[i]->reset();
404
Ericc0f78a32023-05-12 13:00:14 +0200405 band_reset();
Eric7d897cb2022-11-28 19:20:32 +0100406
407 started = false;
408 return true;
409}
410
411int blade_device::readSamples(std::vector<short *> &bufs, int len, bool *overrun, TIMESTAMP timestamp, bool *underrun)
412{
413 ssize_t rc;
414 uint64_t ts;
415
416 if (bufs.size() != chans) {
417 LOGC(DDEV, ALERT) << "Invalid channel combination " << bufs.size();
418 return -1;
419 }
420
421 *overrun = false;
422 *underrun = false;
423
424 // Shift read time with respect to transmit clock
425 timestamp += ts_offset;
426
427 ts = timestamp;
428 LOGC(DDEV, DEBUG) << "Requested timestamp = " << ts;
429
430 // Check that timestamp is valid
431 rc = rx_buffers[0]->avail_smpls(timestamp);
432 if (rc < 0) {
433 LOGC(DDEV, ERROR) << rx_buffers[0]->str_code(rc);
434 LOGC(DDEV, ERROR) << rx_buffers[0]->str_status(timestamp);
435 return 0;
436 }
437
438 struct bladerf_metadata meta = {};
439 meta.timestamp = ts;
440
441 while (rx_buffers[0]->avail_smpls(timestamp) < len) {
442 thread_enable_cancel(false);
443 int status = bladerf_sync_rx(dev, pkt_ptrs[0], len, &meta, 200U);
444 thread_enable_cancel(true);
445
446 if (status != 0)
447 LOGC(DDEV, ERROR) << "RX broken: " << bladerf_strerror(status);
448 if (meta.flags & BLADERF_META_STATUS_OVERRUN)
449 LOGC(DDEV, ERROR) << "RX borken, OVERRUN: " << bladerf_strerror(status);
450
451 size_t num_smpls = meta.actual_count;
452 ;
453 ts = meta.timestamp;
454
455 for (size_t i = 0; i < rx_buffers.size(); i++) {
456 rc = rx_buffers[i]->write((short *)&pkt_bufs[i].front(), num_smpls, ts);
457
458 // Continue on local overrun, exit on other errors
459 if ((rc < 0)) {
460 LOGC(DDEV, ERROR) << rx_buffers[i]->str_code(rc);
461 LOGC(DDEV, ERROR) << rx_buffers[i]->str_status(timestamp);
462 if (rc != smpl_buf::ERROR_OVERFLOW)
463 return 0;
464 }
465 }
466 meta = {};
467 meta.timestamp = ts + num_smpls;
468 }
469
470 for (size_t i = 0; i < rx_buffers.size(); i++) {
471 rc = rx_buffers[i]->read(bufs[i], len, timestamp);
472 if ((rc < 0) || (rc != len)) {
473 LOGC(DDEV, ERROR) << rx_buffers[i]->str_code(rc);
474 LOGC(DDEV, ERROR) << rx_buffers[i]->str_status(timestamp);
475 return 0;
476 }
477 }
478
479 return len;
480}
481
482int blade_device::writeSamples(std::vector<short *> &bufs, int len, bool *underrun, unsigned long long timestamp)
483{
484 *underrun = false;
485 static bool first_tx = true;
486 struct bladerf_metadata meta = {};
487 if (first_tx) {
488 meta.timestamp = timestamp;
489 meta.flags = BLADERF_META_FLAG_TX_BURST_START;
490 first_tx = false;
491 }
492
493 thread_enable_cancel(false);
494 int status = bladerf_sync_tx(dev, (const void *)bufs[0], len, &meta, 200U);
495 thread_enable_cancel(true);
496
497 if (status != 0)
498 LOGC(DDEV, ERROR) << "TX broken: " << bladerf_strerror(status);
499
500 return len;
501}
502
503bool blade_device::updateAlignment(TIMESTAMP timestamp)
504{
505 return true;
506}
507
508bool blade_device::set_freq(double freq, size_t chan, bool tx)
509{
510 if (tx) {
511 bladerf_set_frequency(dev, BLADERF_CHANNEL_TX(chan), freq);
512 bladerf_frequency f;
513 bladerf_get_frequency(dev, BLADERF_CHANNEL_TX(chan), &f);
514 tx_freqs[chan] = f;
515 } else {
516 bladerf_set_frequency(dev, BLADERF_CHANNEL_RX(chan), freq);
517 bladerf_frequency f;
518 bladerf_get_frequency(dev, BLADERF_CHANNEL_RX(chan), &f);
519 rx_freqs[chan] = f;
520 }
521 LOGCHAN(chan, DDEV, INFO) << "set_freq(" << freq << ", " << (tx ? "TX" : "RX") << "): " << std::endl;
522
523 return true;
524}
525
526bool blade_device::setTxFreq(double wFreq, size_t chan)
527{
Eric7d897cb2022-11-28 19:20:32 +0100528 if (chan >= tx_freqs.size()) {
529 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
530 return false;
531 }
532 ScopedLock lock(tune_lock);
533
Ericc0f78a32023-05-12 13:00:14 +0200534 if (!update_band_from_freq(wFreq, chan, true))
Eric7d897cb2022-11-28 19:20:32 +0100535 return false;
536
537 if (!set_freq(wFreq, chan, true))
538 return false;
539
540 return true;
541}
542
543bool blade_device::setRxFreq(double wFreq, size_t chan)
544{
Eric7d897cb2022-11-28 19:20:32 +0100545 if (chan >= rx_freqs.size()) {
546 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
547 return false;
548 }
549 ScopedLock lock(tune_lock);
550
Ericc0f78a32023-05-12 13:00:14 +0200551 if (!update_band_from_freq(wFreq, chan, false))
Eric7d897cb2022-11-28 19:20:32 +0100552 return false;
553
554 return set_freq(wFreq, chan, false);
555}
556
557double blade_device::getTxFreq(size_t chan)
558{
559 if (chan >= tx_freqs.size()) {
560 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
561 return 0.0;
562 }
563
564 return tx_freqs[chan];
565}
566
567double blade_device::getRxFreq(size_t chan)
568{
569 if (chan >= rx_freqs.size()) {
570 LOGC(DDEV, ALERT) << "Requested non-existent channel " << chan;
571 return 0.0;
572 }
573
574 return rx_freqs[chan];
575}
576
577bool blade_device::requiresRadioAlign()
578{
579 return false;
580}
581
582GSM::Time blade_device::minLatency()
583{
584 return GSM::Time(6, 7);
585}
586
587TIMESTAMP blade_device::initialWriteTimestamp()
588{
589 return ts_initial;
590}
591
592TIMESTAMP blade_device::initialReadTimestamp()
593{
594 return ts_initial;
595}
596
597double blade_device::fullScaleInputValue()
598{
599 return (double)2047;
600}
601
602double blade_device::fullScaleOutputValue()
603{
604 return (double)2047;
605}
606
Eric19e134a2023-05-10 23:50:38 +0200607RadioDevice *RadioDevice::make(InterfaceType type, const struct trx_cfg *cfg)
Eric7d897cb2022-11-28 19:20:32 +0100608{
Eric19e134a2023-05-10 23:50:38 +0200609 return new blade_device(type, cfg);
Eric7d897cb2022-11-28 19:20:32 +0100610}