Commit cadcf24a5c8ba23c58d10e26ffdf4689eba96331
1 parent
32017af3
Setting up working version
Showing
6 changed files
with
525 additions
and
19 deletions
CMakeLists.txt
src/ConnectionConfig.h
1 | 1 | #pragma once |
2 | 2 | |
3 | 3 | #include <string> |
4 | +#include <unordered_map> | |
4 | 5 | |
5 | -enum class ConnectionPort | |
6 | +enum class ConnectionPort : unsigned int | |
6 | 7 | { |
7 | - CP_EXTERNAL, | |
8 | + CP_EXTERNAL = 0, | |
8 | 9 | CP_IOBUS, |
9 | 10 | CP_TCP |
10 | 11 | }; |
11 | 12 | |
12 | -enum class Parity | |
13 | +enum class Parity : unsigned int | |
13 | 14 | { |
14 | - P_ODD, | |
15 | - P_EVEN, | |
16 | - P_NONE | |
15 | + PAR_ODD, | |
16 | + PAR_EVEN, | |
17 | + PAR_NONE | |
18 | +}; | |
19 | + | |
20 | +enum class ConnectionType : unsigned int | |
21 | +{ | |
22 | + CT_SERIAL, | |
23 | + CT_TCP, | |
24 | + CT_UNKNOWN | |
17 | 25 | }; |
18 | 26 | |
19 | 27 | class ConnectionConfig |
20 | 28 | { |
21 | 29 | public: |
22 | - ConnectionConfig( ConnectionPort port, int baud = 115200, Parity parity = Parity::P_NONE, int dataBits = 8, int stopBits = 1 ) | |
23 | - { | |
24 | - | |
25 | - } | |
30 | + ConnectionConfig( ConnectionPort port, int baud = 115200, Parity parity = Parity::PAR_NONE, int dataBits = 8, int stopBits = 1, int timeOut = -1 ) | |
31 | + : m_port( port ) | |
32 | + , m_baudRate( baud ) | |
33 | + , m_parity( parity ) | |
34 | + , m_dataBits( dataBits ) | |
35 | + , m_stopBits( stopBits ) | |
36 | + , m_ipaddress() | |
37 | + , m_portnumber( -1 ) | |
38 | + , m_timeOut( timeOut ) | |
39 | + {} | |
26 | 40 | |
27 | 41 | ConnectionConfig( ConnectionPort port, const std::string &ip, int portnum, int timeOut = -1 ) |
28 | - { | |
42 | + : m_port( port ) | |
43 | + , m_baudRate( -1 ) | |
44 | + , m_parity( Parity::PAR_NONE ) | |
45 | + , m_dataBits( -1 ) | |
46 | + , m_stopBits( -1 ) | |
47 | + , m_ipaddress( ip ) | |
48 | + , m_portnumber( portnum ) | |
49 | + , m_timeOut( timeOut ) | |
50 | + {} | |
29 | 51 | |
30 | - } | |
52 | + // Getters and Setters. Implemented to avoid outside meddling on the member variables. | |
53 | + ConnectionType getType() const { return m_conType; } | |
54 | + std::string getPort() const { return m_portMap.at(m_port); } | |
55 | + int getBaudRate() const { return m_baudRate; } | |
56 | + char getParity() const { return m_parityMap.at(m_parity); } | |
57 | + int getDataBits() const { return m_dataBits; } | |
58 | + int getStopBits() const { return m_stopBits; } | |
59 | + | |
60 | + std::string getIpAddress() const { return m_ipaddress; } | |
61 | + int getTcpPort() const { return m_portnumber; } | |
62 | + int getTimeOut() const { return m_timeOut; } | |
31 | 63 | |
32 | 64 | private: |
33 | 65 | |
66 | + ConnectionType m_conType; | |
67 | + | |
34 | 68 | /// Serial connections |
35 | - ConnectionPort m_serPort; | |
69 | + ConnectionPort m_port; | |
36 | 70 | int m_baudRate; |
37 | 71 | Parity m_parity; |
38 | 72 | int m_dataBits; |
39 | 73 | int m_stopBits; |
40 | 74 | |
41 | 75 | /// TCP connections |
76 | + std::string m_ipaddress; | |
77 | + int m_portnumber; | |
42 | 78 | |
79 | + /// Generic | |
80 | + int m_timeOut; | |
81 | + std::unordered_map<ConnectionPort, std::string> m_portMap = | |
82 | + { | |
83 | + { ConnectionPort::CP_EXTERNAL, "/dev/ttyUSB0" }, | |
84 | + { ConnectionPort::CP_IOBUS, "/dev/ttyUSB1" } | |
85 | + }; | |
86 | + | |
87 | + std::unordered_map<Parity, char> m_parityMap = | |
88 | + { | |
89 | + { Parity::PAR_EVEN, 'E' }, | |
90 | + { Parity::PAR_ODD, 'O' }, | |
91 | + { Parity::PAR_NONE, 'N' } | |
92 | + }; | |
43 | 93 | }; | ... | ... |
src/IModbusAdapter.h
... | ... | @@ -4,7 +4,69 @@ |
4 | 4 | |
5 | 5 | #pragma once |
6 | 6 | |
7 | +// std | |
8 | +#include <string> | |
9 | +#include <variant> | |
10 | +#include <vector> | |
11 | + | |
12 | +using modbusData = std::vector<std::variant<uint8_t, uint16_t>>; | |
13 | + | |
14 | +// Forward Declaration | |
15 | +class ConnectionConfig; | |
16 | + | |
17 | +/*! | |
18 | + * \brief The IModbusAdapter class | |
19 | + */ | |
7 | 20 | class IModbusAdapter |
8 | 21 | { |
22 | + | |
23 | +public: | |
9 | 24 | virtual ~IModbusAdapter() {} |
25 | + | |
26 | + virtual void ModbusConnect( const ConnectionConfig &conncfg ) = 0; | |
27 | + | |
28 | + /*! | |
29 | + * \brief ModbusDisconnect | |
30 | + */ | |
31 | + virtual void ModbusDisconnect() = 0; | |
32 | + | |
33 | + /*! | |
34 | + * \brief ModbusReadData | |
35 | + * \param slaveId | |
36 | + * \param startAddress | |
37 | + * \param noOfItems | |
38 | + */ | |
39 | + virtual modbusData ModbusReadData( int slaveId, int startAddress, int noOfItems ) = 0; | |
40 | + | |
41 | + /*! | |
42 | + * \brief ModbusReadHoldReg | |
43 | + * \param slaveId | |
44 | + * \param startAddress | |
45 | + * \param noOfItems | |
46 | + * \return | |
47 | + */ | |
48 | + virtual modbusData ModbusReadHoldReg( int slaveId, int startAddress, int noOfItems ) = 0; | |
49 | + | |
50 | + /*! | |
51 | + * \brief ModBusWriteData | |
52 | + * \param slaveId | |
53 | + * \param funtionCode | |
54 | + * \param startAddress | |
55 | + * \param noOfItems | |
56 | + * \param values | |
57 | + */ | |
58 | + virtual void ModBusWriteData( int slaveId, int funtionCode, int startAddress, int noOfItems, std::vector<int>values ) = 0; | |
59 | + | |
60 | + /*! | |
61 | + * \brief isConnected | |
62 | + * \return | |
63 | + */ | |
64 | + virtual bool isConnected() const = 0; | |
65 | + | |
66 | + /*! | |
67 | + * \brief ErrorString | |
68 | + * \param errnum | |
69 | + * \return | |
70 | + */ | |
71 | + virtual std::string ErrorString( int errnum ) const = 0; | |
10 | 72 | }; | ... | ... |
src/ModbusAdapter.cpp
1 | + | |
2 | +#include "ConnectionConfig.h" | |
3 | +#include "ModbusAdapter.h" | |
4 | + | |
5 | +#include <cstring> /// Added for memset functionality | |
6 | + | |
7 | +ModbusAdapter::ModbusAdapter() | |
8 | + : m_modbus( nullptr ) | |
9 | +{ | |
10 | + this->InitBuffers(); | |
11 | +} | |
12 | + | |
13 | +ModbusAdapter::~ModbusAdapter() | |
14 | +{ | |
15 | + | |
16 | +} | |
17 | + | |
18 | +void ModbusAdapter::ModbusConnect( const ConnectionConfig &conncfg ) | |
19 | +{ | |
20 | + if( m_connected ) | |
21 | + { | |
22 | + this->ModbusDisconnect(); | |
23 | + } | |
24 | + | |
25 | + this->m_connType = conncfg.getType(); | |
26 | + | |
27 | + switch( m_connType ) | |
28 | + { | |
29 | + case ConnectionType::CT_SERIAL: | |
30 | + this->ModbusConnectRTU( conncfg.getPort(), conncfg.getBaudRate(), conncfg.getParity(), conncfg.getDataBits(), conncfg.getStopBits(), conncfg.getTimeOut(), MODBUS_RTU_RTS_NONE ); | |
31 | + break; | |
32 | + | |
33 | + case ConnectionType::CT_TCP: | |
34 | + break; | |
35 | + | |
36 | + default: | |
37 | + // throw a sensible message or return an errorcode. | |
38 | + break; | |
39 | + } | |
40 | +} | |
41 | + | |
42 | +void ModbusAdapter::ModbusDisconnect() | |
43 | +{ | |
44 | + if( m_modbus != nullptr ) | |
45 | + { | |
46 | + if( m_connected ) | |
47 | + { | |
48 | + modbus_close( m_modbus ); | |
49 | + modbus_free( m_modbus ); | |
50 | + } | |
51 | + // Clean up after ourselves. | |
52 | + m_modbus = nullptr; | |
53 | + } | |
54 | + m_connected = false; | |
55 | +} | |
56 | + | |
57 | +modbusData ModbusAdapter::ModbusReadData( int slaveId, int functionCode, int startAddress, int noOfItems ) | |
58 | +{ | |
59 | + if( m_modbus == nullptr ) | |
60 | + { | |
61 | + // No context | |
62 | + return modbusData(); | |
63 | + } | |
64 | + | |
65 | + | |
66 | + | |
67 | + int resultValue = -1; | |
68 | + bool is16Bit = false; | |
69 | + | |
70 | + modbus_set_slave( m_modbus, slaveId ); | |
71 | + | |
72 | + // Request data from modbus. | |
73 | + switch( functionCode ) | |
74 | + { | |
75 | + case MODBUS_FC_READ_COILS: | |
76 | + resultValue = modbus_read_bits( m_modbus, startAddress, noOfItems, m_dest ); | |
77 | + break; | |
78 | + case MODBUS_FC_READ_DISCRETE_INPUTS: | |
79 | + resultValue = modbus_read_input_bits( m_modbus, startAddress, noOfItems, m_dest ); | |
80 | + break; | |
81 | + case MODBUS_FC_READ_HOLDING_REGISTERS: | |
82 | + resultValue = modbus_read_registers( m_modbus, startAddress, noOfItems, m_dest16 ); | |
83 | + break; | |
84 | + case MODBUS_FC_READ_INPUT_REGISTERS: | |
85 | + resultValue = modbus_read_input_registers( m_modbus, startAddress, noOfItems, m_dest16 ); | |
86 | + break; | |
87 | + | |
88 | + default: | |
89 | + break; | |
90 | + | |
91 | + } | |
92 | + | |
93 | + // Read the databuffers | |
94 | + if( resultValue != noOfItems ) | |
95 | + { | |
96 | + return modbusData(); | |
97 | + } | |
98 | + | |
99 | + modbusData resultData; | |
100 | + for( int index = 0; index < noOfItems; ++index ) | |
101 | + { | |
102 | + resultData.push_back( (is16Bit ? static_cast<uint16_t>(m_dest16[index]) : static_cast<uint16_t>(m_dest[index])) ); | |
103 | + } | |
104 | + | |
105 | + modbus_flush( m_modbus ); // flush data | |
106 | + this->ClearBuffers(); | |
107 | + | |
108 | + return resultData; | |
109 | +} | |
110 | + | |
111 | +modbusData ModbusAdapter::ModbusReadHoldReg( int slaveId, int startAddress, int noOfItems ) | |
112 | +{ | |
113 | + if( m_modbus == nullptr ) | |
114 | + { | |
115 | + return modbusData(); | |
116 | + } | |
117 | + | |
118 | + int resultValue = -1; /// Return value from read functions | |
119 | + | |
120 | + // -- Start Critical Section --- ?? // | |
121 | + | |
122 | + modbus_set_slave( m_modbus, slaveId ); | |
123 | + /// Request data from modbus | |
124 | + resultValue = modbus_read_registers( m_modbus, startAddress, noOfItems, m_dest16 ); | |
125 | + | |
126 | + /// Read the databuffers | |
127 | + if( resultValue != noOfItems ) | |
128 | + { | |
129 | + return modbusData(); | |
130 | + } | |
131 | + | |
132 | + modbusData resultData; | |
133 | + for( int index = 0; index < noOfItems; ++index ) | |
134 | + { | |
135 | + resultData.push_back( static_cast<uint16_t>(m_dest16[index]) ); | |
136 | + } | |
137 | + | |
138 | + modbus_flush( m_modbus ); | |
139 | + this->ClearBuffers(); | |
140 | + | |
141 | + // -- End Critical Section --- ?? // | |
142 | + | |
143 | + return resultData; | |
144 | +} | |
145 | + | |
146 | +void ModbusAdapter::ModBusWriteData( int slaveId, int functionCode, int startAddress, int noOfItems, std::vector<int>values ) | |
147 | +{ | |
148 | + if( m_modbus == nullptr ) | |
149 | + { | |
150 | + return; | |
151 | + } | |
152 | + | |
153 | + // ------------------------------------------------ | |
154 | + // Create 8 bits databuffer | |
155 | + auto * data8 = new uint8_t[noOfItems]; | |
156 | + for( int index = 0; index < noOfItems; ++index ) | |
157 | + { | |
158 | + data8[index] = values[index]; | |
159 | + } | |
160 | + | |
161 | + // Create same buffer for 16 bits data | |
162 | + auto * data16 = new uint8_t[noOfItems]; | |
163 | + for( int index = 0; index < noOfItems; ++index ) | |
164 | + { | |
165 | + data16[index] = values[index]; | |
166 | + } | |
167 | + // ------------------------------------------------ | |
168 | + | |
169 | + int resultValue = -1; | |
170 | + | |
171 | + modbus_set_slave( m_modbus, slaveId ); | |
172 | + | |
173 | + // Request data from modbus | |
174 | + switch( functionCode ) | |
175 | + { | |
176 | + case MODBUS_FC_WRITE_SINGLE_COIL: | |
177 | + resultValue = modbus_write_bit( m_modbus, startAddress, values[0] ); | |
178 | + noOfItems = 1; | |
179 | + break; | |
180 | + case MODBUS_FC_WRITE_SINGLE_REGISTER: | |
181 | + resultValue = modbus_write_register( m_modbus, startAddress, values[0] ); | |
182 | + noOfItems = 1; | |
183 | + break; | |
184 | + case MODBUS_FC_WRITE_MULTIPLE_COILS: | |
185 | + | |
186 | + resultValue = modbus_write_bits( m_modbus, startAddress, noOfItems, data8 ); | |
187 | + break; | |
188 | + case MODBUS_FC_WRITE_MULTIPLE_REGISTERS: | |
189 | + resultValue = modbus_write_bits( m_modbus, startAddress, noOfItems, data16 ); | |
190 | + break; | |
191 | + } | |
192 | + | |
193 | + delete[] data8; | |
194 | + delete[] data16; | |
195 | + modbus_flush(m_modbus); // Flush data. | |
196 | + | |
197 | + if( resultValue != noOfItems ) | |
198 | + { | |
199 | + // Create a log line that writing the register failed. Maybe increment ErrorCounter(s) | |
200 | + } | |
201 | +} | |
202 | + | |
203 | +bool ModbusAdapter::isConnected() const | |
204 | +{ | |
205 | + return m_connected; | |
206 | +} | |
207 | + | |
208 | +std::string ModbusAdapter::ErrorString( int errnum ) | |
209 | +{ | |
210 | + switch(errnum) | |
211 | + { | |
212 | + case EINVAL: | |
213 | + return "Protocol context is NULL"; | |
214 | + break; | |
215 | + case ETIMEDOUT: | |
216 | + return "Timeout"; | |
217 | + break; | |
218 | + case ECONNRESET: | |
219 | + return "Connection reset"; | |
220 | + break; | |
221 | + case ECONNREFUSED: | |
222 | + return "Connection refused"; | |
223 | + break; | |
224 | + case EPIPE: | |
225 | + return "Socket error"; | |
226 | + break; | |
227 | + default: | |
228 | + return modbus_strerror(errno); | |
229 | + } | |
230 | +} | |
231 | + | |
232 | +/* ============= PRIVATE METHODS ============= */ | |
233 | +void ModbusAdapter::ModbusConnectRTU( const std::string &serialport, int baud, char parity, int dataBits, int stopBits, int RTS, int timeOut ) | |
234 | +{ | |
235 | + m_modbus = modbus_new_rtu( serialport.c_str(), baud, parity, dataBits, stopBits, RTS ); | |
236 | + | |
237 | +#ifdef LIB_MODBUS_DEBUG_OUTPUT | |
238 | + // Do sensible logging through PRIVA_LOG | |
239 | + m_modbus_set_debug( m_modbus, 1 ); | |
240 | +#endif | |
241 | + if( m_modbus == nullptr ) | |
242 | + { | |
243 | + // We can stop here. Nothing to be done as we don't have a valid context | |
244 | + // Log to PRIVA_LOG | |
245 | + return; | |
246 | + } | |
247 | + else if( m_modbus && modbus_connect( m_modbus ) == -1 ) | |
248 | + { | |
249 | + // We could not connect to the selected serial port. | |
250 | + // We can stop here. Nothing to be done as we don't have a valid connection | |
251 | + modbus_free( m_modbus ); | |
252 | + m_connected = false; | |
253 | + return; | |
254 | + } | |
255 | + | |
256 | + // Set recovery mode | |
257 | + modbus_set_error_recovery( m_modbus, MODBUS_ERROR_RECOVERY_PROTOCOL ); | |
258 | + | |
259 | + // Set the response timeout | |
260 | + modbus_set_response_timeout( m_modbus, timeOut, 0 ); | |
261 | + | |
262 | + m_connected = true; | |
263 | +} | |
264 | + | |
265 | +void ModbusAdapter::ModbusConnectTCP( const std::string &ip, int port, int timeOut ) | |
266 | +{ | |
267 | + if( ip.empty() ) | |
268 | + { | |
269 | + // Nothing to be done. Set an Logline to PRIVA_LOG and exit. | |
270 | + return; | |
271 | + } | |
272 | + | |
273 | + m_modbus = modbus_new_tcp( ip.c_str(), port ); | |
274 | + | |
275 | +#ifdef LIB_MODBUS_DEBUG_OUTPUT | |
276 | + // Do sensible logging through PRIVA_LOG | |
277 | + m_modbus_set_debug( m_modbus, 1 ); | |
278 | +#endif | |
279 | + | |
280 | + if( m_modbus == nullptr ) | |
281 | + { | |
282 | + // We can stop here. Nothing to be done as we don't have a valid context | |
283 | + // Log to PRIVA_LOG | |
284 | + return; | |
285 | + } | |
286 | + else if( m_modbus && modbus_connect( m_modbus ) == -1 ) | |
287 | + { | |
288 | + // We could not connect to the selected serial port. | |
289 | + // We can stop here. Nothing to be done as we don't have a valid connection | |
290 | + modbus_free( m_modbus ); | |
291 | + m_connected = false; | |
292 | + return; | |
293 | + } | |
294 | + | |
295 | + // Set recovery mode | |
296 | + modbus_set_error_recovery( m_modbus, MODBUS_ERROR_RECOVERY_PROTOCOL ); | |
297 | + | |
298 | + // Set the response timeout | |
299 | + modbus_set_response_timeout( m_modbus, timeOut, 0 ); | |
300 | + | |
301 | + m_connected = true; | |
302 | +} | |
303 | + | |
304 | +void ModbusAdapter::InitBuffers() | |
305 | +{ | |
306 | + // Setup memory for Data. | |
307 | + m_dest = (uint8_t *)malloc(2000 + sizeof(uint8_t)); | |
308 | + m_dest16 = (uint16_t *)malloc(125 * sizeof(uint16_t)); | |
309 | + | |
310 | + this->ClearBuffers(); | |
311 | +} | |
312 | + | |
313 | +void ModbusAdapter::ClearBuffers() | |
314 | +{ | |
315 | + memset(m_dest, 0, 2000 * sizeof(uint8_t)); | |
316 | + memset(m_dest16, 0, 125 * sizeof(uint16_t)); | |
317 | +} | ... | ... |
src/ModbusAdapter.h
... | ... | @@ -6,17 +6,94 @@ |
6 | 6 | |
7 | 7 | #include "ConnectionConfig.h" |
8 | 8 | #include "IModbusAdapter.h" |
9 | +#include "modbus.h" | |
9 | 10 | |
10 | -/// \brief This class represents a single modbus context. Each context will | |
11 | -/// result in an instance of this class. It is not intended to be | |
12 | -/// created directly but through a factory. The factory will create | |
13 | -/// the object and return the pointer to its interface. | |
11 | +// std | |
12 | +#include <memory> | |
13 | +#include <variant> | |
14 | +#include <vector> | |
14 | 15 | |
16 | +/// @brief The ModbusAdapter class represents a single modbus context. Each context will | |
17 | +/// result in an instance of this class. It is not intended to be | |
18 | +/// created directly but through a factory. The factory will create | |
19 | +/// the object and return the pointer to its interface. | |
15 | 20 | class ModbusAdapter : public IModbusAdapter |
16 | 21 | { |
17 | 22 | public: |
23 | + /*! | |
24 | + * \brief ModbusAdapter | |
25 | + */ | |
18 | 26 | ModbusAdapter(); |
27 | + | |
28 | + /*! | |
29 | + * \brief ~ModbusAdapter | |
30 | + */ | |
19 | 31 | virtual ~ModbusAdapter(); |
20 | 32 | |
21 | - void ModbusConnectRTU(const ConnectionConfig &conncfg); | |
33 | + /*! | |
34 | + * \brief ModbusConnect | |
35 | + * \param conncfg | |
36 | + */ | |
37 | + void ModbusConnect( const ConnectionConfig &conncfg ); | |
38 | + | |
39 | + /*! | |
40 | + * \brief ModbusDisconnect | |
41 | + */ | |
42 | + void ModbusDisconnect(); | |
43 | + | |
44 | + /*! | |
45 | + * \brief ModbusReadData | |
46 | + * \param slaveId | |
47 | + * \param startAddress | |
48 | + * \param noOfItems | |
49 | + */ | |
50 | + modbusData ModbusReadData( int slaveId, int functionCode, int startAddress, int noOfItems ); | |
51 | + | |
52 | + /*! | |
53 | + * \brief ModbusReadHoldReg | |
54 | + * \param slaveId | |
55 | + * \param startAddress | |
56 | + * \param noOfItems | |
57 | + * \return | |
58 | + */ | |
59 | + modbusData ModbusReadHoldReg( int slaveId, int startAddress, int noOfItems ); | |
60 | + | |
61 | + /*! | |
62 | + * \brief ModBusWriteData | |
63 | + * \param slaveId | |
64 | + * \param funtionCode | |
65 | + * \param startAddress | |
66 | + * \param noOfItems | |
67 | + * \param values | |
68 | + */ | |
69 | + void ModBusWriteData( int slaveId, int functionCode, int startAddress, int noOfItems, std::vector<int>values ); | |
70 | + | |
71 | + /*! | |
72 | + * \brief isConnected | |
73 | + * \return | |
74 | + */ | |
75 | + bool isConnected() const; | |
76 | + | |
77 | + /*! | |
78 | + * \brief ErrorString | |
79 | + * \param errnum | |
80 | + * \return | |
81 | + */ | |
82 | + std::string ErrorString( int errnum ); | |
83 | + | |
84 | +private: // Methods | |
85 | + void ModbusConnectRTU( const std::string &serialport, int baud, char parity, int dataBits, int stopBits, int RTS, int timeOut ); | |
86 | + void ModbusConnectTCP( const std::string &ip, int port, int timeOut = -1 ); | |
87 | + | |
88 | + void InitBuffers(); | |
89 | + void ClearBuffers(); | |
90 | + | |
91 | +private: // Members | |
92 | + ConnectionType m_connType { ConnectionType::CT_UNKNOWN }; ///> The type of connection this instance provides. Needed for administration | |
93 | + bool m_connected { false }; ///> Shows if the connection is still active. | |
94 | + modbus_t *m_modbus; ///> The actual low-level modbus instance as a raw-pointer. ( unique_pointer gives an error on this struct ) | |
95 | + | |
96 | + // Return value Buffers ( Room for improvement ) | |
97 | + uint8_t *m_dest; | |
98 | + uint16_t *m_dest16; | |
22 | 99 | }; | ... | ... |
tests/main.cpp renamed to src/main.cpp