• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 35347425313

18 Sep 2026 12:57PM UTC coverage: 39.907% (+0.005%) from 39.902%
35347425313

push

github

jcelerier
bitfocus: reload the protocol settings of a saved device

The configuration a module was set up with never came back: the settings
widget only restored it when the settings still carried a live handler,
which never survives a save, and it appended the widget values to the
stored list instead of replacing them. A document therefore accumulated
duplicate keys whose trailing - empty - copy won in reconnect(), so a
configured webhook url was lost both in the dialog and in the module.

Restore the values once the module answers with its fields, whichever
path the settings came in through, and write each key once.
Deduplicating on load repairs the documents that already doubled.

The module path was also stored absolute, which only resolves on the
machine the device was created on: relativize it against the library
like every other file score references.

Along the way: keep the name of a device that already has one instead of
resetting it to the module's short name on every edit, keep the values of
fields the widget does not know how to render, and stop restarting the
module process when its own entry is picked again in the device list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

12 of 75 new or added lines in 4 files covered. (16.0%)

39 existing lines in 6 files now uncovered.

114929 of 287991 relevant lines covered (39.91%)

77343.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/plugins/score-plugin-protocols/Protocols/Bitfocus/BitfocusDevice.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
#include "BitfocusDevice.hpp"
4

5
#include <Device/Protocol/DeviceSettings.hpp>
6

7
#include <Explorer/DeviceList.hpp>
8
#include <Explorer/DeviceLogging.hpp>
9
#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
10

11
#include <Protocols/Bitfocus/BitfocusContext.hpp>
12
#include <Protocols/Bitfocus/BitfocusSpecificSettings.hpp>
13

14
#include <score/application/ApplicationContext.hpp>
15
#include <score/document/DocumentContext.hpp>
16

17
#include <ossia/network/context.hpp>
18
#include <ossia/network/generic/generic_device.hpp>
19
#include <ossia/network/generic/generic_parameter.hpp>
20
#include <ossia/network/rate_limiting_protocol.hpp>
21

22
#include <ossia-qt/js_utilities.hpp>
23

