1 ///
2 module modbus.protocol.master;
3 
4 import modbus.protocol.base;
5 
6 ///
7 class ModbusMaster : Modbus
8 {
9     ///
10     this(Backend be, Connection con) { super(be, con); }
11 
12     /++ Read from connection
13 
14         Params:
15             dev = modbus device address (number)
16             fnc = function number
17             bytes = expected response data length in bytes
18                     if < 0 any bytes count can be received
19         Returns:
20             result in big endian
21      +/
22     const(void)[] read(ulong dev, ubyte fnc, ptrdiff_t bytes)
23     {
24         import std.datetime.stopwatch : StopWatch, AutoStart;
25         import std.algorithm : max;
26 
27         size_t minRead = be.aduLength(1); // one byte data in error messages
28         size_t mustRead;
29 
30         mustRead = bytes >= 0 ? be.aduLength(bytes) : buffer.length;
31 
32         Message msg;
33 
34         const tm = con.readTimeout; // save timeout for restoring
35         const cw = StopWatch(AutoStart.yes);
36 
37         con.read(buffer[0..minRead]);
38 
39         // next read must have less time
40         con.readTimeout = max(Duration.zero, tm - cw.peek);
41         scope (exit) con.readTimeout = tm; // restore origin timeout
42 
43         if (be.ParseResult.success != be.parseMessage(buffer[0..minRead], msg))
44         {
45             if (minRead == mustRead)
46                 throwCheckFailException(dev, fnc);
47 
48             con.read(buffer[minRead..mustRead], bytes < 0 ?
49                                    con.CanRead.anyNonZero : con.CanRead.allOrNothing);
50             if (be.ParseResult.success != be.parseMessage(buffer[0..mustRead], msg))
51                 throwCheckFailException(dev, fnc);
52         } // else it's error message
53 
54         version (modbus_verbose)
55             if (msg.dev != dev)
56                 .warningf("receive from unexpected device "~
57                             "%d (expect %d)", msg.dev, dev);
58         
59         if (msg.fnc != fnc)
60             throwFunctionErrorException(dev, fnc,
61                 cast(FunctionErrorCode)((cast(ubyte[])msg.data)[0]));
62 
63         if (bytes > 0 && msg.data.length != bytes)
64             throwReadDataLengthException(dev, fnc, bytes, msg.data.length);
65 
66         return msg.data;
67     }
68 
69     auto requestReadTimeInterval = 20.msecs;
70     void delegate(void[] buf, size_t writed) requestPreReadHook;
71 
72     /++ Write and read to modbus
73 
74         Params:
75             dev = slave device number
76             fnc = called function number
77             bytes = expected response data bytes
78             args = sending data
79         Returns:
80             result in big endian
81      +/
82     const(void)[] request(Args...)(ulong dev, ubyte fnc,
83                                    ptrdiff_t bytes, Args args)
84     {
85         import modbus.msleep;
86 
87         auto tmp = write(dev, fnc, args);
88 
89         msleep(requestReadTimeInterval);
90 
91         if (requestPreReadHook !is null)
92             requestPreReadHook(buffer[], tmp.length);
93 
94         try return read(dev, fnc, bytes);
95         catch (ModbusDevException e)
96         {
97             e.writed = tmp[];
98             throw e;
99         }
100     }
101 
102     /// 01 (0x01) Read Coils
103     const(BitArray) readCoils(ulong dev, ushort start, ushort cnt)
104     {
105         if (cnt >= 2000) throwModbusException("very big count");
106         return const(BitArray)(cast(void[])request(
107                 dev, FunctionCode.readCoils,
108                 1+(cnt+7)/8, start, cnt)[1..$],
109                 cnt);
110     }
111 
112     /// 02 (0x02) Read Discrete Inputs
113     const(BitArray) readDiscreteInputs(ulong dev, ushort start, ushort cnt)
114     {
115         if (cnt >= 2000) throwModbusException("very big count");
116         return const(BitArray)(cast(void[])request(
117                 dev, FunctionCode.readDiscreteInputs,
118                 1+(cnt+7)/8, start, cnt)[1..$],
119                 cnt);
120     }
121 
122     private alias be2na = bigEndianToNativeArr;
123 
124     /++ 03 (0x03) Read Holding Registers
125         Returns: data in native endian
126      +/ 
127     const(ushort)[] readHoldingRegisters(ulong dev, ushort start, ushort cnt)
128     {
129         if (cnt >= 125) throwModbusException("very big count");
130         return be2na(cast(ushort[])request(
131                 dev, FunctionCode.readHoldingRegisters, 1+cnt*2, start, cnt)[1..$]);
132     }
133 
134     /++ 04 (0x04) Read Input Registers
135         Returns: data in native endian
136      +/ 
137     const(ushort)[] readInputRegisters(ulong dev, ushort start, ushort cnt)
138     {
139         if (cnt >= 125) throwModbusException("very big count");
140         return be2na(cast(ushort[])request(dev, FunctionCode.readInputRegisters,
141                                             1+cnt*2, start, cnt)[1..$]);
142     }
143 
144     /// 05 (0x05) Write Single Coil
145     void writeSingleCoil(ulong dev, ushort addr, bool val)
146     {
147         request(dev, FunctionCode.writeSingleCoil, 4, addr,
148                 cast(ushort)(val ? 0xff00 : 0x0000));
149     }
150     /// 06 (0x06) Write Single Register
151     void writeSingleRegister(ulong dev, ushort addr, ushort value)
152     { request(dev, FunctionCode.writeSingleRegister, 4, addr, value); }
153 
154     /// 15 (0x0F) Write Multiple Coils
155     void writeMultipleCoils(ulong dev, ushort addr, BitArray arr)
156     {
157         auto c = arr.length;
158         writeMultipleCoils(dev, addr, cast(ushort)arr.length, (cast(void[])arr)[0..(c+7)/8]);
159     }
160 
161     /// ditto
162     void writeMultipleCoils(ulong dev, ushort addr, ushort cnt, const(void)[] arr)
163     {
164         if (cnt >= 2000) throwModbusException("very big count");
165         if ((cnt+7)/8 != arr.length) throwModbusException("count mismatch");
166         request(dev, FunctionCode.writeMultipleCoils, 4, addr, cnt, arr[0..(cnt+7)/8]);
167     }
168 
169     /// 16 (0x10) Write Multiple Registers
170     void writeMultipleRegisters(ulong dev, ushort addr, const(ushort)[] values)
171     {
172         if (values.length >= 125) throwModbusException("very big count");
173         request(dev, FunctionCode.writeMultipleRegisters, 4, addr,
174                 cast(ushort)values.length, cast(byte)(values.length*2), values);
175     }
176 }
177 
178 unittest
179 {
180     static import std.bitmanip;
181     alias bwrite = std.bitmanip.write;
182     alias bread = std.bitmanip.read;
183 
184     static class ModbusEmulator
185     {
186         align(1)
187         static struct DeviceData
188         {
189             align(1):
190             ushort[4] simpleRegister; // 0..3
191             int intValue; // 4
192             float floatValue; // 6
193         }
194 
195         SpecRules sr;
196         DeviceData[ulong] regs;
197         ubyte[256] res;
198         size_t idx;
199 
200         this(SpecRules sr)
201         {
202             regs[70] = DeviceData([1234, 10405, 12, 42], 3^^12, 3.14);
203             regs[1] = DeviceData([2345, 50080, 34, 42], 7^^9, 2.71);
204             this.sr = sr;
205         }
206 
207         size_t write(const(void)[] msg)
208         {
209             idx = 0;
210             auto ubmsg = cast(const(ubyte)[])msg;
211             ulong dev;
212             ubyte fnc;
213             sr.unpackDF(ubmsg, dev, fnc);
214             ubmsg = ubmsg[sr.deviceTypeSize+1..$];
215 
216             if (dev !in regs) return msg.length;
217 
218             res[idx..idx+sr.deviceTypeSize] = cast(ubyte[])sr.packDF(dev, fnc)[0..sr.deviceTypeSize];
219             idx += sr.deviceTypeSize;
220 
221             import std.stdio;
222             if (!checkCRC(msg))
223                 storeFail(fnc, FunctionErrorCode.illegalDataValue);
224             else
225             {
226                 bwrite(res[], fnc, &idx);
227 
228                 switch (fnc)
229                 {
230                     case 4:
231                         auto d = (cast(ushort*)(dev in regs))[0..DeviceData.sizeof/2];
232                         auto st = bread!ushort(ubmsg);
233                         auto cnt = cast(ubyte)bread!ushort(ubmsg);
234                         bwrite(res[], cnt, &idx);
235                         foreach (i; 0 .. cnt)
236                             bwrite(res[], d[st+i], &idx);
237                         break;
238                     default:
239                         storeFail(fnc, FunctionErrorCode.illegalDataValue);
240                         break;
241                 }
242             }
243             storeCRC();
244             readResult = res[0..idx];
245             return msg.length;
246         }
247 
248         ubyte[] readResult;
249         void[] read(void[] buffer)
250         {
251             import std.range;
252             auto ubbuf = cast(ubyte[])buffer;
253             foreach (i; 0 .. buffer.length)
254             {
255                 if (readResult.empty)
256                     return buffer[0..i];
257                 ubbuf[i] = readResult.front;
258                 readResult.popFront();
259             }
260             return buffer;
261         }
262 
263         void storeFail(ubyte fnc, FunctionErrorCode c)
264         {
265             bwrite(res[], cast(ubyte)(fnc|0x80), &idx);
266             bwrite(res[], cast(ubyte)c, &idx);
267         }
268 
269         void storeCRC()
270         {
271             auto crc = crc16(res[0..idx]);
272             bwrite(res[], crc[0], &idx);
273             bwrite(res[], crc[1], &idx);
274         }
275     }
276 
277     BasicSpecRules sr = new BasicSpecRules;
278 
279     auto com = new ModbusEmulator(sr);
280 
281     auto mbus = new ModbusMaster(new RTU(sr),
282     new class Connection {
283         override:
284             Duration readTimeout() @property { return Duration.zero; }
285             Duration writeTimeout() @property { return Duration.zero; }
286             void readTimeout(Duration) {}
287             void writeTimeout(Duration) {}
288             void write(const(void)[] msg) { com.write(msg); }
289             void[] read(void[] buffer, CanRead cr=CanRead.allOrNothing)
290             { return com.read(buffer); }
291             void reconnect() {}
292         }
293     );
294 
295     assert(mbus.readInputRegisters(70, 0, 1)[0] == 1234);
296     import std.algorithm : equal;
297     assert(equal(mbus.readInputRegisters(1, 0, 4), [2345, 50080, 34, 42]));
298 }