Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mmyster/feature/quad digital to analog converter #933

Merged
merged 3 commits into from
Aug 18, 2022

Conversation

MMyster
Copy link
Contributor

@MMyster MMyster commented Jul 10, 2022

  • ✨ (interface): Add QDAC
  • ✨ (drivers): Add CoreQDAC

@codecov
Copy link

codecov bot commented Jul 10, 2022

Codecov Report

Merging #933 (cfeffdf) into develop (b45ac7d) will increase coverage by 0.15%.
The diff coverage is 100.00%.

❗ Current head cfeffdf differs from pull request most recent head 0109d91. Consider uploading reports for the commit 0109d91 to get more accurate results

@@             Coverage Diff             @@
##           develop     #933      +/-   ##
===========================================
+ Coverage    95.75%   95.91%   +0.15%     
===========================================
  Files          123      125       +2     
  Lines         2826     2936     +110     
===========================================
+ Hits          2706     2816     +110     
  Misses         120      120              
Impacted Files Coverage Δ
drivers/CoreQDAC/include/CoreQDAC.h 100.00% <100.00%> (ø)
drivers/CoreQDAC/source/CoreQDAC.cpp 100.00% <100.00%> (ø)

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@github-actions
Copy link

github-actions bot commented Jul 10, 2022

File comparision analysis report

🔖 Info

Target Flash Used (%) Flash Available (%) Static RAM (%)
bootloader 181260 (69%) 80884 (30%) 37456 (7%)
os 396892 (25%) 1167780 (74%) 80632 (15%)
Click to show memory sections
| -          |      Hex |     Bytes |  KiB |
|------------|---------:|----------:|-----:|
| Flash      | 0x200000 | 2 097 152 | 2048 |
| SRAM       |  0x80000 |   524 288 |  512 |
| Bootloader |  0x40000 |   262 144 |  256 |
| Header     |   0x1000 |     4 096 |    4 |
| OS         | 0x17E000 | 1 564 672 | 1528 |
| Tail       |   0x1000 |     4 096 |    4 |
| Scratch    |  0x40000 |   262 144 |  256 |

📝 Summary

Click to show summary
  • ✔️ - existing target
  • ✨ - new target
  • ⚰️ - deleted target
  • ✅ - files are the same
  • ❌ - files are different
Target Status .bin .map Total Flash (base/head) Total Flash Δ Static RAM (base/head) Static RAM Δ
LekaOS ✔️ 396892 (18%) ø 80632 (15%) ø
bootloader ✔️ 181260 (8%) ø 37456 (7%) ø
spike_lk_audio ✔️ 132380 (6%) ø 20984 (4%) ø
spike_lk_behavior_kit ✔️ 196052 (9%) ø 54528 (10%) ø
spike_lk_ble ✔️ 233676 (11%) ø 34712 (6%) ø
spike_lk_bluetooth ✔️ 92236 (4%) ø 18064 (3%) ø
spike_lk_cg_animations ✔️ 152200 (7%) ø 53064 (10%) ø
spike_lk_color_kit ✔️ 88352 (4%) ø 20216 (3%) ø
spike_lk_command_kit ✔️ 199956 (9%) ø 57856 (11%) ø
spike_lk_config_kit ✔️ 136100 (6%) ø 20976 (4%) ø
spike_lk_coreled ✔️ 87916 (4%) ø 20104 (3%) ø
spike_lk_event_queue ✔️ 84088 (4%) ø 18744 (3%) ø
spike_lk_file_manager_kit ✔️ 143648 (6%) ø 21304 (4%) ø
spike_lk_file_reception ✔️ 331036 (15%) ø 34072 (6%) ø
spike_lk_flash_memory ✔️ 86712 (4%) ø 18056 (3%) ø
spike_lk_fs ✔️ 171024 (8%) ø 47968 (9%) ø
spike_lk_lcd ✔️ 177100 (8%) ø 53992 (10%) ø
spike_lk_led_kit ✔️ 115708 (5%) ø 21024 (4%) ø
spike_lk_log_kit ✔️ 84440 (4%) ø 19248 (3%) ø
spike_lk_motors ✔️ 86008 (4%) ø 18088 (3%) ø
spike_lk_reinforcer ✔️ 112188 (5%) ø 21024 (4%) ø
spike_lk_rfid ✔️ 84636 (4%) ø 18016 (3%) ø
spike_lk_sensors_battery ✔️ 87056 (4%) ø 19120 (3%) ø
spike_lk_sensors_light ✔️ 83944 (4%) ø 18056 (3%) ø
spike_lk_sensors_microphone ✔️ 84696 (4%) ø 18056 (3%) ø
spike_lk_sensors_temperature_humidity ✔️ 90336 (4%) ø 18032 (3%) ø
spike_lk_sensors_touch ✔️ 91608 (4%) ø 18296 (3%) ø
spike_lk_serial_number ✔️ 82176 (3%) ø 18336 (3%) ø
spike_lk_ticker_timeout ✔️ 82584 (3%) ø 18072 (3%) ø
spike_lk_update_process_app_base ✔️ 144644 (6%) ø 21896 (4%) ø
spike_lk_update_process_app_update ✔️ 100328 (4%) ø 19080 (3%) ø
spike_lk_watchdog_isr ✔️ 87940 (4%) ø 19952 (3%) ø
spike_lk_wifi ✔️ 130640 (6%) ø 21368 (4%) ø
spike_mbed_blinky ✔️ 57616 (2%) ø 11496 (2%) ø
spike_mbed_watchdog_ticker_vs_thread ✔️ 84112 (4%) ø 18920 (3%) ø
spike_stl_cxxsupport ✔️ 83424 (3%) ø 18144 (3%) ø