24
#include <memory>
25
namespace ossia::net
26
{
27
class bitfocus_protocol
28
    : public QObject
29
    , public ossia::net::protocol_base
30
{
31
public:
32
  bitfocus_protocol(
×
33
      std::shared_ptr<bitfocus::module_handler> rc, ossia::net::network_context_ptr ctx)
34
      : m_rc{rc}
×
35
      , m_context{ctx}
×
36
  {
×
37
    QObject::connect(
×
38
        m_rc.get(), &bitfocus::module_handler::variableChanged, this,
×
39
        [self = QPointer{this},
×
40
         ctx = m_context](const QString& name, const QVariant& v) {
×
41
      boost::asio::post(ctx->context, [self, name, val = ossia::qt::qt_to_ossia{}(v)] {
×
42
        if(self)
×
43
        {
44
          auto it = self->m_variables_recv.find(name);
×
45
          if(it != self->m_variables_recv.end())
×
46
          {
47
            it->second->set_value(std::move(val));
×
48
          }
×
49
        }
×
50
      });
×
51
    },
×
52
        Qt::DirectConnection);
53

54
    QObject::connect(
×
55
        m_rc.get(), &bitfocus::module_handler::feedbackValueChanged, this,
×
56
        [self = QPointer{this}, ctx = m_context](
×
57
            const QString& id, const QString& controlId, const QVariant& v) {
58
      boost::asio::post(ctx->context, [self, id, val = ossia::qt::qt_to_ossia{}(v)] {
×
59
        if(self)
×
60
        {
61
          auto it = self->m_feedbacks_recv.find(id);
×
62
          if(it != self->m_feedbacks_recv.end())
×
63
          {
64
            it->second->set_value(std::move(val));
×
65
          }
×
66
        }
×
67
      });
×
68
    },
×
69
        Qt::DirectConnection);
70
  }
×
71

72
  bool pull(ossia::net::parameter_base&) override { return true; }
×
73
  bool push(const ossia::net::parameter_base& p, const ossia::value& v) override
×
74
  {
75
    auto parent = p.get_node().get_parent();
×
76
    if(parent == nodes.actions)
×
77
    {
78
      ossia::value_map_type options;
×
79
      {
80
        const auto& cld = p.get_node().children();
×
81
        options.reserve(cld.size());
×
82
        for(auto& cld : cld)
×
83
          if(auto option_p = cld->get_parameter())
×
84
            options.emplace_back(cld->get_name(), option_p->value());
×
85
      }
×
86
      QMetaObject::invokeMethod(
×
87
          m_rc.get(),
×
88
          [m = m_rc, name = p.get_node().get_name(), options = std::move(options)] {
×
89
        QVariantMap options_map;
×
90
        for(auto& [k, v] : options)
×
91
          options_map[QString::fromStdString(k)]
×
92
              = v.apply(ossia::qt::ossia_to_qvariant{});
×
93

94
        m->actionRun(name, options_map);
×
95
      });
×
96
    }
×
97
    else if(
98
        parent && parent->get_parent() == nodes.actions && parent->children_count() == 1)
×
99
    {
100
      ossia::value_map_type options;
×
101
      options.reserve(1);
×
102
      options.emplace_back(p.get_node().get_name(), v);
×
103

104
      QMetaObject::invokeMethod(
×
105
          m_rc.get(),
×
106
          [m = m_rc, name = parent->get_name(), options = std::move(options)] {
×
107
        QVariantMap options_map;
×
108
        for(auto& [k, v] : options)
×
109
          options_map[QString::fromStdString(k)]
×
110
              = v.apply(ossia::qt::ossia_to_qvariant{});
×
111

112
        m->actionRun(name, options_map);
×
113
      });
×
114
    }
×
115
    // else if(parent == nodes.presets)
116
    // {
117
    // }
118
    else if(parent == nodes.feedbacks)
×
119
    {
120
    }
×
121
    return true;
×
122
  }
×
123
  bool push_raw(const ossia::net::full_parameter_data&) override { return true; }
×
124
  bool observe(ossia::net::parameter_base&, bool) override { return true; }
×
125
  bool update(ossia::net::node_base& node_base) override { return true; }
×
126

127
  void setup_node(auto& config, ossia::net::node_base* node)
×
128
  {
129
    ossia::net::set_description(*node, config.name.toStdString());
×
130
    switch(config.options.size())
×
131
    {
132
      case 0: {
133
        node->create_parameter(ossia::val_type::IMPULSE);
×
134
        break;
×
135
      }
136
      // FIXME when we have only one parameter, simplify things?
137
      // We need to store the option name somewhere though
138
      // case 1: {
139
      //   auto& opt = config.options[0];
140
      //   if(opt.type == "static-text")
141
      //     node->create_parameter(ossia::val_type::IMPULSE);
142
      //   else
143
      //     setup_option_parameter(opt, node);
144
      //   break;
145
      // }
146
      default: {
147
        node->create_parameter(ossia::val_type::IMPULSE);
×
148
        for(auto& opt : config.options)
×
149
        {
150
          if(opt.type == "static-text")
×
151
            continue;
×
152

153
          auto cld = node->create_child(opt.id.toStdString());
×
154
          setup_option_parameter(opt, cld);
×
155
        }
156
      }
157
    }
×
158
  }
×
159

160
  void setup_option_parameter(
×
161
      const bitfocus::module_data::config_field& opt, ossia::net::node_base* cld)
162
  {
163
    ossia::net::set_description(*cld, opt.label.toStdString());
×
164
    if(opt.type == "textinput" || opt.type == "bonjourdevice")
×
165
    {
166
      auto p = cld->create_parameter(ossia::val_type::STRING);
×
167
      p->set_value(opt.default_value.toString().toStdString());
×
168
    }
×
169
    else if(opt.type == "number")
×
170
    {
171
      // FIXME int
172
      auto p = cld->create_parameter(ossia::val_type::FLOAT);
×
173
      p->set_value(opt.default_value.toDouble());
×
174
      auto dom = ossia::domain_base<float>{};
×
175
      dom.min = opt.min.toDouble();
×
176
      dom.max = opt.max.toDouble();
×
177
      p->set_domain(std::move(dom));
×
178
    }
×
179
    else if(opt.type == "checkbox" || opt.type == "boolean")
×
180
    {
181
      auto p = cld->create_parameter(ossia::val_type::BOOL);
×
182
      p->set_value(opt.default_value.toBool());
×
183
    }
×
184
    else if(opt.type == "choices" || opt.type == "dropdown")
×
185
    {
186
      auto p = cld->create_parameter(ossia::val_type::STRING);
×
187
      auto dom = ossia::domain_base<std::string>{};
×
188
      for(const auto& choice : opt.choices)
×
189
        dom.values.push_back(choice.id.toStdString());
×
190
      p->set_value(opt.default_value.toString().toStdString());
×
191
      p->set_domain(std::move(dom));
×
192
    }
×
193
  }
×
194

195
  void init_device()
×
196
  {
197
    if(!m_dev)
×
198
      return;
×
199
    if(nodes.actions || nodes.feedbacks || nodes.variables)
×
200
      return;
×
201

202
    auto& dev = *m_dev;
×
203
    // Start creating the tree
204
    auto& m = m_rc->model();
×
205
    nodes.actions
×
206
        = m.actions.empty() ? nullptr : dev.get_root_node().create_child("action");
×
207
    // nodes.presets
208
    //     = m.presets.empty() ? nullptr : dev.get_root_node().create_child("presets");
209
    nodes.feedbacks
×
210
        = m.feedbacks.empty() ? nullptr : dev.get_root_node().create_child("feedback");
×
211
    nodes.variables
×
212
        = m.variables.empty() ? nullptr : dev.get_root_node().create_child("variable");
×
213

214
    for(auto& v : m.actions)
×
215
    {
216
      auto node = nodes.actions->create_child(v.first.toStdString());
×
217
      setup_node(v.second, node);
×
218
    }
219

220
    // for(auto& v : m.presets)
221
    // {
222
    //   auto node = nodes.presets->create_child(v.first.toStdString());
223
    //   ossia::net::set_description(*node, v.second.name.toStdString());
224
    //   auto param = node->create_parameter(ossia::val_type::IMPULSE);
225
    // }
226

227
    std::map<QString, bitfocus::module_data::feedback_instance> fb_instances;
×
228
    for(auto& v : m.feedbacks)
×
229
    {
230
      auto node = nodes.feedbacks->create_child(v.first.toStdString());
×
231
      setup_node(v.second, node);
×
232
      if(auto param = node->get_parameter())
×
233
        m_feedbacks_recv[v.first] = param;
×
234

235
      // Create a feedback instance to subscribe
236
      bitfocus::module_data::feedback_instance inst;
×
237
      inst.id = v.first;
×
238
      inst.controlId = "ossia";
×
239
      inst.definitionId = v.first;
×
240
      fb_instances[v.first] = std::move(inst);
×
241
    }
×
242

243
    // Subscribe to all feedbacks
244
    if(!fb_instances.empty())
×
245
    {
246
      QMetaObject::invokeMethod(
×
247
          m_rc.get(), [rc = m_rc, fb = std::move(fb_instances)]() mutable {
×
248
        rc->updateFeedbacks(fb);
×
249
      });
×
250
    }
×
251

252
    for(auto& v : m.variables)
×
253
    {
254
      auto node = nodes.variables->create_child(v.first.toStdString());
×
255
      ossia::net::set_description(*node, v.second.name.toStdString());
×
256
      auto val = ossia::qt::qt_to_ossia{}(v.second.value);
×
257

258
      if(val.get_type() != ossia::val_type::NONE)
×
259
      {
260
        auto param = node->create_parameter(val.get_type());
×
261
        param->set_value(val);
×
262
        m_variables_send[param] = v.first;
×
263
        m_variables_recv[v.first] = param;
×
264
      }
×
265
    }
×
266
  }
×
267

268
  void set_device(ossia::net::device_base& dev) override
×
269
  {
270
    m_dev = &dev;
×
271
    init_device();
×
272
  }
×
273

274
  std::shared_ptr<bitfocus::module_handler> m_rc;
275
  ossia::net::network_context_ptr m_context;
276
  ossia::net::device_base* m_dev{};
×
277
  struct
×
278
  {
279
    ossia::net::node_base* actions{};
×
280
    // ossia::net::node_base* presets{};
281
    ossia::net::node_base* feedbacks{};
×
282
    ossia::net::node_base* variables{};
×
283
  } nodes;
284

285
  ossia::flat_map<ossia::net::parameter_base*, QString> m_actions;
286
  // ossia::flat_map<ossia::net::parameter_base*, QString> m_presets;
287
  ossia::flat_map<QString, ossia::net::parameter_base*> m_variables_recv;
288
  ossia::flat_map<ossia::net::parameter_base*, QString> m_variables_send;
289
  ossia::flat_map<QString, ossia::net::parameter_base*> m_feedbacks_recv;
290
};
291
}
292
namespace Protocols
293
{
294

295
BitfocusDevice::BitfocusDevice(
×
296
    const Device::DeviceSettings& settings, const ossia::net::network_context_ptr& ctx)
297
    : OwningDeviceInterface{settings}
×
298
    , m_ctx{ctx}
×
299
{
×
300
  m_capas.canRefreshTree = true;
×
301
  m_capas.canAddNode = false;
×
302
  m_capas.canRemoveNode = false;
×
303
  m_capas.canRenameNode = false;
×
304
  m_capas.canSetProperties = false;
×
305
  m_capas.canSerialize = true;
×
306
  m_capas.canLearn = false;
×
307
  m_capas.hasCallbacks = false;
×
308
}
×
309

310
bool BitfocusDevice::reconnect()
×
311
{
312
  disconnect();
×
313

314
  try
315
  {
316
    BitfocusSpecificSettings stgs
317
        = settings().deviceSpecificSettings.value<BitfocusSpecificSettings>();
×
NEW
318
    stgs.deduplicateConfiguration();
×
319

320
    auto conf = bitfocus::module_configuration{};
×
321
    {
322
      if(!stgs.product.isEmpty())
×
323
      {
324
        conf["product"] = stgs.product;
×
325
      }
×
326
      for(auto& [k, v] : stgs.configuration)
×
327
      {
328
        conf[k] = v.apply(ossia::qt::ossia_to_qvariant{});
×
329
      }
330
    }
331

332
    if(!stgs.handler)
×
333
    {
334
      stgs.handler = std::make_shared<bitfocus::module_handler>(
×
335
          stgs.path, stgs.entrypoint, stgs.nodeVersion, stgs.apiVersion,
×
336
          std::move(conf));
337
      m_settings.deviceSpecificSettings = QVariant::fromValue(stgs);
×
338
    }
×
339

340
    stgs.handler->afterRegistration(
×
341
        [name = stgs.name, conf, h = std::weak_ptr{stgs.handler}] {
×
342
      if(auto handler = h.lock())
×
343
      {
344
        handler->updateConfigAndLabel(name, conf);
×
345
      }
×
346
    });
×
347

348
    const auto& name = settings().name.toStdString();
×
349
    if(auto proto = std::make_unique<ossia::net::bitfocus_protocol>(stgs.handler, m_ctx))
×
350
    {
351
      auto pproto = proto.get();
×
352
      m_dev = std::make_shared<ossia::net::generic_device>(std::move(proto), name);
×
353

354
      stgs.handler->afterRegistration([dev = std::weak_ptr{m_dev}, pproto] {
×
355
        if(auto d = dev.lock())
×
356
          pproto->init_device();
×
357
      });
×
358
      deviceChanged(nullptr, m_dev.get());
×
359
      setLogging_impl(Device::get_cur_logging(isLogging()));
×
360
    }
×
361
    else
362
    {
363
      qDebug() << "Could not create Bitfocus protocol";
×
364
    }
365
  }
×
366
  catch(std::exception& e)
367
  {
368
    qDebug() << "Bitfocus Protocol error: " << e.what();
×
369
  }
×
370
  catch(...)
371
  {
372
    SCORE_TODO;
×
373
  }
×
374
  return connected();
×
375
}
×
376

377
bool BitfocusDevice::isLearning() const
×
378
{
379
  /*
380
  auto& proto = static_cast<ossia::net::bitfocus5_protocol&>(m_dev->get_protocol());
381
  return proto.learning();
382
  */
383
  return false;
×
384
}
385

386
void BitfocusDevice::setLearning(bool b)
×
387
{
388
  /*
389
  if(!m_dev)
390
    return;
391
  auto& proto = static_cast<ossia::net::bitfocus5_protocol&>(m_dev->get_protocol());
392
  auto& dev = *m_dev;
393
  if(b)
394
  {
395
    dev.on_node_created.connect<&DeviceInterface::nodeCreated>((DeviceInterface*)this);
396
    dev.on_node_removing.connect<&DeviceInterface::nodeRemoving>((DeviceInterface*)this);
397
    dev.on_node_renamed.connect<&DeviceInterface::nodeRenamed>((DeviceInterface*)this);
398
    dev.on_parameter_created.connect<&DeviceInterface::addressCreated>(
399
        (DeviceInterface*)this);
400
    dev.on_attribute_modified.connect<&DeviceInterface::addressUpdated>(
401
        (DeviceInterface*)this);
402
  }
403
  else
404
  {
405
    dev.on_node_created.disconnect<&DeviceInterface::nodeCreated>(
406
        (DeviceInterface*)this);
407
    dev.on_node_removing.disconnect<&DeviceInterface::nodeRemoving>(
408
        (DeviceInterface*)this);
409
    dev.on_node_renamed.disconnect<&DeviceInterface::nodeRenamed>(
410
        (DeviceInterface*)this);
411
    dev.on_parameter_created.disconnect<&DeviceInterface::addressCreated>(
412
        (DeviceInterface*)this);
413
    dev.on_attribute_modified.disconnect<&DeviceInterface::addressUpdated>(
414
        (DeviceInterface*)this);
415
  }
416

417
  proto.set_learning(b);*/
418
}
×
419
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc