1 module puppeteer.puppet_link.puppet_link;
2 
3 import std.file;
4 import std.exception;
5 import std.format;
6 import std.conv;
7 import std.stdio;
8 import std.typecons;
9 import std.meta;
10 
11 import core.thread;
12 
13 import puppeteer.serial.i_serial_port;
14 import puppeteer.serial.ox_serial_port_wrapper;
15 
16 import puppeteer.var_monitor_utils;
17 
18 import puppeteer.puppet_link.is_puppet_link;
19 
20 public class PuppetLink(IVTypes...)
21 {
22     static assert(isPuppetLink!(typeof(this), IVTypes));
23 
24     private ISerialPort serialPort;
25 
26     private OnAIUpdateCallback onAIUpdateCallback;
27 
28     @property
29     AIMonitorCallback(OnAIUpdateCallback callback)
30     {
31         onAIUpdateCallback = callback;
32     }
33 
34     private Tuple!(staticMap!(OnIVUpdateCallback, IVTypes)) onIVUpdateCallbacks;
35     private alias onIVUpdateCallback(IVType) = Alias!(onIVUpdateCallbacks[staticIndexOf!(IVType, IVTypes)]);
36 
37     @property
38     IVMonitorCallback(IVType)(OnIVUpdateCallback!IVType callback)
39     {
40         onIVUpdateCallbacks[staticIndexOf!(IVType, IVTypes)] = callback;
41     }
42 
43     private enum ubyte commandControlByte = 0xff;
44 
45     @property
46     bool isCommunicationOpen()
47     {
48         return serialPort.isOpen;
49     }
50 
51     this(string devFileName)
52     in
53     {
54         assert(devFileName !is null);
55     }
56     body
57     {
58         enforce(exists(devFileName));
59         enforce(isFile(devFileName));
60 
61         enum portReadTimeoutMs = 200;
62         serialPort = new OxSerialPortWrapper(devFileName, Parity.none, BaudRate.B9600, portReadTimeoutMs);
63     }
64 
65     bool startCommunication()
66     {
67         if(serialPort.open())
68         {
69             //Some puppets seems to need some time between port opening and communication start
70             Thread.sleep(dur!"seconds"(1));
71 
72             enum ubyte[] puppeteerReadyCommand = [0x0, 0x0];
73             serialPort.write([commandControlByte] ~ puppeteerReadyCommand);
74 
75             enum ubyte[] puppetReadyCommand = [0x0, 0x0];
76             ubyte[] readCache = [];
77 
78             enum msBetweenChecks = 100;
79             int readCounter = 0;
80             enum readsUntilFailure = 30;
81 
82             while(readCounter++ < readsUntilFailure)
83             {
84                 ubyte[] readBytes = serialPort.read(1);
85 
86                 if(readBytes !is null)
87                 {
88                     readCache ~= readBytes;
89                     debug writeln("handshake cache is currently ", readCache);
90 
91                     if(readCache.length == 3)
92                     {
93                         if(readCache == [commandControlByte] ~ puppetReadyCommand)
94                             return true; //Ready!
95                         else
96                             readCache = readCache[1..$]; //pop front and continue
97                     }
98                 }
99 
100                 Thread.sleep(dur!"msecs"(msBetweenChecks));
101             }
102         }
103 
104         return false;
105     }
106 
107     void endCommunication()
108     {
109         serialPort.close();
110     }
111 
112     void setAIMonitor(ubyte AIPin, bool monitor)
113     {
114         ubyte opCode = monitor ? 0x01 : 0x02;
115         serialPort.write([commandControlByte, opCode, AIPin]);
116     }
117 
118     void setIVMonitor(VarMonitorTypeCode IVTypeCode, ubyte IVIndex, bool monitor)
119     {
120         ubyte opCode = monitor ? 0x05 : 0x06;
121         serialPort.write([commandControlByte, opCode, IVTypeCode, IVIndex]);
122     }
123 
124     void setPWMOut(ubyte PWMOutPin, ubyte value)
125     {
126         ubyte opCode = 0x04;
127         serialPort.write([commandControlByte, opCode, PWMOutPin, value]);
128     }
129 
130     void readPuppet(long communicationMsTime)
131     {
132         enum bytesReadAtOnce = 1;
133 
134         ubyte[] readBytes = serialPort.read(bytesReadAtOnce);
135 
136         if(readBytes !is null)
137         {
138             debug(3) writeln("Read bytes ", readBytes);
139             handleReadBytes(readBytes, communicationMsTime);
140         }
141     }
142 
143     private void handleReadBytes(ubyte[] readBytes, long communicationTimeMillis)
144     in
145     {
146         assert(readBytes !is null);
147     }
148     body
149     {
150         static ubyte[] readBuffer;
151 
152         void popReadBuffer(size_t elements)
153         {
154             readBuffer = readBuffer[elements .. $];
155         }
156 
157         if(readBytes.length > 0)
158         {
159             readBuffer ~= readBytes;
160 
161             if(readBuffer[0] != commandControlByte)
162             {
163                 debug writeln("Received corrupt command. Discarding first byte and returning");
164                 popReadBuffer(1);
165             }
166             else if (readBuffer.length >= 2)
167             {
168                 with (ReadCommands) switch(readBuffer[1])
169                 {
170                     case analogMonitor:
171                         if(readBuffer.length < 5)
172                             return;
173 
174                         handleAIMonitorCommand(readBuffer[1 .. 5], communicationTimeMillis);
175                         popReadBuffer(5);
176                         break;
177 
178                     case varMonitor:
179                         if(readBuffer.length < 6)
180                             return;
181 
182                         handleVarMonitorCommand(readBuffer[1..6], communicationTimeMillis);
183                         popReadBuffer(6);
184                         break;
185 
186                     case error:
187                         //TODO
188                         break;
189 
190                     default:
191                         writeln("Unhandled ubyte command received: ", readBuffer[0], ". Cleaning command buffer.");
192                         readBuffer = [];
193                 }
194             }
195         }
196     }
197 
198     private void handleAIMonitorCommand(ubyte[] cmd, long communicationMsTime)
199     in
200     {
201         assert(cmd !is null);
202         assert(cmd.length == 4);
203         assert(cmd[0] == ReadCommands.analogMonitor);
204     }
205     body
206     {
207         debug(2) writeln("Handling analogMonitorCommand ", command);
208 
209         if(onAIUpdateCallback is AIMonitorListenerT.init)
210             return;
211 
212         ubyte pin = cmd[1];
213 
214         enum analogReadMax = 1023;
215         enum analogReference = 5;
216         enum possibleValues = 256;
217 
218         ushort encodedValue = cmd[2] * possibleValues + cmd[3];
219         float readValue =  analogReference * to!float(encodedValue) / analogReadMax;
220 
221         onAIUpdateCallback.onAIUpdate(pin, readValue, communicationMsTime);
222     }
223 
224     private void handleVarMonitorCommand(ubyte[] cmd, long communicationMsTime)
225     in
226     {
227         assert(cmd !is null);
228         assert(cmd.length == 5);
229         assert(cmd[0] == ReadCommands.varMonitor);
230     }
231     body
232     {
233         debug(2) writeln("Handling varMonitorCommand ", cmd);
234 
235         void delegate (ubyte, ubyte[], long) selectDelegate(VarMonitorTypeCode typeCode)
236         {
237             string generateSwitch()
238             {
239                 string str = "switch (typeCode) with (VarMonitorTypeCode) {";
240 
241                 foreach(IVType; IVTypes)
242                     str ~= format("case %s: return &handleData!%s;",
243                                     to!string(varMonitorTypeCode!IVType),
244                                     IVType.stringof);
245 
246                 str ~= "default: return null; }" ;
247 
248                 return str;
249             }
250 
251             mixin(generateSwitch());
252         }
253 
254         try
255         {
256             auto handler = selectDelegate(to!VarMonitorTypeCode(cmd[1]));
257             if(handler)
258                 handler(cmd[2], cmd[3..$], communicationMsTime);
259             else
260             {
261                 writefln("Received unhandled VarMonitorTypeCode %s", cmd[1]);
262             }
263         }
264         catch(ConvException e)
265         {
266             writeln("Received invalid varMonitor type: ",e);
267         }
268     }
269 
270     private void handleData(IVType)(ubyte varIndex, ubyte[] data, long communicationMsTime)
271     {
272         IVType decodeData(IVType : short)(ubyte[] data) pure
273         {
274             enum ubytePossibleValues = 256;
275             return to!short(data[0] * ubytePossibleValues + data[1]);
276         }
277 
278         auto decodedData = decodeData!IVType(data);
279 
280         _IVMonitorListener.onIVUpdate(varIndex, decodedData, communicationMsTime);
281     }
282 
283     private enum ReadCommands : ubyte
284     {
285         analogMonitor = 0x1,
286         varMonitor = 0x2,
287         error = 0xfe
288     }
289 }