🗺️ Map files diff output

Click to show diff list

No differenes where found in map files.

@github-actions
Copy link

github-actions bot commented Jul 10, 2022

File comparision analysis report

🔖 Info

Target Flash Used (%) Flash Available (%) Static RAM (%)
bootloader 168820 (64%) 93324 (35%) 30920 (5%)
os 363496 (23%) 1201176 (76%) 73704 (14%)
Click to show memory sections
| -          |      Hex |     Bytes |  KiB |
|------------|---------:|----------:|-----:|
| Flash      | 0x200000 | 2 097 152 | 2048 |
| SRAM       |  0x80000 |   524 288 |  512 |
| Bootloader |  0x40000 |   262 144 |  256 |
| Header     |   0x1000 |     4 096 |    4 |
| OS         | 0x17E000 | 1 564 672 | 1528 |
| Tail       |   0x1000 |     4 096 |    4 |
| Scratch    |  0x40000 |   262 144 |  256 |

📝 Summary

Click to show summary
  • ✔️ - existing target
  • ✨ - new target
  • ⚰️ - deleted target
  • ✅ - files are the same
  • ❌ - files are different
Target Status .bin .map Total Flash (base/head) Total Flash Δ Static RAM (base/head) Static RAM Δ
LekaOS ✔️ 363496 (17%) ø 73704 (14%) ø
bootloader ✔️ 168820 (8%) ø 30920 (5%) ø
spike_lk_audio ✔️ 122324 (5%) ø 14568 (2%) ø
spike_lk_behavior_kit ✔️ 187568 (8%) ø 48112 (9%) ø
spike_lk_ble ✔️ 225792 (10%) ø 28056 (5%) ø
spike_lk_bluetooth ✔️ 82948 (3%) ø 11544 (2%) ø
spike_lk_cg_animations ✔️ 144632 (6%) ø 46528 (8%) ø
spike_lk_color_kit ✔️ 65712 (3%) ø 13744 (2%) ø
spike_lk_command_kit ✔️ 189760 (9%) ø 50928 (9%) ø
spike_lk_config_kit ✔️ 124188 (5%) ø 14312 (2%) ø
spike_lk_coreled ✔️ 76164 (3%) ø 13688 (2%) ø
spike_lk_event_queue ✔️ 74736 (3%) ø 12072 (2%) ø
spike_lk_file_manager_kit ✔️ 128680 (6%) ø 14384 (2%) ø
spike_lk_file_reception ✔️ 326968 (15%) ø 27576 (5%) ø
spike_lk_flash_memory ✔️ 63880 (3%) ø 11448 (2%) ø
spike_lk_fs ✔️ 171728 (8%) ø 47880 (9%) ø
spike_lk_lcd ✔️ 167468 (7%) ø 47320 (9%) ø
spike_lk_led_kit ✔️ 103992 (4%) ø 14608 (2%) ø
spike_lk_log_kit ✔️ 63736 (3%) ø 12256 (2%) ø
spike_lk_motors ✔️ 62600 (2%) ø 11488 (2%) ø
spike_lk_reinforcer ✔️ 103480 (4%) ø 14608 (2%) ø
spike_lk_rfid ✔️ 80856 (3%) ø 11512 (2%) ø
spike_lk_sensors_battery ✔️ 78196 (3%) ø 12568 (2%) ø
spike_lk_sensors_light ✔️ 60056 (2%) ø 11440 (2%) ø
spike_lk_sensors_microphone ✔️ 72496 (3%) ø 11504 (2%) ø
spike_lk_sensors_temperature_humidity ✔️ 67048 (3%) ø 11424 (2%) ø
spike_lk_sensors_touch ✔️ 68600 (3%) ø 11432 (2%) ø
spike_lk_serial_number ✔️ 58904 (2%) ø 11480 (2%) ø
spike_lk_ticker_timeout ✔️ 69052 (3%) ø 11632 (2%) ø
spike_lk_update_process_app_base ✔️ 122804 (5%) ø 15288 (2%) ø
spike_lk_update_process_app_update ✔️ 77632 (3%) ø 12352 (2%) ø
spike_lk_watchdog_isr ✔️ 81968 (3%) ø 13280 (2%) ø
spike_lk_wifi ✔️ 116392 (5%) ø 14808 (2%) ø
spike_mbed_blinky ✔️ 57968 (2%) ø 11400 (2%) ø
spike_mbed_watchdog_ticker_vs_thread ✔️ 63208 (3%) ø 12448 (2%) ø
spike_stl_cxxsupport ✔️ 58456 (2%) ø 11400 (2%) ø

🗺️ Map files diff output

Click to show diff list

No differenes where found in map files.

@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch 3 times, most recently from fe28ff7 to 13ba488 Compare July 13, 2022 10:19
@MMyster MMyster self-assigned this Jul 13, 2022
@MMyster MMyster added the 01 - type: task Something to do label Jul 13, 2022
@MMyster MMyster added this to the Next Release milestone Jul 13, 2022
@MMyster MMyster linked an issue Jul 13, 2022 that may be closed by this pull request
5 tasks
@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch from 13ba488 to ac18117 Compare July 15, 2022 07:44
Copy link
Member

@ladislas ladislas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

premier passage 👍

drivers/CoreQDAC/include/CoreQDAC.h Outdated Show resolved Hide resolved
drivers/CoreQDAC/include/external/MCP4728.h Outdated Show resolved Hide resolved
drivers/CoreQDAC/include/external/MCP4728.h Outdated Show resolved Hide resolved
selectGain(mcp4728::data::gain::x1::all);
}

void CoreQDACMCP4728::write(uint8_t channel, uint16_t data, bool b_eep)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ça veut dire quoi le bool b_eep? ça correspond à une condition de quoi? dans quels cas on le veut true ? dans quels cas on le veut false?

channel c'est A, B, C ou D?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true : écriture dans les inputs registers + sauvegarde dans l'EEPROM Memory
false : écriture dans les inputs registers seulement

Channel c'est bien ça

Comment on lines 28 to 30
void fastWrite();
void multiWrite();
void sequentialWrite();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

je trouve ça bizarre d'avoir des write sans argument de data

et quelles sont les différences entre fast, multi et sequential?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fastWrite : écriture séquentielle vers les inputs registers
multiWrite : comme fastWrite mais avec possibilité de définir le ou les channels
sequential : écriture séquentielle vers les inputs registers + EEPROM Memory à partir d'un channel défini
single : écriture "unique" vers un input register + EEPROM Memory

Voir https://www.dropbox.com/home/Development/hardware/electronics/datasheets?preview=Aceltis-Flex-DAC-MCP4728.pdf
Page 34

Les data, ce sont les tableaux en variables privées

drivers/CoreQDAC/include/CoreQDAC.h Outdated Show resolved Hide resolved
include/interface/drivers/QDAC.h Outdated Show resolved Hide resolved
include/interface/drivers/QDAC.h Outdated Show resolved Hide resolved
include/interface/drivers/QDAC.h Outdated Show resolved Hide resolved
include/interface/drivers/QDAC.h Outdated Show resolved Hide resolved
tests/unit/mocks/mocks/leka/CoreQDAC.h Outdated Show resolved Hide resolved
drivers/CoreQDAC/include/external/MCP4728.h Show resolved Hide resolved
drivers/CoreQDAC/tests/CoreQDAC_test.cpp Outdated Show resolved Hide resolved
drivers/CoreQDAC/include/CoreQDAC.h Outdated Show resolved Hide resolved
drivers/CoreQDAC/source/CoreQDAC.cpp Outdated Show resolved Hide resolved
drivers/CoreQDAC/source/CoreQDAC.cpp Outdated Show resolved Hide resolved
@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch 5 times, most recently from 00972cd to e6ca578 Compare July 19, 2022 13:33
Copy link
Member

@YannLocatelli YannLocatelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est bon pour moi en l'état, Bravo! ✅

Il me reste le spike à valider et tout ce qui sera QDAC sera bon selon moi :)

Comment on lines 28 to 29
virtual void write(Channel channel, uint16_t data, bool eeprom = false) = 0;
virtual auto read(Channel channel, bool eeprom = false) -> uint16_t = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci de confirmer si c'est bien ces actions qui en découle selon la valeur booléenne~

Suggested change
virtual void write(Channel channel, uint16_t data, bool eeprom = false) = 0;
virtual auto read(Channel channel, bool eeprom = false) -> uint16_t = 0;
virtual void write(Channel channel, uint16_t data, bool save_in_eeprom = false) = 0;
virtual auto read(Channel channel, bool load_from_eeprom = false) -> uint16_t = 0;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MMyster tu n'as pas répondu ici

virtual ~QDAC() = default;

virtual void init() = 0;
virtual void write(Channel channel, uint16_t data, bool eeprom = false) = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Un commentaire aussi pour la notion d'EEPROM pour tous les QDAC: est-ce qu'ils en possèdent tous un?

Mon opinion est que non, et que celui qu'on a cet avantage de posséder une mémoire en plus.

Dans l'immédiat, ça ne me gêne pas de le laisser, on pourra y revenir une fois que tout le TouchKit sera fonctionnel~

@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch from e6ca578 to ed3cfbb Compare July 21, 2022 14:42
@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch from ed3cfbb to c8e19a2 Compare August 1, 2022 14:46
Copy link
Member

@ladislas ladislas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merci @MMyster! C'est pas mal du tout!

quelques retours sur la forme et changements pour rendre les choses plus lisibles et compréhensible pour les gens qui ne savent pas ce qui se passe

Comment on lines 13 to 19
enum class Channel : uint8_t
{
A,
B,
C,
D
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ici c'est bizarre d'avoir un enum class Channel dans namespace leka directement.

faut plutôt que tu la mettes dans l'interface

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

et comme je dis dans l'autre commentaire, en fait pas besoin, donc ça peut même aller dans l'implémentation directement

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

et d'autre part, c'est bien de donner la première valeur, voir même toutes, pour ne pas avoir de UB ou de bug difficile à déceler dans le futur:

Suggested change
enum class Channel : uint8_t
{
A,
B,
C,
D
};
enum class Channel : uint8_t
{
A = 0,
B = 1,
C = 2,
D = 3
};

Comment on lines 28 to 29
virtual void write(Channel channel, uint16_t data, bool eeprom = false) = 0;
virtual auto read(Channel channel, bool eeprom = false) -> uint16_t = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MMyster tu n'as pas répondu ici

drivers/CoreQDAC/CMakeLists.txt Outdated Show resolved Hide resolved
drivers/CoreQDAC/include/external/MCP4728.h Outdated Show resolved Hide resolved
drivers/CoreQDAC/include/external/MCP4728.h Outdated Show resolved Hide resolved
Comment on lines 69 to 73
command.at(0) = static_cast<uint8_t>(mcp4728::command::multi_write | ((0x03 & channel) << 1));
command.at(1) = static_cast<uint8_t>(_tx_registers.at(channel).vref << 7 | _tx_registers.at(channel).pd << 5 |
_tx_registers.at(channel).gain << 4 |
(0x0F & utils::memory::getHighByte(_tx_registers.at(channel).data)));
command.at(2) = utils::memory::getLowByte(_tx_registers.at(channel).data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pareil ici, les lambdas vont simplifier la compréhension pour comprendre:

  • ce qu'on calcule pour 0
  • ce qu'on calcule pour 1
  • ce qu'on calcule pour 2

Comment on lines 79 to 63
auto command = std::array<uint8_t, 9> {};
command.at(0) = static_cast<uint8_t>(mcp4728::command::sequential_write | ((0x03 & starting_channel) << 1));
for (uint8_t ch = starting_channel; ch <= mcp4728::channel::D; ch++) {
command.at((ch - starting_channel) * 2 + 1) =
static_cast<uint8_t>(_tx_eeprom.at(ch).vref << 7 | _tx_eeprom.at(ch).pd << 5 | _tx_eeprom.at(ch).gain << 4 |
(0x0F & utils::memory::getHighByte(_tx_eeprom.at(ch).data)));
command.at((ch - starting_channel) * 2 + 2) = utils::memory::getLowByte(_tx_eeprom.at(ch).data);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pareil ici
même le loop il pourrait être dans un lambda:

auto set_all_channels_values = []{
	// ...
};

par exemple

Comment on lines 93 to 97
command.at(0) = static_cast<uint8_t>(mcp4728::command::single_write | ((0x03 & channel) << 1));
command.at(1) = static_cast<uint8_t>(_tx_eeprom.at(channel).vref << 7 | _tx_eeprom.at(channel).pd << 5 |
_tx_eeprom.at(channel).gain << 4 |
(0x0F & utils::memory::getHighByte(_tx_eeprom.at(channel).data)));
command.at(2) = utils::memory::getLowByte(_tx_eeprom.at(channel).data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pareil ici

Comment on lines 103 to 106
for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
_tx_registers.at(ch).vref = 1 & (data >> (3 - ch));
_tx_eeprom.at(ch).vref = 1 & (data >> (3 - ch));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ici un peu pareil, un petit lambda c'est plus clair pour comprendre la magie

Suggested change
for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
_tx_registers.at(ch).vref = 1 & (data >> (3 - ch));
_tx_eeprom.at(ch).vref = 1 & (data >> (3 - ch));
}
auto compute_vref_for_channel = [data](auto ch) {
auto vref = 1 & (data >> (3 - ch));
return vref;
};
auto set_vref_for_all_channels = [data] {
for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
auto vref = compute_vref_for_channel(ch);
_tx_registers.at(ch).vref = vref;
_tx_eeprom.at(ch).vref = vref;
}
};

et là ça rend les choses plus claires parce que ça cache les détails d'implémentation dans un lambda qui me dit ce qu'il fait.

Comment on lines 113 to 101
for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
_tx_registers.at(ch).pd = 3 & (data >> (6 - 2 * ch));
_tx_eeprom.at(ch).pd = 3 & (data >> (6 - 2 * ch));
}
auto command = std::array<uint8_t, 2> {};
command.at(0) = static_cast<uint8_t>(mcp4728::command::set_power_down | (0xF0 & data) >> 4);
command.at(1) = static_cast<uint8_t>((0x0F & data) << 4);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pareil ici

@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch 2 times, most recently from aff29a3 to eb90226 Compare August 18, 2022 15:11
@ladislas
Copy link
Member

Patch containing fastWrite and sequentialWrite

2022_08_18-QDAC-Patch.diff.zip

diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt
index 26e1b4db..f3154b56 100644
--- a/drivers/CMakeLists.txt
+++ b/drivers/CMakeLists.txt
@@ -39,3 +39,6 @@ add_subdirectory(${DRIVERS_DIR}/CoreMotor)
 
 # Touch drivers
 add_subdirectory(${DRIVERS_DIR}/CoreIOExpander)
+add_subdirectory(${DRIVERS_DIR}/CoreQDAC)
+
+
diff --git a/drivers/CoreQDAC/CMakeLists.txt b/drivers/CoreQDAC/CMakeLists.txt
new file mode 100644
index 00000000..a2aa8949
--- /dev/null
+++ b/drivers/CoreQDAC/CMakeLists.txt
@@ -0,0 +1,28 @@
+# Leka - LekaOS
+# Copyright 2022 APF France handicap
+# SPDX-License-Identifier: Apache-2.0
+
+add_library(CoreQDAC STATIC)
+
+target_include_directories(CoreQDAC
+	PUBLIC
+		include
+)
+
+target_sources(CoreQDAC
+	PRIVATE
+		source/CoreQDAC.cpp
+)
+
+target_link_libraries(CoreQDAC
+	mbed-os
+	CoreI2C
+)
+
+if (${CMAKE_PROJECT_NAME} STREQUAL "LekaOSUnitTests")
+
+	leka_unit_tests_sources(
+		tests/CoreQDAC_test.cpp
+	)
+
+endif()
diff --git a/drivers/CoreQDAC/include/CoreQDAC.h b/drivers/CoreQDAC/include/CoreQDAC.h
new file mode 100644
index 00000000..b4046d07
--- /dev/null
+++ b/drivers/CoreQDAC/include/CoreQDAC.h
@@ -0,0 +1,55 @@
+// Leka - LekaOS
+// Copyright 2022 APF France handicap
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <array>
+#include <span>
+
+#include "external/MCP4728.h"
+#include "interface/drivers/I2C.h"
+#include "interface/drivers/QDAC.h"
+
+namespace leka {
+
+class CoreQDACMCP4728 : public interface::QDAC
+{
+  public:
+	CoreQDACMCP4728(interface::I2C &i2c, uint8_t address) : _i2c(i2c), _address(address) {};
+
+	void init() final;
+	void write(Channel channel, uint16_t data, bool eeprom = false) final;
+	void writeAllChannels(uint16_t data, bool eeprom = false);
+
+	auto read(Channel channel, bool eeprom = false) -> uint16_t final;
+
+  private:
+	void fastWrite();
+	void multiWrite(uint8_t channel);
+	void sequentialWrite(uint8_t starting_channel);
+	void singleWrite(uint8_t channel);
+
+	void setVoltageReference(uint8_t data);
+	void setPowerDown(uint8_t data);
+	void setGain(uint8_t data);
+
+	void readInputRegistersAndMemory();
+
+	interface::I2C &_i2c;
+	uint8_t _address;
+
+	struct QDACInputData {
+		uint8_t vref  = 0x00;
+		uint8_t pd	  = 0x00;
+		uint8_t gain  = 0x00;
+		uint16_t data = 0x0000;
+	};
+
+	std::array<QDACInputData, 4> _tx_registers {};
+	std::array<QDACInputData, 4> _tx_eeprom {};
+	std::array<QDACInputData, 4> _rx_registers {};
+	std::array<QDACInputData, 4> _rx_eeprom {};
+};
+
+}	// namespace leka
diff --git a/drivers/CoreQDAC/include/external/MCP4728.h b/drivers/CoreQDAC/include/external/MCP4728.h
new file mode 100644
index 00000000..493fd4ea
--- /dev/null
+++ b/drivers/CoreQDAC/include/external/MCP4728.h
@@ -0,0 +1,114 @@
+// Leka - LekaOS
+// Copyright 2022 APF France handicap
+// SPDX-License-Identifier: Apache-2.0
+
+// Source: https://www.dropbox.com/home/Development/hardware/electronics/datasheets?preview=Aceltis-Flex-DAC-MCP4728.pdf
+
+#pragma once
+
+#include <cstddef>
+
+#include "cstdint"
+
+namespace leka::mcp4728 {
+
+namespace command {
+
+	inline constexpr auto fast_write	   = uint8_t {0x00};
+	inline constexpr auto multi_write	   = uint8_t {0x40};
+	inline constexpr auto sequential_write = uint8_t {0x50};
+	inline constexpr auto single_write	   = uint8_t {0x58};
+
+	inline constexpr auto set_vref		 = uint8_t {0x80};
+	inline constexpr auto set_power_down = uint8_t {0xA0};
+	inline constexpr auto set_gain		 = uint8_t {0xC0};
+
+	namespace read {
+		inline constexpr auto buffer_size = std::size_t {24};
+	}	// namespace read
+
+}	// namespace command
+
+namespace channel {
+
+	inline constexpr auto A = uint8_t {0x00};
+	inline constexpr auto B = uint8_t {0x01};
+	inline constexpr auto C = uint8_t {0x02};
+	inline constexpr auto D = uint8_t {0x03};
+
+}	// namespace channel
+
+namespace data {
+
+	namespace voltage_reference {
+
+		inline constexpr auto Vdd = uint8_t {0x00};
+		namespace internal {
+			namespace channel {
+				inline constexpr auto A = uint8_t {0x08};
+				inline constexpr auto B = uint8_t {0x04};
+				inline constexpr auto C = uint8_t {0x02};
+				inline constexpr auto D = uint8_t {0x01};
+			}	// namespace channel
+			inline constexpr auto all = uint8_t {0x0f};
+		}	// namespace internal
+
+	}	// namespace voltage_reference
+
+	namespace power_down {
+
+		inline constexpr auto normal = uint8_t {0x00};
+
+		namespace channel {
+			namespace A {
+				inline constexpr auto normal		= uint8_t {0x00};
+				inline constexpr auto powerDown1K	= uint8_t {0x40};
+				inline constexpr auto powerDown100K = uint8_t {0x80};
+				inline constexpr auto powerDown500K = uint8_t {0xC0};
+			}	// namespace A
+
+			namespace B {
+				inline constexpr auto normal		= uint8_t {0x00};
+				inline constexpr auto powerDown1K	= uint8_t {0x10};
+				inline constexpr auto powerDown100K = uint8_t {0x20};
+				inline constexpr auto powerDown500K = uint8_t {0x30};
+			}	// namespace B
+
+			namespace C {
+				inline constexpr auto normal		= uint8_t {0x00};
+				inline constexpr auto powerDown1K	= uint8_t {0x04};
+				inline constexpr auto powerDown100K = uint8_t {0x08};
+				inline constexpr auto powerDown500K = uint8_t {0x0C};
+			}	// namespace C
+
+			namespace D {
+				inline constexpr auto normal		= uint8_t {0x00};
+				inline constexpr auto powerDown1K	= uint8_t {0x01};
+				inline constexpr auto powerDown100K = uint8_t {0x02};
+				inline constexpr auto powerDown500K = uint8_t {0x03};
+			}	// namespace D
+
+		}	// namespace channel
+	}		// namespace power_down
+
+	namespace gain {
+
+		namespace x1 {
+			inline constexpr auto all = uint8_t {0x00};
+		}
+
+		namespace x2 {
+			namespace channel {
+				inline constexpr auto A = uint8_t {0x08};
+				inline constexpr auto B = uint8_t {0x04};
+				inline constexpr auto C = uint8_t {0x02};
+				inline constexpr auto D = uint8_t {0x01};
+			}	// namespace channel
+			inline constexpr auto all = uint8_t {0x0f};
+		}	// namespace x2
+
+	}	// namespace gain
+
+}	// namespace data
+
+}	// namespace leka::mcp4728
diff --git a/drivers/CoreQDAC/source/CoreQDAC.cpp b/drivers/CoreQDAC/source/CoreQDAC.cpp
new file mode 100644
index 00000000..2547d602
--- /dev/null
+++ b/drivers/CoreQDAC/source/CoreQDAC.cpp
@@ -0,0 +1,153 @@
+// Leka - LekaOS
+// Copyright 2022 APF France handicap
+// SPDX-License-Identifier: Apache-2.0
+
+#include "CoreQDAC.h"
+#include <array>
+
+#include "MemoryUtils.h"
+
+using namespace leka;
+
+void CoreQDACMCP4728::init()
+{
+	setVoltageReference(mcp4728::data::voltage_reference::Vdd);
+	setPowerDown(mcp4728::data::power_down::normal);
+	setGain(mcp4728::data::gain::x1::all);
+}
+
+void CoreQDACMCP4728::write(Channel channel, uint16_t data, bool eeprom)
+{
+	auto ch = static_cast<uint8_t>(channel);
+	if (eeprom) {
+		_tx_registers.at(ch).data = data;
+		_tx_eeprom.at(ch).data	  = data;
+		singleWrite(ch);
+	} else {
+		_tx_registers.at(ch).data = data;
+		multiWrite(ch);
+	}
+}
+
+void CoreQDACMCP4728::writeAllChannels(uint16_t data, bool eeprom)
+{
+	if (eeprom) {
+		for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+			_tx_registers.at(ch).data = data;
+			_tx_eeprom.at(ch).data	  = data;
+		}
+		sequentialWrite(mcp4728::channel::A);
+	} else {
+		for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+			_tx_registers.at(ch).data = data;
+		}
+		fastWrite();
+	}
+}
+
+auto CoreQDACMCP4728::read(Channel channel, bool eeprom) -> uint16_t
+{
+	auto ch = static_cast<uint8_t>(channel);
+	readInputRegistersAndMemory();
+	return eeprom ? _rx_eeprom.at(ch).data : _rx_registers.at(ch).data;
+}
+
+void CoreQDACMCP4728::fastWrite()
+{
+	auto command = std::array<uint8_t, 8> {};
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		command.at(ch * 2)	   = static_cast<uint8_t>(mcp4728::command::fast_write | _tx_registers.at(ch).pd << 4 |
+												  (0x0F & utils::memory::getHighByte(_tx_registers.at(ch).data)));
+		command.at(ch * 2 + 1) = utils::memory::getLowByte(_tx_registers.at(ch).data);
+	}
+	_i2c.write(_address, command.data(), command.size(), false);
+}
+
+void CoreQDACMCP4728::multiWrite(uint8_t channel)
+{
+	auto command  = std::array<uint8_t, 3> {};
+	command.at(0) = static_cast<uint8_t>(mcp4728::command::multi_write | ((0x03 & channel) << 1));
+	command.at(1) = static_cast<uint8_t>(_tx_registers.at(channel).vref << 7 | _tx_registers.at(channel).pd << 5 |
+										 _tx_registers.at(channel).gain << 4 |
+										 (0x0F & utils::memory::getHighByte(_tx_registers.at(channel).data)));
+	command.at(2) = utils::memory::getLowByte(_tx_registers.at(channel).data);
+	_i2c.write(_address, command.data(), command.size(), false);
+}
+
+void CoreQDACMCP4728::sequentialWrite(uint8_t starting_channel)
+{
+	auto command  = std::array<uint8_t, 9> {};
+	command.at(0) = static_cast<uint8_t>(mcp4728::command::sequential_write | ((0x03 & starting_channel) << 1));
+	for (uint8_t ch = starting_channel; ch <= mcp4728::channel::D; ch++) {
+		command.at((ch - starting_channel) * 2 + 1) =
+			static_cast<uint8_t>(_tx_eeprom.at(ch).vref << 7 | _tx_eeprom.at(ch).pd << 5 | _tx_eeprom.at(ch).gain << 4 |
+								 (0x0F & utils::memory::getHighByte(_tx_eeprom.at(ch).data)));
+		command.at((ch - starting_channel) * 2 + 2) = utils::memory::getLowByte(_tx_eeprom.at(ch).data);
+	}
+	_i2c.write(_address, command.data(), command.size(), false);
+}
+
+void CoreQDACMCP4728::singleWrite(uint8_t channel)
+{
+	auto command  = std::array<uint8_t, 3> {};
+	command.at(0) = static_cast<uint8_t>(mcp4728::command::single_write | ((0x03 & channel) << 1));
+	command.at(1) = static_cast<uint8_t>(_tx_eeprom.at(channel).vref << 7 | _tx_eeprom.at(channel).pd << 5 |
+										 _tx_eeprom.at(channel).gain << 4 |
+										 (0x0F & utils::memory::getHighByte(_tx_eeprom.at(channel).data)));
+	command.at(2) = utils::memory::getLowByte(_tx_eeprom.at(channel).data);
+	_i2c.write(_address, command.data(), command.size(), false);
+}
+
+void CoreQDACMCP4728::setVoltageReference(uint8_t data)
+{
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		_tx_registers.at(ch).vref = 1 & (data >> (3 - ch));
+		_tx_eeprom.at(ch).vref	  = 1 & (data >> (3 - ch));
+	}
+	auto command = static_cast<uint8_t>(mcp4728::command::set_vref | (0x0F & data));
+	_i2c.write(_address, &command, 1, false);
+}
+
+void CoreQDACMCP4728::setPowerDown(uint8_t data)
+{
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		_tx_registers.at(ch).pd = 3 & (data >> (6 - 2 * ch));
+		_tx_eeprom.at(ch).pd	= 3 & (data >> (6 - 2 * ch));
+	}
+	auto command  = std::array<uint8_t, 2> {};
+	command.at(0) = static_cast<uint8_t>(mcp4728::command::set_power_down | (0xF0 & data) >> 4);
+	command.at(1) = static_cast<uint8_t>((0x0F & data) << 4);
+
+	_i2c.write(_address, command.data(), command.size(), false);
+}
+
+void CoreQDACMCP4728::setGain(uint8_t data)
+{
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		_tx_registers.at(ch).gain = 1 & (data >> (3 - ch));
+		_tx_eeprom.at(ch).gain	  = 1 & (data >> (3 - ch));
+	}
+	auto command = static_cast<uint8_t>(mcp4728::command::set_gain | (0x0F & data));
+	_i2c.write(_address, &command, 1, false);
+}
+
+void CoreQDACMCP4728::readInputRegistersAndMemory()
+{
+	auto buffer = std::array<uint8_t, mcp4728::command::read::buffer_size> {};
+
+	_i2c.read(_address, buffer.data(), buffer.size(), false);
+
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		_rx_registers.at(ch).vref = static_cast<uint8_t>((buffer.at(ch * 6 + 1) & 0b10000000) >> 7);
+		_rx_registers.at(ch).pd	  = static_cast<uint8_t>((buffer.at(ch * 6 + 1) & 0b01100000) >> 5);
+		_rx_registers.at(ch).gain = static_cast<uint8_t>((buffer.at(ch * 6 + 1) & 0b00010000) >> 4);
+		_rx_registers.at(ch).data =
+			static_cast<uint16_t>(((buffer.at(ch * 6 + 1) & 0b00001111) << 8) | buffer.at(ch * 6 + 2));
+
+		_rx_eeprom.at(ch).vref = static_cast<uint8_t>((buffer.at(ch * 6 + 4) & 0b10000000) >> 7);
+		_rx_eeprom.at(ch).pd   = static_cast<uint8_t>((buffer.at(ch * 6 + 4) & 0b01100000) >> 5);
+		_rx_eeprom.at(ch).gain = static_cast<uint8_t>((buffer.at(ch * 6 + 4) & 0b00010000) >> 4);
+		_rx_eeprom.at(ch).data =
+			static_cast<uint16_t>(((buffer.at(ch * 6 + 4) & 0b00001111) << 8) | buffer.at(ch * 6 + 5));
+	}
+}
diff --git a/drivers/CoreQDAC/tests/CoreQDAC_test.cpp b/drivers/CoreQDAC/tests/CoreQDAC_test.cpp
new file mode 100644
index 00000000..c3d895be
--- /dev/null
+++ b/drivers/CoreQDAC/tests/CoreQDAC_test.cpp
@@ -0,0 +1,154 @@
+// Leka - LekaOS
+// Copyright 2022 APF France handicap
+// SPDX-License-Identifier: Apache-2.0
+
+#include "CoreQDAC.h"
+
+#include "MemoryUtils.h"
+#include "external/MCP4728.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "mocks/leka/CoreI2C.h"
+
+using namespace leka;
+
+using ::testing::Args;
+using ::testing::DoAll;
+using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
+using ::testing::InSequence;
+using ::testing::Return;
+using ::testing::SetArrayArgument;
+
+class CoreQDACTest : public ::testing::Test
+
+{
+  protected:
+	// void SetUp() override {}
+	// void TearDown() override {}
+
+	const uint8_t i2c_address {0xC0};
+	mock::CoreI2C mocki2c;
+	CoreQDACMCP4728 dac {mocki2c, i2c_address};
+};
+
+TEST_F(CoreQDACTest, initializationDefault)
+{
+	auto new_dac = CoreQDACMCP4728 {mocki2c, i2c_address};
+	ASSERT_NE(&new_dac, nullptr);
+}
+
+TEST_F(CoreQDACTest, init)
+{
+	const auto expected_vref		= static_cast<uint8_t>(mcp4728::command::set_vref | 0x00);
+	const auto expected_buffer_vref = ElementsAre(expected_vref);
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer_vref));
+
+	const auto expected_pd_first_byte	  = static_cast<uint8_t>(mcp4728::command::set_power_down | 0x00);
+	const auto expected_pd_second_byte	  = uint8_t {0x00};
+	const auto expected_buffer_power_down = ElementsAre(expected_pd_first_byte, expected_pd_second_byte);
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer_power_down));
+
+	const auto expected_gain		= static_cast<uint8_t>(mcp4728::command::set_gain | 0x00);
+	const auto expected_buffer_gain = ElementsAre(expected_gain);
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer_gain));
+
+	dac.init();
+}
+
+TEST_F(CoreQDACTest, write)
+{
+	auto value_to_write = uint16_t {0x0ABC};
+
+	auto command = std::array<uint8_t, 3> {};
+
+	command.at(0)			   = static_cast<uint8_t>(mcp4728::command::multi_write | mcp4728::channel::B << 1);
+	command.at(1)			   = 0x00 | (0x0F & utils::memory::getHighByte(value_to_write));
+	command.at(2)			   = utils::memory::getLowByte(value_to_write);
+	const auto expected_buffer = ElementsAreArray(command);
+
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer)).Times(1);
+
+	dac.write(leka::Channel::B, value_to_write);
+}
+
+TEST_F(CoreQDACTest, writeMemory)
+{
+	auto value_to_write = uint16_t {0x0ABC};
+
+	auto command = std::array<uint8_t, 3> {};
+
+	command.at(0)			   = static_cast<uint8_t>(mcp4728::command::single_write | mcp4728::channel::B << 1);
+	command.at(1)			   = 0x00 | (0x0F & utils::memory::getHighByte(value_to_write));
+	command.at(2)			   = utils::memory::getLowByte(value_to_write);
+	const auto expected_buffer = ElementsAreArray(command);
+
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer));
+
+	dac.write(leka::Channel::B, value_to_write, true);
+}
+
+TEST_F(CoreQDACTest, writeAllChannels)
+{
+	auto value_to_write = uint16_t {0x0ABC};
+
+	auto command = std::array<uint8_t, 8> {};
+
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		command.at(ch * 2) =
+			static_cast<uint8_t>(mcp4728::command::fast_write | (0x0F & utils::memory::getHighByte(value_to_write)));
+		command.at(ch * 2 + 1) = utils::memory::getLowByte(value_to_write);
+	}
+
+	const auto expected_buffer = ElementsAreArray(command);
+
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer)).Times(1);
+
+	dac.writeAllChannels(value_to_write);
+}
+
+TEST_F(CoreQDACTest, writeAllChannelsMemory)
+{
+	auto value_to_write = uint16_t {0x0ABC};
+
+	auto command = std::array<uint8_t, 9> {};
+
+	command.at(0) = static_cast<uint8_t>(mcp4728::command::sequential_write | mcp4728::channel::A << 1);
+
+	for (uint8_t ch = mcp4728::channel::A; ch <= mcp4728::channel::D; ch++) {
+		command.at((ch - mcp4728::channel::A) * 2 + 1) = 0x00 | (0x0F & utils::memory::getHighByte(value_to_write));
+		command.at((ch - mcp4728::channel::A) * 2 + 2) = utils::memory::getLowByte(value_to_write);
+	}
+
+	const auto expected_buffer = ElementsAreArray(command);
+
+	EXPECT_CALL(mocki2c, write).With(Args<1, 2>(expected_buffer)).Times(1);
+
+	dac.writeAllChannels(value_to_write, true);
+}
+
+TEST_F(CoreQDACTest, read)
+{
+	auto expected_buffer = std::array<uint8_t, 24> {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+	auto expected_data	 = static_cast<uint16_t>(((expected_buffer.at(1) & 0x0F) << 8) | expected_buffer.at(2));
+
+	EXPECT_CALL(mocki2c, read)
+		.WillOnce(DoAll(SetArrayArgument<1>(begin(expected_buffer), end(expected_buffer)), Return(0)));
+
+	auto data = dac.read(leka::Channel::A);
+
+	ASSERT_EQ(expected_data, data);
+}
+
+TEST_F(CoreQDACTest, readMemory)
+{
+	auto expected_buffer = std::array<uint8_t, 24> {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+	auto expected_data	 = static_cast<uint16_t>(((expected_buffer.at(4) & 0x0F) << 8) | expected_buffer.at(5));
+
+	EXPECT_CALL(mocki2c, read)
+		.WillOnce(DoAll(SetArrayArgument<1>(begin(expected_buffer), end(expected_buffer)), Return(0)));
+
+	auto data = dac.read(leka::Channel::A, true);
+
+	ASSERT_EQ(expected_data, data);
+}
diff --git a/include/interface/drivers/QDAC.h b/include/interface/drivers/QDAC.h
new file mode 100644
index 00000000..dcbfb881
--- /dev/null
+++ b/include/interface/drivers/QDAC.h
@@ -0,0 +1,22 @@
+// Leka - LekaOS
+// Copyright 2022 APF France handicap
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <span>
+
+namespace leka::interface {
+class QDAC
+{
+  public:
+	virtual ~QDAC() = default;
+
+	virtual void init()								   = 0;
+	virtual void write(uint8_t channel, uint16_t data) = 0;
+	virtual auto read(uint8_t channel) -> uint16_t	   = 0;
+};
+
+}	// namespace leka::interface
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index e8447fbf..413fafea 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -244,6 +244,7 @@ add_subdirectory(template)
 # Register drivers
 leka_register_unit_tests_for_driver(CoreBattery)
 leka_register_unit_tests_for_driver(CoreBufferedSerial)
+leka_register_unit_tests_for_driver(CoreQDAC)
 leka_register_unit_tests_for_driver(CoreEventFlags)
 leka_register_unit_tests_for_driver(CoreEventQueue)
 leka_register_unit_tests_for_driver(CoreFlashMemory)
diff --git a/tests/unit/mocks/mocks/leka/CoreQDAC.h b/tests/unit/mocks/mocks/leka/CoreQDAC.h
new file mode 100644
index 00000000..280894b3
--- /dev/null
+++ b/tests/unit/mocks/mocks/leka/CoreQDAC.h
@@ -0,0 +1,20 @@
+// Leka - LekaOS
+// Copyright 2022 APF France handicap
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "gmock/gmock.h"
+#include "interface/drivers/QDAC.h"
+
+namespace leka::mock {
+
+class CoreQDAC : public interface::QDAC
+{
+  public:
+	MOCK_METHOD(void, init, (), (override));
+	MOCK_METHOD(void, write, (Channel, uint16_t, bool), (override));
+	MOCK_METHOD(uint16_t, read, (Channel, bool), (override));
+};
+
+}	// namespace leka::mock

@MMyster MMyster force-pushed the mmyster/feature/quad-digital-to-analog-converter branch from adfa308 to cfeffdf Compare August 18, 2022 16:06
@ladislas ladislas force-pushed the mmyster/feature/quad-digital-to-analog-converter branch from cfeffdf to 0109d91 Compare August 18, 2022 16:13
@ladislas ladislas merged commit 4abe07d into develop Aug 18, 2022
@sonarcloud
Copy link

sonarcloud bot commented Aug 18, 2022

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

100.0% 100.0% Coverage
0.0% 0.0% Duplication

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
01 - type: task Something to do
Projects
None yet
Development

Successfully merging this pull request may close these issues.

TouchSensorKit - Make Touch Sensor works
3 participants