Spectroscopy

This chapter is a collection of instruments and analysis techniques used throughout the thesis. The sample preparation and synthesis were described previously and the diameter reduction with nitrogen poisoning during synthesis is described afterwards. The setup for taking spectra and images is described in more detail after the introduction of these basic methods here. All methods serve the investigations on Surface Adsorption on carbon nanotubes.

Wet Dispersion

If not stated otherwise, aqueous samples were debundled using Deoxycholate (DOC).[101] Typically 50mg of the raw CCVD material was dispersed in 10ml aqueous solution containing 1.5wt.% DOC. Water was purified with a millipore filter. Samples were sonicated for 1h with a Branson Horn Sonicator in a 50% duty cycle and subsequently centrifuged for 3-5min with a Haereus Spatech Biofuge 15 at 14000rpm.

Density Gradient Ultracentrifugation

Density Gradient Ultracentrifugation (DGU) was performed using a linear density gradient [102][103] with a Beckmann Optimatm L-90K ultra centrifuge. Fractioning of the centrifuged material was achieved with a KD Scientific KDS 210 syringe pump.

Absorption Spectroscopy

Absorption spectroscopy was performed on a Varian Cary 5000 UV-Vis-NIR spectrophotometer. The background of the pure solution was measured as a reference before every experiment. The spectra are processed and analyzed with a custom analysis program

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
#pragma rtGlobals=3
#include "utilities-peakfind"

Function AbsorptionLoadFolder()
    String strFolder

    String strFiles, strWave
    String basename, concentration
    Variable i, numFiles, numBegin, numEnd

    String strBackground = "mkbg"

    strFolder = PopUpChooseDirectory("X:Documents:RAW:Absorption")

    // get files in directory
    NewPath/O/Q path, strFolder
    strFiles = IndexedFile(path,-1,".csv")

    // search for mkbg[0-9]{2}
    numBegin = strsearch(strFiles, strBackground, 0, 2)
    numEnd = strsearch(strFiles, ";", numBegin, 2)
    strBackground = strBackground + strFiles[(numBegin + 4), (numEnd -1)]
    print "Background file is " + strBackground

    // load background
    wave background = $AbsorptionLoadFile(strFile = strFolder + strBackground)
    strFiles = RemoveFromList(strBackground, strFiles)

    // loop
    numFiles = ItemsInList(strFiles)
    for(i = 0; i < numFiles; i += 1)
        strWave = AbsorptionLoadFile(strFile = strFolder + StringFromList(i, strFiles))
        wave rootwave = root:$strWave

        // remove background
        rootwave[] -= background[p]

//        // rescale to dilution factor
//        SplitString/E="(.*)[_]{1}([0-9]*)" strWave, basename, concentration
//        if(strlen(basename) * strlen(concentration) > 0)
//            duplicate/o rootwave $basename
//            wave corrected = root:$basename
//            corrected[] = 10^(-corrected[p])
//            corrected /= (str2num(concentration) / 100)
//            corrected[] = -log(corrected[p])
//            killwaves/Z rootwave
//        endif

        print "Loaded " + strWave

    endfor

End

Function/S AbsorptionLoadFile([strFile])
    String strFile

    String strFileName, strFileType, strWave
    String listWaves
    Variable numWaves,i,j, numStart, numEnd
    DFREF dfrSave, dfrTemp, dfr

    if(ParamIsDefault(strFile))
        strFile=PopUpChooseFile(strPrompt="Choose Absorption File")
    endif
    if (strlen(strFile) == 0)
        return ""
    endif

    // load all waves to temp
    dfrSave = GetDataFolderDFR()
    dfrTemp = AbsorptionDFR(subDFR = "temp")
    SetDataFolder dfrTemp
    //Headings are in 2nd(0-1-2-->1) line, data starts in 2nd line, load all (0) from 1 to 2 columns, where d
    LoadWave/A/D/J/K=1/L={1,2,0,0,2}/O/Q strFile
    SetDataFolder dfrSave

    dfr = AbsorptionDFR()
    listWaves = S_waveNames

    numWaves = ItemsInList(listWaves)
    i = 0
    if (numWaves == 2) // make to for loop
        j = 2*i
        // import columns from csv
        wave wavWaveLength    = dfrTemp:$StringFromList(j,S_waveNames)
        wave wavIntensity     = dfrTemp:$StringFromList(j+1,S_waveNames)

        //Delete last point.
        WaveStats/Q/Z/M=1 wavWaveLength
        DeletePoints/M=0 (V_npnts),1, wavWaveLength, wavIntensity

        strFileName = ParseFilePath(3, strFile, ":", 0, 0)
        strFileType = ParseFilePath(4, strFile, ":", 0, 0)

        strWave = CleanupName(strFileName, 0)
        //strWave = UniqueName(strWave, 1, 0)

        SetWaveScale(wavX = wavWaveLength, wavY = wavIntensity)

        Redimension/N=(-1,3) wavIntensity
        wavIntensity[][1] = wavWaveLength[p]
        wavIntensity[][2] = 1240/wavWaveLength[p] //λ (nm) = 1240/E(eV)

        SetDimLabel 1, 0, intensity, wavIntensity
        SetDimLabel 1, 1, wavelength, wavIntensity
        SetDimLabel 1, 2, electronVolt, wavIntensity

        Duplicate/O/R=[0,*][0] wavIntensity root:$strWave
        Redimension/N=(-1, 0) root:$strWave
        Duplicate/O wavIntensity dfr:$strWave

        KillWaves/Z wavWaveLength, wavIntensity
    endif

    return strWave
End

Function/Wave AbsorptionPrompt()
    string redirectMe
    string strWave = "absorption"

    DFREF dfr = root:Packages:Absorption:

    Prompt strWave, "Wave for Peak-Analysis",popup TraceNameList("", ";",1)    //top graph "", seperate by ";", option 1: Include contour traces
    DoPrompt "Enter wave", strWave

    //$strIntensity is not possible for renamed traces: tracename#1 tracename#2 (see Instance Notation)
    wave wavInput = TraceNameToWaveRef("",strWave)

    redirectMe = NameOfWave(wavInput)
    if(WaveExists(dfr:$redirectMe))
    	WAVE wavInput = dfr:$redirectMe
    endif

    redirectMe = NameOfWave(wavInput) + "j"
    if(WaveExists(dfr:$redirectMe))
    	WAVE wavInput = dfr:$redirectMe
    endif

    print "input taken from", GetWavesDataFolder(wavInput, 2)
    return wavInput
End

Function AbsorptionBgLinear()
    AbsorptionBgLinearWave(AbsorptionPrompt())
End

// no error checking here.
// create linar background correction between pcsrA and pcsrB
Function AbsorptionBgLinearWave(wavInput)
    wave wavInput

    SetDataFolder root:
    make/o/n=2 line
    line={wavInput[pcsr(A)],wavInput[pcsr(B)]}
    SetScale x, x2pnt(wavInput,pcsr(A)), x2pnt(wavInput, pcsr(B)), line
    string strNewWave = nameofwave(wavInput) + "bgcorr"
    duplicate/O wavInput $strNewWave
    wave wavOutput = $strNewWave
    interpolate2/T=1/I=3/Y=wavOutput line
    wavOutput = wavInput-wavOutput
    Killwaves/Z line
End

Function/Wave AbsorptionDifferentiateWave(wavInput, numSmooth, type)
    Wave wavInput
    Variable numSmooth, type

    DFREF dfr = AbsorptionDFR(subDFR = NameOfWave(wavInput))

    wave temp = Utilities#DifferentiateWave(wavInput, numSmooth, type)
    Duplicate/O temp dfr:$(NameOfWave(wavInput) + "d")/WAVE=wavOutput

    return wavOutput
End

Function/Wave AbsorptionDifferentiate2Wave(wavInput, numSmooth)
    Wave wavInput
    Variable numSmooth

    // smooth and build second derivative
    Wave wavFirst  = AbsorptionDifferentiateWave(wavInput, numSmooth, 0)
    Wave wavSecond = AbsorptionDifferentiateWave(wavFirst, numSmooth, 0)

    return wavSecond
End

Function/Wave AbsorptionRemoveJump(wavInput)
    wave wavInput

    Variable gap, gapLeft, gapRight
    Variable i, count
    String newName

    // create wave for jump correction
    DFREF dfr = GetWavesDataFolderDFR(wavInput)
    newName = NameOfWave(wavInput) + "j"
    Duplicate/O wavInput, dfr:$newName
    Wave wavJump = dfr:$newName
    wavJump = 0

    // get gap
    Wave wavGaps = AbsorptionSearchGap(wavInput, 700, 900, 1.95)
    count = Dimsize(wavGaps, 0)
    print num2str(count) + " gaps found."

    // if no gap was found return original wave
    if (count == 0)
        print "AbsorptionRemoveJump: No gap found"
        wavJump = wavInput
        return wavJump
    endif

    // currently only for one gap possible. 1 gap --> two values are found at start of Gap and end of Gap
    if (count > 2)
        print "AbsorptionRemoveJump: Too many gaps in defined range"
        return wavJump
    endif

    // estimate values if no gap was present by differentiation
    Wave wavDifferentiated = AbsorptionDifferentiateWave(wavInput, 0, 2) // type = Backward difference
    // estimated = f(x0) + f'(x0) * delta x
    // delta x = x_start - x_end
    // gap = estimated - real

    gap = wavInput[wavGaps[1]]    - wavInput[wavGaps[0]] + wavDifferentiated[wavGaps[0]] * DimDelta(wavInput, 0) * (wavGaps[1] - wavGaps[0])
    wavJump[0,wavGaps[0]] = gap

    wavJump = wavInput + wavJump

    return wavJump
End

Function/Wave AbsorptionSearchGap(wavInput, wlmin, wlmax, tolerance)
    Wave wavInput
    Variable wlmin, wlmax, tolerance

    Variable i
    Variable estimated, follower
    Variable found = 0
    Variable Pstart, Pend

    Wave wavDifferentiated = AbsorptionDifferentiateWave(wavInput, 0, 2) // type = Backward difference
    WaveStats/Q/R=(wlmin, wlmax) wavDifferentiated

    Make/I/FREE/N=0 wavFound

    tolerance = V_rms + tolerance * V_sdev

    // assure ascending p points
    Pstart = x2pnt(wavInput,wlmin)
    Pend = x2pnt(wavInput,wlmax)
    If (Pstart > Pend)
        Pend = Pstart
        Pstart = x2pnt(wavInput,wlmax)
    endif

    // search for gap in defined rannge. Maybe clean returned gaps.
    for (i = Pstart; i < Pend; i += 1)
        estimated = wavInput[i] + wavDifferentiated[i] * DimDelta(wavInput, 0)
        follower = wavInput[i+1]
        if (abs(estimated - follower) > tolerance)
            FindValue/I=(i) wavFound
            if (V_value == -1)
                found += 1
                Redimension/N=(found) wavFound
                wavFound[(found-1)] = i
            endif
        endif
    endfor

    return wavFound
End

// see AutomaticallyFindPeaks from WM's <Peak AutoFind>
Function/Wave AbsorptionPeakFind(wavInput)
    Wave wavInput

    WAVE wavOutput = Utilities#PeakFind(wavInput)

    DFREF dfr = AbsorptionDFR(subDFR = NameOfWave(wavInput))
    KillWaves/Z dfr:$(NameOfWave(wavInput) + "peaks")
    MoveWave wavOutput dfr:$(NameOfWave(wavInput) + "peaks")

    return wavOutput
End

Function/Wave AbsorptionBackgroundConstruct(wavInput, [debugging, doubleExp])
    Wave wavInput
    Variable debugging, doubleExp
    if (ParamIsDefault(debugging))
        debugging = 0
    endif
    if (ParamIsDefault(doubleExp))
        doubleExp = 1
    endif

    Variable i, j, startx, endx
    Variable numMaxima, numMinima

    String newName
    DFREF dfr = GetWavesDataFolderDFR(wavInput)
    Wave/Z wavOutput, wavDifferentiation, wavSmooth

    newName = NameOfWave(wavInput) + "_bg"
    Duplicate/O wavInput, dfr:$newName
    Wave wavOutput = dfr:$newName

    if (Dimsize(wavInput, 1) == 3)
        Duplicate/FREE/R=[][0] wavInput wavIntensity
        Redimension/N=(-1,0) wavInput
    else
        Duplicate/FREE wavInput wavIntensity
    endif

    wave/wave minima = AbsorptionDeleteDoubleMinima(wavIntensity, debugging = debugging)
    wave/wave smoothed = AbsorptionDeleteSmooth(minima, smoothing = 3, logarithmic = 1, debugging = debugging)

    wave fit_coeff = AbsorptionBackgroundFit(smoothed, expDouble=doubleExp)
    if (!doubleExp)
        if (Dimsize(wavOutput, 1) == 3)
            wavOutput[][%intensity] = AbsorptionBackgroundExp(fit_coeff, wavOutput[p][%wavelength])
        else
            wavOutput = AbsorptionBackgroundExp(fit_coeff, x)
        endif
        print fit_coeff
        print "y = K0+K1*exp(-(x-K5)/K2)"
    else
        if (Dimsize(wavOutput, 1) == 3)
            wavOutput[][%intensity] = AbsorptionBackgroundExpDouble(fit_coeff, wavOutput[p][%wavelength])
        else
            wavOutput = AbsorptionBackgroundExpDouble(fit_coeff, x)
        endif
        print fit_coeff
        print "y = K0+K1*exp(-(x-K5)/K2)+K3*exp(-(x-K5)/K4)"
    endif

    if (Dimsize(wavInput, 1) == 3)
        Duplicate/FREE/R=[][0] wavInput wavIntensity
        Redimension/N=(-1,0) wavInput
    else
        Duplicate/FREE wavInput wavIntensity
    endif

    return wavOutput
End

Function/Wave AbsorptionBackgroundRemove(wavInput, [doubleExp])
    Wave wavInput
    Variable doubleExp
    if (ParamIsDefault(doubleExp))
        doubleExp = 1
    endif

    Variable i, j, startx, endx
    Variable numMaxima, numMinima

    String newName
    DFREF dfr = GetWavesDataFolderDFR(wavInput)
    Wave/Z wavOutput, wavDifferentiation, wavSmooth

    newName = NameOfWave(wavInput) + "_bgcorr"
    Duplicate/O wavInput, dfr:$newName
    Wave wavOutput = dfr:$newName

    wave wavBackground = AbsorptionBackgroundConstruct(wavInput)
    if (Dimsize(wavBackground, 1) == 3)
        wavOutput[][%intensity] = wavInput[p][%intensity] - wavBackground[p][%intensity]
    else
        wavOutput = wavInput - wavBackground
    endif
    return wavOutput
End

// function is used to search for points in "minima" between two points from "maxima" wave
// returns p-index and positions in "minima" wave
// as input the x values are used
Function/Wave AbsorptionDoubleMinima(maxima, minima)
    Wave maxima, minima

    Variable startx, endx
    variable i,j
    Variable numMaxima, numMinima

    numMaxima = Dimsize(maxima, 0)
    numMinima = Dimsize(minima, 0)

    // search for number of minima between two points.
    // plot numMinima[][%delta] vs freeMaximaX for bugtracking
    Make/FREE/I/N=(numMaxima, 3) output
    // label columns of wave for readability
    SetDimLabel 1, 0, startX, output
    SetDimLabel 1, 1, endX, output
    SetDimLabel 1, 2, delta, output

    startx = 0
    endx = 0
    for(i = 0; i < (numMaxima - 1); i += 1)
        // searching "minima" between following maxima:
        // freeMaximaX[i], freeMaximaX[i+1]

        startx = endx
        for (j = startx; j < numMinima; j += 1)
            if (minima[j] > maxima[i])
                break
            endif
        endfor
        startx = j

        for (j = startx; j < numMinima; j += 1)
            if (minima[j] > maxima[i+1])
                break
            endif
        endfor
        endx = j

        output[i][%startX] = startx
        output[i][%endX] = endx
        output[i][%delta] = endx - startx
    endfor

    return output
End


Function/Wave AbsorptionGetMinima(wavInput, [addBorders])
    wave wavInput
    Variable addBorders

    if (ParamIsDefault(addBorders))
        addBorders = 0
    endif

    Variable numMinima

    wave wavDifferentiation = Utilities#Differentiate2Wave(wavInput, 10)
    wave wavMinima = AbsorptionPeakFind(wavDifferentiation)

    numMinima = Dimsize(wavMinima, 0)
    if (addBorders)
        numMinima += 2
        Wavestats/Q wavInput
        Make/FREE/N=(numMinima) freeMinimaX = wavMinima[((p-2) < 0 ? 0 : (p-2))][%wavelength]
        freeMinimaX[0,1] = {V_minloc, V_maxloc}
        Make/FREE/N=(numMinima) freeMinimaY = wavInput[wavMinima[((p-2) < 0 ? 0 : (p-2))][%positionX]]
        freeMinimaY[0,1] = {V_min, V_max}
    else
        Make/FREE/N=(numMinima) freeMinimaX = wavMinima[p][%wavelength]
        Make/FREE/N=(numMinima) freeMinimaY = wavInput[wavMinima[p][%positionX]]
    endif
    Sort freeMinimaX, freeMinimaX, freeMinimaY

    Make/Wave/FREE/N=2 output
    output[0] = freeMinimaX
    output[1] = freeMinimaY

    return output
End

Function/Wave AbsorptionGetMaxima(wavInput)
    wave wavInput

    Variable numMaxima

    wave wavMaxima = AbsorptionPeakFind(wavInput)

    numMaxima = Dimsize(wavMaxima, 0)
    Make/FREE/N=(numMaxima) freeMaximaX = wavMaxima[p][%wavelength]
    Make/FREE/N=(numMaxima) freeMaximaY = wavInput[wavMaxima[p][%positionX]]
    Sort freeMaximaX, freeMaximaX, freeMaximaY

    Make/Wave/FREE/N=2 output
    output[0] = freeMaximaX
    output[1] = freeMaximaY

    return output
End

// Delete minimum if two or more minima exist between one maximum in a wave
Function/Wave AbsorptionDeleteDoubleMinima(wavInput, [minima, maxima, debugging])
    wave wavInput
    wave/wave minima, maxima
    Variable debugging
    if (ParamIsDefault(debugging))
        debugging = 0
    endif
    if (ParamIsDefault(minima))
        wave/wave minima = AbsorptionGetMinima(wavInput, addBorders = 1)
    endif
    if (ParamIsDefault(maxima))
        wave/wave maxima = AbsorptionGetMaxima(wavInput)
    endif

    Variable numMinima, numMaxima, i

    Wave minimaX = minima[0]
    wave minimaY = minima[1]
    wave maximaX = maxima[0]
    Wave maximaY = maxima[1]
    Wave minimaPositions = AbsorptionDoubleMinima(maximaX, minimaX)

    // create BoxWave with number of minima
    if (debugging)
        Make/O/I/N=(Dimsize(minimaPositions, 0), 2) root:absDoubleMinima
        WAVE/I absDoubleMinima
        absDoubleMinima[][0] = maximaX[p]
        absDoubleMinima[][1] = minimaPositions[p][%delta]
    endif

    // Create Minima Wave
    if (debugging)
        Make/O/N=(Dimsize(minimaPositions, 0), 2) root:absMinimaInitial
        WAVE absMinimaInitial
        absMinimaInitial[][0] = minimaX[p]
        absMinimaInitial[][1] = minimaY[p]
    endif


    numMinima = Dimsize(minimaX, 0)
    numMaxima = Dimsize(maxima[0], 0)

    // process points to yield only one point (the local minimum) between two maxima
    Make/FREE/N=(numMinima) minimaXout = minimaX
    Make/FREE/N=(numMinima) minimaYout = minimaY
    for(i = numMaxima - 2; i > -1; i -= 1) // backwards due to DeletePoints
        if (minimaPositions[i][%delta] > 1)
            // get minimum
            Wavestats/Q/R=(minimaX[minimaPositions[i][%startX]], minimaX[minimaPositions[i][%endX]]) wavInput
            // set minimum
            minimaXout[minimaPositions[i][%startX]] = V_minloc
            minimaYout[minimaPositions[i][%startX]] = V_min
            // delete remaining points
            DeletePoints (minimaPositions[i][%startX] + 1), (minimaPositions[i][%endX] - minimaPositions[i][%startX] - 1), minimaYout, minimaXout
        endif
    endfor

    // Create Minima Wave
    if (debugging)
        Make/O/N=(Dimsize(minimaPositions, 0), 2) root:absMinimaDouble
        WAVE absMinimaDouble
        absMinimaDouble[][0] = minimaXout[p]
        absMinimaDouble[][1] = minimaYout[p]
    endif

    Make/Wave/FREE/N=2 output
    output[0] = minimaXout
    output[1] = minimaYout

    return output
End

// delete points that are not smooth enough
Function/wave AbsorptionDeleteSmooth(minima, [smoothing, logarithmic, interpolate, debugging])
    wave/wave minima
    Variable smoothing, logarithmic, interpolate, debugging

    if (ParamIsDefault(debugging))
        debugging = 0
    endif
    if (ParamIsDefault(smoothing))
        smoothing = 0
    endif
    if (ParamIsDefault(logarithmic))
        logarithmic = 0
    endif
    if (ParamIsDefault(interpolate))
        interpolate = 0
    endif

    Variable numMinima

    wave minimaX = minima[0]
    wave minimaY = minima[1]

    numMinima = Dimsize(minimaY, 0)

    Make/FREE/N=(numMinima) afterSmoothY    = minimaY
    Make/FREE/N=(numMinima) smoothed            = minimaY
    Make/FREE/N=(numMinima) valid = 1

    if (logarithmic)
        smoothed = ln(smoothed)
    endif

    switch(smoothing)
        case 0:
            // normal boxcar smoothing
            Smooth/E=3 1, smoothed
            break
        case 1:
            // Savitzky-Golay smoothing
            Smooth/E=3/S=4 9, smoothed
            break
        case 2:
            Smooth/E=3 1, smoothed
            break
        case 3:
            Loess/Z/SMTH=0.75 srcWave=smoothed
            if (!V_flag)
                Smooth/E=3 1, smoothed
            endif
            break
    endswitch

    if (logarithmic)
        smoothed = exp(smoothed)
    endif

    if (interpolate)
        interpolate2/T=1/I=3/Y=smoothed minimaX, minimaY
    endif

    // check if value changed too much and drop particulary those values
    afterSmoothY = minimaY - smoothed
    Wavestats/Q afterSmoothY
    valid = afterSmoothY[p] > V_rms  ? NaN : 1 // delete only positive deviation (peaks)
    afterSmoothY = minimaY * valid

    Make/Wave/FREE/N=2 output
    output[0] = minimaX
    output[1] = afterSmoothY

    if (debugging)
        Make/O/N=(numMinima, 2) root:absSmooth
        Wave absSmooth = root:absSmooth
        absSmooth[][0] = minimaX[p]
        absSmooth[][1] = smoothed[p]

        Make/O/N=(numMinima, 2) root:absSmoothValid
        Wave absSmoothValid = root:absSmoothValid
        absSmoothValid[][0] = minimaX[p]
        absSmoothValid[][1] = valid[p]
    endif

    return output
End

Function/wave AbsorptionBackgroundFit(wavInput, [expDouble])
    wave/wave wavInput
    Variable expDouble
    if (ParamIsDefault(expDouble))
        expDouble = 0
    endif

    wave smoothedX = wavInput[0]
    wave smoothedY = wavInput[1]

    // no need to guess parameters with curvefit
    if (!expDouble)
        // exponential fit with curve fit.
        CurveFit/Q/X=1/NTHR=0 exp_XOffset smoothedY /X=smoothedX /F={0.95, 4}
        Wave W_coef
        Wave W_fitConstants

        // store results for custom function
        Make/FREE/N=4 coeff
        coeff[0,2] = W_coef
        coeff[3] = W_fitConstants[0]

        // fit again to custom function
        Wavestats/Q smoothedY
        // initial guesses
        // Make/FREE/N=4 coeff = {V_min, V_max-V_min, smoothedX[V_maxloc], 3/abs(smoothedX[V_minloc] - smoothedX[V_maxloc])}
        Make/FREE/T/N=1 T_Constraints  = {"K0 < " + num2str(V_min)}
        FuncFit/Q/X=1/NTHR=0 AbsorptionBackgroundExp coeff smoothedY /X=smoothedX /F={0.95, 4} /C=T_Constraints
        // y = K0+K1*exp(-(x-K5)/K2).
    else
        // double exponential fit with curve fit.
        CurveFit/Q/X=1/NTHR=0 dblexp_XOffset smoothedY /X=smoothedX /F={0.95, 4}
        Wave W_coef
        Wave W_fitConstants

        // store results for custom function
        Make/FREE/N=6 coeff
        coeff[0,4] = W_coef
        coeff[5] = W_fitConstants[0]

        // fit again to custom function
        Wavestats/Q smoothedY
        Make/FREE/T/N=3 T_Constraints  = {"K0 < " + num2str(V_min)}
        FuncFit/Q/X=1/NTHR=0 AbsorptionBackgroundExpDouble coeff smoothedY /X=smoothedX /F={0.95, 4} /C=T_Constraints
        // y = K0+K1*exp(-(x-K5)/K2)+K3*exp(-(x-x0)/K4).
    endif

    return coeff
End

Function AbsorptionBackgroundExp(w,x) : FitFunc
    Wave w
    Variable x

    //CurveFitDialog/ Equation:
    //CurveFitDialog/ f(x) = a + b*exp(-1/c*(x-d))
    //CurveFitDialog/ End of Equation
    //CurveFitDialog/ Independent Variables 1
    //CurveFitDialog/ x
    //CurveFitDialog/ Coefficients 4
    //CurveFitDialog/ w[0] = a
    //CurveFitDialog/ w[1] = b
    //CurveFitDialog/ w[2] = c
    //CurveFitDialog/ w[3] = d

    return w[0] + w[1]*exp(-(x-w[3])/w[2])
End

Function AbsorptionBackgroundExpDouble(w,x) : FitFunc
    Wave w
    Variable x

    //CurveFitDialog/ Equation:
    //CurveFitDialog/ f(x) = a + b*exp(-1/c*(x-f)) + d*exp(-1/e*(x-f))
    //CurveFitDialog/ End of Equation
    //CurveFitDialog/ Independent Variables 1
    //CurveFitDialog/ x
    //CurveFitDialog/ Coefficients 7
    //CurveFitDialog/ w[0] = a
    //CurveFitDialog/ w[1] = b
    //CurveFitDialog/ w[2] = c
    //CurveFitDialog/ w[3] = d
    //CurveFitDialog/ w[4] = e
    //CurveFitDialog/ w[5] = f

    return w[0]+w[1]*exp(-(x-w[5])/w[2])+w[3]*exp(-(x-w[5])/w[4])
End

Function AbsorptionChiralityLoad([type])
    String type
    If (ParamIsDefault(type))
        type = "sds"
    endif
    String strPath, strFile
    String listFiles, listWaves, listFullPath
    Variable numFiles, i
    DFREF dfrSave, dfrChirality

    dfrSave = GetDataFolderDFR()
    dfrChirality = AbsorptionChiralityDFR(type=type)

    // build path to files
    strPath = SpecialDirPath("Igor Pro User Files", 0, 0, 0 ) + "User Procedures:chirality:"
    strswitch(type)
        case "free":

            break
        case "sds":
        default:
            strPath += "SDS:"
    endswitch

    // get all fileNames from path
    GetFileFolderInfo/Q/Z=1 strPath
    if (V_flag)
        print "AbsorptionLoadChirality: Path not found " + strPath
    endif
    NewPath/O/Q path, strPath
    listFiles = IndexedFile(path,-1,".ibw")

    // load files in listFiles to waves in listWaves
    numFiles = ItemsInList(listFiles)
    listWaves = ""
    for (i = 0; i < numFiles; i += 1)
        strFile = StringFromList(i, listFiles)

        SetDataFolder dfrChirality
        LoadWave/Q/W/A/O/P=path strFile
        SetDataFolder dfrSave

        if (ItemsInList(S_waveNames) > 0)
            listWaves = listWaves + S_waveNames
        endif

    endfor

    return AbsorptionChiralityCheck(listWaves)
End

Function AbsorptionChiralityCheck(listLoaded, [listCheck])
    String listLoaded
    String listCheck
    if (ParamIsDefault(listCheck))
        listCheck = "diameter;lambda11;lambda22;nmindex;"
    endif

    String strCheck
    Variable numWaves, i, found

    numWaves = ItemsInList(listCheck)
    found = 0
    for (i = 0; i < numWaves; i += 1)
        strCheck = StringFromList(i, listCheck)
        if (WhichListItem(strCheck, listLoaded) != -1)
            found += 1
        endif
    endfor

    return (found == numWaves)
End
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#pragma TextEncoding = "UTF-8"	// For details execute DisplayHelpTopic "The TextEncoding Pragma"
#pragma rtGlobals=3					// Use modern global access method and strict wave access.

Function/DF AbsorptionChiralityDFR([type])
	String type
	If (ParamIsDefault(type))
		type = "sds"
	endif
	DFREF dfrSave, dfr

	dfrSave = GetDataFolderDFR()
	
	dfr = AbsorptionDFR(subDFR = "chirality")
	SetDataFolder dfr
	strswitch(type)
		case "free":
			NewDataFolder/O/S dfr:free
			break
		case "sds":
			NewDataFolder/O/S dfr:sds
	endswitch
	dfr = GetDataFolderDFR()
	
	SetDataFolder dfrSave
	
	return dfr
End

Function/DF AbsorptionDFR([subDFR])
	String subDFR
	if (ParamIsDefault(subDFR))
		subDFR = ""
	Endif
	DFREF dfrSave, dfr

	dfrSave = GetDataFolderDFR()
	
	SetDataFolder root:
	NewDataFolder/O/S root:Packages
	NewDataFolder/O/S root:Packages:Absorption
	if (strlen(subDFR) > 0)
		subDFR = CleanupName(subDFR, 0)
		NewDataFolder/O/S $subDFR
	endif
	dfr = GetDataFolderDFR()
	
	SetDataFolder dfrSave
	
	return dfr
End
	
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
#pragma rtGlobals=3		// Use modern global access method and strict wave access.
#include <Peak AutoFind>

//Absorption-Load
// Version 5: removed "default unit" in SetWaveScale
// Version 6: Added interpolate2 function to SetWaveScale, Added test for not equally spaced waves.
// Version 7: added linear Background removal function
// Version 8: added Differentition
// Version 9: split to different Files
// Version 10: Peak Find
// Version 11: Background Fit to Minima

Menu "AKH"
	Submenu "absorption"
		"Load File", AbsorptionLoadFile()
		"Load Directory", AbsorptionLoadFolder()
		"Differentiate", AbsorptionDifferentiateDisplay()
		"Differentiate2", AbsorptionDifferentiate2Display(0)
		"Differentiate2 offset", AbsorptionDifferentiate2Display(1)
		"Show Peaks", AbsorptionPeakDisplay()
		"Show Background 2exp", AbsorptionBackgroundDisplay()
		"Show Background exp", AbsorptionBackgroundDisplay(doubleExp = 0)
		"Remove Background 2exp", AbsorptionBackgroundDisplay(bgcorr = 1)
		"Remove Background exp", AbsorptionBackgroundDisplay(bgcorr = 1, doubleExp = 0)
		"Remove Jump at 800nm", AbsorptionRemoveJumpDisplay()
		"Linear Background on cursor", AbsorptionBgLinear()
		"Kataura", AbsorptionKatauraDisplay()
		"-"
		"SetWaveScale", SetWaveScale()
		"DisplayWave", DisplayWave()
		"-"
	end
End

Function AbsorptionDifferentiateDisplay()
	wave input = AbsorptionPrompt()
	wave output = AbsorptionDifferentiateWave(input, 10, 0)

	RemoveFromGraph/Z derivative1
	RemoveFromGraph/Z derivative2

	if (Dimsize(output, 1) == 3)
		AppendToGraph/R=axisderivative1 output[][%intensity]/TN=derivative1 vs output[][%wavelength]
	else
		AppendToGraph/R=axisderivative1 output/TN=derivative1
	endif

	SetAxis/A=2 axisderivative1
	ModifyGraph freePos(axisderivative1)=0
	ModifyGraph noLabel(axisderivative1)=1
	ModifyGraph lblPosMode(axisderivative1)=1
	ModifyGraph axisEnab(axisderivative1)={0.5,1}
	ModifyGraph zero(axisderivative1)=1
	Label axisderivative1 "1st derivative"

	ModifyGraph mode(derivative1)=7
	ModifyGraph rgb(derivative1)=(0,0,0)
End

Function AbsorptionDifferentiate2Display(setMinimum)
	Variable setMinimum
	wave output = AbsorptionDifferentiate2Wave(AbsorptionPrompt(), 10)
	if (setMinimum)
		Wavestats/Q output
		output-=V_max
	endif

	RemoveFromGraph/Z derivative1
	RemoveFromGraph/Z derivative2

	if (Dimsize(output, 1) == 3)
		AppendToGraph/R=axisderivative2 output[][%intensity]/TN=derivative2 vs output[][%wavelength]
	else
		AppendToGraph/R=axisderivative2 output/TN=derivative2
	endif

	SetAxis/A=2 axisderivative2
	ModifyGraph freePos(axisderivative2)=0
	ModifyGraph noLabel(axisderivative2)=1
	ModifyGraph axisEnab(axisderivative2)={0.5,1}
	Label axisderivative2 "2nd derivative"

	ModifyGraph mode(derivative2)=7
	ModifyGraph usePlusRGB(derivative2)=0,useNegRGB(derivative2)=1
#if IgorVersion() >= 7
	ModifyGraph negRGB(derivative2)=(65535,54607,32768)
	ModifyGraph plusRGB(derivative2)=(65535,0,0,32768)
#else
	ModifyGraph negRGB(derivative2)=(65535,54607,32768)
	ModifyGraph plusRGB(derivative2)=(65535,0,0)
#endif
	ModifyGraph hbFill=0, hBarNegFill(derivative2)=2
	ModifyGraph useNegPat(derivative2)=1
	ModifyGraph rgb(derivative2)=(0,0,0)

	ModifyGraph grid(axisderivative2)=1,lblPosMode(axisderivative2)=1
	ModifyGraph nticks(axisderivative2)=10
End

Function AbsorptionRemoveJumpDisplay()
	Wave wavInput = AbsorptionPrompt()
	Wave wavOutput = AbsorptionRemoveJump(wavInput)
	AppendToGraph wavOutput
end

// see AutoFindPeaksWorker from WM's <Peak AutoFind>
Function AbsorptionPeakDisplay()
	Wave/Z wavInput, wavOutput

	String tablename, tracename

	Wave wavInput = AbsorptionPrompt()
	Wave wavOutput = AbsorptionPeakFind(wavInput)

	tracename = "peaks_" + NameOfWave(wavInput)
	RemoveFromGraph/Z $tracename

	AppendToGraph wavOutput[][%positionY]/TN=$tracename vs wavOutput[][%wavelength]
	ModifyGraph rgb($tracename)=(0,0,65535)
	ModifyGraph mode($tracename)=3
	ModifyGraph marker($tracename)=19

	// show table for peak wave if not yet present
	tablename = "table_" + NameOfWave(wavOutput)
	DoWindow $tablename
	if (V_flag)
		DoWindow/F $tablename
		CheckDisplayed/W=$tablename wavOutput
		if(!V_Flag)
			AppendToTable wavOutput.ld // .ld: table with column names
		endif
	else
		Edit/N=$tablename wavOutput.ld as "Peaks for " + NameOfWave(wavInput) 
	endif
End

Function AbsorptionBackgroundDisplay([bgcorr, debugging, doubleExp])
	Variable bgcorr, debugging, doubleExp
	String trace_bgcorr, trace_bg
	if (ParamIsDefault(bgcorr))
		bgcorr = 0
	endif
	if (ParamIsDefault(debugging))
		debugging = 0
	endif
	if (ParamIsDefault(doubleExp))
		doubleExp = 1
	endif

	Wave wavInput = AbsorptionPrompt()
	if (bgcorr)
		Wave wavOutput = AbsorptionBackgroundRemove(wavInput, doubleExp = doubleExp)
	else
		Wave wavOutput = AbsorptionBackgroundConstruct(wavInput, debugging = debugging, doubleExp = doubleExp)
	endif

	trace_bgcorr = "corrected_" + NameOfWave(wavInput)
	trace_bg = "background_" + NameOfWave(wavInput)

	RemoveFromGraph/Z $trace_bgcorr
	RemoveFromGraph/Z $trace_bg
	if (bgcorr)
		AppendToGraph wavOutput/TN=$trace_bgcorr
		ModifyGraph zero(left)=1
	else
		AppendToGraph wavOutput/TN=$trace_bg
		ModifyGraph mode($trace_bg)=7,usePlusRGB($trace_bg)=1
#if (IgorVersion() >= 7.0)
		ModifyGraph plusRGB($trace_bg)=(65535,0,0,16384)
#else
		ModifyGraph plusRGB($trace_bg)=(65535,0,0)
#endif
		ModifyGraph hbFill($trace_bg)=2
		ModifyGraph zero(left)=0
	endif

	if (debugging)

	endif
End

Function AbsorptionKatauraDisplay()
	DFREF dfr
	String tableName, katauraWindow, traceAbsorption, oldTraceAbsorption, tracePeaks
	Variable numColumns
	Wave/Z peaks, diameter, lambda11, lambda22, absorption
	Wave/Z/T nmindex

	// SHOW ABSORPTION Spectrum in new Window
	// get wave
	Wave absorption = AbsorptionPrompt()
	numColumns = DimSize(absorption,1)
	katauraWindow = "kataura_" + NameOfWave(absorption)
	traceAbsorption = "absorption_" + NameOfWave(absorption)
	oldTraceAbsorption = ""
	DoWindow $katauraWindow
	if (V_flag)
		// modifiy old window
		DoWindow/F $katauraWindow
		// remember old trace
		CheckDisplayed/W=$katauraWindow absorption
		if(V_Flag)
			oldTraceAbsorption = AbsorptionWaveRefToTraceName(katauraWindow, absorption)
		endif
		// append new
		if(numColumns == 0)
			AppendToGraph/W=$katauraWindow/B=bottom_right/L=wavelength absorption/TN=$traceAbsorption
		elseif(numColumns == 3)
			AppendToGraph/W=$katauraWindow/B=bottom_right/L=wavelength absorption[][%wavelength]/TN=$traceAbsorption vs absorption[][%intensity]
		endif
		// remove old
		RemoveFromGraph/Z/W=$katauraWindow $oldTraceAbsorption
	else
		// create new window
		if(numColumns == 0)
			Display/B=bottom_right/L=wavelength/N=$katauraWindow absorption/TN=$traceAbsorption as "Kataura Plot for " + NameOfWave(absorption) 
		elseif(numColumns == 3)
			Display/B=bottom_right/L=wavelength/N=$katauraWindow absorption[][%wavelength]/TN=$traceAbsorption vs absorption[][%intensity] as "Kataura Plot for " + NameOfWave(absorption)
		endif
	endif
	Label/W=$katauraWindow bottom_right "optical density"

	// SHOW KATAURA
	// get waves
	if (!AbsorptionChiralityLoad(type="sds"))
		return 0
	endif
	dfr = AbsorptionChiralityDFR(type="sds")

	Wave diameter = dfr:diameter
	Wave lambda11 = dfr:lambda11
	Wave lambda22 = dfr:lambda22
	Wave/T nmindex = dfr:nmindex

	// remove old traces
	RemoveFromGraph/W=$katauraWindow/Z kataura1
	RemoveFromGraph/W=$katauraWindow/Z kataura2
	RemoveFromGraph/W=$katauraWindow/Z kataura3
	RemoveFromGraph/W=$katauraWindow/Z kataura4

	// add new traces
	AppendToGraph/W=$katauraWindow/B=bottom_left/L=wavelength lambda11/TN=kataura1 vs diameter
	AppendToGraph/W=$katauraWindow/B=bottom_left/L=wavelength lambda22/TN=kataura2 vs diameter
	AppendToGraph/W=$katauraWindow/B=bottom_left/L=wavelength lambda11/TN=kataura3 vs diameter
	AppendToGraph/W=$katauraWindow/B=bottom_left/L=wavelength lambda22/TN=kataura4 vs diameter
	ModifyGraph rgb(kataura2)=(0,0,0), rgb(kataura4)=(0,0,0)
	ModifyGraph mode(kataura1)=3, mode(kataura2)=3, mode(kataura3)=3, mode(kataura4)=3
	ModifyGraph marker(kataura1)=1,marker(kataura2)=1
	ModifyGraph textMarker(kataura3)={:Packages:Absorption:chirality:sds:nmindex,"default",0,0,5,0.00,10.00}
	ModifyGraph textMarker(kataura4)={:Packages:Absorption:chirality:sds:nmindex,"default",0,0,5,0.00,10.00}

	Label/W=$katauraWindow wavelength "wavelength / nm"
	Label/W=$katauraWindow bottom_left "diameter / nm"

	// show peaks
	tracePeaks = "peaks_" + NameOfWave(absorption)
	Wave peaks =  AbsorptionPeakFind(absorption)
	RemoveFromGraph/Z $tracePeaks
	AppendToGraph/W=$katauraWindow/B=bottom_right/L=wavelength peaks[][%wavelength]/TN=$tracePeaks vs peaks[][%positionY]
	ModifyGraph/W=$katauraWindow rgb($tracePeaks)=(0,0,65535)
	ModifyGraph/W=$katauraWindow mode($tracePeaks)=3
	ModifyGraph/W=$katauraWindow marker($tracePeaks)=19

	// set axis
	SetAxis/A=2
	SetAxis bottom_left 0.65,1.15
	ModifyGraph axisEnab(bottom_left)={0,0.79}, axisEnab(bottom_right)={0.8,1}
	ModifyGraph freePos=0
	ModifyGraph lblPosMode=1

	// set axis grid
	dfr = AbsorptionDFR(subDFR = NameOfWave(absorption))
	Make/O/T/N=(Dimsize(peaks, 0)) 	dfr:peakAxis_label/WAVE=axis_label 	= num2str(round(peaks[p][%wavelength]))
	Make/O/N=(Dimsize(peaks, 0)) 	dfr:peakAxis_tick/WAVE=axis_tick 	= peaks[p][%wavelength]
	ModifyGraph userticks(wavelength)={axis_tick,axis_label}
	ModifyGraph grid=1

	// add legend
	Legend/C/N=text0/J/F=0/A=MC "\\s(kataura1) E11\r\\s(kataura2) E22\r"

	// show table for peak wave if not yet present
	tablename = "table_" + NameOfWave(absorption)
	DoWindow $tablename
	if (V_flag)
		DoWindow/F $tablename
		CheckDisplayed/W=$tablename peaks
		if(!V_Flag)
			AppendToTable peaks.ld // .ld: table with column names
		endif
	else
		Edit/N=$tablename peaks.ld as "Peaks for " + NameOfWave(absorption)
	endif
End

Function DisplayWave()
	wave wavWave = $GetWave(strPrompt="test")
	if (WaveExists(wavWave))
		display wavWave
	endif
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#pragma rtGlobals=3		// Use modern global access method and strict wave access.

//adapted function GetListOfFolderCont() from http://www.entorb.net/wickie/IGOR_Pro
Function /S GetListOfFolderCont(objectType)
	Variable objectType //1==Waves, 2==Vars, 3==Strings, 4==Folders
	//local variables
	String strSourceFolder, strList
	Variable i
	
	//init
	strSourceFolder = GetDataFolder(1) //":" is already added at the end of the string
	strList = ""
	
	//get List
	for(i=0;i<CountObjects(strSourceFolder, objectType ); i+=1)
		strList += strSourceFolder + GetIndexedObjName(strSourceFolder, objectType, i )+";"
	endFor
	
	return strList
End

//adapted function PopUpChooseFolder() from http://www.entorb.net/wickie/IGOR_Pro 
Function/S PopUpChooseFolder()
	//init local var
	String strSaveDataFolder, strList, strFolders
	
	//Save current DataFolder
	strSaveDataFolder = GetDataFolder(1)	
	//Move to root	
	SetDataFolder root:
	//Get List of Folders in root and add root foler
	strList = GetListOfFolderCont(4)
	strList = "root:;" + strList
	strFolders = "root:"
	Prompt strFolders,"Folder",popup,strList
	DoPrompt "",strFolders
	if (V_Flag == 1) 
		strFolders="" //return ""
	endif 
	//Move back to Old Data Folder
	SetDataFolder $strSaveDataFolder	
	Return strFolders
End


Function/S PopUpChooseWave(strDataFolder, [strText])
	String strDataFolder, strText
	strText = selectstring(paramIsDefault(strText), strText, "choose wave")
	//init local var
	String strSaveDataFolder, strList, strWave
	
	//Save current DataFolder
	strSaveDataFolder = GetDataFolder(1)	
	//Move to root	
	SetDataFolder $strDataFolder
	//Get List of Waves in root and add root foler
	strList = GetListOfFolderCont(1)

	Prompt strWave,strText,popup,strList
	DoPrompt "",strWave
	if (V_Flag == 1) 
		strWave=""//return ""
	endif 
	//Move back to Old Data Folder
	SetDataFolder $strSaveDataFolder	
	Return strWave
End

//adapted from function OpenFileDialog on http://www.entorb.net/wickie/IGOR_Pro
Function/S PopUpChooseFile([strPrompt])
	String strPrompt
	strPrompt = selectstring(paramIsDefault(strPrompt), strPrompt, "choose file")
	
	Variable refNum
	String outputPath
	String fileFilters = "Delimited Text Files (*.csv):.csv;"

	//Browse to Absorption-Folder
	String strPath = "Z:RAW:Absorption:"
	//String strPath = "C:Users:mak24gg:Documents:RAW:Absorption:"
	NewPath/O/Q path, strPath
	PathInfo/S path
	
	Open/D/F=fileFilters/R/M=strPrompt refNum
	outputPath = S_fileName
	return outputPath
End 

Function/S PopUpChooseDirectory(strPath)
    String strPath

	// set start path
	NewPath/Z/O/Q path, strPath
	PathInfo/S path
	if(!V_flag)
		strPath = SpecialDirPath("Documents", 0, 0, 0)
		NewPath/Z/O/Q path, strPath
		PathInfo/S path
	endif

	NewPath/Z/M="choose Folder"/O/Q path
	PathInfo path
	if(!V_flag)
		return PopUpChooseDirectory(strPath)
	endif
	strPath = S_path

	GetFileFolderInfo/Q/Z=1 strPath
	if (!V_isFolder)
		return ""
	endif

    return strPath
End

Function/S PopUpChooseFileFolder([strPrompt])
	String strPrompt
	strPrompt = selectstring(paramIsDefault(strPrompt), strPrompt, "choose file")
	String strPath, strFiles, strFile	
	
	strPath = PopUpChooseDirectory("X:Documents:RAW:Absorption")
	NewPath/O/Q path, strPath
	strFiles = IndexedFile(path,-1,".csv")
	if (strlen(strFiles)==0)
		print "No Files in selected Folder"
		return ""
	endif
	
	Prompt strFile,strPrompt,popup,strFiles
	DoPrompt "",strFile
	if (V_Flag == 1) 
		return ""
	endif
	
	return (strPath + strFile)
End

Function/S GetWave([strPrompt])
	String strPrompt
	//This Function basically tests the String for convertability to a wave reference.
	String strWave = PopUpChooseWave(PopUpChooseFolder(), strText=strPrompt)
	wave wavWave = $strWave
	if (WaveExists(wavWave))
	//if (stringmatch(GetWavesDataFolder(wavWave, 2), strWave))
		return strWave
	else
		return ""
	endif
End

Function SetWaveScale([wavX, wavY, strUnit])
	Wave wavX, WavY
	String strUnit
	strUnit	= SelectString(ParamIsDefault(strUnit), strUnit, "")	
	
	if (ParamIsDefault(wavX))
		// todo: strDirectory = PopUpChooseFolder()
		wave wavX = $PopUpChooseWave("root:", strText="choose x wave")
	endif
	if (ParamIsDefault(wavY))
		wave wavY = $PopUpChooseWave("root:", strText="choose y wave")
	endif

	Variable numOffset, numDelta

	if (!WaveExists(wavX) || !WaveExists(wavY))
		print "Error: Waves do not exist"
		return 0
	endif

	numOffset	= wavX[0]
	numDelta 	= AbsorptionDelta(wavX, normal=1)
	

	SetScale/P x, numOffset, numDelta, strUnit, wavY	

	return 1
End

Function AbsorptionDelta(wavInput, [normal])
	Wave wavInput
	Variable normal
	if (ParamIsDefault(normal))
		normal = 0
	endif
	
	Variable numSize, numDelta, i
	String strDeltaWave

	numSize		= DimSize(wavInput,0)
	if (numSize > 1)
		if (normal)
			numDelta = (wavInput[inf] - wavInput[0])/(numSize-1)
		else
			// calculate numDelta
			Make/FREE/O/N=(numSize-1) wavDeltaWave
			for (i=0; i<(numSize-1); i+=1)
				wavDeltaWave[i] = (wavInput[(i+1)] - wavInput[i])
			endfor
			WaveStats/Q/W wavDeltaWave
	
			wave M_WaveStats
			numDelta = M_WaveStats[3] //average
			//print "Wave " + nameofwave(wavInput) + " has a Delta of " + num2str(numDelta) + " with a standard deviation of " + num2str(M_WaveStats[4])
			//if X-Wave is not equally spaced, set the half minimum delta at all points.
			// controll by calculating statistical error 2*sigma/rms		
			if ((2*M_WaveStats[4]/M_WaveStats[5]*100)>5)
				print "PLEMd2Delta: Wave is not equally spaced. Check Code and calculate new Delta."
				// minimum
				numDelta = M_WaveStats[10]
				// avg - 2 * sdev : leave out the minimum 5% for statistical resaons
				if (M_WaveStats[3] > 0)		// sdev is always positive ;-)
					numDelta = M_WaveStats[3] - 2 * M_WaveStats[4]
				else
					numDelta = M_WaveStats[3] + 2 * M_WaveStats[4]
				endif
			endif
			// not used put possibly needed, when a new Delta Value is returned.
			
			KillWaves/Z  M_WaveStats
		endif
	else
		numDelta = 0
	endif
	return numDelta
End

Function RemoveWaveScale(wavWave)
	Wave wavWave
	Variable numXOffset, numXDelta, numYOffset, numYDelta
	String strXUnit, strYUnit
	
	strXUnit = ""
	strYUnit = ""
	numYOffset = DimOffset(wavWave,1)
	numXOffset = DimOffset(wavWave,0)
	numYDelta = DimDelta(wavWave,1)
	numXDelta = DimDelta(wavWave,0)
	SetScale/P x, numXOffset, numXDelta, strXUnit, wavWave
	SetScale/P y, numYOffset, numYDelta, strYUnit, wavWave
End

// See WM's CheckDisplayed
Function AbsorptionIsWaveInGraph(search)
	Wave search

	String currentTraces
	Variable countTraces, i
	Variable isPresent = 0

	currentTraces = TraceNameList("",";",1)
	countTraces = ItemsInList(currentTraces)

	for (i=0;i<countTraces;i+=1)
		Wave wv = TraceNameToWaveRef("", StringFromList(i,currentTraces) )
			if (cmpstr(NameOfWave(wv),NameOfWave(search)) == 0)
				isPresent = 1
			endif
		WaveClear wv
	endfor

	return isPresent
End

Function/S AbsorptionWaveRefToTraceName(graphNameStr, WaveRef)
	String graphNameStr
	Wave WaveRef
	
	String traces, trace
	Variable numTraces, i
	Variable isPresent = 0
		
	traces = TraceNameList(graphNameStr,";",1)
	numTraces = ItemsInList(traces)
	trace = ""
	for(i = 0; i < numTraces; i += 1)
		trace = StringFromList(i,traces)
		Wave wv = TraceNameToWaveRef(graphNameStr, trace)
			if (cmpstr(GetWavesDataFolder(wv, 1), GetWavesDataFolder(WaveRef, 1)) == 0)
				break
			endif
		WaveClear wv
	endfor

	return trace
End

Raman Spectroscopy

Raman spectroscopy at 1064nm was done using a Nd:YAG laser together with a high resolution FT-spectrometer (Bruker IFS 120 HR coupled to a FRA-106).

Raman spectroscopy at 488nm was achieved using an Argon laser and a dichroitic mirror (520nm) coupled to an Andor Shamrock spectrometer with an Si-CCD array (Andor Newton). The grating has 300 lines/mm and a blaze wavelength at 500nm.

Raman spectroscopy at 570nm was done at a SP2500 spectrograph with a Princton Instruments Pixis 256 CCD array

PLE Spectroscopy

Photoluminescence excitation/emission spectroscopy and -microscopy was performed using a home-built setup. Spectra were acquired with a LabVIEW program. It is based on a similar setup [104] that was taken for taking PLE maps in solution. The generated spectra and images are loaded and processed with a Igor Pro program for loading measured data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

#include "plem-main"
#include "plem-menu"
#include "plem-prefs"
#include "plem-structure"
#include "plem-helper"
#include "plem-gui"
#include "plem-correction"
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3
#pragma IgorVersion=8

// Programmed by Matthias Kastner
// mail@matthias-kastner.de
// https://github.com/ukos-git/igor-swnt-plem
//
// LICENSE: MIT
//

// requires igor-common-utilites
// https://github.com/ukos-git/igor-common-utilities
#include "common-utilities"

// Variables for current Project only. See also the LoadPreferences towards the end of the procedure for additional settings that are saved system-wide.
Constant 	cPLEMd2Version = 4001
StrConstant cstrPLEMd2root = "root:PLEMd2"
StrConstant cstrPLEMd2correction = "root:PLEMd2:correction"

static Constant PLEM_SINGLE_BACKGROUND   = 1
static Constant PLEM_MULTIPLE_BACKGROUND = 2

Function PLEMd2initVar()
	print "PLEMd2initVar: intialization of global variables"
	//Init Data Folder
	String strSaveDataFolder = GetDataFolder(1)
	SetDataFolder root:
	NewDataFolder/O/S $cstrPLEMd2root

	//Save Data Folder befor execution of this program so we can always switch after our program terminates
	//definition here only for initialization.
	//during run time it is set in PMRinit() but we will already switch folders here. so better also here safe the path.
	String/G gstrSaveDataFolder = strSaveDataFolder

	String/G gstrPLEMd1root = cstrPLEMd2root + ":" + "PLEMd1"
	String/G gstrMapsFolder = cstrPLEMd2root + ":" + "maps"

	SetDataFolder $cstrPLEMd2root
	//Maps: Create Folder and Initialize Strings where we store the maps of the current project

	NewDataFolder/O	 $gstrMapsFolder
	String/G gstrMapsAvailable = ""
	Variable/G gnumMapsAvailable = 0
	PLEMd2MapStringReInit()

	//save current init-version in project folder.
	Variable/G gnumPLEMd2Version = cPLEMd2Version
	//set a variable in root folder to recognize if the module was initialized.
	SetDataFolder root:
	Variable/G gnumPLEMd2IsInit = 1
	SetDataFolder $cstrPLEMd2root
End

Function PLEMd2isInitialized()
	NVAR/Z gnumPLEMd2IsInit = root:gnumPLEMd2IsInit
	if(!NVAR_EXISTS(gnumPLEMd2IsInit))
		return 0
	endif
	return gnumPLEMd2IsInit
End

// check if the Experiment was created with the same version as the program.
//
// Note: use PLEMd2init() for initialization
Function PLEMd2CheckExperimentVersion()
	DFREF packageRoot = $cstrPLEMd2root
	if(!DataFolderRefStatus(packageRoot))
		return 0
	endif

	NVAR/Z ExperimentVersion = packageRoot:gnumPLEMd2Version
	if(ExperimentVersion == cPLEMd2Version)
		return 1
	endif
End

// automatically initialize the current experiment to default values if necessary
Function PLEMd2initialize()
	if(PLEMd2isInitialized() && PLEMd2CheckExperimentVersion())
		return 0 // already initialized.
	endif

	PLEMd2Clean()
	PLEMd2initVar()
End

// force reset package
Function PLEMd2reset()
	Variable i, numMaps

	if(PLEMd2isInitialized())
		NVAR gnumPLEMd2IsInit = root:gnumPLEMd2IsInit
		gnumPLEMd2IsInit = 0
	endif
	PLEMd2initialize()

	// reset IBW files
	numMaps = PLEMd2MapStringReInit()
	for(i = 0; i < numMaps; i += 1)
		PLEMd2ProcessIBW(PLEMd2strPLEM(i))
	endfor
End

// cleanup package root
Function PLEMd2Clean()
	String strVariables, strStrings, strAvailable
	Variable i, numAvailable

	DFREF dfrSave = GetDataFolderDFR()
	DFREF packageRoot = $cstrPLEMd2root

	// we assume that all paths belong to previous versions of the program.
	if(PLEMd2CheckExperimentVersion())
		KillPath/A/Z
	endif

	if(!DataFolderRefStatus(packageRoot))
		return 0
	endif

	SetDataFolder packageRoot

	strVariables  = VariableList("V_*", ";", 4)	 // Scalar Igor Variables
	strVariables += VariableList("*", ";", 2)	//System Variables
	strStrings    = StringList("S_*", ";") // Scalar Igor Strings

	// @todo add all variables except those that are needed in the current Experiment Version

	numAvailable = ItemsInList(strVariables)
	for(i = 0; i < numAvailable; i += 1)
		strAvailable = StringFromList(i, strVariables)
		Killvariables/Z $strAvailable
	endfor

	numAvailable = ItemsInList(strStrings)
	for(i = 0; i < numAvailable; i += 1)
		strAvailable = StringFromList(i, strStrings)
		Killstrings/Z $strAvailable
	endfor

	SetDataFolder dfrSave
End

Function PLEMd2Open([strFile, display])
	String strFile
	Variable display

	Variable retry, err, numLoaded
	String strFileName, strFileType, strPartialPath, strBasePath
	String strWave, strPLEM

	Struct PLEMd2Prefs prefs
	PLEMd2LoadPackagePrefs(prefs)

	// this function serves as entrypoint for most activities
	PLEMd2initialize()

	if(ParamIsDefault(strFile))
		strFile = PLEMd2PopUpChooseFile()
	endif
	if(ParamIsDefault(display))
		display = 1
	endif

	// check for valid filename
	GetFileFolderInfo/Q/Z=1 strFile
	if(!!V_Flag || !V_isFile)
		print "PLEMd2Open: Invalid filename for " + strFile
		return 1
	endif

	// DisplayHelpTopic "Symbolic Paths"
	strBasePath = prefs.strBasePath
	GetFileFolderInfo/Q/Z=1 strBasePath
	if(!V_flag && V_isFolder)
		PathInfo PLEMbasePath
		if(!V_flag)
			NewPath/O/Q/Z PLEMbasePath, strBasePath
		endif
	endif

	strFileName = ParseFilePath(3, strFile, ":", 0, 0)
	strPLEM = CleanupName(strFileName, 0)

	strFileType = ParseFilePath(4, strFile, ":", 0, 0)
	strPartialPath = ReplaceString(RemoveEnding(strBasePath, ":"), strFile, "", 0, 1)

	// Loading Procedure (LoadWave is not dfr aware)
	strswitch(strFileType)
		case "ibw":
			// create dfr for loaded data
			DFREF dfrLoad = NewFreeDataFolder()

			// load wave to dfrLoad using original name from IBW (overwrite)
			DFREF dfrSave = GetDataFolderDFR()
			SetDataFolder dfrLoad
			do
				PathInfo PLEMbasePath
				try
					if(V_flag)
						LoadWave/H/Q/N/O/P=PLEMbasePath strPartialPath; AbortOnRTE
					else
						LoadWave/H/Q/N/O strFile; AbortOnRTE
					endif
					numLoaded = V_flag
				catch
					err = GetRTError(1)
					numLoaded = 0
					printf "failed %d time(s) at %s with error: %s", retry + 1, strFile, GetErrMessage(err)
				endtry
				retry += 1
			while(!numLoaded && retry < 3)
			SetDataFolder dfrSave

			if(numLoaded != 1)
				KillDataFolder/Z dfrLoad
				Abort "PLEMd2Open: Error Loaded more than one or no Wave from Igor Binary File"
			endif

			// reference loaded wave and lock it.
			WAVE/Z wavIBW = PLEMd2getWaveFromDFR(dfrLoad)
			SetWaveLock 0, wavIBW
			Rename wavIBW IBW
			SetWaveLock 1, wavIBW
			String/G dfrLoad:sha256 = WaveHash(wavIBW, 1)
			SVAR sha256 = dfrLoad:sha256

			break
		default:
			Abort "PLEMd2Open: Could not open file"
		break
	endswitch

	// check if PLEM exists with calculated name and hash
	DFREF dfrPLEM = PLEMd2MapFolder(strPLEM)
	if(DataFolderRefStatus(dfrPLEM) != 0)
		SVAR/Z savedSha = dfrPLEM:sha256
		if(SVAR_EXISTS(savedSha) && !!cmpstr(savedSha,sha256))
			strPLEM = CleanupName(sha256, 0) // name conflict
		endif
	endif

	DFREF dfrPLEM = PLEMd2MapFolder(strPLEM)
	DFREF dfrMaps = PLEMd2MapsFolder()
	if(DataFolderRefStatus(dfrPLEM) == 0)
		MoveDataFolder/O=1 dfrLoad dfrMaps
		RenameDataFolder dfrLoad, $strPLEM
	else
		DuplicateDataFolder/Z/O=3 dfrLoad, dfrMaps:$strPLEM
		if(V_flag)
			DFREF dfrMap = PLEMd2MapFolder(strPLEM)
			WAVE/Z wavIBW = dfrMap:IBW
			if(!WaveExists(wavIBW) || !!cmpstr(sha256, WaveHash(wavIBW, 1)))
				Abort "PLEMd2Open: Could not Save loaded IBW"
			endif
		endif
		KillDataFolder/Z dfrLoad
	endif

	// init stats
	PLEMd2statsInitialize(strPLEM)

	// create and display waves
	PLEMd2ProcessIBW(strPLEM)
	if(display)
		PLEMd2Display(strPLEM)
	endif
End

Function PLEMd2ProcessIBW(strPLEM)
	String strPLEM

	WAVE/Z wavIBW = PLEMd2wavIBW(strPLEM)
	if(WaveExists(wavIBW))
		PLEMd2ExtractInfo(strPLEM, wavIBW)
		PLEMd2ExtractIBW(strPLEM, wavIBW)
	else
		print "Error: Reload IBW from disk using Plemd2Open() for full IBW processing"
	endif

	PLEMd2BuildMaps(strPLEM)
End

Function/DF PLEMd2ExtractWaves(wavIBW)
	WAVE wavIBW

	String strWaveNames, strWaveExtract
	Variable numTotalX, numTotalY, i, j

	// load waves from IBW file to dfr
	DFREF dfr = NewFreeDataFolder()

	// check consistency
	strWaveNames = PLEMd2ExtractWaveList(wavIBW)
	numTotalX	= DimSize(wavIBW, 0)
	numTotalY	= DimSize(wavIBW, 1)
	if(numTotalY == 0 || numTotalX == 0)
		Abort "PLEMd2ExtractWaves: Binary File has no waves"
	endif
	if(numTotalY != ItemsInList(strWaveNames))
		print "PLEMd2ExtractWaves: Error WaveNames not correct in WaveNotes."
		PLEMd2FixWavenotes(wavIBW)
		strWaveNames = PLEMd2ExtractWaveList(wavIBW)
	endif
	if(numTotalY != ItemsInList(strWaveNames))
		printf "PLEMd2ExtractWaves: Missmatch in wavenames:\r%s\rWaveNames not correct in WaveNote. Counting %d data columns and %d labels.\r", strWaveNames, numTotalY, ItemsInList(strWaveNames)
		if(numTotalY < ItemsInList(strWaveNames))
			print "PLEMd2ExtractWaves: probably canceled measurement"
			do
				strWaveNames = RemoveListItem(ItemsInList(strWaveNames) - 1, strWaveNames)
			while(numTotalY < ItemsInList(strWaveNames))
		else
			Abort "Manual interaction needed."
		endif
	endif

	//Extract Columns from Binary Wave and give them proper names
	for(i = 0; i < numTotalY; i += 1)
		strWaveExtract = StringFromList(i, strWaveNames)
		Make/D/O/N=(numTotalX) dfr:$strWaveExtract/WAVE=wv
		wv[] = wavIBW[p][i]
		WaveClear wv
	endfor

	return dfr
End

Function PLEMd2CopyWaveNote(wavIBW, wv)
	WAVE wavIBW, wv

	String strHeader

	strHeader = Note(wavIBW)
	strHeader = strHeader[0,(strsearch(strHeader, "IGOR0",0)-1)] // clean WaveNotes
	Note/K/NOCR wv strHeader
End

Function PLEMd2ExtractIBW(strPLEM, wavIBW)
	String strPLEM
	WAVE wavIBW

	Struct PLEMd2stats stats
	String strWaveBG, strWavePL, corrections
	Variable numExcitationFrom, numExcitationTo
	Variable dim0, dim1
	Variable i,j, numItems

	DFREF packageRoot = $cstrPLEMd2root
	SVAR gstrMapsFolder = packageRoot:gstrMapsFolder

	if(!PLEMd2MapExists(strPLEM))
		Abort "PLEMd2ExtractIBW: Map does not exist"
	endif

	//There are 3 different structures for DATA
	//1) WAVES: WL, BG, PL ...
	//2) WAVES: WL, BG_498_502, PL_498_502,BG_502_506,PL_502_506 ...
	//3) WAVES: WL, BG, PL_498_502,PL_502_506,PL_506_510,PL_510_514 ...
	//Possibilities handled:
	//1)+3) stats.strbackground = single --> wavexists(background)
	//2) stats. strBackground = multiple --> count(BG_*) = count (PL_*)
	//4) image mode:
	//   special case of 1) size of WL wave is no equal to BG or PL.

	// collect strWavePL und strWaveBG. WaveList is not DFR aware
	DFREF saveDFR = GetDataFolderDFR()
	PLEMd2statsLoad(stats, strPLEM)
	DFREF dfr = PLEMd2ExtractWaves(wavIBW)
	SetDataFolder dfr
	strWavePL = WaveList("PL*", ";", "")
	strWaveBG = WaveList("BG*", ";", "")
	stats.numPLEMTotalY = ItemsInList(strWavePL, ";")
	if(ItemsInList(strWavePL) == 0 || ItemsInList(strWaveBG) == 0)
		Abort "No PL or BG Waves in :ORIGINAL"
	endif
	SetDataFolder saveDFR

	// quick fix for wrong single backgrounds on PLE setup
	if(stats.numbackground == PLEM_SINGLE_BACKGROUND && ItemsInList(strWaveBG) > 1)
		stats.numBackground = PLEM_MULTIPLE_BACKGROUND
	endif

	// error checking for multiple background
	if(stats.numbackground == PLEM_MULTIPLE_BACKGROUND && ItemsInList(strWaveBG, ";") != ItemsInList(strWavePL, ";"))
		printf "number of bg: %d\t number of pl: %d\r", ItemsInList(strWaveBG, ";"), ItemsInList(strWavePL, ";")
		if(WhichListItem("BG", strWaveBG) != -1)
			print "mixed Single and multiple BG mode. Switching to single Background mode."
			stats.numbackground = PLEM_SINGLE_BACKGROUND
		else
			Abort "PLEMd2ExtractIBW: Error Size Missmatch between Background Maps and PL Maps"
		endif
	endif

	stats.numCalibrationMode = 0
	if(stats.numPLEMTotalY == 1)
		stats.numCalibrationMode = 1
	endif

	wave wavWavelength = dfr:WL
	if(!WaveExists(wavWavelength))
		Abort "PLEMd2ExtractIBW: Wavelength Wave not found within ORIGINAL Folder"
	endif
	stats.numPLEMTotalX = NumPnts(wavWavelength)

	if(stats.numReadOutMode == 1)
		// quick fix for image mode on cameras @todo: redefine readout mode and data storage in data format specifications
		if(stats.numPLEMTotalX == 81919)
			stats.numDetector = PLEMd2cameraXeva
			stats.numPLEMTotalY = 256
			stats.numPLEMTotalX = 320
		else
			stats.numDetector = PLEMd2cameraClara
			stats.numPLEMTotalY = 1040
			stats.numPLEMTotalX = 1392
		endif
	endif

	// Redimension the Waves to proper size
	dim0 = stats.numPLEMTotalX
	dim1 = stats.numPLEMTotalY
	Redimension/N=(dim0, dim1) stats.wavPLEM, stats.wavMeasure, stats.wavBackground
	PLEMd2CopyWaveNote(wavIBW, stats.wavPLEM)
	if(stats.numReadOutMode == 1)
		// dim0 = 0 // no wavelength
		// dim1 = 1 // save power and excitation wl
	endif
	Redimension/N=(dim0) stats.wavWavelength, stats.wavGrating, stats.wavQE
	Redimension/N=(dim1) stats.wavExcitation, stats.wavYpower, stats.wavYphoton

	// set x-Scaling
	stats.wavWavelength = wavWavelength

	// Grating Waves
	corrections = PLEMd2getGratingString(stats.numGrating, PLEMd2getSystem(stats.strUser))
	PLEMd2SetCorrection(corrections, stats.wavGrating, stats.wavWavelength)

	// Quantum efficiency
	corrections = PLEMd2getDetectorQEstring(stats.numDetector, stats.numCooling, PLEMd2getSystem(stats.strUser))
	WAVE/Z qe = PLEMd2SetCorrection(corrections, stats.wavQE, stats.wavWavelength)

	// emission filter
	corrections = PLEMd2getFilterEmiString(PLEMd2getSystem(stats.strUser), stats.numDetector)
	PLEMd2SetCorrection(corrections, stats.wavFilterEmi, stats.wavWavelength)

	// different handling for spectra in calibration mode (1) and for maps (0)
	if(stats.numCalibrationMode == 1)
		wave wavMeasure 	= dfr:$(StringFromList(0, strWavePL))
		wave wavBackground 	= dfr:$(StringFromList(0, strWaveBG))

		if(stats.numReadOutMode == 1)
			// image mode. currently no information for images is saved
			Redimension/N=(stats.numPLEMTotalY * stats.numPLEMTotalX) wavMeasure, wavBackground // workaround for XEVA (2 pixel missing)
			stats.wavMeasure = wavMeasure[p + stats.numPLEMTotalX * q]
			stats.wavBackground = wavBackground[p + stats.numPLEMTotalX * q]
		else
			stats.wavMeasure 	= wavMeasure
			stats.wavBackground = wavBackground
		endif

		if(stats.numReadOutMode == 1)
			// image mode. currently no information for images is saved
			stats.wavMeasure = wavMeasure[p + stats.numPLEMTotalX * q]
			stats.wavBackground = wavBackground[p+stats.numPLEMTotalX*q]
		endif

		WaveClear wavBackground
		WaveClear wavMeasure

		// Excitation wave
		stats.wavExcitation 	= (stats.numEmissionStart + stats.numEmissionEnd) / 2
	else
		for(i = 0; i < stats.numPLEMTotalY; i += 1)
			// Original Waves: load
			wave wavMeasure 	= dfr:$(StringFromList(i, strWavePL))
			wave wavBackground 	= dfr:$(StringFromList(i, strWaveBG))

			stats.wavMeasure[][i] 		= wavMeasure[p]
			if(stats.numbackground == PLEM_MULTIPLE_BACKGROUND)
				stats.wavBackground[][i] 	= wavBackground[p]
			elseif(i == 0 && stats.numbackground == PLEM_SINGLE_BACKGROUND)
				stats.wavBackground[][] 	= wavBackground[p]
				//Redimension/N=(-1, 0) stats.wavBackground
			endif

			// Original Waves: unload
			WaveClear wavBackground
			WaveClear wavMeasure

			// Excitation wave
			numExcitationFrom 	= str2num(StringFromList(1,StringFromList(i,strWavePL), "_"))
			numExcitationTo 		= str2num(StringFromList(2,StringFromList(i,strWavePL), "_"))
			stats.wavExcitation[i] 	= (numExcitationFrom + numExcitationTo) / 2

			// since PLEMv3.0 excitation is saved multiplied by 10.
			if(stats.wavExcitation[i] > 1e3)
				stats.wavExcitation[i] /= 10
			endif
		endfor
	endif

	// excitation filter
	corrections = PLEMd2getFilterExcString(PLEMd2getSystem(stats.strUser), stats.numDetector)
	PLEMd2SetCorrection(corrections, stats.wavFilterExc, stats.wavExcitation)

	// Power correction waves
	// requires Excitation wave for Photon Energy
	if(stats.numDetector == PLEMd2detectorNewton || stats.numDetector == PLEMd2detectorIdus)
		stats.wavYpower  = str2num(StringFromList(p, PLEMd2ExtractPower(wavIBW), ";"))
		stats.wavYphoton = (stats.wavYpower * 1e-6) / (6.62606957e-34 * 2.99792458e+8 / (stats.wavExcitation * 1e-9)) 		// power is in uW and Excitation is in nm
	endif

	// init camera specific corrections.
	// Please note that sizeadjustment and rotationadjustment are not
	// changeable on a "per file base" but on a "per experiment base"
	stats.numPixelPitch = 1
	NVAR/Z numSizeAdjustment = root:numSizeAdjustment
	NVAR/Z numRotationAdjustment = root:numRotationAdjustment

	// set camera specific corrections
	if(stats.numDetector == PLEMd2detectorNewton)
		// 26x26µm * (binning factors)
		stats.numPixelPitch = 26
	elseif(stats.numDetector == PLEMd2detectorIdus)
		// 25x500µm
		stats.numPixelPitch = 25
	elseif(stats.numDetector == PLEMd2cameraClara)
		// add camera specific settings
		stats.numPixelPitch = 6.45 	// 6.45um

		// magnification adjustment
		if(!NVAR_EXISTS(numSizeAdjustment))
			Variable/G root:numSizeAdjustment = 0.960
			NVAR numSizeAdjustment = root:numSizeAdjustment
		endif

		// rotation adjustments
		if(!NVAR_EXISTS(numRotationAdjustment))
			// mounted camera is rotated depending on setup
			Variable/G root:numRotationAdjustment = -0.95
			NVAR numRotationAdjustment = root:numRotationAdjustment
		endif
		numRotationAdjustment = -0.95 // overwrite! better rotation for mkl23clarascan
		PLEMd2rotateLaser(stats)
	elseif(stats.numDetector == plemd2cameraXeva)
		// add camera specific settings
		stats.numPixelPitch = 30 // 30um

		// magnification adjustment
		if(!NVAR_EXISTS(numSizeAdjustment))
			Variable/G root:numSizeAdjustment = 0.977
			NVAR numSizeAdjustment = root:numSizeAdjustment
		endif

		stats.booSwitchX = !stats.booSwitchX // xeva has reverse readout
	endif
	if(!NVAR_EXISTS(numRotationAdjustment))
		Variable/G root:numRotationAdjustment = 0
		NVAR numRotationAdjustment = root:numRotationAdjustment
	endif

	if(stats.numCalibrationMode != 1)
		//todo counterpart in PLEMd2setScale
		stats.numPLEMBottomY = (str2num(StringFromList(1, StringFromList(0, strWavePL), "_")) + str2num(StringFromList(2, StringFromList(0, strWavePL), "_"))) / 2
	endif

	PLEMd2statsSave(stats)

	print GetWavesDataFolder(stats.wavPLEM, 2)
End

static Function PLEMd2setScale(stats)
	Struct PLEMd2stats &stats

	NVAR/Z numSizeAdjustment = root:numSizeAdjustment
	if(!NVAR_EXISTS(numSizeAdjustment))
		Variable/G root:numSizeAdjustment = 1
		NVAR numSizeAdjustment = root:numSizeAdjustment
		print "PLEMd2setScale: sizeAdjustment set to 1"
	endif

	// handle spectra
	if(stats.numReadOutMode != 1)
		stats.numPLEMLeftX = stats.wavWavelength[0]
		stats.numPLEMDeltaX = PLEMd2Delta(stats.wavWavelength, normal = 1)
		if(stats.numCalibrationMode == 1)
			stats.numPLEMDeltaY 	= (stats.numEmissionEnd - stats.numEmissionStart)
			stats.numPLEMBottomY 	= stats.numEmissionStart
		else
			stats.numPLEMBottomY	= stats.wavExcitation[0]
			stats.numPLEMDeltaY	= PLEMd2Delta(stats.wavExcitation)

			// since PLEMv3.0 excitation is saved multiplied by 10.
			if(stats.numPLEMBottomY > 1e3)
				stats.numPLEMBottomY /= 10
			endif
		endif
	endif

	// handle microscope images
	if(stats.numReadOutMode == 1)
		stats.numPLEMDeltaX =  (stats.booSwitchY == 1 ? +1 : -1) * numSizeAdjustment * stats.numPixelPitch / stats.numMagnification
		stats.numPLEMLeftX 	=  stats.numPositionY - stats.numPLEMDeltaX * (stats.numLaserPositionX)
		stats.numPLEMDeltaY 	= (stats.booSwitchX == 1 ? -1 : +1) * numSizeAdjustment * stats.numPixelPitch / stats.numMagnification
		stats.numPLEMBottomY 	= stats.numPositionX - stats.numPLEMDeltaY * stats.numLaserPositionY
	endif

	PLEMd2statsSave(stats)
	
	SetScale/P x stats.numPLEMLeftX, stats.numPLEMDeltaX, "", stats.wavPLEM, stats.wavMeasure, stats.wavBackground
	SetScale/P y stats.numPLEMBottomY, stats.numPLEMDeltaY, "", stats.wavPLEM, stats.wavMeasure, stats.wavBackground
End

// recalculate laserposition for rotated image
static Function PLEMd2rotateLaser(stats)
	Struct PLEMd2stats &stats

	variable dim0, dim1

	dim0 = stats.numLaserPositionX
	dim1 = stats.numLaserPositionY

	NVAR numRotationAdjustment = root:numRotationAdjustment
	PLEMd2rotatePoint(dim0, dim1, stats.numPLEMTotalX, stats.numPLEMTotalY, numRotationAdjustment)

	stats.numLaserPositionX = dim0
	stats.numLaserPositionY = dim1
End

static Function PLEMd2rotatePoint(pointX, pointY, totalX, totalY, rotation)
	Variable &pointX, &pointY
	Variable totalX, totalY, rotation

	// calculate LaserPosition (x,y) for rotated image when numRotationAdjustment != 0
	Make/FREE/U/I/N=(totalX, totalY) wv = 0
	wv[pointX][pointY] = 1000
	ImageRotate/Q/E=(0)/O/A=(rotation) wv
	WaveStats/M=1/Q wv

	pointX = V_maxRowLoc
	pointY = V_maxColLoc

	return 0
End

Function PLEMd2BuildMaps(strPLEM)
	String strPLEM

	variable i, numExcitation

	Struct PLEMd2stats stats
	PLEMd2statsLoad(stats, strPLEM)
	PLEMd2setScale(stats)
	WAVE wavPLEM = stats.wavPLEM // work around bug in Multithread assignment which can not use stats.wavPLEM

	// reset PLEM size to measurement (rotation changes it)
	Redimension/N=(DimSize(stats.wavMeasure, 0), DimSize(stats.wavMeasure, 1)) stats.wavPLEM

	if(stats.booBackground)
		Multithread wavPLEM[][] = (stats.wavMeasure[p][q] - stats.wavBackground[p][q])
	else
		Multithread wavPLEM = stats.wavMeasure
	endif

	if(stats.booTime)
		Multithread wavPLEM /= stats.numExposure
	endif

	if(stats.booWavelengthPitch)
		Multithread wavPLEM[][] /= p > 0 ? abs(stats.wavWavelength[p] - stats.wavWavelength[p - 1]) : abs(stats.wavWavelength[1] - stats.wavWavelength[0])
	endif

	if(stats.numDetector == PLEMd2cameraClara || stats.numDetector == plemd2cameraXeva)
		NVAR numRotationAdjustment = root:numRotationAdjustment
		if(numRotationAdjustment != 0)
			ImageRotate/Q/E=(NaN)/O/A=(numRotationAdjustment) stats.wavPLEM
		endif
		return NaN
	endif

	// spectrum corrections

	if(stats.booPower)
		if(DimSize(stats.wavPLEM, 1) == DimSize(stats.wavYpower, 0))
			if(stats.booFilter)
				Multithread wavPLEM /= (stats.wavYpower[q] / stats.wavFilterExc[q])
			else
				Multithread wavPLEM /= stats.wavYpower[q]
			endif
		else
			Multithread wavPLEM /= stats.wavYpower[0]
		endif
	endif

	if(stats.booPhoton)
		if(stats.booFilter)
			Multithread wavPLEM /= (stats.wavYphoton[q] / stats.wavFilterExc[q])
		else
			Multithread wavPLEM /= stats.wavYphoton[q]
		endif
	endif

	if(stats.booFilter)
		Multithread wavPLEM /= stats.wavFilterEmi[p]
	endif

	if(stats.booNormalization)
		Multithread wavPLEM /= stats.numNormalization
	endif

	if(stats.booGrating)
		Multithread wavPLEM /= stats.wavGrating[p]
	endif

	if(stats.booQuantumEfficiency)
		stats.wavPLEM /= stats.wavQE[p]
	endif
End

// function modified from absorption-load-v6
// calculates the mean distance between points in wave.
// mainly used in the process of transforming waves to igor wave format.
Function PLEMd2Delta(wavInput, [normal])
	Wave wavInput
	Variable normal
	if(ParamIsDefault(normal))
		normal = 0
	endif

	Variable numSize, numDelta, i
	String strDeltaWave

	numSize		= DimSize(wavInput,0)
	if(numSize > 1)
		if(normal)
			numDelta = abs((wavInput[numSize-1] - wavInput[0]))/(numSize-1)
		else
			// calculate numDelta
			Make/FREE/O/N=(numSize-1) wavDeltaWave
			for(i = 0; i < (numSize - 1); i += 1)
				wavDeltaWave[i] = (wavInput[(i+1)] - wavInput[i])
			endfor
			WaveStats/Q/W wavDeltaWave

			wave M_WaveStats
			numDelta = M_WaveStats[3] //average
			//print "Wave " + nameofwave(wavInput) + " has a Delta of " + num2str(numDelta) + " with a standard deviation of " + num2str(M_WaveStats[4])
			//if X-Wave is not equally spaced, set the half minimum delta at all points.
			// controll by calculating statistical error 2*sigma/rms
			if((2 * M_WaveStats[4] / M_WaveStats[5] * 100) > 5)
				print "PLEMd2Delta: Wave is not equally spaced. Check Code and calculate new Delta."
				// minimum
				numDelta = M_WaveStats[10]
				// avg - 2 * sdev : leave out the minimum 5% for statistical resaons
				if(M_WaveStats[3] > 0)		// sdev is always positive ;-)
					numDelta = M_WaveStats[3] - 2 * M_WaveStats[4]
				else
					numDelta = M_WaveStats[3] + 2 * M_WaveStats[4]
				endif
			endif
			// not used put possibly needed, when a new Delta Value is returned.

			KillWaves/Z  M_WaveStats
		endif
	else
		numDelta = 0
	endif
	return numDelta
End

Function PLEMd2ExtractInfo(strPLEM, wavIBW)
	String strPLEM
	WAVE wavIBW

	String strFound
	Struct PLEMd2Stats stats

	PLEMd2statsLoad(stats, strPLEM)

	stats.strPLEM = strPLEM

	stats.strDate 		= PLEMd2ExtractSearch(wavIBW, "Date") /// @see PLEMd2Date2Minutes
	stats.strUser 		= PLEMd2ExtractSearch(wavIBW, "User")
	stats.strFileName 	= PLEMd2ExtractSearch(wavIBW, "File")

	stats.numCalibrationMode = PLEMd2ExtractVariables(wavIBW, "numCalibrationMode")
	stats.numSlit 		= PLEMd2ExtractVariables(wavIBW, "numSlit")
	stats.numGrating 	= PLEMd2ExtractVariables(wavIBW, "numGrating")
	stats.numFilter 	= PLEMd2ExtractVariables(wavIBW, "numFilter")
	stats.numShutter 	= PLEMd2ExtractVariables(wavIBW, "numShutter")
	stats.numWLcenter 	= PLEMd2ExtractVariables(wavIBW, "numWLcenter")
	stats.numDetector 	= PLEMd2ExtractVariables(wavIBW, "numDetector")
	stats.numCooling 	= PLEMd2ExtractVariables(wavIBW, "numCooling")
	stats.numExposure 	= PLEMd2ExtractVariables(wavIBW, "numExposure")
	stats.numBinning 	= PLEMd2ExtractVariables(wavIBW, "numBinning")
	stats.numScans 		= PLEMd2ExtractVariables(wavIBW, "numScans")
	stats.numBackground = PLEMd2ExtractVariables(wavIBW, "numBackground")
	stats.numWLfirst 	= 0 // deprecated
	stats.numWLlast 	= 0 // deprecated
	stats.numWLdelta 	= PLEMd2ExtractVariables(wavIBW, "numWLdelta")
	stats.numEmissionMode 	= PLEMd2ExtractVariables(wavIBW, "numEmissionMode")
	stats.numEmissionPower 	= PLEMd2ExtractVariables(wavIBW, "numEmissionPower")
	stats.numEmissionStart 	= PLEMd2ExtractVariables(wavIBW, "numEmissionStart")
	stats.numEmissionEnd 	= PLEMd2ExtractVariables(wavIBW, "numEmissionEnd")
	stats.numEmissionDelta 	= PLEMd2ExtractVariables(wavIBW, "numEmissionDelta")
	stats.numEmissionStep 	= PLEMd2ExtractVariables(wavIBW, "numEmissionStep")

	stats.numPositionX = PLEMd2ExtractVariables(wavIBW, "numPositionX")
	stats.numPositionY = PLEMd2ExtractVariables(wavIBW, "numPositionY")
	stats.numPositionZ = PLEMd2ExtractVariables(wavIBW, "numPositionZ")
	stats.booSwitchX = PLEMd2ExtractVariables(wavIBW, "numSwitchX")
	stats.booSwitchY = PLEMd2ExtractVariables(wavIBW, "numSwitchY")

	stats.numReadOutMode 	= PLEMd2ExtractVariables(wavIBW, "numReadoutMode")
	stats.numLaserPositionX = PLEMd2ExtractVariables(wavIBW, "numLaserX")
	stats.numLaserPositionY = PLEMd2ExtractVariables(wavIBW, "numLaserY")
	stats.numMagnification 	= PLEMd2ExtractVariables(wavIBW, "numMagnification")

	PLEMd2statsSave(stats)
End

// @brief convert the stats.strDate string and return the time in minutes
//
// expected format for strDateTime: "DD.MM.YYYY/HH:MM"
Function PLEMd2Date2Minutes(strDateTime)
	string strDateTime

	string strDate, strTime
	variable minutes

	// format is DD.MM.YYYY
	strDate = StringFromList(0, strDateTime, "/")
	// format is HH:MM
	strTime = StringFromList(1, strDateTime, "/")

	minutes = date2secs(str2num(StringFromList(2, strDate, ".")), str2num(StringFromList(1, strDate, ".")), str2num(StringFromList(0, strDate, "."))) / 60
	minutes += str2num(StringFromList(0, strTime, ":")) * 60
	minutes += str2num(StringFromList(1, strTime, ":"))

	return minutes
End

//This Function is called every time. we probably could make it more efficient. ;_(
Function PLEMd2ExtractVariables(wavIBW, strVariableName)
	Wave wavIBW
	String strVariableName

	String strHeader, strReadLine, strItem
	String strListVariableNames, strListVariables
	Variable i, numCount

	String strReturn = ""

	strHeader = Note(wavIBW)
	numCount = ItemsInList(strHeader, "\r\n")

	i=0
	do
		i += 1
		strReadLine = StringFromList(i, strHeader, "\r\n")
	while ((StringMatch(strReadLine, "*IGOR0:*") != 1) && (i<numCount))
	strListVariableNames = StringFromList(1, strReadLine, ":")

	do
		i += 1
		strReadLine = StringFromList(i, strHeader, "\r\n")
	while ((StringMatch(strReadLine, "*IGOR1:*") != 1) && (i<numCount))
	strListVariables = StringFromList(1, strReadLine, ":")

	strItem = StringFromList(WhichListItem(strVariableName, strListVariableNames), strListVariables)
	//print "for " + strVariableName + " at item number: " + num2str(WhichListItem(strVariableName, strListVariableNames)) + " found item: " + strItem

	return str2num(strItem)
End

Function/S PLEMd2ExtractSearch(wavIBW, strFind)
	Wave wavIBW
	String strFind

	String strHeader, strReadLine, strItem
	Variable i, numCount

	String strReturn = ""

	strHeader = Note(wavIBW)
	numCount = ItemsInList(strHeader, "\r\n")

	i=0
	do
		i += 1
		strReadLine = StringFromList(i, strHeader, "\r\n")
	while ((StringMatch(strReadLine, "*" + strFind + "*") != 1) && (i<numCount))

	strItem = TrimString(strReadLine[strsearch(strReadLine, ":", 0) + 1, inf])
	if((strlen(strReadLine)>0) && (strlen(strItem)>0))
		strReturn = strItem
	else
		strReturn = ""
	endif

	return strReturn
End

Function/S PLEMd2ExtractWaveList(wavIBW)
	Wave wavIBW

	String strHeader, strList, strReadLine
	Variable i, numLines, startLine, endLine

	strHeader=note(wavIBW)
	startLine = strsearch(strHeader, "IGOR3:", 0) + 6
	if(startLine < 0)
		Abort "Critical: String IGOR3: missing in WaveNote"
	endif

	endLine = strsearch(strHeader, "\r", startLine) - 1
	if(endLine < 0)
		endLine = strlen(strHeader)
	endif

	return strHeader[startLine, endLine]
End

Function/S PLEMd2ExtractPower(wavIBW)
	//wavIBW can be any wave with correct wavenotes
	Wave wavIBW

	String strHeader, strReadLine
	String strListPower, strListParse
	Variable i, numCount, numItem

	String strReturn = ""

	strHeader = Note(wavIBW)
	numCount = ItemsInList(strHeader, "\r\n")

	i=0
	do
		i += 1
		strReadLine = StringFromList(i, strHeader, "\r\n")
	while ((StringMatch(strReadLine, "*Power at*") != 1) && (i<numCount)) //Power at Glass Plate (µW):
	strListParse = StringFromList(1, strReadLine, ":")

	numCount = ItemsInList(strListParse)
	strListPower = ""
	//assure to return numbers (not strings) in liststring
	for(i = 0; i < numCount; i += 1)
		numItem = str2num(StringFromList(i, strListParse))
		strListPower = AddListItem(num2str(numItem), strListPower, ";",Inf)
	endfor
	//print "for " + strVariableName + " at item number: " + num2str(WhichListItem(strVariableName, strListVariableNames)) + " found item: " + strItem

	return strListPower
End

Function/WAVE PLEMd2DuplicateByNum(numPLEM)
	Variable numPLEM
	if(numPLEM < 0)
		print "PLEMd2DuplicateByNum: Wrong Function Call numPLEM out of range"
		return $""
	endif
	String strPLEM
	strPLEM = PLEMd2strPLEM(numPLEM)
	return PLEMd2Duplicate(strPLEM)
End

Function/WAVE PLEMd2Duplicate(strPLEM, [overwrite])
	String strPLEM
	variable overwrite

	String strTemp, strWavename
	Variable i

	Struct PLEMd2Stats stats
	PLEMd2statsLoad(stats, strPLEM)
	
	overwrite = ParamIsDefault(overwrite) ? 1 : !!overwrite

	strWavename = "root:" + stats.strPLEM
	strTemp = strWavename

	if(!overwrite && WaveExists($strWavename))
		print "PLEMd2Duplicate: Wave already exists. Using incremental WaveName"
		i = -1
		do
			i += 1
			strTemp = strWavename + "_" + num2str(i)
			wave/Z wv = $strTemp
		while(WaveExists(wv))
		strWavename = strTemp
	endif

	Duplicate/O stats.wavPLEM $strWavename/WAVE=wv
	print "PLEMd2Duplicate: WaveName is " + strWavename

	return wv
End

Function PLEMd2FixWavenotes(wavIBW)
	WAVE wavIBW

	String strHeader, strWaveNames, strWaveNamesNew, falseBackground, i

	print "PLEMd2FixWavenotes: Trying to correct WaveNote"

	strHeader = Note(wavIBW)
	if((StringMatch(strHeader, "*IGOR2:*")) == 0)
		//IGOR2 not found so the error is probably related to that. (caused by early version of LabView program)
		print "PLEMd2FixWavenotes: Error: Did not find IGOR2 in WaveNote. Fixing...."
		//rename IGOR4 to IGOR3 and IGOR3 to IGOR2.
		strHeader = ReplaceString("IGOR3:",strHeader, "IGOR2:")
		strHeader = ReplaceString("IGOR4:",strHeader, "IGOR3:")
	Endif

	strWaveNames = PLEMd2ExtractWaveList(wavIBW)
	if(StringMatch(strWaveNames, "*BG;*") && StringMatch(strWaveNames, "*BG_*"))
		//mixed multiple and single background during measurement, revert to single bg
		strWaveNamesNew = RemoveFromList(ListMatch(strWaveNames, "BG_*"), strWaveNames)
		//strWaveNames	= strHeader[strsearch(strHeader, "IGOR3:",0), strlen(strHeader)]
		strHeader = ReplaceString("IGOR3:" + strWaveNames, strHeader, "IGOR3:" + strWaveNamesNew)
	endif
End

Function PLEMd2AtlasReload(strPLEM)
	String strPLEM
	Struct PLEMd2stats stats
	PLEMd2statsLoad(stats, strPLEM)

	String strDataFolder = stats.strDataFolder + "CHIRALITY"
	DFREF dfr = $strDataFolder

	// create waves
	Make/O/N=41/D dfr:atlasS1nm/WAVE=atlasS1nm
	Make/O/N=41/D dfr:atlasS2nm/WAVE=atlasS2nm
	Make/O/N=41/T dfr:atlasText/WAVE=atlasText

	// fill waves with data
	atlasS1nm[0]= {613.783,688.801,738.001,765.334,821.087,861.001,898.436,939.274,939.274,961.118,1008,1033.2,1078.12,1097.21,1107,1116.97,1148,1148,1169.66,1227.57,1227.57,1239.84,1239.84,1239.84,1278.19}
	atlasS1nm[25]= {1291.5,1318.98,1347.65,1347.65,1347.65,1362.46,1377.6,1393.08,1441.68,1441.68,1441.68,1458.64,1458.64,1512,1549.8,1549.8}
	atlasS2nm[0]= {568.735,510.223,613.783,596.078,480.559,576.671,688.801,497.928,659.49,563.564,639.094,729.319,712.553,582.085,642.405,548.603,789.708,708.481,784.71,629.361,779.775,720.838,666.582,607.766}
	atlasS2nm[24]= {849.207,784.71,849.207,746.893,681.232,708.481,849.207,799.898,918.401,861.001,925.255,918.401,751.419,789.708,885.601,991.873,932.212}
	atlasText[0] = {"(6,1)","(5,3)","(8,0)","(7,2)","(5,4)","(6,4)","(9,1)","(7,3)","(8,3)","(6,5)","(7,5)","(10,2)","(9,4)","(8,4)","(7,6)","(9,2)","(12,1)","(8,6)","(11,3)","(10,3)","(10,5)"}
	atlasText[21]= {"(8,7)","(9,5)","(11,1)","(13,2)","(9,7)","(12,4)","(10,6)","(12,2)","(11,4)","(11,6)","(9,8)","(15,1)","(10,8)","(14,3)","(13,5)","(13,3)","(12,5)","(10,9)","(16,2)"}
End

Function PLEMd2AtlasRecalculate(strPLEM)
	String strPLEM
	Struct PLEMd2stats stats
	PLEMd2statsLoad(stats, strPLEM)
	Variable numPlank = 4.135667516E-12 //meV s
	Variable numLight =  	299792458E9 //nm/s
	stats.wavEnergyS1	= numPlank * numLight / (numPlank * numLight / stats.wavAtlasS1nm[p] - stats.numS1offset)
	stats.wavEnergyS2	= numPlank * numLight / (numPlank * numLight / stats.wavAtlasS2nm[p] - stats.numS2offset)
End

Function PLEMd2AtlasInit(strPLEM, [init, initT])
	String strPLEM
	WAVE init
	WAVE/T initT

	Struct PLEMd2stats stats
	STRUCT cntRange range
	Variable i, numChiralities, j
	Variable xmin, xmax, ymin, ymax
	Variable chirality_start, chirality_end
	Variable tolerance = 5 // nm

	PLEMd2AtlasReload(strPLEM)
	PLEMd2statsLoad(stats, strPLEM)

	if(!ParamIsDefault(init))
		if(DimSize(init, 1) < 2)
			Abort "PLEMd2AtlasInit: Invalid init wave"
		endif
		Redimension/N=(DimSize(init, 0)) stats.wavAtlasS1nm, stats.wavAtlasS2nm, stats.wavAtlasText
		stats.wavAtlasS2nm[] = init[p][0]
		stats.wavAtlasS1nm[] = init[p][1]
		if(!ParamIsDefault(initT))
			stats.wavAtlasText = initT[p]
		else
			stats.wavAtlasText = ""
		endif
	endif

	// get boundaries from PLEM
	PLEMd2GetAcceptableRange(stats, range)
	ymin = range.yMin - tolerance
	ymax = range.yMax + tolerance
	xmin = range.xMin - tolerance
	xmax = range.xMax + tolerance

	// search for all chiralities within current window
	Duplicate/O stats.wavAtlasS1nm stats.wavEnergyS1
	Duplicate/O stats.wavAtlasS2nm stats.wavEnergyS2
	Redimension/N=(DimSize(stats.wavAtlasText, 0)) stats.wavChiralityText
	stats.wavChiralityText = stats.wavAtlasText[p]
	stats.wavEnergyS2   = NaN
	stats.wavEnergyS1   = NaN

	numChiralities = Dimsize(stats.wavAtlasS1nm, 0)
    j = 0
	for(i = 0; i < numChiralities; i += 1)
		if((stats.wavAtlasS2nm[i] > ymin) && (stats.wavAtlasS1nm[i] > xmin) && (stats.wavAtlasS2nm[i] < ymax) && (stats.wavAtlasS1nm[i] < xmax))
            stats.wavEnergyS1[j]   = stats.wavAtlasS1nm[i]
            stats.wavEnergyS2[j]   = stats.wavAtlasS2nm[i]
			stats.wavChiralityText[j] = stats.wavAtlasText[i]
            j += 1
        endif
	endfor
	Redimension/N=(j) stats.wavEnergyS1, stats.wavEnergyS2, stats.wavChiralityText, stats.wavFWHMS1, stats.wavFWHMS2

	// overwrite reset point.
	Redimension/N=(j) stats.wavAtlasText, stats.wavAtlasS1nm, stats.wavAtlasS2nm, stats.wav2Dfit, stats.wav1Dfit
	stats.wavAtlasText = stats.wavChiralityText[p]
	stats.wavAtlasS1nm = stats.wavEnergyS1[p]
	stats.wavAtlasS2nm = stats.wavEnergyS2[p]
	stats.wav2Dfit = NaN
	stats.wav1Dfit = NaN

	stats.numS1offset = 0
	stats.numS2offset = 0

	PLEMd2statsSave(stats)
End

Function PLEMd2AtlasEdit(strPLEM)
	String strPLEM
	Struct PLEMd2stats stats
	String winPLEMedit

	if(PLEMd2MapExists(strPLEM) == 0)
		print "PLEMd2AtlasFit: Map does not exist properly"
		return 0
	endif

	PLEMd2statsLoad(stats, strPLEM)

	winPLEMedit = PLEMd2getWindow(stats.strPLEM) + "_edit"
	DoWindow/F $winPLEMedit
	if(V_flag == 0)
		Edit stats.wavchiralityText, stats.wav2Dfit, stats.wav1Dfit, stats.wavEnergyS1, stats.wavEnergyS2, stats.wavFWHMS1, stats.wavFWHMS2
		DoWindow/C/N/R $winPLEMedit
	endif

End

// uses 2d fit result to clean
Function PLEMd2AtlasClean(strPLEM, [ threshold ])
	String strPLEM
	Variable threshold

	Variable i, numPoints
	Variable xmin, xmax, ymin, ymax
	Variable tolerance = 50
	Variable accuracy

	STRUCT cntRange range
	Struct PLEMd2stats stats
	PLEMd2statsLoad(stats, strPLEM)

	threshold = ParamIsDefault(threshold) ? 0 : threshold

	// get boundaries from PLEM
	// get boundaries from PLEM
	PLEMd2GetAcceptableRange(stats, range)
	ymin = range.yMin - tolerance
	ymax = range.yMax + tolerance
	xmin = range.xMin - tolerance
	xmax = range.xMax + tolerance

	numPoints = DimSize(stats.wavchiralityText, 0)
	for(i = numPoints - 1; i >= 0; i -= 1)
		if(DimSize(stats.wavPLEM, 1) > 1)
			if(stats.wav2Dfit[i] > threshold)
				// value is greater than threshold
				if((stats.wavEnergyS2[i] > ymin) && (stats.wavEnergyS2[i] < ymax))
					// AND within y range
					if((stats.wavEnergyS1[i] > xmin) && (stats.wavEnergyS1[i] < xmax))
						// AND within x range
						continue
					endif
				endif
			endif
		else
			if(stats.wav1Dfit[i] > threshold)
				// value is greater than threshold
				if((stats.wavEnergyS1[i] > xmin) && (stats.wavEnergyS1[i] < xmax))
					// AND within x range
					continue
				endif
			endif
		endif
		DeletePoints i, 1, stats.wavchiralityText, stats.wav2Dfit, stats.wav1Dfit, stats.wavEnergyS1, stats.wavEnergyS2, stats.wavFWHMS1, stats.wavFWHMS2
	endfor

	// Remove Duplicates
	if(DimSize(stats.wavchiralityText, 0) < 2)
		return 0
	endif
	FindDuplicates/FREE/INDX=indexS1/TOL=25 stats.wavEnergyS1
	FindDuplicates/FREE/INDX=indexS2/TOL=25 stats.wavEnergyS2
	Concatenate/FREE {indexS1, indexS2}, indices
	if(DimSize(indices, 0) > 1)
		FindDuplicates/FREE/DN=duplicates indices
	else
		Duplicate/FREE indices duplicates
	endif
	if(DimSize(duplicates, 0) == 0 || numtype(duplicates[0]) != 0)
		return 0
	endif
	numPoints = DimSize(duplicates, 0)
	Sort duplicates, duplicates
	for(i = numPoints - 1; i >= 0; i -= 1)
		DeletePoints duplicates[i], 1, stats.wavchiralityText, stats.wav2Dfit, stats.wav1Dfit, stats.wavEnergyS1, stats.wavEnergyS2, stats.wavFWHMS1, stats.wavFWHMS2
	endfor

	// find text labels in atlas wave
	numPoints = DimSize(stats.wavchiralityText, 0)
	for(i = 0; i < numPoints; i += 1)
		accuracy = 1
		do
			Extract/FREE/INDX/U/I stats.wavAtlasText, index, \
				((round(stats.wavAtlasS1nm[p] / accuracy) * accuracy == round(stats.wavEnergyS1[i] / accuracy) * accuracy) && \
				(round(stats.wavAtlasS2nm[p] / accuracy) * accuracy == round(stats.wavEnergyS2[i] / accuracy) * accuracy))
			accuracy += 0.5
		while(DimSize(index, 0) < 1)
		if(DimSize(index, 0) > 1)
			Make/FREE/N=(DimSize(index, 0)) temp = stats.wavAtlasS1nm[index[p]]
			Sort index, temp
		endif
		stats.wavchiralityText[i] = stats.wavAtlasText[index[0]]
	endfor

	Sort/A=1 stats.wavchiralityText, stats.wavchiralityText, stats.wav2Dfit, stats.wav1Dfit, stats.wavEnergyS1, stats.wavEnergyS2, stats.wavFWHMS1, stats.wavFWHMS2
End

static Constant cminS1fwhm = 3
static Constant cmaxS1fwhm = 30
static Constant cminS2fwhm = 10
static Constant cmaxS2fwhm = 150

Function PLEMd2AtlasFit2D(strPLEM)
	String strPLEM

	Variable i, j, numAtlas, numFits
	Variable pStart, pEnd, qStart, qEnd, pAccuracy, qAccuracy, minpqAccuracy = 5
	String strWavPLEMfitSingle, strWavPLEMfit
	Variable s1FWHM, s2FWHM, s2nm, s1nm, intensity

	Variable numDeltaS1 = 25 //nm
	Variable numDeltaS2 = 25 //nm
	Variable V_fitOptions = 4 // used to suppress CurveFit dialog
	Variable err

	Struct PLEMd2stats stats
	PLEMd2statsLoad(stats, strPLEM)
	WAVE PLEM = removeSpikes(stats.wavPLEM)
	pAccuracy = max(minpqAccuracy, ceil(numDeltaS1 / DimDelta(PLEM, 0)))
	qAccuracy = max(minpqAccuracy, ceil(numDeltaS2 / DimDelta(PLEM, 1)))

	Make/O/T/N=5 root:T_Constraints/WAVE=T_Constraints
	T_Constraints[0] = "K1 > 0"

	numFits = numpnts(stats.wavEnergyS1)
	for(i = 0; i < numFits; i += 1)
		intensity = stats.wav1Dfit[i]
		s1nm = stats.wavEnergyS1[i]
		s2nm = stats.wavEnergyS2[i]
		s1FWHM = stats.wavFWHMS1[i]
		s2FWHM = stats.wavFWHMS2[i]

		// fit emission wavelength to get center wavelength for excitation fit
		T_Constraints[1] = "K2 > " + num2str(s1nm - numDeltaS1 / 2)
		T_Constraints[2] = "K2 < " + num2str(s1nm + numDeltaS1 / 2)
		T_Constraints[3] = "K3 > " + num2str(cminS1fwhm / 1.66511) // 2*sqrt(ln(2)) = 1.66511
		T_Constraints[4] = "K3 < " + num2str(cmaxS1fwhm / 1.66511) // 2*sqrt(ln(2)) = 1.66511

		[ pStart, pEnd ] = FindLevelWrapper(stats.wavWavelength, s1nm, accuracy = pAccuracy)
		[ qStart, qEnd ] = FindLevelWrapper(stats.wavExcitation, s2nm, accuracy = qAccuracy)

		Duplicate/FREE/R=[pStart, pEnd][qStart, qEnd] PLEM dummy
		MatrixOP/FREE fitme = sumRows(dummy)
		Duplicate/FREE/R=[pStart, pEnd] stats.wavWavelength xfitme
		try
			CurveFit/Q gauss fitme/X=xfitme/C=T_Constraints; AbortOnRTE
			WAVE W_coef
			s1FWHM = W_coef[3] * 1.66511 // 2*sqrt(ln(2)) = 1.66511 @see GaussPeakParams
			s1nm = W_coef[2]
			WaveClear W_coef
		catch
			err = GetRTError(1)
		endtry
		WaveClear fitme, xfitme

		// fit excitation wavelength
		T_Constraints[1] = "K2 > " + num2str(s2nm - numDeltaS2 / 2)
		T_Constraints[2] = "K2 < " + num2str(s2nm + numDeltaS2 / 2)
		T_Constraints[3] = "K3 > " + num2str(cminS2fwhm / 1.66511)
		T_Constraints[4] = "K3 < " + num2str(cmaxS2fwhm / 1.66511)

		[ pStart, pEnd ] = FindLevelWrapper(stats.wavWavelength, s1nm, accuracy = pAccuracy)
		[ qStart, qEnd ] = FindLevelWrapper(stats.wavExcitation, s2nm, accuracy = qAccuracy)

		Duplicate/FREE/R=[pStart, pEnd][qStart, qEnd] PLEM dummy
		MatrixOP/FREE fitme = sumCols(dummy)^t
		Duplicate/FREE/R=[qStart, qEnd] stats.wavExcitation xfitme
		try
			CurveFit/Q gauss fitme/X=xfitme/C=T_Constraints; AbortOnRTE
			WAVE W_coef
			s2FWHM = W_coef[3] * 1.66511 // 2*sqrt(ln(2)) = 1.66511 @see GaussPeakParams
			s2nm = W_coef[2]
			WaveClear W_coef
		catch
			err = GetRTError(1)
		endtry
		WaveClear fitme, xfitme

		stats.wavEnergyS2[i] = s2nm
		stats.wavFWHMS2[i] = s2FWHM

		// fit emission to get max intensity
		T_Constraints[1] = "K2 > " + num2str(s1nm - numDeltaS1 / 2)
		T_Constraints[2] = "K2 < " + num2str(s1nm + numDeltaS1 / 2)
		T_Constraints[3] = "K3 > " + num2str(cminS1fwhm / 1.66511) // 2*sqrt(ln(2)) = 1.66511
		T_Constraints[4] = "K3 < " + num2str(cmaxS1fwhm / 1.66511) // 2*sqrt(ln(2)) = 1.66511

		[ pStart, pEnd ] = FindLevelWrapper(stats.wavWavelength, s1nm, accuracy = pAccuracy * 4)
		FindLevel/Q/P/T=(qAccuracy) stats.wavExcitation, s2nm
		qStart = V_Flag ? 0 : min(DimSize(stats.wavExcitation, 0) - 1, max(0, round(V_levelX)))

		Duplicate/FREE/R=[pStart, pEnd][qStart] PLEM fitme
		Redimension/N=(-1, 0) fitme
		Duplicate/FREE/R=[pStart, pEnd] stats.wavWavelength xfitme
		try
			CurveFit/Q gauss fitme/X=xfitme/C=T_Constraints; AbortOnRTE
			WAVE W_coef
			intensity = W_coef[1]
			s1FWHM = W_coef[3] * 1.66511 // 2*sqrt(ln(2)) = 1.66511 @see GaussPeakParams
			s1nm = W_coef[2]
			WaveClear W_coef
		catch
			err = GetRTError(1)
		endtry
		WaveClear fitme, xfitme

		stats.wav1Dfit[i] = intensity
		stats.wavEnergyS1[i] = s1nm
		stats.wavFWHMS1[i] = s1FWHM
	endfor
End

Function PLEMd2AtlasFit3D(strPLEM, [show])
	String strPLEM
	Variable show

	Variable i, numS1, numS2
	Variable numDeltaS1left, numDeltaS1right, numDeltaS2bottom, numDeltaS2top
	Variable rightXvalue, topYvalue
	Variable err
	Variable s1FWHM, s2FWHM, s2emi, s1exc, distortion, intensity
	String winPLEMfit, winPLEM
	Struct PLEMd2stats stats

	Variable V_fitOptions = 4 // used to suppress CurveFit dialog
	Variable numDeltaS1 = 30
	Variable numDeltaS2 = 30

	show = ParamIsDefault(show) ? 0 : show

	if(PLEMd2MapExists(strPLEM) == 0)
		print "PLEMd2AtlasFit: Map does not exist properly"
		return 1
	endif

	PLEMd2statsLoad(stats, strPLEM)
	WAVE PLEM = removeSpikes(PLEMd2NanotubeRangePLEM(stats))
	Redimension/N=(DimSize(PLEM, 0), DimSize(PLEM, 1), numpnts(stats.wavEnergyS1)) stats.wavPLEMfit
	CopyScales PLEM, stats.wavPLEMfit, stats.wavPLEMfitSingle

	rightXvalue = stats.numPLEMleftX + stats.numPLEMTotalX * stats.numPLEMdeltaX
	topYvalue = stats.numPLEMbottomY + stats.numPLEMTotalY * stats.numPLEMdeltaY

	// input
	for(i = 0; i < numpnts(stats.wavEnergyS1); i += 1)
		numS1 = stats.wavEnergyS1[i] // x
		numS2 = stats.wavEnergyS2[i] // y

		numDeltaS1left   = numS1 - numDeltaS1
		numDeltaS1right  = numS1 + numDeltaS1
		numDeltaS2bottom = numS2 - numDeltaS2
		numDeltaS2top    = numS2 + numDeltaS2

		// 2*sqrt(ln(2)) = 1.66511 @see GaussPeakParams
		Make/O/T/N=10 root:T_Constraints/WAVE=T_Constraints
		T_Constraints[0] = "K2 > " + num2str(numDeltaS1left)
		T_Constraints[1] = "K2 < " + num2str(numDeltaS1right)
		T_Constraints[2] = "K4 > " + num2str(numDeltaS2bottom)
		T_Constraints[3] = "K4 < " + num2str(numDeltaS2top)
		T_Constraints[4] = "K3 > " + num2str(cminS1fwhm / 1.66511)
		T_Constraints[5] = "K3 < " + num2str(cmaxS1fwhm / 1.66511)
		T_Constraints[6] = "K5 > " + num2str(cminS2fwhm / 1.66511)
		T_Constraints[7] = "K5 < " + num2str(cmaxS2fwhm / 1.66511)
		T_Constraints[8] = "K6 > -0.1"
		T_Constraints[9] = "K6 < 0.1"

		stats.wavPLEMfit[][][i] = 0
		stats.wav2Dfit[i] = 0

		try
			CurveFit/Q gauss2D PLEM(numDeltaS1left, numDeltaS1right)(numDeltaS2bottom, numDeltaS2top)/T=T_Constraints; AbortOnRTE
		catch
			err = GetRTError(1)
			continue
		endtry

		Wave W_coef
		intensity = W_coef[1]
		s1FWHM = W_coef[3] * 1.66511
		s2FWHM = W_coef[5] * 1.66511
		distortion = abs(W_coef[6])
		s2emi = W_coef[4]
		s1exc = W_coef[2]

		// check if distorted gaussian
		if(distortion > 0.5)
			continue
		endif
		// check if fwhm is within a valid range s2 is typically 15nm and s1 5nm
		if((s1FWHM > cmaxS1fwhm * 2) || (s1FWHM < cminS1fwhm / 2) || (s2FWHM > cmaxS2fwhm * 2) || (s2FWHM < cminS2fwhm / 2))
			continue
		endif
		// error checking
		if((abs((numS1 - s1exc) / numS1) > 0.25 ) || (abs((numS2 - s2emi) / numS2) > 0.25 ))
			continue
		endif

		stats.wavEnergyS1[i] = s1exc
		stats.wavEnergyS2[i] = s2emi
		stats.wavFWHMS1[i] = s1FWHM
		stats.wavFWHMS2[i] = s2FWHM
		stats.wav2Dfit[i] = W_coef[1]*2*pi* W_coef[3]* W_coef[5]*sqrt(1-W_coef[6]^2) // volume of 2d gauss without baseline
		stats.wav1Dfit[i] = intensity
		stats.wavPLEMfit[][][i] = Gauss2D(W_coef, x, y)
		WaveClear W_coef
	endfor

	// add all maps to one map
	PLEMd2AtlasMerge3d(stats.wavPLEMfit, stats.wavPLEMfitSingle)

	if(!show)
		return 0
	endif

	// check if window already exists
	winPLEM = PLEMd2getWindow(stats.strPLEM)
	DoWindow $winPLEM
	// DoWindow sets the variable V_flag:
	// 	1 window existed
	// 	0 no such window
	// 	2 window is hidden.
	if(!!V_flag)
		String listContour = ContourNameList("", ";")
		for(i = 0; i < ItemsInList(listContour); i += 1)
			RemoveContour/W=$winPLEM $(StringFromList(i, listContour))
		endfor
		AppendMatrixContour/W=$winPLEM stats.wavPLEMfitSingle
		ModifyContour/W=$winPLEM ''#0 labels=0,autoLevels={0,*,10}
	endif

	// check if window already exists
	winPLEMfit = PLEMd2getWindow(stats.strPLEM) + "_fit"
	DoWindow/F $winPLEMfit
	if(V_flag == 2)
		print "PLEMd2AtlasFit: Fit-Graph was hidden. Case not handled. check code"
	elseif(V_flag == 0)
		Display
		DoWindow/C/N/R $winPLEMfit
		Appendimage stats.wavPLEMfitSingle
		PLEMd2Decorate()
	endif
End

Function PLEMd2AtlasMerge3d(wave3d, wave2d)
	Wave wave3d, wave2d

	Variable i

	Duplicate/O/R=[][][0] wave3d wave2d
	Redimension/N=(Dimsize(wave3d, 0), Dimsize(wave3d, 1)) wave2d

	for(i = 1; i < Dimsize(wave3d, 2); i += 1)
		wave2d += wave3d[p][q][i]
	endfor
End

Function PLEMd2AtlasShow(strPLEM)
	String strPLEM
	Struct PLEMd2stats stats
	if(PLEMd2MapExists(strPLEM) == 0)
		print "PLEMd2AtlasShow: Map does not exist properly"
		return 0
	endif
	PLEMd2statsLoad(stats, strPLEM)
	PLEMd2Display(strPLEM)
	PLEMd2AtlasHide(strPLEM) //prevent multiple traces

	AppendToGraph stats.wavEnergyS2/TN=plem01 vs stats.wavEnergyS1
	AppendToGraph stats.wavEnergyS2/TN=plem02 vs stats.wavEnergyS1
	AppendToGraph stats.wavEnergyS2/TN=plem03 vs stats.wavEnergyS1

	ModifyGraph mode(plem02)=3 //cross
	ModifyGraph marker(plem02)=1
	ModifyGraph rgb(plem02)=(0,0,0)

	ModifyGraph mode(plem03)=3 //dots
	ModifyGraph useMrkStrokeRGB(plem03)=1
	ModifyGraph textMarker(plem03)={stats.wavChiralityText, "default",0,0,5,0.00,10.00} //labels top
	ModifyGraph rgb(plem03)=(65535,65535,65535)
	ModifyGraph mrkStrokeRGB(plem03)=(65535,65535,65535)

	ModifyGraph mode(plem01)=3 , msize(plem01)=2
	ModifyGraph marker(plem01)=5 // squares
	ModifyGraph marker(plem01)=8 // circles
	ModifyGraph msize(plem01)=5
	ModifyGraph height={Plan,1,left,bottom}
	ModifyGraph height=0
End

Function PLEMd2AtlasHide(strPLEM)
	String strPLEM
	if(PLEMd2MapExists(strPLEM) == 0)
		print "PLEMd2AtlasHide: Map does not exist properly"
		return 0
	endif
	PLEMd2Display(strPLEM)
	RemoveFromGraph/Z plem01
	RemoveFromGraph/Z plem02
	RemoveFromGraph/Z plem03

	Variable i
	String listContour = ContourNameList("", ";")
	for(i = 0; i < ItemsInList(listContour); i += 1)
		RemoveContour $(StringFromList(i, listContour))
	endfor
End

//adapted from function OpenFileDialog on http://www.entorb.net/wickie/IGOR_Pro
Function/S PLEMd2PopUpChooseFile([strPrompt])
	String strPrompt

	Variable refNum
	String strFileName, strLastPath, strFolderName
	String fileFilters = "Igor Binary File (*.ibw):.ibw;General Text Files (*.txt, *.csv):.txt,.csv;All Files:.*;"

	// get last path
	Struct PLEMd2Prefs prefs
	PLEMd2LoadPackagePrefs(prefs)
	strLastPath = prefs.strLastPath

	GetFileFolderInfo/Q/Z=1 strLastPath
	if(V_Flag || !V_isFolder)
		strLastPath = prefs.strBasePath
		GetFileFolderInfo/Q/Z=1 strLastPath
		if(V_Flag || !V_isFolder)
			strLastPath = SpecialDirPath("Documents", 0, 0, 0 )
		endif
	endif

	//Display file Dialog starting with last path
	NewPath/O/Q PLEMd2LastPath, strLastPath
	if(V_flag)
		Abort "Invalid Path"
	endif
	PathInfo/S PLEMd2LastPath
	if(!V_flag)
		Abort "Symbolic path does not exist"
	endif
	strPrompt = SelectString(ParamIsDefault(strPrompt), strPrompt, "choose file")
	Open/D/R/Z=2/F=fileFilters/M=strPrompt refNum
	if(V_flag)
		KillPath/Z PLEMd2LastPath
		return ""
	endif
	strFileName = S_fileName
	KillPath/Z PLEMd2LastPath // we don't need the path reference

	// save as last path
	strFolderName = ParseFilePath(1, strFileName, ":", 1, 0)
	GetFileFolderInfo/Q/Z=1 strFolderName
	if(V_isFolder)
		prefs.strLastPath = strFolderName
		PLEMd2SavePackagePrefs(prefs)
	endif

	return strFileName
End

// @todo only use wave @c mapsAvailable here
// @see PLEMd2getAllstrPLEM
Function PLEMd2getMapsAvailable()
	DFREF dfr = $cstrPLEMd2root
	NVAR/Z numMaps = dfr:gnumMapsAvailable
	if(!NVAR_EXISTS(numMaps))
		return ItemsInList(PLEMd2getStrMapsAvailable())
	endif

	return numMaps
End

// @todo only use wave @c mapsAvailable here
// @see PLEMd2getAllstrPLEM
Function/S PLEMd2getStrMapsAvailable()
	DFREF dfr = $cstrPLEMd2root
	SVAR/Z strMaps = dfr:gstrMapsAvailable
	if(!SVAR_EXISTS(strMaps))
		return ""
	endif

	return strMaps
End

Function PLEMd2AddMap(strMap)
	String strMap

	DFREF dfr = $cstrPLEMd2root
	SVAR gstrMapsAvailable = dfr:gstrMapsAvailable
	NVAR gnumMapsAvailable = dfr:gnumMapsAvailable

	Variable numFind

	numFind = FindListItem(strMap, gstrMapsAvailable)
	if(numFind == -1)
		gstrMapsAvailable += strMap + ";"
		numFind = ItemsInList(gstrMapsAvailable)
		gnumMapsAvailable = numFind
	else
		gnumMapsAvailable = ItemsInList(gstrMapsAvailable)
	endif

	return numFind
End

Function PLEMd2KillMap(strMap)
	String strMap

	DFREF dfr = $cstrPLEMd2root
	SVAR gstrMapsAvailable = dfr:gstrMapsAvailable
	NVAR gnumMapsAvailable = dfr:gnumMapsAvailable

	if(FindListItem(strMap, gstrMapsAvailable) != -1)
		gstrMapsAvailable = RemoveFromList(strMap, gstrMapsAvailable)
		gnumMapsAvailable = ItemsInList(gstrMapsAvailable)
	endif

	DFREF dfrMap = PLEMd2MapFolder(strMap)
	if(DataFolderRefStatus(dfrMap) != 0)
		WAVE/Z wavIBW = dfrMap:IBW
		SetWaveLock 0, wavIBW
		WaveClear wavIBW
		KillDataFolder/Z dfrMap
		if(V_flag)
			printf "PLEMd2KillMap: DataFolder %s could not be deleted.\r", strMap
		endif
	endif
End

Function PLEMd2KillMapByNum(numPLEM)
	Variable numPLEM
	if(numPLEM < 0)
		print "PLEMd2KillMapByNum: Wrong Function Call numPLEM out of range"
		return 0
	endif
	String strPLEM
	strPLEM = PLEMd2strPLEM(numPLEM)
	PLEMd2KillMap(strPLEM)
End

Function PLEMd2MapExists(strMap)
	String strMap

	String strMaps = PLEMd2getStrMapsAvailable()

	if(FindListItem(strMap, strMaps) != -1)
		return 1
	endif

	return 0
End

// check maps folder for content
//
// return number of maps
Function PLEMd2MapStringReInit()
	DFREF dfr = $cstrPLEMd2root
	SVAR gstrMapsFolder = dfr:gstrMapsFolder
	SVAR gstrMapsAvailable = dfr:gstrMapsAvailable
	NVAR gnumMapsAvailable = dfr:gnumMapsAvailable

	Variable i, numMapsAvailable
	String strMap
	gstrMapsAvailable 	= ""
	gnumMapsAvailable 	= 0
	numMapsAvailable = CountObjects(gstrMapsFolder, 4) // number of data folders

	for(i = 0; i < numMapsAvailable; i += 1)
		strMap = GetIndexedObjName(gstrMapsFolder,4,i)
		gstrMapsAvailable += strMap + ";"
	endfor
	gnumMapsAvailable = ItemsInList(gstrMapsAvailable)

	return gnumMapsAvailable
End

//Helper Function
Function/S PLEMd2SetWaveScale(wavX, wavY, strOut)
	Wave wavX, wavY
	String strOut
	Variable numSize, numOffset, numDelta
	if(!waveExists(wavX) && !waveExists(wavY))
		print "Error: Waves Do not exist or user cancelled at Prompt"
		return ""
	endif

	if(!WaveExists($strOut))
		Duplicate/O wavY $strOut
	else
		if(!StringMatch(GetWavesDataFolder(wavY, 2),GetWavesDataFolder($strOut, 2)))
			Duplicate/O wavY $strOut
		endif
	endif
	wave wavOut = $strOut

	numSize		= DimSize(wavX,0)
	numOffset	= wavX[0]
	numDelta 	= (wavX[(numSize-1)] - wavX[0]) / (numSize-1)

	SetScale/P x, numOffset, numDelta, "", wavOut
	SetScale/P y, 1, 1, "", wavOut

	return GetWavesDataFolder(wavOut, 2)
End

//Function sorts two Numbers
Function PLEMd2sort(left, right)
	Variable &left,&right

	Variable temp
	if(left > right)
		temp=left
		left=right
		right=temp
		temp=0
	endif
End

//Get number of map. in Menu-List.
Function PLEMd2numPLEM(strPLEM)
	String strPLEM

	DFREF dfr = $cstrPLEMd2root
	string strMaps = PLEMd2getStrMapsAvailable()

	return FindListItem(strPLEM, strMaps)
End

// @todo only use wave @c mapsAvailable
// @see PLEMd2getAllstrPLEM PLEMd2getStrMapsAvailable
Function/S PLEMd2strPLEM(numPLEM)
	Variable numPLEM

	DFREF dfr = $cstrPLEMd2root
	string strMaps = PLEMd2getStrMapsAvailable()

	return StringFromList(numPLEM, strMaps)
End

// @todo make this the main dependency for @c PLEMd2getStrMapsAvailable and related
//       make it more robust with crc and datafolder / original IBW checkings
Function/WAVE PLEMd2getAllstrPLEM([forceRenew])
	Variable forceRenew

	String strPLEM
	Variable i
	Variable numMaps = PLEMd2getMapsAvailable() // @todo remove dependency

	DFREF dfr = $cstrPLEMd2root
	Struct PLEMd2stats stats

	forceRenew = ParamIsDefault(forceRenew) ? 0 : !!forceRenew

	WAVE/T/Z wv = dfr:mapsAvailable
	if(WaveExists(wv) && !forceRenew)
		if(DimSize(wv, 0) == numMaps)
			return wv
		else
			Redimension/N=(numMaps) wv
		endif
	else
		Make/O/T/N=(numMaps) dfr:mapsAvailable/WAVE=wv
	endif

	wv[] = PLEMd2strPLEM(p)

	return wv
End

Function/WAVE PLEMd2getCoordinates([forceRenew])
	Variable forceRenew

	Variable i
	Variable numMaps = PLEMd2getMapsAvailable()

	DFREF dfr = $cstrPLEMd2root
	Struct PLEMd2stats stats

	forceRenew = ParamIsDefault(forceRenew) ? 0 : !!forceRenew

	WAVE/Z wv = dfr:coordinates
	if(WaveExists(wv) && !forceRenew)
		if(DimSize(wv, 0) == numMaps)
			return wv
		else
			Redimension/N=(numMaps, -1) wv
		endif
	else
		Make/O/N=(numMaps, 3) dfr:coordinates/WAVE=wv = NaN
	endif

	WAVE/T wavStrPLEM = PLEMd2getAllstrPLEM()
	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, wavStrPLEM[i])
		wv[i][0] = stats.numPositionX
		wv[i][1] = stats.numPositionY
		wv[i][2] = stats.numPositionZ
	endfor

	return wv
End

// Get the excitation Power from the maps in the given range
//
// @param overwrite if set to 1: recreate the wave if it already exists
// @param range     specify the spectra ids with a numeric, uint wave
Function/WAVE PLEMd2getPower([overwrite, range])
	Variable overwrite
	WAVE/U/I range

	Variable i, dim0, dim1

	DFREF dfr = $cstrPLEMd2root
	Struct PLEMd2stats stats

	overwrite = ParamIsDefault(overwrite) ? 0 : !!overwrite

	if(ParamIsDefault(range))
		Make/FREE/U/I/N=(PLEMd2getMapsAvailable()) range = p
	endif
	dim0 = DimSize(range, 0)

	WAVE/Z wv = dfr:power
	if(WaveExists(wv) && !overwrite)
		if(DimSize(wv, 0) == dim0)
			return wv
		endif
	endif

	PLEMd2statsLoad(stats, PLEMd2strPLEM(range[0]))
	dim1 = DimSize(stats.wavPLEM, 1)
	if(dim1 == 1)
		dim1 = 0
	endif
	Make/O/N=(dim0, dim1) dfr:power/WAVE=wv = NaN

	WAVE/T wavStrPLEM = PLEMd2getAllstrPLEM()
	for(i = 0; i < dim0; i += 1)
		PLEMd2statsLoad(stats, wavStrPLEM[range[i]])
		wv[i][] = stats.wavYpower[q]
	endfor

	printf "PLEMd2getPower: created %s\r", GetWavesDataFolder(wv, 2)

	return wv
End

Function/WAVE PLEMd2getPhoton([forceRenew])
	Variable forceRenew

	Variable i
	Variable numMaps = PLEMd2getMapsAvailable()

	DFREF dfr = $cstrPLEMd2root
	Struct PLEMd2stats stats

	forceRenew = ParamIsDefault(forceRenew) ? 0 : !!forceRenew

	WAVE/Z wv = dfr:photon
	if(WaveExists(wv) && !forceRenew)
		if(DimSize(wv, 0) == numMaps)
			return wv
		else
			Redimension/N=(numMaps, -1) wv
		endif
	else
		PLEMd2statsLoad(stats, PLEMd2strPLEM(0))
		Make/O/N=(numMaps, DimSize(stats.wavPLEM, 1)) dfr:photon/WAVE=wv = NaN
	endif

	WAVE/T wavStrPLEM = PLEMd2getAllstrPLEM()
	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, wavStrPLEM[i])
		wv[i][] = stats.wavYphoton[q]
	endfor

	print GetWavesDataFolder(wv, 0)
	return wv
End


Function/WAVE PLEMd2getDetectors()
	Variable i
	Variable numMaps

	DFREF dfr = $cstrPLEMd2root
	Struct PLEMd2stats stats

	WAVE/T wavStrPLEM = PLEMd2getAllstrPLEM()

	WAVE/U/B/Z wv = dfr:detectors
	NVAR/Z crc = dfr:gnumDetectors
	if(WaveExists(wv) && NVAR_Exists(crc) && WaveCRC(0, wavStrPLEM) == crc)
		return wv
	endif

	if(!NVAR_Exists(crc))
		Variable/G dfr:gnumDetectors
		NVAR crc = dfr:gnumDetectors
	endif
	numMaps = PLEMd2getMapsAvailable()
	Make/O/U/B/N=(numMaps) dfr:detectors/WAVE=wv = NaN

	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, wavStrPLEM[i])
		wv[i] = stats.numDetector
	endfor
	crc = WaveCRC(0, wavStrPLEM)

	return wv
End

// return PLEM with measurement range (setup specific to microscope with 830lp)
Function/WAVE PLEMd2NanotubeRangePLEM(stats)
	Struct PLEMd2stats &stats

	STRUCT cntRange range
	PLEMd2GetAcceptableRange(stats, range)

	Variable qMin, qMax
	Variable pMin = ScaleToIndex(stats.wavPLEM, range.xMin, 0)
	Variable pMax = ScaleToIndex(stats.wavPLEM, range.xMax, 0)

	if(DimSize(stats.wavPLEM, 1) > 1)
		qMin = ScaleToIndex(stats.wavPLEM, range.yMin, 1)
		qMax = ScaleToIndex(stats.wavPLEM, range.yMax, 1)
		Duplicate/FREE/R=[pMin, pMax][qMin, qMax] stats.wavPLEM, wv
	else
		Duplicate/FREE/R=[pMin, pMax] stats.wavPLEM, wv
	endif
	return wv
End

Structure cntRange
	Variable xMin, xMax
	Variable yMin, yMax
EndStructure

Function PLEMd2GetAcceptableRange(stats, range)
	STRUCT PLEMd2stats &stats
	STRUCT cntRange &range

	if(stats.numDetector > 1)
		Abort "PLEMd2GetAcceptableRange: Only for Newton and Idus"
	endif

	range.xMin = stats.numDetector == PLEMd2detectorNewton ? 830 : 1000
	range.xMax = stats.numDetector == PLEMd2detectorNewton ? 1040 : 1280
	range.yMin = 525
	range.yMax = 760
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

Menu "PLEM", dynamic //create menu bar entry
	PLEMd2MenuInit() + "Init", /q, PLEMd2initialize()
	"Set Paths", PLEMd2SetPaths()
	"Open", PLEMd2open()
	"-"
	SubMenu "Display"
	//List all available Maps in current project (max 30)
		PLEMd2Menu(0), PLEMd2DisplayByNum(0)
		PLEMd2Menu(1), PLEMd2DisplayByNum(1)
		PLEMd2Menu(2), PLEMd2DisplayByNum(2)
		PLEMd2Menu(3), PLEMd2DisplayByNum(3)
		PLEMd2Menu(4), PLEMd2DisplayByNum(4)
		PLEMd2Menu(5), PLEMd2DisplayByNum(5)
		PLEMd2Menu(6), PLEMd2DisplayByNum(6)
		PLEMd2Menu(7), PLEMd2DisplayByNum(7)
		PLEMd2Menu(8), PLEMd2DisplayByNum(8)
		PLEMd2Menu(9), PLEMd2DisplayByNum(9)
		PLEMd2Menu(10), PLEMd2DisplayByNum(10)
		PLEMd2Menu(11), PLEMd2DisplayByNum(11)
		PLEMd2Menu(12), PLEMd2DisplayByNum(12)
		PLEMd2Menu(13), PLEMd2DisplayByNum(13)
		PLEMd2Menu(14), PLEMd2DisplayByNum(14)
		PLEMd2Menu(15), PLEMd2DisplayByNum(15)
		PLEMd2Menu(16), PLEMd2DisplayByNum(16)
		PLEMd2Menu(17), PLEMd2DisplayByNum(17)
		PLEMd2Menu(18), PLEMd2DisplayByNum(18)
		PLEMd2Menu(19), PLEMd2DisplayByNum(19)
		PLEMd2Menu(20), PLEMd2DisplayByNum(20)
		PLEMd2Menu(21), PLEMd2DisplayByNum(21)
		PLEMd2Menu(22), PLEMd2DisplayByNum(22)
		PLEMd2Menu(23), PLEMd2DisplayByNum(23)
		PLEMd2Menu(24), PLEMd2DisplayByNum(24)
		PLEMd2Menu(25), PLEMd2DisplayByNum(25)
		PLEMd2Menu(26), PLEMd2DisplayByNum(26)
		PLEMd2Menu(27), PLEMd2DisplayByNum(27)
		PLEMd2Menu(28), PLEMd2DisplayByNum(28)
		PLEMd2Menu(29), PLEMd2DisplayByNum(29)
	End

	"Info", PLEMd2Panel()
	"Atlas", PLEMd2PanelAtlas()
	"-"
	SubMenu "Duplicate"
		PLEMd2Menu(0), PLEMd2DuplicateByNum(0)
		PLEMd2Menu(1), PLEMd2DuplicateByNum(1)
		PLEMd2Menu(2), PLEMd2DuplicateByNum(2)
		PLEMd2Menu(3), PLEMd2DuplicateByNum(3)
		PLEMd2Menu(4), PLEMd2DuplicateByNum(4)
		PLEMd2Menu(5), PLEMd2DuplicateByNum(5)
		PLEMd2Menu(6), PLEMd2DuplicateByNum(6)
		PLEMd2Menu(7), PLEMd2DuplicateByNum(7)
		PLEMd2Menu(8), PLEMd2DuplicateByNum(8)
		PLEMd2Menu(9), PLEMd2DuplicateByNum(9)
		PLEMd2Menu(10), PLEMd2DuplicateByNum(10)
		PLEMd2Menu(11), PLEMd2DuplicateByNum(11)
		PLEMd2Menu(12), PLEMd2DuplicateByNum(12)
		PLEMd2Menu(13), PLEMd2DuplicateByNum(13)
		PLEMd2Menu(14), PLEMd2DuplicateByNum(14)
		PLEMd2Menu(15), PLEMd2DuplicateByNum(15)
		PLEMd2Menu(16), PLEMd2DuplicateByNum(16)
		PLEMd2Menu(17), PLEMd2DuplicateByNum(17)
		PLEMd2Menu(18), PLEMd2DuplicateByNum(18)
		PLEMd2Menu(19), PLEMd2DuplicateByNum(19)
		PLEMd2Menu(20), PLEMd2DuplicateByNum(20)
		PLEMd2Menu(21), PLEMd2DuplicateByNum(21)
		PLEMd2Menu(22), PLEMd2DuplicateByNum(22)
		PLEMd2Menu(23), PLEMd2DuplicateByNum(23)
		PLEMd2Menu(24), PLEMd2DuplicateByNum(24)
		PLEMd2Menu(25), PLEMd2DuplicateByNum(25)
		PLEMd2Menu(26), PLEMd2DuplicateByNum(26)
		PLEMd2Menu(27), PLEMd2DuplicateByNum(27)
		PLEMd2Menu(28), PLEMd2DuplicateByNum(28)
		PLEMd2Menu(29), PLEMd2DuplicateByNum(29)
	End
	SubMenu "Kill"
		PLEMd2Menu(0), PLEMd2KillMapByNum(0)
		PLEMd2Menu(1), PLEMd2KillMapByNum(1)
		PLEMd2Menu(2), PLEMd2KillMapByNum(2)
		PLEMd2Menu(3), PLEMd2KillMapByNum(3)
		PLEMd2Menu(4), PLEMd2KillMapByNum(4)
		PLEMd2Menu(5), PLEMd2KillMapByNum(5)
		PLEMd2Menu(6), PLEMd2KillMapByNum(6)
		PLEMd2Menu(7), PLEMd2KillMapByNum(7)
		PLEMd2Menu(8), PLEMd2KillMapByNum(8)
		PLEMd2Menu(9), PLEMd2KillMapByNum(9)
		PLEMd2Menu(10), PLEMd2KillMapByNum(10)
		PLEMd2Menu(11), PLEMd2KillMapByNum(11)
		PLEMd2Menu(12), PLEMd2KillMapByNum(12)
		PLEMd2Menu(13), PLEMd2KillMapByNum(13)
		PLEMd2Menu(14), PLEMd2KillMapByNum(14)
		PLEMd2Menu(15), PLEMd2KillMapByNum(15)
		PLEMd2Menu(16), PLEMd2KillMapByNum(16)
		PLEMd2Menu(17), PLEMd2KillMapByNum(17)
		PLEMd2Menu(18), PLEMd2KillMapByNum(18)
		PLEMd2Menu(19), PLEMd2KillMapByNum(19)
		PLEMd2Menu(20), PLEMd2KillMapByNum(20)
		PLEMd2Menu(21), PLEMd2KillMapByNum(21)
		PLEMd2Menu(22), PLEMd2KillMapByNum(22)
		PLEMd2Menu(23), PLEMd2KillMapByNum(23)
		PLEMd2Menu(24), PLEMd2KillMapByNum(24)
		PLEMd2Menu(25), PLEMd2KillMapByNum(25)
		PLEMd2Menu(26), PLEMd2KillMapByNum(26)
		PLEMd2Menu(27), PLEMd2KillMapByNum(27)
		PLEMd2Menu(28), PLEMd2KillMapByNum(28)
		PLEMd2Menu(29), PLEMd2KillMapByNum(29)
	End
End

Function/S PLEMd2Menu(numPLEM)
	//directory persistent
	Variable numPLEM
	String strReturn = ""

	//dynamic Menus are called every time the menu bar is pressed.
	//global Variables should not automatically occur in other projects. so don't create them.
	if(PLEMd2isInitialized())
		SVAR gstrMapsAvailable = $(cstrPLEMd2root + ":gstrMapsAvailable")
		NVAR gnumMapsAvailable	 = $(cstrPLEMd2root + ":gnumMapsAvailable")
		if(numPLEM<gnumMapsAvailable)
			strReturn = StringFromList(numPLEM, gstrMapsAvailable)
		endif
	endif

	return strReturn
End

Function/t PLEMd2MenuInit()
	if(PLEMd2isInitialized())
		return "!" + num2char(18) //on
	else
		return "" // off
	endif
End

Function PLEMd2SetPaths()
	PLEMd2SetBasePath()
	PLEMd2SetCorrectionPath()
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

StrConstant PLEMd2PackageName = "PLEM-displayer2"
static StrConstant PLEMd2PrefsFileName = "PLEMd2Preferences.bin"
static Constant PLEMd2PrefsRecordID = 0
static Constant reserved = 70  // Reserved uint32 capacity for future use

// Global Preferences stored in Igor Folder
Structure PLEMd2Prefs
	uint32 version
	double panelCoords[4]
	uchar  strLastPath[256]
	char   strBasePath[40]
	char   strCorrectionPath[80]
	uint32 reserved[reserved]
EndStructure

//	Sets prefs structure to default values.
static Function PLEMd2DefaultPackagePrefsStruct(prefs)
	STRUCT PLEMd2Prefs &prefs

	prefs.version = cPLEMd2Version

	prefs.panelCoords[0] = 5			// Left
	prefs.panelCoords[1] = 40		// Top
	prefs.panelCoords[2] = 5+190	// Right
	prefs.panelCoords[3] = 40+125	// Bottom

	prefs.strLastPath = SpecialDirPath("Documents", 0, 0, 0)
	prefs.strBasePath = ""
	prefs.strCorrectionPath = ""
	Variable i
	for(i = 0; i < reserved; i += 1)
		prefs.reserved[i] = 0
	endfor
End

// SyncPackagePrefsStruct(prefs)
// Syncs package prefs structures to match state of panel. Call this only if the panel exists.
static Function PLEMd2SyncPackagePrefsStruct(prefs)
	STRUCT PLEMd2Prefs &prefs

	// Panel does exists. Set prefs to match panel settings.
	prefs.version = cPLEMd2Version

	GetWindow  PLEMd2Panel wsize
	// NewPanel uses device coordinates. We therefore need to scale from
	// points (returned by GetWindow) to device units for windows created
	// by NewPanel.
	Variable scale = ScreenResolution / 72
	prefs.panelCoords[0] = V_left * scale
	prefs.panelCoords[1] = V_top * scale
	prefs.panelCoords[2] = V_right * scale
	prefs.panelCoords[3] = V_bottom * scale

//	ControlInfo /W=PLEMd2Panel PhaseLock
//	prefs.phaseLock = V_Value		// 0=unchecked; 1=checked

End

// InitPackagePrefsStruct(prefs)
// Sets prefs structures to match state of panel or to default values if panel does not exist.
static Function PLEMd2InitPackagePrefsStruct(prefs)
	STRUCT PLEMd2Prefs &prefs

	DoWindow PLEMd2Panel
	if(V_flag == 0)
		// Panel does not exist. Set prefs struct to default.
		PLEMd2DefaultPackagePrefsStruct(prefs)
	else
		// Panel does exists. Sync prefs struct to match panel state.
		PLEMd2SyncPackagePrefsStruct(prefs)
	endif
End

Function PLEMd2LoadPackagePrefs(prefs)
	STRUCT PLEMd2Prefs &prefs

	// This loads preferences from disk if they exist on disk.
	LoadPackagePreferences PLEMd2PackageName, PLEMd2PrefsFileName, PLEMd2PrefsRecordID, prefs

	// If error or prefs not found or not valid, initialize them.
	if(V_flag!=0 || V_bytesRead==0 || prefs.version!= cPLEMd2Version)
		//print "PLEMd2:LoadPackagePrefs: Loading from " + SpecialDirPath("Packages", 0, 0, 0)
		PLEMd2InitPackagePrefsStruct(prefs)	// Set based on panel if it exists or set to default values.
		PLEMd2SavePackagePrefs(prefs)		// Create initial prefs record.
	endif
End

Function PLEMd2SavePackagePrefs(prefs)
	STRUCT PLEMd2Prefs &prefs
	//print "PLEMd2:SavePackagePrefs: Saving to " + SpecialDirPath("Packages", 0, 0, 0)
	SavePackagePreferences PLEMd2PackageName, PLEMd2PrefsFileName, PLEMd2PrefsRecordID, prefs
End

// Save the location of the base path where all ibw files are saved.
//
// DisplayHelpTopic "Symbolic Paths"
Function PLEMd2SetBasePath()
	String strBasePath

	Struct PLEMd2Prefs prefs
	PLEMd2LoadPackagePrefs(prefs)

	strBasePath = prefs.strBasePath
	NewPath/O/Q/Z PLEMbasePath, strBasePath
	if(!V_flag)
		PathInfo/S PLEMbasePath
	endif

	NewPath/O/Q/Z/M="Set PLEM base path" PLEMbasePath
	if(V_flag)
		return 0 // user canceled
	endif

	PathInfo PLEMbasePath
	strBasePath = S_path
	if(!V_flag)
		return 0 // invalid path
	endif

	GetFileFolderInfo/Q/Z=1 strBasePath
	if(!V_flag && V_isFolder)
		prefs.strBasePath = strBasePath
		PLEMd2SavePackagePrefs(prefs)
	endif
End

// Save the location of the base path where all ibw files are saved.
//
// DisplayHelpTopic "Symbolic Paths"
Function PLEMd2SetCorrectionPath()
	String strCorrectionPath

	Struct PLEMd2Prefs prefs
	PLEMd2LoadPackagePrefs(prefs)

	strCorrectionPath = prefs.strCorrectionPath
	NewPath/O/Q/Z PLEMCorrectionPath, strCorrectionPath
	if(!V_flag)
		PathInfo/S PLEMCorrectionPath
	endif

	NewPath/O/Q/Z/M="Set PLEMCorrectionPath path" PLEMCorrectionPath
	if(V_flag)
		return 0 // user canceled
	endif

	PathInfo PLEMCorrectionPath
	strCorrectionPath = S_path
	if(!V_flag)
		return 0 // invalid path
	endif

	GetFileFolderInfo/Q/Z=1 strCorrectionPath
	if(!V_flag && V_isFolder)
		prefs.strCorrectionPath = strCorrectionPath
		PLEMd2SavePackagePrefs(prefs)
	endif
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

Constant PLEMd2detectorNewton = 0 // Andor Newton
Constant PLEMd2detectorIdus   = 1 // Andor Idus
Constant PLEMd2cameraClara    = 2 // Andor Clara
Constant PLEMd2cameraXeva     = 3 // Xenics Xeva

//Structure for storing Information about a PLE-Map
Structure PLEMd2stats
	//Versioning System to update/create new Global vars.
	Variable numVersion
	//strPLEM is Name of Map
	//strDataFolder is Folder to Main directory
	//strDataFolderOriginal is Folder to Processed IBW data Main:ORIGINAL
	//numPLEM is number of Map in Menu Entry-List
	//wavPLEM is the wave reference to :PLEM
	String strPLEM, strDataFolder, strDataFolderOriginal
	Variable numPLEM
	//2D-Waves
	Wave wavPLEM, wavMeasure, wavBackground
	//1D-Waves
	Wave wavExcitation, wavWavelength
	Wave wavYpower, wavYphoton, wavGrating, wavQE, wavFilterExc, wavFilterEmi
	// Normalization Value
	Variable numNormalization
	// Switches for calculation
	Variable booBackground, booPower, booPhoton, booGrating, booQuantumEfficiency, booNormalization, booFilter, booTime, booWavelengthPitch
	// chirality waves
	Wave/D wavAtlasS1nm, wavAtlasS2nm
	WAVE/T wavAtlasText
	Wave/D wavEnergyS1, wavEnergyS2, wav2Dfit, wav1Dfit, wavFWHMS1, wavFWHMS2
	
	WAVE/T wavChiralityText
	Wave wavPLEMfit, wavPLEMfitSingle
	// Variables for chirality offset
	Variable 	numS1offset, numS2offset
	// calculated variables
	Variable numPLEMTotalX, numPLEMLeftX, numPLEMDeltaX
	Variable numPLEMTotalY, numPLEMBottomY, numPLEMDeltaY

	// nanostage position
	variable numPositionX, numPositionY, numPositionZ, booSwitchX, booSwitchY

	// image mode information (PLEMv3.3)
	variable numReadOutMode, numLaserPositionX, numLaserPositionY, numMagnification, numPixelPitch

	// variables from IBW file
	// Note: numEmission* is the emission from the excitation source
	String strDate, strUser, strFileName
	Variable numCalibrationMode, numSlit, numGrating, numFilter, numShutter, numWLcenter, numDetector, numCooling, numExposure, numBinning, numWLfirst, numWLlast, numWLdelta, numEmissionMode, numEmissionPower, numEmissionStart, numEmissionEnd, numEmissionDelta, numEmissionStep, numScans, numBackground
Endstructure

Function PLEMd2statsLoad(stats, strMap)
	Struct PLEMd2stats &stats
	String strMap

	DFREF dfrMap = PLEMd2MapFolder(strMap)
	Wave stats.wavPLEM 			= createWave(dfrMap, "PLEM")
	Wave stats.wavPLEMfitSingle	= createWave(dfrMap, "PLEMfit")
	Wave stats.wavMeasure 		= createWave(dfrMap, "MEASURE", setWaveType = PLEMd2WaveTypeUnsigned16)
	Wave stats.wavBackground 	= createWave(dfrMap, "BACKGROUND", setWaveType = PLEMd2WaveTypeUnsigned16)
	Wave stats.wavExcitation 	= createWave(dfrMap, "yExcitation")
	Wave stats.wavWavelength 	= createWave(dfrMap, "xWavelength")
	Wave stats.wavYpower 		= createWave(dfrMap, "yPower")
	Wave stats.wavYphoton 		= createWave(dfrMap, "yPhoton")
	Wave stats.wavGrating 		= createWave(dfrMap, "yGrating")
	Wave stats.wavQE            = createWave(dfrMap, "yQuantum") /// @todo re-calculate qe and grating when needed instead of storing them to save disk space
	Wave stats.wavFilterExc     = createWave(dfrMap, "yFilter")
	Wave stats.wavFilterEmi     = createWave(dfrMap, "xFilter")

	DFREF dfrAtlas = returnMapChiralityFolder(strMap)
	Wave/D stats.wavEnergyS1 		= createWave(dfrAtlas, "PLEMs1nm")
	Wave/D stats.wavEnergyS2 		= createWave(dfrAtlas, "PLEMs2nm")
	Wave/T stats.wavChiralityText   = createWave(dfrAtlas, "PLEMchirality", setWaveType = PLEMd2WaveTypeText)
	Wave/D stats.wavAtlasS1nm 		= createWave(dfrAtlas, "atlasS1nm")
	Wave/D stats.wavAtlasS2nm 		= createWave(dfrAtlas, "atlasS2nm")
	Wave/D stats.wavFWHMS1 = createWave(dfrAtlas, "atlasS1FWHMnm")
	Wave/D stats.wavFWHMS2 = createWave(dfrAtlas, "atlasS2FWHMnm")
	Wave/T stats.wavAtlasText       = createWave(dfrAtlas, "atlasText", setWaveType = PLEMd2WaveTypeText)
	Wave/D stats.wav1Dfit 			= createWave(dfrAtlas, "fit1D")
	Wave/D stats.wav2Dfit 			= createWave(dfrAtlas, "fit2D")
	Wave/D stats.wavPLEMfit 		= createWave(dfrAtlas, "fitPLEM")

	// PLEMd2statsInitialize(strMap)
	stats.numVersion = getMapVariable(strMap, "gnumVersion")

	stats.numPLEM 				= getMapVariable(strMap, "gnumPLEM")
	stats.strPLEM				= getMapString(strMap, "gstrPLEM") // no magic here
	stats.strDataFolder			= getMapString(strMap, "gstrDataFolder")
	stats.strDataFolderOriginal	= getMapString(strMap, "gstrDataFolderOriginal")

	stats.numNormalization 	= getMapVariable(strMap, "gnumNormalization")
	stats.numNormalization 	= SelectNumber(stats.numNormalization != 0, 1, stats.numNormalization)

	stats.booBackground 		= getMapVariable(strMap, "gbooBackground")
	stats.booPower 				= getMapVariable(strMap, "gbooPower")
	stats.booPhoton 			= getMapVariable(strMap, "gbooPhoton")
	stats.booGrating  			= getMapVariable(strMap, "gbooGrating")
	stats.booQuantumEfficiency 	= getMapVariable(strMap, "gbooQuantumEfficiency")
	stats.booNormalization		= getMapVariable(strMap, "gbooNormalization")
	stats.booFilter				= getMapVariable(strMap, "gbooFilter")
	stats.booTime              = getMapVariable(strMap, "gbooTime")
	stats.booWavelengthPitch   = getMapVariable(strMap, "gbooWavelengthPitch")

	stats.numS1offset 	= getMapVariable(strMap, "gnumS1offset")
	stats.numS2offset 	= getMapVariable(strMap, "gnumS2offset")

	stats.numPLEMTotalX = getMapVariable(strMap, "gnumPLEMTotalX")
	stats.numPLEMLeftX 	= getMapVariable(strMap, "gnumPLEMLeftX")
	stats.numPLEMDeltaX = getMapVariable(strMap, "gnumPLEMDeltaX")

	stats.numPLEMTotalY		= getMapVariable(strMap, "gnumPLEMTotalY")
	stats.numPLEMBottomY 	= getMapVariable(strMap, "gnumPLEMBottomY")
	stats.numPLEMDeltaY		= getMapVariable(strMap, "gnumPLEMDeltaY")

	stats.strDate 		= getMapString(strMap, "gstrDate")
	stats.strUser		= getMapString(strMap, "gstrUser")
	stats.strFileName 	= getMapString(strMap, "gstrFileName")

	stats.numCalibrationMode 	= getMapVariable(strMap, "gnumCalibrationMode")
	stats.numSlit 				= getMapVariable(strMap, "gnumSlit")
	stats.numGrating 			= getMapVariable(strMap, "gnumGrating")
	stats.numFilter 			= getMapVariable(strMap, "gnumFilter")
	stats.numShutter 			= getMapVariable(strMap, "gnumShutter")
	stats.numWLcenter 			= getMapVariable(strMap, "gnumWLcenter")
	stats.numDetector 			= getMapVariable(strMap, "gnumDetector")
	stats.numCooling 			= getMapVariable(strMap, "gnumCooling")
	stats.numExposure 			= getMapVariable(strMap, "gnumExposure")
	stats.numBinning 			= getMapVariable(strMap, "gnumBinning")
	stats.numWLfirst 			= getMapVariable(strMap, "gnumWLfirst")
	stats.numWLlast 			= getMapVariable(strMap, "gnumWLlast")
	stats.numWLdelta 			= getMapVariable(strMap, "gnumWLdelta")
	stats.numEmissionMode 		= getMapVariable(strMap, "gnumEmissionMode")
	stats.numEmissionPower 		= getMapVariable(strMap, "gnumEmissionPower")
	stats.numEmissionStart 		= getMapVariable(strMap, "gnumEmissionStart")
	stats.numEmissionEnd 		= getMapVariable(strMap, "gnumEmissionEnd")
	stats.numEmissionDelta 		= getMapVariable(strMap, "gnumEmissionDelta")
	stats.numEmissionStep 		= getMapVariable(strMap, "gnumEmissionStep")
	stats.numScans 				= getMapVariable(strMap, "gnumScans")
	stats.numBackground 		= getMapVariable(strMap, "gnumBackground")

	stats.numPositionX 	= getMapVariable(strMap, "gnumPositionX")
	stats.numPositionY 	= getMapVariable(strMap, "gnumPositionY")
	stats.numPositionZ 	= getMapVariable(strMap, "gnumPositionZ")
	stats.booSwitchX 	= getMapVariable(strMap, "gbooSwitchX")
	stats.booSwitchY 	= getMapVariable(strMap, "gbooSwitchY")

	stats.numReadOutMode 	= getMapVariable(strMap, "gnumReadOutMode")
	stats.numLaserPositionX = getMapVariable(strMap, "gnumLaserPositionX")
	stats.numLaserPositionY = getMapVariable(strMap, "gnumLaserPositionY")
	stats.numMagnification 	= getMapVariable(strMap, "gnumMagnification")
	stats.numPixelPitch		= getMapVariable(strMap, "gnumPixelPitch")
End

Function PLEMd2statsSave(stats)
	Struct PLEMd2stats &stats
	String strMap = stats.strPLEM

	setMapVariable(strMap, "gnumVersion", stats.numVersion)

	setMapVariable(strMap, "gnumPLEM", stats.numPLEM)
	setMapString(strMap, "gstrPLEM", stats.strPLEM)
	setMapString(strMap, "gstrDataFolder", stats.strDataFolder)
	setMapString(strMap, "gstrDataFolderOriginal", stats.strDataFolderOriginal)

	setMapVariable(strMap, "gnumNormalization", stats.numNormalization)
	setMapVariable(strMap, "gbooBackground", stats.booBackground)
	setMapVariable(strMap, "gbooPower", stats.booPower)

	setMapVariable(strMap, "gbooPhoton", stats.booPhoton)
	setMapVariable(strMap, "gbooGrating", stats.booGrating)
	setMapVariable(strMap, "gbooQuantumEfficiency", stats.booQuantumEfficiency)
	setMapVariable(strMap, "gbooNormalization", stats.booNormalization)
	setMapVariable(strMap, "gbooFilter", stats.booFilter)
	setMapVariable(strMap, "gbooTime", stats.booTime)
	setMapVariable(strMap, "gbooWavelengthPitch", stats.booWavelengthPitch)

	setMapVariable(strMap, "gnumS1offset", stats.numS1offset)
	setMapVariable(strMap, "gnumS2offset", stats.numS2offset)

	setMapVariable(strMap, "gnumPLEMTotalX", stats.numPLEMTotalX)
	setMapVariable(strMap, "gnumPLEMLeftX", stats.numPLEMLeftX)
	setMapVariable(strMap, "gnumPLEMDeltaX", stats.numPLEMDeltaX)

	setMapVariable(strMap, "gnumPLEMTotalY", stats.numPLEMTotalY)
	setMapVariable(strMap, "gnumPLEMBottomY ", stats.numPLEMBottomY)
	setMapVariable(strMap, "gnumPLEMDeltaY", stats.numPLEMDeltaY)

	setMapString(strMap, "gstrDate", stats.strDate)
	setMapString(strMap, "gstrUser", stats.strUser)
	setMapString(strMap, "gstrFileName", stats.strFileName)

	setMapVariable(strMap, "gnumCalibrationMode", stats.numCalibrationMode)
	setMapVariable(strMap, "gnumSlit", stats.numSlit)
	setMapVariable(strMap, "gnumGrating", stats.numGrating)
	setMapVariable(strMap, "gnumFilter", stats.numFilter)
	setMapVariable(strMap, "gnumShutter", stats.numShutter)
	setMapVariable(strMap, "gnumWLcenter", stats.numWLcenter)
	setMapVariable(strMap, "gnumDetector", stats.numDetector)
	setMapVariable(strMap, "gnumCooling", stats.numCooling)
	setMapVariable(strMap, "gnumExposure", stats.numExposure)
	setMapVariable(strMap, "gnumBinning", stats.numBinning)
	setMapVariable(strMap, "gnumScans", stats.numScans)
	setMapVariable(strMap, "gnumBackground", stats.numBackground)
	setMapVariable(strMap, "gnumWLfirst", stats.numWLfirst)
	setMapVariable(strMap, "gnumWLlast", stats.numWLlast)
	setMapVariable(strMap, "gnumWLdelta", stats.numWLdelta)
	setMapVariable(strMap, "gnumEmissionMode", stats.numEmissionMode)
	setMapVariable(strMap, "gnumEmissionPower", stats.numEmissionPower)
	setMapVariable(strMap, "gnumEmissionStart", stats.numEmissionStart)
	setMapVariable(strMap, "gnumEmissionEnd", stats.numEmissionEnd)
	setMapVariable(strMap, "gnumEmissionDelta", stats.numEmissionDelta)
	setMapVariable(strMap, "gnumEmissionStep", stats.numEmissionStep)

	setMapVariable(strMap, "gnumPositionX", stats.numPositionX)
	setMapVariable(strMap, "gnumPositionY", stats.numPositionY)
	setMapVariable(strMap, "gnumPositionZ", stats.numPositionZ)
	setMapVariable(strMap, "gbooSwitchX", stats.booSwitchX)
	setMapVariable(strMap, "gbooSwitchY", stats.booSwitchY)

	setMapVariable(strMap, "gnumReadOutMode", stats.numReadOutMode)
	setMapVariable(strMap, "gnumLaserPositionX", stats.numLaserPositionX)
	setMapVariable(strMap, "gnumLaserPositionY", stats.numLaserPositionY)
	setMapVariable(strMap, "gnumMagnification", stats.numMagnification)
	setMapVariable(strMap, "gnumPixelPitch", stats.numPixelPitch)
End

Function PLEMd2statsInitialize(strMap)
//Initialize the stats struct without calling the load procedure
//**on Version-Missmatch
//**if Folder INFO is not there.
	String strMap
	Struct PLEMd2Stats stats

	if(!PLEMd2isInitialized())
		Abort "PLEMd2statsInitialize: PLEMd2 is not initialized."
	endif
	PLEMd2statsLoad(stats, strMap)

	stats.numPLEM = PLEMd2AddMap(strMap)
	stats.strPLEM = strMap
	stats.strDataFolder =  GetDataFolder(1, PLEMd2MapFolder(strMap))
	stats.numVersion = cPLEMd2Version

	stats.numNormalization = 1

	stats.booBackground 	= 1
	stats.booPower 		= 0
	stats.booPhoton 		= 1
	stats.booGrating  	= 1
	stats.booQuantumEfficiency = 1
	stats.booNormalization		= 0
	stats.booFilter		= 1
	stats.booTime = 1
	stats.booWavelengthPitch = 1

	PLEMd2statsSave(stats)
End

Function/S PLEMd2FullPathByString(strPLEM)
	String strPLEM

	String fullPath

	DFREF packageRoot = $cstrPLEMd2root
	SVAR/Z mapsFolder = packageRoot:gstrMapsFolder
	if(!SVAR_EXISTS(mapsFolder))
		Abort "Function can not return proper results if SVAR is missing"
	endif

	fullPath  = ParseFilePath(2, mapsFolder, ":", 0, 0)
	fullPath += strPLEM + ":PLEM"

	return fullPath
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

#include "utilities-peakfind"

static StrConstant cstrPLEMd2maps 	= ":maps"
static StrConstant cstrPLEMd2info 	= ":INFO"
static StrConstant cstrPLEMd2chirality = ":CHIRALITY"
static StrConstant cstrPLEMd2originals = ":ORIGINAL"
static StrConstant cstrPLEMd2windowPrefix = ""
static StrConstant cstrPLEMd2windowSuffix = "_graph"

Function/S PLEMd2getWindow(strPLEM)
	String strPLEM

	return cstrPLEMd2windowPrefix + strPLEM + cstrPLEMd2windowSuffix

End

Function/S PLEMd2window2strPLEM(strWindow)
	String strWindow
	Variable numStart, numEnd

	numStart = strlen(cstrPLEMd2windowPrefix) > 0 ? strsearch(strWindow, cstrPLEMd2windowPrefix, 0) : 0
	numEnd = strlen(cstrPLEMd2windowSuffix) > 0 ? strsearch(strWindow, cstrPLEMd2windowSuffix, 0) : strsearch(strWindow, "#", 0)

	if(numEnd == -1 && numStart == -1)
		return strWindow
	elseif(numEnd == -1 && numStart != -1)
		return strWindow[numStart,inf]
	else
		return strWindow[numStart,numEnd-1]
	endif
End

Function/DF DataFolderReference(dataFolderNameStr)
	String dataFolderNameStr

	if(!DataFolderExists(dataFolderNameStr))
		NewDataFolder/O $dataFolderNameStr // can only create one level
	endif

	DFREF dfr = $dataFolderNameStr
	return dfr
End

// Function returns DataFolder reference to package root.
static Function/DF returnPackageRoot()
	DFREF dfrPackage = DataFolderReference(cstrPLEMd2root)
	return dfrPackage
End

// Function returns DataFolder reference to base directory where maps are stored
Function/DF PLEMd2MapsFolder()
	DFREF dfrPackage = returnPackageRoot()
	DFREF dfrMaps = DataFolderReference(cstrPLEMd2root + cstrPLEMd2maps)
	return dfrMaps
End

// Function returns DataFolder reference to base directory of map specified by strMap
Function/DF PLEMd2MapFolder(strMap)
	String strMap

	if(strlen(strMap) == 0)
		Abort "Need a valid Map String"
	endif

	DFREF dfrMaps = PLEMd2MapsFolder()
	DFREF dfrMap = dfrMaps:$strMap

	return dfrMap
End

// Function returns DataFolder reference to current map's info folder where NVAR and SVAR are saved
static Function/DF returnMapInfoFolder(strMap)
	String strMap

	if(strlen(strMap) == 0)
		Abort "Need a valid Map String"
	endif

	DFREF dfrMap = PLEMd2MapFolder(strMap)
	DFREF dfrInfo = DataFolderReference(cstrPLEMd2root + cstrPLEMd2maps + ":" + strMap + cstrPLEMd2info)
	return dfrInfo
End

// Function returns DataFolder reference to current map's info folder where NVAR and SVAR are saved
Function/DF returnMapChiralityFolder(strMap)
	String strMap
	DFREF dfrMap = PLEMd2MapFolder(strMap)
	DFREF dfrChirality = DataFolderReference(cstrPLEMd2root + cstrPLEMd2maps + ":" + strMap + cstrPLEMd2chirality)
	return dfrChirality
End

// Function returns value of Global String "name" in "dataFolder"
static Function/S getGstring(name, dataFolder)
	String name
	DFREF dataFolder
	SVAR/Z/SDFR=dataFolder myVar = $name
	if(!SVAR_EXISTS(myVar))
		String/G dataFolder:$name = ""
		return ""
	else
		return myVar
	endif
End

// Function returns value of Global (numeric) Variable "name" in "dataFolder"
static Function getGvar(name, dataFolder)
	String name
	DFREF dataFolder
	NVAR/Z/SDFR=dataFolder myVar = $name
	if(!NVAR_EXISTS(myVar))
		Variable/G dataFolder:$name = NaN
		return NaN
	else
		return myVar
	endif
End

// Wrapper Functions for creating Waves
Constant PLEMd2WaveTypeDouble     = 0
Constant PLEMd2WaveTypeUnsigned32 = 1
Constant PLEMd2WaveTypeUnsigned16 = 2
Constant PLEMd2WaveTypeText       = 3

Function/WAVE createWave(dfr, strWave, [setWaveType])
	DFREF dfr
	String strWave
	Variable setWaveType

	setWaveType = ParamIsDefault(setWaveType) ? PLEMd2WaveTypeDouble : setWaveType

	WAVE/Z/SDFR=dfr wv = $strWave
	if(WaveExists(wv))
		return wv
	endif

	switch(setWaveType)
		case PLEMd2WaveTypeDouble:
			WAVE wv = createDoubleWave(dfr, strWave)
			break
		case PLEMd2WaveTypeUnsigned32:
			WAVE wv = createUnsigned32Wave(dfr, strWave)
			break
		case PLEMd2WaveTypeUnsigned16:
			WAVE wv = createUnsigned16Wave(dfr, strWave)
			break
		case PLEMd2WaveTypeText:
			WAVE wv = createTextWave(dfr, strWave)
			break
		default:
			WAVE wv = createDoubleWave(dfr, strWave)
	endswitch

	return wv
End

static Function/WAVE createDoubleWave(dfr, strWave)
	DFREF dfr
	String strWave

	Make/D/N=0 dfr:$strWave/WAVE=wv
	return wv
End

// 32bit unsigned wave
static Function/WAVE createUnsigned32Wave(dfr, strWave)
	DFREF dfr
	String strWave

	Make/I/U/N=0 dfr:$strWave/WAVE=wv
	return wv
End

// 16bit unsigned wave
static Function/WAVE createUnsigned16Wave(dfr, strWave)
	DFREF dfr
	String strWave

	Make/W/U/N=0 dfr:$strWave/WAVE=wv
	return wv
End

static Function/WAVE createTextWave(dfr, strWave)
	DFREF dfr
	String strWave

	Make/T/N=0 dfr:$strWave/WAVE=wv
	return wv
End

// Function sets Global String "name" in "dataFolder" to "value"
static Function setGstring(name, value, dataFolder)
	String name, value
	DFREF dataFolder

	SVAR/Z/SDFR=dataFolder myVar = $name
	if(!SVAR_EXISTS(myVar))
		String/G dataFolder:$name
		SVAR/Z/SDFR=dataFolder myVar = $name
		if(!SVAR_EXISTS(myVar))
			Abort "Could not create global String"
		endif
	endif

	myVar = value
End

// Function sets Global Variable "name" in "dataFolder" to "value"
static Function setGvar(name, value, dataFolder)
	String name
	Variable value
	DFREF dataFolder

	NVAR/Z/SDFR=dataFolder myVar = $name
	if(!NVAR_EXISTS(myVar))
		Variable/G dataFolder:$name
		NVAR/Z/SDFR=dataFolder myVar = $name
		if(!NVAR_EXISTS(myVar))
			Abort "Could not create global Variable"
		endif
	endif

	myVar = value
End

// Abbreviated Functions for returning Variables from Package root.
Function/S getPackageString(name)
	String name
	DFREF dfrPackage = returnPackageRoot()
	return getGstring(name, dfrPackage)
End

Function getPackageVariable(name)
	String name
	DFREF dfrPackage = returnPackageRoot()
	return getGvar(name, dfrPackage)
End

Function setPackageString(name, value)
	String name, value
	DFREF dfrPackage = returnPackageRoot()
	setGstring(name, value, dfrPackage)
End

Function setPackageVariable(name, value)
	String name
	Variable value
	DFREF dfrPackage = returnPackageRoot()
	setGvar(name, value, dfrPackage)
End

// Abbreviated Functions for returning Variables from current map.
Function/S getMapString(strMap, var)
	String strMap, var
	DFREF dfrInfo = returnMapInfoFolder(strMap)
	return getGstring(var, dfrInfo)
End

Function getMapVariable(strMap, var)
	String strMap, var
	DFREF dfrInfo = returnMapInfoFolder(strMap)
	return getGvar(var, dfrInfo)
End

Function setMapString(strMap, var, value)
	String strMap, var, value
	DFREF dfrInfo = returnMapInfoFolder(strMap)
	setGstring(var, value, dfrInfo)
End

Function setMapVariable(strMap, var, value)
	String strMap, var
	Variable value
	DFREF dfrInfo = returnMapInfoFolder(strMap)
	setGvar(var, value, dfrInfo)
End

// Abbreviated Functions for returning Variables from CHIRALITY FOLDER
Function/S getAtlasString(strMap, var)
	String strMap, var
	DFREF dfrInfo = returnMapInfoFolder(strMap)
	return getGstring(var, dfrInfo)
End

Function getAtlasVariable(strMap, var)
	String strMap, var
	DFREF dfrAtlas = returnMapChiralityFolder(strMap)
	return getGvar(var, dfrAtlas)
End

Function setAtlasString(strMap, var, value)
	String strMap, var, value
	DFREF dfrAtlas = returnMapChiralityFolder(strMap)
	setGstring(var, value, dfrAtlas)
End

Function setAtlasVariable(strMap, var, value)
	String strMap, var
	Variable value
	DFREF dfrAtlas = returnMapChiralityFolder(strMap)
	setGvar(var, value, dfrAtlas)
End

Function/WAVE PLEMd2wavIBW(strPLEM)
	String strPLEM

	DFREF dfr = PLEMd2MapFolder(strPLEM)
	WAVE/Z wv = dfr:IBW

	return wv
End

// @brief get the wave from the given datafolder
Function/WAVE PLEMd2getWaveFromDFR(dfr)
	DFREF dfr

	if(DataFolderRefStatus(dfr) == 0)
		return $""
	endif

	if(CountObjectsDFR(dfr, 1) != 1)
		Abort "no or more than one wave in map's DataFolder!"
	endif

	return dfr:$GetIndexedObjNameDFR(dfr, 1, 0)
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3
#include <Graph Utility Procs>

Function PLEMd2Display(strPLEM)
	String strPLEM

	Struct PLEMd2Stats stats
	String winPLEM
	PLEMd2statsLoad(stats, strPLEM)

	// check if spectrum is a valid input
	if(strlen(stats.strPLEM) == 0)
		print "PLEMd2Display: Error stats.strPLEM not set for Map: " + strPLEM + " check code"
		return 0
	endif
	if(!WaveExists(stats.wavPLEM))
		print "PLEMd2Display: Wave Not Found"
		return 0
	endif

	// check if window already exists
	winPLEM = PLEMd2getWindow(stats.strPLEM)
	DoWindow/F $winPLEM
	// DoWindow sets the variable V_flag:
	// 	1 window existed
	// 	0 no such window
	// 	2 window is hidden.
	if(V_flag == 1)
		CheckDisplayed/W=$winPLEM stats.wavPLEM
		if(V_Flag)
			print stats.strPLEM, "window found"
		endif
		return 0
	elseif(V_flag == 2)
		print "PLEMd2Display: Graph was hidden. Case not handled. check code"
	elseif(V_flag == 0)
		Display
		DoWindow/C/N/R $winPLEM
		if((stats.numCalibrationMode == 1) && (stats.numReadOutMode != 1))
			AppendToGraph stats.wavPLEM vs stats.wavWavelength
			ModifyGraph/W=$winPLEM standoff=0
			SetAxis/W=$winPLEM/A left
			Label/W=$winPLEM left "intensity / a.u."
			Label/W=$winPLEM bottom "wavelength / nm (Excitation at "+num2str(stats.numEmissionStart)+"-"+num2str(stats.numEmissionEnd)+")"
		else
			//AppendImage stats.wavPLEM vs {stats.wavWavelength, stats.wavExcitation}
			AppendImage stats.wavPLEM
			PLEMd2Decorate(strWinPLEM = winPLEM, booImage = (stats.numReadOutMode == 1))
		endif
	endif
End

Function PLEMd2DisplayByNum(numPLEM)
	Variable numPLEM
	if(numPLEM < 0)
		print "PLEMd2DisplayByNum: Wrong Function Call numPLEM out of range"
		return 0
	endif
	String strPLEM
	strPLEM = PLEMd2strPLEM(numPLEM)
	PLEMd2Display(strPLEM)
End

Function PLEMd2Decorate([strWinPLEM, booImage])
	String strWinPLEM
	Variable booImage
	Variable numZmin, numZmax
	Variable numXmin, numXmax
	Variable numYmin, numYmax
	String strImages

	// if no argument was selected, take top graph window
	if(ParamIsDefault(strWinPLEM))
		strWinPLEM = WinName(0, 1, 1)
	endif
	if(ParamIsDefault(booImage))
		booImage = 0
	endif

	if(strlen(strWinPLEM) == 0)
		Print "PLEMd2Decorate: No window to append to"
		return 0
	endif

	// get the image name and wave reference.
	strImages = ImageNameList(strWinPLEM, ";")
	if(ItemsInList(strImages) != 1)
		Print "PLEMd2Decorate: No Image found in top graph or More than one Image present"
	endif
	wave wavImage = ImageNameToWaveRef(strWinPLEM,StringFromList(0,strImages))

	// get min and max of wave (statistically corrected)
	WaveStats/Q/W wavImage
	wave M_WaveStats
	numZmin = M_WaveStats[10]	//minimum
	numZmin = M_WaveStats[3]-sign(M_WaveStats[3])*2*M_WaveStats[4] //statistical minimum
	if(numZmin<0)
		numZmin = 0
	endif
	numZmax = M_WaveStats[12]	//maximum
	//Images start a little earlier as the minimum due to the quadratic size of a pixel.
	numYmin 	= DimOffset(wavImage,1) + sign(DimOffset(wavImage,1)) * (-1) * DimDelta(wavImage,1)/2
	numYmax 	= DimOffset(wavImage,1) + DimDelta(wavImage,1)*(DimSize(wavImage,1)-1) + sign(DimOffset(wavImage,1)) * (+1) * DimDelta(wavImage,1)/2
	numXmin 	= DimOffset(wavImage,0) + sign(DimOffset(wavImage,0)) * (-1) * DimDelta(wavImage,0)/2
	numXmax 	= DimOffset(wavImage,0) + DimDelta(wavImage,0)*(DimSize(wavImage,0)-1) + sign(DimOffset(wavImage,0)) * (+1) * DimDelta(wavImage,0)/2
	Killwaves/Z M_Wavestats

	ModifyImage/W=$strWinPLEM ''#0 ctab= {numZmin,numZmax,Terrain256,0}
	ModifyImage/W=$strWinPLEM ''#0 ctab= {*,*,Terrain256,0}
	ModifyGraph/W=$strWinPLEM standoff=0
	ModifyGraph/W=$strWinPLEM height={Aspect,((numYmax-numYmin)/(numXmax-numXmin))}
	ModifyGraph/W=$strWinPLEM height = 0

	SetAxis/W=$strWinPLEM left,numYmin, numYmax
	SetAxis/W=$strWinPLEM/A left
	SetAxis/W=$strWinPLEM bottom,numXmin,numXmax
	SetAxis/W=$strWinPLEM/A bottom

	if(booImage)
		ModifyGraph zero=1
		Label/W=$strWinPLEM left "position / µm"
		Label/W=$strWinPLEM bottom "position / µm"
	else
		Label/W=$strWinPLEM left "excitation / nm"
		Label/W=$strWinPLEM bottom "emission / nm"
	endif

	ModifyGraph width={Plan,1,bottom,left}
End

Function PLEMd2ShowNote([strWinPLEM])
	String strWinPLEM

	String strWinPLEMbase, strPLEM
	String strImages, strTraces, strDataFolderMap, strDataFolderInfo
	Struct PLEMd2Stats stats

	// if no argument was selected, take top graph window
	if(ParamIsDefault(strWinPLEM))
		strWinPLEM = WinName(0, 1, 1)
	endif
	if(strlen(strWinPLEM) == 0)
		Print "PLEMd2ShowNote: base window not found"
		return 0
	endif
	// take parent window
	strWinPLEMbase = strWinPLEM[0,(strsearch(strWinPLEM, "#",0)-1)]
	// if the panel is already shown, do nothing
	DoUpdate /W=$strWinPLEMbase#PLEMd2WaveNote
	if(V_flag != 0)
		Print "PLEMd2ShowNote: Panel already exists."
		return 0
	endif

	// get the image name and wave reference.
	strPLEM = PLEMd2window2strPLEM(strWinPLEMbase)
	PLEMd2statsLoad(stats, strPLEM)
	wave wavPLEM = stats.wavPLEM
	CheckDisplayed wavPLEM
	if( V_Flag != 1 )
		print "PLEMd2ShowNote: wrong panel. wave not found"
	endif

	// display Note
	String strWavenote = note(wavPLEM)
	NewPanel /N=PLEMd2WaveNote/W=(0,0,300,400) /EXT=0 /HOST=$strWinPLEMbase
	NewNotebook/F=0 /N=Note /W=(0,0,300,400) /HOST=$(strWinPLEMbase + "#PLEMd2WaveNote") as ("wavenote " + GetWavesDataFolder(wavPLEM,2))
	Notebook # text=strWavenote
	//TitleBox/Z gstrPLEMfull	title=strWavenote,	pos={10,10}, size={50,50}, frame=0, font="Helvetica"
End

Function PLEMd2Panel([strWinPLEM])
	String strWinPLEM

	String strImages, strTraces, strDataFolderMap, strDataFolderInfo
	// if no argument was selected, take top graph window
	if(ParamIsDefault(strWinPLEM))
		strWinPLEM = WinName(0, 1, 1)
	endif
	DoWindow $strWinPLEM
	if(V_flag != 1)
		Abort "PLEMd2Panel: No window to append to"
	endif

	// if the panel is already shown, do nothing
	DoUpdate/W=$strWinPLEM#PLEMd2Panel
	if(V_flag != 0)
		Print "PLEMd2Panel: Panel already exists."
		return 0
	endif

	// get INFO folder (method only works if one trace is present)
	WAVE wavPLEM = getTopWindowWave()
	strDataFolderMap = GetWavesDataFolder(wavPLEM, 1)
	strDataFolderInfo = strDataFolderMap + "INFO:"
	if(DataFolderExists(strDataFolderInfo) == 0)
		Print "PLEMd2Panel: INFO Data Folder for Image in top graph not found."
		return 0
	endif

	// build panel
	NewPanel /N=PLEMd2Panel/W=(0,0,300,250) /EXT=0 /HOST=$strWinPLEM
	TitleBox/Z gstrPLEMfull       variable=$(strDataFolderInfo + "gstrPLEMfull"),          pos={0,0},     size={130,0}, disable=0, frame=0, font="Helvetica"
	SetVariable normalization,    value=$(strDataFolderInfo + "gnumNormalization"),        pos={150,80},  title="normalization", size={130,0}, proc=SetVarProcCalculate
	SetVariable delatX,           value=$(strDataFolderInfo + "gnumPLEMDeltaX"),	          pos={150,100}, title="deltaX", size={130,0}, noedit=1
	CheckBox boxBackground        variable=$(strDataFolderInfo + "gbooBackground"),        pos={10,20},   title="background", proc=CheckProcCalculate
	CheckBox boxPower             variable=$(strDataFolderInfo + "gbooPower"),             pos={10,40},   title="power", proc=CheckProcCalculate
	CheckBox boxPhoton            variable=$(strDataFolderInfo + "gbooPhoton"),            pos={10,60},   title="photon", proc=CheckProcCalculate
	CheckBox boxGrating           variable=$(strDataFolderInfo + "gbooGrating"),           pos={10,120},  title="grating", proc=CheckProcCalculate
	CheckBox boxQuantumEfficiency variable=$(strDataFolderInfo + "gbooQuantumEfficiency"), pos={10,160},  title="detector", proc=CheckProcCalculate
	CheckBox boxNormalization     variable=$(strDataFolderInfo + "gbooNormalization"),     pos={10,80},   title="normalization", proc=CheckProcCalculate
	CheckBox boxFilter            variable=$(strDataFolderInfo + "gbooFilter"),            pos={10,140},  title="filter", proc=CheckProcCalculate
	CheckBox boxWLdelta       variable=$(strDataFolderInfo + "gbooWavelengthPitch"),       pos={10,100},  title="cts/nm", proc=CheckProcCalculate
	CheckBox boxTime              variable=$(strDataFolderInfo + "gbooTime"),              pos={10,180},  title="cts/s", proc=CheckProcCalculate

	Button ProcessIBW, pos={150, 30}, size={130,30}, proc=ButtonProcProcessIBW,title="reset"
	Button ShowNote, pos={150, 140}, size={130,30}, proc=ButtonProcShowNote,title="WaveNote"
	DoWindow PLEMd2Panel
End

Function PLEMd2PanelAtlas([strWinPLEM])
	String strWinPLEM
	String strImages, strTraces, strDataFolderMap, strDataFolderInfo
	// if no argument was selected, take top graph window
	if(ParamIsDefault(strWinPLEM))
		strWinPLEM = WinName(0, 1, 1)
	endif
	if(strlen(strWinPLEM) == 0)
		Print "PLEMd2Atlas: No window to append to"
		return 0
	endif

	// if the panel is already shown, do nothing
	DoUpdate /W=$strWinPLEM#PLEMd2PanelAtlas
	if(V_flag != 0)
		Print "PLEMd2Atlas: Panel already exists."
	endif

	// get the image name and wave reference.
	strImages = ImageNameList(strWinPLEM, ";")
	if(ItemsInList(strImages) == 0)
		strTraces = TraceNameList(strWinPLEM, ";",1)
		if(ItemsInList(strTraces) == 1)
			wave wavPLEM = TraceNameToWaveRef(strWinPLEM,StringFromList(0,strTraces))
			Print "PLEMd2Atlas: Traces are experimental"
		else
			Print "PLEMd2Atlas: No Image found. More than one or no trace found in top graph."
			return 0
		endif
	elseif(ItemsInList(strImages) > 1)
		Print "PLEMd2Atlas: More than one image found in top graph."
		return 0
	else
		wave wavPLEM = ImageNameToWaveRef(strWinPLEM,StringFromList(0,strImages))
	endif
	// check for INFO folder
	strDataFolderMap = GetWavesDataFolder(wavPLEM,1)
	strDataFolderInfo = strDataFolderMap + "INFO:"
	if(DataFolderExists(strDataFolderInfo) == 0)
		Print "PLEMd2Panel: INFO Data Folder for Image in top graph not found."
		return 0
	endif
	NewPanel /N=PLEMd2PanelAtlas/W=(0,0,300,100) /EXT=2 /HOST=$strWinPLEM
	SetVariable 	gnumS1offset,	proc=VariableProcAtlasRecalculate,	value=$(strDataFolderInfo + "gnumS1offset"),	pos={10,10}, 	size={130,0}
	SetVariable 	gnumS2offset,	proc=VariableProcAtlasRecalculate,	value=$(strDataFolderInfo + "gnumS2offset"),	pos={10,30}, 	size={130,0}
	Button 		AtlasReset,		proc=ButtonProcAtlasReset,	title="reset",			pos={10, 50},	size={50,25}
	Button 		AtlasShow,		proc=ButtonProcAtlasShow,	title="show",			pos={150, 10},	size={50,25}
	Button		AtlasHide,		proc=ButtonProcAtlasHide,		title="hide",			pos={150, 40},	size={50,25}
	Button		AtlasFit3D,		proc=ButtonProcAtlasFit3D,	title="fit3D",			pos={200, 10},	size={50,25}
	Button		AtlasFit2D,		proc=ButtonProcAtlasFit2D,	title="fit2D",			pos={200, 40},	size={50,25}
	Button		AtlasFit1D,		proc=ButtonProcAtlasFit1D,	title="fit1D",			pos={200, 40},	size={50,25}
	Button		AtlasEdit,		proc=ButtonProcAtlasEdit,		title="edit",		pos={250, 10},	size={50,25}
	Button		AtlasClean,		proc=ButtonProcAtlasClean,	title="clean",		pos={250, 40},	size={50,25}
	//gnumPLEM;gnumVersion;gstrPLEM;gstrPLEMfull;gstrDataFolder;gstrDataFolderOriginal;gstrDate;gstrUser;gstrFileName;
	//gnumPLEMTotalX;gnumPLEMLeftX;gnumPLEMDeltaX;gnumPLEMRightX;gnumPLEMTotalY;gnumPLEMBottomY;gnumPLEMDeltaY;gnumPLEMTopY;gnumCalibrationMode;
	//gnumBackground; gnumSlit;gnumGrating;gnumFilter;
	//gnumShutter;gnumWLcenter;gnumDetector;gnumCooling;gnumExposure;gnumBinning;gnumScans;gnumWLfirst;gnumWLlast;gnumWLdelta;
	//gnumEmissionMode;gnumEmissionPower;gnumEmissionStart;gnumEmissionEnd;gnumEmissionDelta;gnumEmissionStep;
	DoWindow PLEMd2PanelAtlas
End

Function ButtonProcAtlasShow(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasShow(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcAtlasHide(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasHide(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcAtlasReset(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasInit(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function VariableProcAtlasRecalculate(sva) : SetVariableControl
	STRUCT WMSetVariableAction &sva

	switch( sva.eventCode )
		case 1: // mouse up
		case 2: // Enter key
		case 3: // Live update
			Variable dval = sva.dval
			String sval = sva.sval
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(sva.win)
			PLEMd2AtlasRecalculate(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End

Function ButtonProcAtlasFit3D(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasFit3D(strPLEM, show = 1)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcAtlasFit1D(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasFit2D(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcAtlasEdit(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasEdit(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcAtlasClean(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2AtlasClean(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcProcessIBW(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2BuildMaps(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcShowNote(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			//String strPLEM
			//strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2ShowNote(strWinPLEM=ba.win)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function ButtonProcBuildMaps(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	switch( ba.eventCode )
		case 2: // mouse up
			String strPLEM
			strPLEM = PLEMd2window2strPLEM(ba.win)
			PLEMd2BuildMaps(strPLEM)
			break
		case -1: // control being killed
			break
	endswitch
	return 0
End

Function CheckProcCalculate(cba) : CheckBoxControl
	STRUCT WMCheckboxAction &cba

	String strPLEM
	Variable mini, maxi

	switch( cba.eventCode )
		case 2: // mouse up
			Variable checked = cba.checked
			PLEMd2GetGraphMinMax(cba.win, mini, maxi)
			strPLEM = PLEMd2window2strPLEM(cba.win)
			PLEMd2BuildMaps(strPLEM)
			PLEMd2SetGraphMinMax(cba.win, mini, maxi)
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End

// @brief get the minimum and maximum scaling of the wave in the displayed range in percent
static Function PLEMd2GetGraphMinMax(win, mini, maxi)
	String win
	Variable &mini, &maxi

	Variable Ymin, Ymax
	Variable Pmin, Pmax, Qmin, Qmax, Zmin, Zmax
	string info
	String strPLEM = PLEMd2window2strPLEM(win)
	Struct PLEMd2stats stats

	PLEMd2statsload(stats, strPLEM)
	WAVE PLEM = stats.wavPLEM

	GetAxis/Q bottom
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting bottom axis in top Graph"
		return -1
	endif
	Pmin = ScaleToIndex(PLEM, V_min, 0)
	Pmax = ScaleToIndex(PLEM, V_max, 0)

	GetAxis/Q left
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting bottom axis in top Graph"
		return -1
	endif
	Ymin = V_min
	Ymax = V_max

	if(DimSize(PLEM, 1) > 1) // map
		Qmin = ScaleToIndex(PLEM, Ymin, 1)
		Qmax = ScaleToIndex(PLEM, Ymax, 1)
		info = WMGetRECREATIONFromInfo(ImageInfo(StringFromList(0, win, "#"), "PLEM", 0))
		info = info[strsearch(info, "{", 0) + 1, strsearch(info, "}", 0) - 1]
		Zmin = str2num(StringFromList(0, info, ","))
		Zmax = str2num(StringFromList(1, info, ","))
		Duplicate/FREE/R=[Pmin, Pmax][Qmin, Qmax] PLEM temp
	else
		Duplicate/FREE/R=[Pmin, Pmax] PLEM temp
	endif
	WaveStats/Q/M=1 temp
	mini = (Zmin - V_min) / (V_max - V_min)
	maxi = (Zmax - V_min) / (V_max - V_min)
End

// @brief get the minimum and maximum scaling of the wave in the window range
static Function PLEMd2SetGraphMinMax(win, mini, maxi)
	String win
	Variable mini, maxi

	Variable Ymin, Ymax
	Variable Pmin, Pmax, Qmin, Qmax, Zmin, Zmax
	string info
	String strPLEM = PLEMd2window2strPLEM(win)
	Struct PLEMd2stats stats

	if(!!numType(mini) && !!numType(maxi))
		return 1
	endif

	PLEMd2statsload(stats, strPLEM)
	WAVE PLEM = stats.wavPLEM

	GetAxis/Q bottom
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting bottom axis in top Graph"
		return -1
	endif
	Pmin = ScaleToIndex(PLEM, V_min, 0)
	Pmax = ScaleToIndex(PLEM, V_max, 0)

	GetAxis/Q left
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting bottom axis in top Graph"
		return -1
	endif
	Ymin = V_min
	Ymax = V_max

	if(DimSize(PLEM, 1) > 1) // map
		Qmin = ScaleToIndex(PLEM, Ymin, 1)
		Qmax = ScaleToIndex(PLEM, Ymax, 1)
		info = WMGetRECREATIONFromInfo(ImageInfo(StringFromList(0, win, "#"), "PLEM", 0))
		info = info[strsearch(info, "{", 0) + 1, strsearch(info, "}", 0) - 1]
		Zmin = str2num(StringFromList(0, info, ","))
		Zmax = str2num(StringFromList(1, info, ","))
		Duplicate/FREE/R=[Pmin, Pmax][Qmin, Qmax] PLEM temp
	else
		Zmin = Ymin
		Zmax = Ymax
		Duplicate/FREE/R=[Pmin, Pmax] PLEM temp
	endif
	WaveStats/Q/M=1 temp
	mini = V_min + mini * (V_max - V_min)
	maxi = V_min + maxi * (V_max - V_min)

	if(DimSize(PLEM, 1) > 1) // map
		if(!!numType(mini))
			ModifyImage PLEM ctab= {*,maxi,$StringFromList(2, info, ","),str2num(StringFromList(3, info, ","))}
		elseif(!!numType(maxi))
			ModifyImage PLEM ctab= {mini,*,$StringFromList(2, info, ","),str2num(StringFromList(3, info, ","))}
		else
			ModifyImage PLEM ctab= {mini,maxi,$StringFromList(2, info, ","),str2num(StringFromList(3, info, ","))}
		endif
	else
		if(!!numType(mini))
			SetAxis left, *, maxi
		elseif(!!numType(maxi))
			SetAxis left, mini, *
		else
			SetAxis left, mini, maxi
		endif
	endif
End

Function SetVarProcCalculate(sva) : SetVariableControl
	STRUCT WMSetVariableAction &sva

	switch( sva.eventCode )
		case 1: // mouse up
		case 2: // Enter key
		case 3: // Live update
			Variable dval = sva.dval
			if(dval != 0)
				String strPLEM
				strPLEM = PLEMd2window2strPLEM(sva.win)
				PLEMd2BuildMaps(strPLEM)
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3		// Use modern global access method and strict wave access.

Constant PLEM_SYSTEM_LIQUID = 0
Constant PLEM_SYSTEM_MICROSCOPE = 1

// @hacky system identification.
Function PLEMd2getSystem(strUser)
	String strUser

	strswitch(TrimString(strUser))
		case "the master of microscopy":
		case "unknown":
			return PLEM_SYSTEM_MICROSCOPE
		case "":
			return -1
		default:
			return PLEM_SYSTEM_LIQUID
	endswitch
End

Function/S PLEMd2getGratingString(numGrating, numSystem)
	Variable numGrating, numSystem

	switch(numSystem)
		case PLEM_SYSTEM_LIQUID:
			switch(numGrating)
				case 1: // 150 l/mm 1055nm Blaze wl
					return "grating150blz1055pplane"
				case 2: // 150 l/mm 800nm Blaze wl
					return "grating150blz800pplane"
				case 3: // 150 l/mm 1250nm Blaze wl
					return "grating150blz1250pplane"
				default:
					break
			endswitch
			break
		case PLEM_SYSTEM_MICROSCOPE:
			switch(numGrating)
				case 1: // 300 l/mm 500nm Blaze wl
					return "grating300blz500"
				case 2: // 300 l/mm 1200nm Balze wl
					return "grating300blz1200"
				case 3: // 150 l/mm 1250nm Blaze wl
					return "grating150blz1250pplane"
				default:
					break
			endswitch
			break
		default:
	endswitch

	return ""
End

// get the file to load as quantum efficiency correction
// @todo: cooling is currently fixed.
Function/S PLEMd2getDetectorQEString(numDetector, numCooling, numSystem)
	Variable numDetector, numCooling, numSystem

	switch(numSystem)
		case PLEM_SYSTEM_LIQUID:
			switch(numDetector)
				case PLEMd2detectorNewton:
					return "qeNewtonDU920POEm75"
				case PLEMd2detectorIdus:
					return "qeIdusDU491A17m90"
				default:
					break
			endswitch
			break
		case PLEM_SYSTEM_MICROSCOPE:
			switch(numDetector)
				case PLEMd2detectorNewton:
					return "qeNewtonDU920POEm100"
				case PLEMd2detectorIdus:
					return "qeIdusDU491A17m90"
				case PLEMd2cameraClara:
				case PLEMd2cameraXeva:
				default:
					break
			endswitch
			break
		default:
	endswitch

	return ""
End

// get the filter for correcting excitation
Function/S PLEMd2getFilterExcString(numSystem, numDetector)
	Variable numSystem, numDetector

	switch(numSystem)
		case PLEM_SYSTEM_LIQUID:
			break
		case PLEM_SYSTEM_MICROSCOPE:
			switch(numDetector)
				case PLEMd2detectorNewton:
				case PLEMd2detectorIdus:
					// it is possible to add filterChroma760refl but it does not have good accuracy.
					return "reflSilver;reflSilver"
				default:
					break
			endswitch
			break
		default:
	endswitch

	return "_none_"
End

// get the filter for correcting excitation
Function/S PLEMd2getFilterEmiString(numSystem, numDetector)
	Variable numSystem, numDetector

	switch(numSystem)
		case PLEM_SYSTEM_LIQUID:
			break // currently no check for liquid system filter wheel
		case PLEM_SYSTEM_MICROSCOPE:
			switch(numDetector)
				case PLEMd2detectorNewton:
				case PLEMd2detectorIdus:
					// manual possiblility: filterFEHL0750
					return "filterCG830;filterChroma760trans;reflSilver;reflSilver;reflSilver"
				default:
					break
			endswitch
			break
		default:
	endswitch

	return ""
End

// Get the (un-interpolated) quantum efficiency wave for the current PLEM
// Loads the wave from PLEMCorrectionPath if not present in the current experiment
Function/WAVE PLEMd2getQuantumEfficiency(stats)
	Struct PLEMd2stats &stats

	String strDetector

	if(stats.numDetector == PLEMd2cameraClara || stats.numDetector == PLEMd2cameraXeva)
		return $""
	endif

	DFREF dfr = DataFolderReference(cstrPLEMd2correction)
	WAVE/Z wv = dfr:$strDetector
	if(WaveExists(wv))
		return wv
	endif

	PLEMd2LoadCorrectionWaves(strDetector)

	WAVE wv = dfr:$strDetector
	return wv
End

Function/S PLEMd2LoadFilters(strFilters)
	String strFilters

	Variable i, numFilters
	String strFilter
	String strFiltersOut = ""

	DFREF dfr = DataFolderReference(cstrPLEMd2correction)
	numFilters = ItemsInList(strFilters)
	for(i = 0; i < numFilters; i += 1)
		strFilter = StringFromList(i, strFilters)
		WAVE/Z wv = dfr:$strFilter
		if(!WaveExists(wv))
			if(PLEMd2LoadCorrectionWaves(strFilter) == 0)
				continue
			endif
		endif
		strFiltersOut = AddListItem(strFilter, strFiltersOut)
	endfor

	return strFiltersOut
End

Function/WAVE PLEMd2SetCorrection(filters, target, targetX)
	String filters
	WAVE target, targetX

	String strFilter
	Variable i, numFilters, mini, maxi

	DFREF dfr = DataFolderReference(cstrPLEMd2correction)

	filters = PLEMd2LoadFilters(filters)
	numFilters = ItemsInList(filters)
	for(i = 0; i < numFilters; i += 1)
		strFilter = StringFromList(i, filters)
		WAVE/Z wv = dfr:$(strFilter)
		if(!WaveExists(wv))
			Abort "Filter not found."
		endif

		WAVE/Z wvX = dfr:$(strFilter + "_wl")
		Duplicate/FREE target dummy
		// linear interpolation to target wave
		if(WaveExists(wvX))
			Interpolate2/T=1/I=3/Y=dummy/X=targetX wvX, wv
		else
			Interpolate2/T=1/I=3/Y=dummy/X=targetX wv
		endif
		mini = WaveMin(wvX)
		maxi = WaveMax(wvX)
		dummy[] = targetX[p] > mini && targetX[p] < maxi ? dummy[p] : NaN

		if(i == 0)
			Duplicate/O dummy target
		else
			target *= dummy
		endif
	endfor

	return target
End

Function PLEMd2LoadCorrectionWaves(strCorrectionWave)
	String strCorrectionWave

	Variable err, i, j, numFiles
	Variable numLoaded = 0
	String ibwList, ibwFile

	PLEMd2LoadGratingPath()
	PathInfo PLEMCorrectionPath
	if(!V_flag)
		Abort "could not find path"
	endif

	ibwList = IndexedFile(PLEMCorrectionPath, -1, ".ibw")
	ibwList = ListMatch(ibwList, strCorrectionWave + "*")
	numFiles = ItemsInList(ibwList)

	DFREF saveDFR = GetDataFolderDFR()
	DFREF dfr = DataFolderReference(cstrPLEMd2correction)
	SetDataFolder dfr
	for(i = 0; i < numFiles; i += 1)
		ibwFile = StringFromList(i, ibwList)
		try
			GetFileFolderInfo/P=PLEMCorrectionPath/Q/Z=1 ibwFile
			if(!V_flag && V_isFile)
				LoadWave/Q/N/O/H/P=PLEMCorrectionPath ibwFile; AbortOnRTE
				numLoaded += V_Flag
			endif
		catch
			err = GetRTError(1)
		endtry
	endfor
	SetWaveLock 1, allinCDF
	SetDataFolder saveDFR

	PathInfo PLEMCorrectionPath
	if(numFiles > 0)
		printf "%d/%d files matching %s loaded from %s.\r", numLoaded, numFiles, strCorrectionWave, S_path
	endif
	return numLoaded
End

// create PLEMCorrectionPath from package prefs
Function PLEMd2LoadGratingPath()
	String strPath

	Struct PLEMd2Prefs prefs

	PathInfo PLEMCorrectionPath
	if(V_flag)
		return 0
	endif

	PLEMd2LoadPackagePrefs(prefs)
			
	// DisplayHelpTopic "Symbolic Paths"
	strPath = prefs.strCorrectionPath
	GetFileFolderInfo/Q/Z=1 strPath
	if(V_flag && !V_isFolder)
		Abort "Could not load symbolic path for gratings"
	endif

	PathInfo strPath
	if(!V_flag)
		NewPath/O/Q/Z PLEMCorrectionPath, strPath
	endif

	return 0
End

Data Processing

Statistical Analysis

Statistical analysis, data preparation, and extraction was performed with a custom Igor Pro program for mass analysis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

#include "sma-coordinates"
#include "sma-covariance"
#include "sma-duplicateRange"
#include "sma-images"
#include "sma-infostructure"
#include "sma-macro"
#include "sma-main"
#include "sma-menu"
#include "sma-peakfind"
#include "sma-prefs"
#include "sma-tools"
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

Function SMA_FindMatchingSpectra(coordinates)
	WAVE coordinates

	variable dim0, numMatches, numMatchesOld, i, j
	variable tolerance = 1 // tolerance in um around coordinate

	Make/O/N=(0) root:matches/WAVE=matches

	WAVE spectra = PLEMd2getCoordinates()
	dim0 = DimSize(coordinates, 0)
	for(i = 0; i < dim0; i += 1)
		WAVE indices = CoordinateFinderXYrange(spectra, coordinates[i][0] - tolerance, coordinates[i][0] + tolerance, coordinates[i][1] - tolerance, coordinates[i][1] + tolerance, verbose = 1)
		numMatches = DimSize(indices, 0)
		numMatchesOld = DimSize(matches, 0)
		Redimension/N=(numMatchesOld + numMatches) matches
		for(j = 0; j < numMatches; j += 1)
			matches[numMatchesOld + j] = indices[j]
			PLEMd2Displaybynum(indices[j])
		endfor
	endfor
End

Function SMA_ExportCoordinates()
	Variable i, numMaps
	String name, file
	String baseName, maps

	SMAupdatePath()

	STRUCT FILO#experiment filos
	FILO#structureLoad(filos)

	baseName = IgorInfo(1)
	WAVE allCoordinates = PLEMd2getCoordinates(forceRenew = 1)
	maps = PLEMd2getStrMapsAvailable()

	WAVE/Z indices = root:peakIndex // one spectrum per exactscan
	if(!WaveExists(indices))
		numMaps = ItemsInList(maps)
		Make/FREE/N=(numMaps) indices = p
	endif

	// quick validation if files were only loaded with SMAread()
	if(ItemsInList(maps) != ItemsInList(filos.strFileList))
		Abort "SMA_ExportCoordinates: Missmatch"
	endif

	numMaps = DimSize(indices, 0)
	Make/O/N=(numMaps, 3) root:$(basename + "_coordinates")/WAVE=coordinates
	Make/T/O/N=(numMaps) root:$(basename + "_originals")/WAVE=files
	for(i = 0; i < numMaps; i += 1)
		name = StringFromList(indices[i], maps)
		file = StringFromList(indices[i], filos.strFileList)
		if(!!cmpstr(name, CleanupName(ParseFilePath(3, file, ":", 0, 0), 0)))
			Abort "smaload not valid for " + num2str(i)
		endif
		files[i] = file
		coordinates[i][] = allCoordinates[indices[i]][q]
	endfor

	file = files[0]
	if(!!cmpstr(file[0], ":"))
		print files
		print filos.strFolder
		Abort "Lecacy Format detected"
	endif

	// save to home
	Save/C/O/P=home files
	Save/C/O/P=home coordinates
End

// merge two PLEM ranges and save them as a new ibw file
// only works for single background
Function SMA_MergeMaps(indices0, indices1)
	WAVE indices0, indices1

	String wavenote0, wavenote1, strPath, strPLEM
	Variable i, numMaps
	Variable start, ende
	STRUCT PLEMd2Stats stats
	Variable skip = 2 // skip the last 2 entries in the first wave

	if(DimSize(indices0, 0) != DimSize(indices1, 0))
		Abort "Size Missmatch"
	endif

	PathInfo home
	strPath = S_Path + IgorInfo(1)
	NewPath/C/O/Z newmaps, strPath

	numMaps = DimSize(indices0, 0)
	for(i = 0; i < numMaps; i += 1)
		strPLEM = PLEMd2strPLEM(indices0[i])
		PLEMd2statsLoad(stats, strPLEM)
		DFREF dfr = $(GetWavesDataFolder(stats.wavPLEM, 1) + "ORIGINAL")
		WAVE original0 = WaveRefIndexedDFR(dfr, 0)

		PLEMd2statsLoad(stats, PLEMd2strPLEM(indices1[i]))
		DFREF dfr = $(GetWavesDataFolder(stats.wavPLEM, 1) + "ORIGINAL")
		WAVE original1 = WaveRefIndexedDFR(dfr, 0)

		Duplicate/O original0 root:$strPLEM/WAVE=dest
		Redimension/N=(-1, DimSize(original0, 1) + DimSize(original1, 1) - 2 - skip) dest // single bg
		dest[][DimSize(original0, 1) - skip, *] = original1[p][q - DimSize(original0, 1) + skip + 2]

		wavenote0 = note(original0)
		start = strsearch(wavenote0, "Max Central Wavelength (nm): ", start)
		ende  = strsearch(wavenote0, "\r", start)
		wavenote0 = wavenote0[0, start - 1] + "Max Central Wavelength (nm): " + num2str(stats.numEmissionEnd) + wavenote0[ende, inf]

		wavenote1 = note(original1)
		start = strsearch(wavenote1, "Power at Glass Plate (µW):", 0) + 28
		ende  = strsearch(wavenote1, "\r", start) - 1
		wavenote1 = wavenote1[start, ende]
		start = strsearch(wavenote0, "Power at Glass Plate", 0) + 28
		ende  = strsearch(wavenote0, "\r", start) - 9 * skip
		wavenote0 = wavenote0[0, ende] + wavenote1 + wavenote0[ende + 9 * skip, inf]

		wavenote1 = note(original1)
		start = strsearch(wavenote1, "IGOR3:WL;BG;PL", 0) + 12 + skip * 12 + skip // skip "IGOR3:WL;BG;" and n x "PL_6875_7125"
		wavenote0 += wavenote1[start, inf]
		Note/K dest wavenote0

		Save/C/O/P=newmaps dest
		KillWaves dest
	endfor
End

// see ACW_EraseMarqueeArea.
Function SMA_EraseMarqueeArea()
	variable dim0, numMatches, i

	GetMarquee left, bottom //V_bottom, V_top, V_left and V_right
	if (V_flag == 0)
		return 0
	endif

	WAVE coordinates = SMA_PromptTrace()
	WAVE indices2 = CoordinateFinderXYrange(coordinates, V_bottom, V_top, V_left, V_right, verbose = 1)
	numMatches = DimSize(indices2, 0)
	if(!numMatches)
		return 0
	endif

	// delete Points
	WAVE/Z deleted = root:coordinates_deleted
	if(!WaveExists(deleted))
		Make/N=(numMatches, 3) root:coordinates_deleted/WAVE=deleted
	else
		dim0 = DimSize(deleted, 0)
		dim0 = 0 // reset deleted wave
		Redimension/N=(dim0 + numMatches, -1) deleted
	endif
	Sort/R indices2, indices2
	if(!cmpstr(NameOfWave(coordinates), "coordinates"))
		WAVE/Z l1 = root:legende
		WAVE/Z l2 = root:legend_text
	endif
	for(i = 0; i < numMatches; i += 1)
		deleted[dim0 + i][] = coordinates[indices2[i]][q]
		DeletePoints/M=0 indices2[i], 1, coordinates
		if(!cmpstr(NameOfWave(coordinates), "coordinates"))
			if(WaveExists(l1) && WaveExists(l2))
				DeletePoints/M=0 indices2[i], 1, l1, l2
			endif
		endif
	endfor

	// Append coordinates_deleted wave to top graph if not present
	CheckDisplayed deleted
	if(V_flag == 0)
		AppendToGraph deleted[][0]/TN=deleted vs deleted[][1]
		ModifyGraph mode(deleted)=4,marker(deleted)=8,opaque(deleted)=1,rgb(deleted)=(0,65535,0)
	endif
End

// @brief Delete entities in @p coordinates that are duplicate values in @p reference
//
// give a @p companion wave that is connected with its rows to coordinates and delete from there as well.
// @returns number of points deleted
Function SMAdeleteDuplicateCoordinates(coordinates, reference, [companion])
	WAVE coordinates, reference, companion

	Variable i, j, numMatches, dim0

	if(ParamIsDefault(companion))
		if(DimSize(companion, 0) != DimSize(coordinates, 0))
			Abort "SMAdeleteDuplicateCoordinates: companion and coordinates need to have the same number of rows."
		endif
	endif

	Make/FREE/N=0 indices
	dim0 = DimSize(reference, 0)
	for(i = 0; i < dim0; i += 1)
		WAVE duplicates = CoordinateFinderXYrange(coordinates, reference[i][0] - 1.5, reference[i][0] + 1.5, reference[i][1] - 1, reference[i][1] + 1, verbose = 0)

		numMatches = DimSize(duplicates, 0)
		if(!cmpstr(GetWavesDataFolder(coordinates, 2), GetWavesDataFolder(reference, 2))) // check if acting on the same wave
			Extract/FREE duplicates, duplicates, (duplicates[p] > i)
		endif
		if(!WaveExists(duplicates))
			continue
		endif

		Concatenate/FREE {indices, duplicates}, dummy
		Duplicate/FREE dummy indices
		WaveClear dummy
	endfor

	if(Dimsize(indices, 0) == 0)
		return 0
	endif

	if(DimSize(indices, 0) > 1)
		Sort indices, indices
		FindDuplicates/FREE/RN=dummy/TOL=0 indices
		if(!WaveExists(dummy))
			return 0
		endif
		Duplicate/FREE dummy indices
	endif

	Extract/FREE indices, matches, numtype(indices[p]) == 0
	numMatches = DimSize(matches, 0)
	for(i = numMatches - 1; i > -1; i -= 1)
		if(numtype(matches[i]) != 0)
			numMatches -= 1
			continue
		endif
		if(ParamIsDefault(companion))
			DeletePoints matches[i], 1, coordinates
		else
			DeletePoints matches[i], 1, coordinates, companion
		endif
	endfor

	// always be verbose due to the delete
	printf "deleted %d matches in %s.\r", numMatches, GetWavesDataFolder(coordinates, 2)
	return numMatches
End

Function/Wave SMA_PromptTrace()
	String itemName
	Variable selectedItem

	string itemsList = ""
	Variable numItems = 0

	String topWindowImages =	ImageNameList("", ";")
	String topWindowTraces =	TraceNameList("", ";", 1)
	
	Variable numTraces = ItemsInList(topWindowTraces)
	Variable numImages = ItemsInList(topWindowImages)
	
	// remove trailing ";"
	if(!cmpstr(topWindowImages[(strlen(topWindowImages) - 1), strlen(topWindowImages)], ";"))
		topWindowImages = topWindowImages[0, strlen(topWindowImages) - 1]
	endif
	itemsList = AddListItem(topWindowImages, topWindowTraces, ";", numTraces)
	numItems = numTraces + numImages

	if(numItems == 0)
		print "No traces found in top graph"
		return $""
	endif

	if(numItems == 1)
		if(numTraces)
			return TraceNameToWaveRef("", StringFromList(0, itemsList))
		else
			return ImageNameToWaveRef("", StringFromList(0, itemsList))
		endif
	endif

	Prompt selectedItem, "Choose Trace", popup, itemsList
	DoPrompt "Enter wave", selectedItem
	if(V_flag)
		print "SMA_PromptTrace: catched Cancel"
		return $""
	endif
	selectedItem -= 1 // zero based index
	itemName = StringFromList(selectedItem, itemsList)

	if(selectedItem < numTraces)
		return TraceNameToWaveRef("", itemName)
	else
		return ImageNameToWaveRef("", itemName)
	endif
End

Function GetCoordinates()
	Variable temp, i, j
	Variable numCoords, numPeaks
	Variable numSize = 1024
	String topWindowImages =	ImageNameList("", ";")
	String topWindowTraces =	TraceNameList("", ";", 1)

	if(ItemsInList(topWindowImages) == 0)
		print "no Image found in top graph"
		return 0
	endif

	WAVE/Z image = ImageNameToWaveRef("", StringFromList(0, topWindowImages))
	if(!WaveExists(image))
		print "image wave does not exist."
		return 0
	endif

	STRUCT PLEMd2Stats stats
	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))

	// start fresh
	Make/O/N=(numSize, 3) root:coordinates/Wave=wavCoordinates = NaN
	Make/O/N=(numSize, 3) root:legende/Wave=wavLegend = NaN
	Make/O/T/N=(numSize) root:legend_text/Wave=wavLegendText = ""

	// display coordinates
	if(FindListItem("coordinates", topWindowTraces) == -1)
		AppendToGraph wavCoordinates[][0]/TN=coordinates vs wavCoordinates[][1]
		ModifyGraph mode(coordinates)=3,marker(coordinates)=1,msize(coordinates)=2
	endif
	if(FindListItem("legend", topWindowTraces) == -1)
		AppendToGraph wavLegend[][0]/TN=legend vs wavLegend[][1]
		ModifyGraph mode(legend)=3
		ModifyGraph textMarker(legend)={wavLegendText,"default",0,0,5,0.00,0.00}
		ModifyGraph msize(legend)=3
	endif

	Duplicate/FREE image, currentImage

	// prepare: remove offset
	ImageFilter/O/N=5 gauss currentImage
	temp = WaveMin(currentImage)
	currentImage -= temp

	for(i = 0; i <= 300; i += 4)
		Duplicate/FREE/R=(0, 300)(i) currentImage lineProfile
		WAVE peaks = SMApeakFind(lineProfile, maxPeaks = 30, minPeakPercent = 0.1, smoothingfactor = NaN)
		WaveClear lineProfile
		numPeaks = DimSize(peaks, 0)
		for(j = 0; j < numPeaks; j += 1)
			if(peaks[j][%fwhm] > 5)
				continue
			endif
			if(numSize < numCoords)
				Redimension/N=(numCoords + 10, -1) wavCoordinates, wavLegend, wavLegendText
				numSize = numCoords + 10
			endif

			wavCoordinates[numCoords][0] = i
			wavCoordinates[numCoords][1] = peaks[j][%location]
			wavCoordinates[numCoords][2] = stats.numPositionZ

			wavLegend[numCoords][0] = wavCoordinates[numCoords][0]
			wavLegend[numCoords][1] = wavCoordinates[numCoords][1]
			wavLegendText[numCoords] = "i=" + num2str(peaks[j][%height]) + "\r f=" + num2str(peaks[j][%fwhm])
			numCoords += 1
		endfor
		DoUpdate
		WaveClear peaks
	endfor

	Redimension/N=(numCoords, -1) wavCoordinates, wavLegendText, wavLegend
	SortColumns/KNDX={0,1} sortwaves=wavCoordinates
End

Function AddCoordinatesFromGraph()
	Variable numItems
	Variable aExists = 0

	aExists= strlen(CsrInfo(A)) > 0
	if(!aExists)
		print "Cursor A not in Graph"
		return 0
	endif
	WAVE/Z coordinates = root:coordinates
	if(!WaveExists(coordinates))
		SMAresetCoordinates()
		WAVE coordinates = root:coordinates
	endif
	WAVE/T/Z legende = root:legend_text

	numItems = DimSize(coordinates, 0)
	Redimension/N=(numItems + 1, 3) coordinates
	coordinates[numItems][0]=vcsr(A)
	coordinates[numItems][1]=hcsr(A)
	WAVE/Z normal = root:SMAcameraPlaneNormal
	WAVE/Z distance = root:SMAcameraPlaneDistance
	if(!WaveExists(normal) || !WaveExists(distance))
		coordinates[numItems][2]=150
		print "AddCoordinatesFromGraph: missing tilt plane parameters"
	else
		coordinates[numItems][2]= SMAcameraGetTiltPlane(coordinates[numItems][0],coordinates[numItems][1])
	endif

	if(WaveExists(legende))
		Redimension/N=(numItems + 1, -1) legende
		legende[numItems]="manual"
	endif

	// Append coordinates_deleted wave to top graph if not present
	CheckDisplayed coordinates
	if(V_flag == 0)
		AppendToGraph coordinates[][0]/TN=coordinates vs coordinates[][1]
		ModifyGraph mode(coordinates)=4,marker(coordinates)=8
	endif
End

Function DeleteCoordinates(rangeMin, rangeMax)
	Variable rangeMin, rangeMax
	Variable numItems, i, deleteMe
	Variable numDelete = 0

	WAVE/Z coordinates = root:coordinates
	WAVE/T/Z legende = root:legend_text

	if(!WaveExists(coordinates))
		print "required waves do not exist. Start SMAgetCoordinates first"
		return 0
	endif

	Duplicate/FREE coordinates original
	numItems = DimSize(original, 0)
	for(i = 0; i < numItems; i += 1)
		deleteMe = 0
		if((coordinates[i - numDelete][0] < rangeMin) || (coordinates[i - numDelete][0] > rangeMax))
			deleteMe = 1
		endif
		if((coordinates[i - numDelete][1] < rangeMin) || (coordinates[i - numDelete][1] > rangeMax))
			deleteMe = 1
		endif
		if(deleteMe)
			DeletePoints/M=0 i - numDelete, 1, coordinates
			if(WaveExists(legende))
				DeletePoints/M=0 i - numDelete, 1, legende
			endif
			numDelete += 1
		endif
	endfor
End

Function SortCoordinates()
	WAVE/Z coordinates = root:coordinates
	WAVE/T/Z legende = root:legend_text

	if(!WaveExists(coordinates))
		print "required waves do not exist. Start SMAgetCoordinates first"
		return 0
	endif

	if(WaveExists(legende))
		SortColumns/KNDX={0,1} keyWaves=coordinates, sortWaves={coordinates, legende}
	else
		SortColumns/KNDX={0,1} sortWaves=coordinates
	endif
End

Function RoundCoordinates([accuracy])
	Variable accuracy

	WAVE/Z coordinates = root:coordinates

	accuracy = ParamIsDefault(accuracy) ? 4 : accuracy

	if(!WaveExists(coordinates))
		print "required waves do not exist. Start SMAgetCoordinates first"
		return 0
	endif

	coordinates[][0] = round(coordinates[p][0] / accuracy) * accuracy
End

Function SMAcalcZcoordinateFromTiltPlane([wv, zOffset])
	WAVE wv
	variable zOffset

	zOffset = ParamIsDefault(zOffset) ? SMAcameraGetTiltPlane(0,0) : zOffset

	if(ParamIsDefault(wv))
		WAVE wv = root:coordinates
	endif
	if(!WaveExists(wv))
		print "SMAcalcZcoordinateFromTiltPlane: input wave does not exist"
		return 0
	endif

	WAVE/Z normal = root:SMAcameraPlaneNormal
	WAVE/Z distance = root:SMAcameraPlaneDistance
	if(!WaveExists(normal) || !WaveExists(distance))
		print "SMAcalcZcoordinateFromTiltPlane: nothing done"
		return 0
	endif

	wv[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zOffset)
End

// @brief search trenches for carbon nanotubes (considered experimental)
Function SMAgetCoordinates()
	Variable i
	STRUCT PLEMd2Stats stats

	variable numMaps = PLEMd2getMapsAvailable()

	SMAresetCoordinates()
	SMAbuildGraphPLEM()
	//WAVE fullimage = SMAmergeImages(0, createNew = 0)
	//Duplicate/FREE fullimage, currentImage
	//SMAparticleAnalysis(currentImage)

	WAVE background = SMAestimateBackground()
	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))
		Duplicate/FREE stats.wavPLEM, currentImage
		ImageFilter/O /N=5 median currentImage // remove spikes
		currentImage -= background
		SMAsearchTrenches(currentImage, trenchpitch = 4)
	endfor

	return 1
End

Function SMAsearchTrenches(currentImage, [trenchpitch])
	WAVE currentImage
	Variable trenchpitch 	// distance from one trench to the next

	Variable i, numPeaks, numCoords
	Variable dim0, dim0offset, dim0delta, dim1
	Variable Pmin, Pmax, Ymin, Ymax, Qdelta
	Variable rangePdelta, rangeYdelta
	Variable currentY, QcenterTrench

	Variable rangeXmin = 5, rangeXmax = 300
	Variable rangeYmin = 0, rangeYmax = 300
	Variable Ydelta = 4/3 * 1 // size of trench area in y-direction

	trenchpitch = ParamIsDefault(trenchpitch) ? 4 : trenchpitch

	dim0 = DimSize(currentImage, 0)
	dim0delta = DimDelta(currentImage, 0)
	dim0offset = DimOffset(currentImage, 0)
	Pmin = limit(ScaleToIndex(currentImage, rangeXmin, 0), 0, dim0 - 1)
	Pmax = limit(ScaleToIndex(currentImage, rangeXmax, 0), 0, dim0 - 1)
	Qdelta = floor(abs(ScaleToIndex(currentImage, Ydelta, 1) - ScaleToIndex(currentImage, 0, 1)))

	dim1 = DimSize(currentImage, 1)
	Ymin = IndexToScale(currentImage, 0, 1)
	Ymax = IndexToScale(currentImage, dim1 - 1, 1)
	SMAorderAsc(Ymin, Ymax)
	Ymin = limit(ceil((Ymin + Ydelta) / trenchpitch) * trenchpitch, rangeYmin, rangeYmax)
	Ymax = limit(floor((Ymax - Ydelta) / trenchpitch) * trenchpitch, rangeYmin, rangeYmax)

	rangePdelta = abs(Pmax - Pmin) + 1
	rangeYdelta = abs(Ymax - Ymin) / trenchpitch + 1

	// differentiate 2 times along trench area
	ImageFilter/O/N=5 gauss currentImage
	Differentiate/DIM=0 currentImage
	Differentiate/DIM=0 currentImage
	ImageFilter/O/N=11 gauss currentImage

	// extract all trenches. Trench width is Qdelta
	Make/O/N=(rangePdelta, rangeYdelta) root:trenches/WAVE=wavTrenches = NaN
	for(i = 0; i < rangeYdelta; i += 1)
		currentY = Ymin + trenchpitch * i
		QcenterTrench = ScaleToIndex(currentImage, currentY, 1)
		Duplicate/FREE/R=[Pmin, Pmax][floor(QcenterTrench - Qdelta), ceil(QcenterTrench + Qdelta)] currentImage, currentTrench
		MatrixOP/FREE currentTrenchAvg = sumRows(currentTrench)
		wavTrenches[][i] = currentTrenchAvg[p]
	endfor

	// substract a median trench to see the peaks better
	Make/O/N=(rangePdelta) root:averageTrench/WAVE=averageTrench
	for(i = 0; i < rangePdelta; i += 1)
		MatrixOP/FREE currentPixel = row(wavTrenches, i)
		averageTrench[i] = median(currentPixel)
	endfor
	Smooth 50, averageTrench
	wavTrenches[][] -= averageTrench[p]

	// find the peaks and store them
	Make/FREE/N=(rangePdelta) positionX = dim0offset + (Pmin + p) * dim0delta
	Make/FREE/N=(0, 2)   currentCoordinates
	Make/FREE/N=(0, 0)/T description
	for(i = 0; i < rangeYdelta; i += 1)
		currentY = Ymin + trenchpitch * i
		Duplicate/FREE/R=[][i] wavTrenches, currentTrenchAvg
		WAVE peaks = PeakFind(currentTrenchAvg, wvXdata = positionX, maxPeaks = 10, minPeakPercent = 90)
		numPeaks = DimSize(peaks, 0)
		if(numPeaks == 0)
			continue
		endif

		Redimension/N=(numPeaks, -1) currentCoordinates, description
		currentCoordinates[][0] = currentY
		currentCoordinates[][1] = peaks[p][%location]
		description[] = num2str(round(peaks[p][%height]))

		SMAaddCoordinates(currentCoordinates, text = description)
		numCoords += numPeaks
	endfor

	return numCoords
End

Function SMAparticleAnalysis(currentImage)
	WAVE currentImage

	variable numPeaks

	// differentiate 2 times to get the peaks without background
	ImageFilter/O/N=5 gauss currentImage
	Differentiate/DIM=0 currentImage
	Differentiate/DIM=0 currentImage
	ImageFilter/O/N=11 gauss currentImage

	// remove 2nd derivative sattelites
	currentImage *= -1 // inverted image
	currentImage[][] = currentImage[p][q] < 0 ? 0 : currentImage[p][q]

	// calculate Threshold
	//StatsQuantiles/Q/ALL currentImage
	//Wave W_StatsQuantiles
	//myThreshold = W_StatsQuantiles[%upperOuterFence]
	//print "Threshold set to ", myThreshold
	//MatrixFilter/O NanZapMedian currentImage
	//ImageThreshold/I/O/Q/M=0/T=(myThreshold) currentImage
	ImageThreshold/I/O/Q/M=5 currentImage

	// particle size: elipse with defined circularity, min and max area
	ImageAnalyzeParticles/A=20/MAXA=500/CIRC={0.75 , 1.75}/W/M=2 stats currentImage

	WAVE W_SpotX, W_spotY
	WAVE W_BoundaryX, W_BoundaryY
	WAVE W_ImageObjArea, M_rawMoments, W_circularity

	numPeaks = DimSize(W_SpotX, 0)

	if((numPeaks == 0) || (numPeaks > 3000))
		print "Error: non reasonable peak count", numPeaks
		return 0
	endif

	Make/FREE/N=(numPeaks, 2) currentCoordinates
	currentCoordinates[][0] = IndexToScale(currentImage, M_rawMoments[p][1] / W_ImageObjArea[p], 1)
	currentCoordinates[][1] = IndexToScale(currentImage, M_rawMoments[p][0] / W_ImageObjArea[p], 0)

	return SMAaddCoordinates(currentCoordinates)
End

Function SMAresetCoordinates()
	Make/O/N=(0, 3) root:coordinates/Wave=wavCoordinates = NaN
End

Function SMAaddCoordinates(currentCoordinates, [text])
	WAVE currentCoordinates
	WAVE/T text

	Variable j, k, numPeaks, numCoords, numSize
	Variable coordinateX, coordinateY, duplicateValue

	numPeaks = DimSize(currentCoordinates, 0)
	if(numPeaks == 0)
		return 0
	endif
	WAVE/Z wavCoordinates = root:coordinates
	if(!WaveExists(wavCoordinates))
		Make/O/N=(0, 3) root:coordinates/Wave=wavCoordinates = NaN
	endif
	numCoords = DimSize(wavCoordinates, 0)
	numSize = numCoords + numPeaks

	WAVE/T/Z wavLegendText = root:legend_text
	if(!WaveExists(wavLegendText))
		Make/O/T/N=(numSize) root:legend_text/Wave=wavLegendText = ""
	endif
	Make/FREE/D/N=(numSize) coordinatesX = 0, coordinatesY = 0

	Redimension/N=(numSize, -1) wavCoordinates, wavLegendText, coordinatesX, coordinatesY
	for(j = 0; j < numPeaks; j += 1)
		if(numSize < numCoords)
			numSize = numCoords + 10
		endif

		// get coordinates (rounded to 0.1)
		coordinateX = round(currentCoordinates[j][0]/0.1)*0.1
		coordinateY = round(currentCoordinates[j][1]/0.1)*0.1

		// compare to currently saved coordinates
		k = -1
		duplicateValue = 0
		do
			FindValue/S=(k+1)/T=1/V=(coordinateX) coordinatesX
			k = V_Value
			if(k == -1)
				break
			endif
			// min. range for a new coordinate is 2 µm
			if(round(coordinatesY[k]/2)*2 == round(coordinateY/2)*2)
				duplicateValue = 1
				break
			endif
		while(k < numCoords)

		if(duplicateValue)
			wavCoordinates[k][0] = (wavCoordinates[V_Value][0] + coordinateX) / 2
			wavCoordinates[k][1] = (wavCoordinates[V_Value][1] + coordinateY) / 2
			continue
		endif

		coordinatesX[numCoords] = coordinateX
		coordinatesY[numCoords] = coordinateY

		wavCoordinates[numCoords][0] = coordinateX
		wavCoordinates[numCoords][1] = coordinateY
		wavCoordinates[numCoords][2] = 150 // fixed

		if(!ParamIsDefault(text))
			wavLegendText[numCoords] = text[j]
		else
			wavLegendText[numCoords] = "(" + num2str(coordinateX) + ", " + num2str(coordinateY) + ")"
		endif

		numCoords += 1
	endfor

	Redimension/N=(numCoords, -1) wavCoordinates, wavLegendText
	SortColumns/KNDX={0,1} sortwaves=wavCoordinates

	return numCoords
End

Function/S SMAsearchCoordinate(coordinateX, coordinateY)
	Variable coordinateX, coordinateY

	Variable k
	String listFound = ""
	WAVE coordinates = PLEMd2getCoordinates()

	// round coordinates
	coordinateX = round(coordinateX/0.1)*0.1
	coordinateY = round(coordinateY/0.1)*0.1

	// separate Waves
	Duplicate/FREE/R=[][0] coordinates coordinatesX
	Duplicate/FREE/R=[][1] coordinates coordinatesY
	Redimension/N=(-1, 0) coordinatesX, coordinatesY

	k = -1
	do
		if(k==57)
			print "hier"
		endif
		FindValue/S=(k+1)/T=1/V=(coordinateX) coordinatesX
		k = V_Value
		if(k == -1)
			break
		endif

		if(round(coordinatesY[k]/2)*2 == round(coordinateY/2)*2)
			listFound = AddListItem(num2str(k), listFound)
			continue
		endif
	while(k < DimSize(coordinates, 0))

	return listFound
End

// Zzero is the new zero position to which the z values will be corrected
// i.e. the new focus point at (x,y) = (0,0)
Function/WAVE SMAcameraCoordinates([Zzero, export])
	Variable Zzero
	Variable export

	export = ParamIsDefault(export) ? 1 : !!export

	Variable xstep = 48, ystep = 80, zstep = -1

	Zzero = ParamIsDefault(Zzero) ? SMAcameraGetTiltPlane(0, 0) : Zzero

	printf "SMAcameraCoordinates(Zzero = %.2f, export = %d)\r", Zzero, export

	// 4 scans in x = 300/64
	// 6 scans in y = 300/48
	// 8 scans in z direction (4um from laserfocus to bottom of trench in 0.5um steps)
	// --> 16 hours when integrating 300s.

	Make/O/N=(4 * 6 * 8, 3) root:SMAfullscan/WAVE=wv

	wv[][0] = 4 * 6 + mod(floor(p / 4), 6) * xstep
	wv[][1] = 30 + mod(p, 4) * ystep
	wv[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero) + floor(p / (4 * 6)) * zstep

	Duplicate/O/R=[0 * 4 * 6, 1 * 4 * 6 - 1] wv root:SMAsinglescan00/WAVE=singlescan00
	singlescan00[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero)

	Duplicate/O/R=[0 * 4 * 6, 1 * 4 * 6 - 1] wv root:SMAsinglescan10/WAVE=singlescan10
	singlescan10[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero) - 1

	Duplicate/O/R=[0 * 4 * 6, 1 * 4 * 6 - 1] wv root:SMAsinglescan15/WAVE=singlescan15
	singlescan15[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero) - 1.5

	Duplicate/O/R=[0 * 4 * 6, 1 * 4 * 6 - 1] wv root:SMAsinglescan20/WAVE=singlescan20
	singlescan20[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero) - 2

	Duplicate/O/R=[0 * 4 * 6, 1 * 4 * 6 - 1] wv root:SMAsinglescan25/WAVE=singlescan25
	singlescan25[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero) - 2.5

	Duplicate/O/R=[0 * 4 * 6, 1 * 4 * 6 - 1] wv root:SMAsinglescan40/WAVE=singlescan40
	singlescan40[][2] = SMAcameraGetTiltPlane(wv[p][0], wv[p][1], zOffset = zZero) - 4

	if(export)
		Save/J/O/DLIM=","/P=home singlescan00 as "singlescan00.csv"
		Save/J/O/DLIM=","/P=home singlescan10 as "singlescan10.csv"
		Save/J/O/DLIM=","/P=home singlescan15 as "singlescan15.csv"
		Save/J/O/DLIM=","/P=home singlescan20 as "singlescan20.csv"
		Save/J/O/DLIM=","/P=home singlescan25 as "singlescan25.csv"
		Save/J/O/DLIM=","/P=home singlescan40 as "singlescan40.csv"
		Save/J/O/DLIM=","/P=home wv as "fullscan.csv"
	endif
	
	return wv
End

Function SMAcameraGetIntensity()
	variable i
	STRUCT PLEMd2Stats stats
	variable numMaps = PLEMd2getMapsAvailable()
	variable V_FitError = 0

	if(numMaps == 0)
		SMAload()
		numMaps = PLEMd2getMapsAvailable()
	endif

	Make/O/N=(numMaps) root:SMAcameraIntensity/WAVE=intensity
	Make/O/N=(numMaps, 3) root:SMAcameraIntensityCoordinates/WAVE=coordinates

	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))
		V_FitError = 0
		if(DimSize(stats.wavPLEM, 1) > 1)
			CurveFit/Q/M=0/W=2 Gauss2D stats.wavPLEM
		else
			CurveFit/Q/M=0/W=2 Gauss stats.wavPLEM
		endif
		if(V_FitError == 0)
			WAVE W_Coef
			intensity[i] = W_Coef[1]
		else
			intensity[i] = WaveMax(stats.wavPLEM)
		endif

		coordinates[i][0] = stats.numPositionX
		coordinates[i][1] = stats.numPositionY
		coordinates[i][2] = stats.numPositionZ
	endfor
End

Function/WAVE SMAcameraGetTiltPlaneParameters([createNew])
	variable createNew

	variable numPeaks

	createNew = ParamIsDefault(createNew) ? 0 : !!createNew

	WAVE/Z focuspoints = root:SMAcameraFocusPoints
	if(createNew || !WaveExists(focuspoints))
		WAVE focuspoints = SMAgetFocuspoints(graph = 1, createNew = createNew)
	endif

	numPeaks = DimSize(focuspoints, 0)
	if(!WaveExists(focuspoints) || (numPeaks != 3))
		print "SMAcameraGetTiltPlaneParameters(): Please correct manually and call again."
		edit focuspoints
		print "call: to add from graph"
		print "root:SMAcameraFocusPoints[2] = root:SMAcameraIntensityCoordinates[pcsr(a)][q]"
		print "call after manual editing root:SMAcameraFocusPoints:"
		print "SMAcameraGetTiltPlaneParameters(createNew=0)"
		Abort "Error in peakfind"
	endif

	return SMAHessePlaneParameters(focuspoints)
End

/// @brief search for laserspot maxima in loaded images
Function/WAVE SMAgetFocuspoints([graph, createNew])
	variable graph, createNew

	variable numPeaks, i, step, pStart, pEnd, pPeak
	variable numSpectra = PLEMd2getMapsAvailable()
	variable numFocuspoints = 3
	variable pOffset = 5

	graph = ParamIsDefault(graph) ? 0 : !!graph
	createNew = ParamIsDefault(createNew) ? 0 : !!createNew

	WAVE/Z intensity = root:SMAcameraIntensity
	WAVE/Z coordinates = root:SMAcameraIntensityCoordinates
	if(!WaveExists(intensity) || !WaveExists(coordinates) || createNew)
		SMAcameraGetIntensity()
		WAVE intensity = root:SMAcameraIntensity
		WAVE coordinates = root:SMAcameraIntensityCoordinates
	endif

	Duplicate/O intensity root:SMAcameraIntensitySmth/WAVE=intensity_smooth
	Smooth 5, intensity_smooth

	Make/O/N=(numFocuspoints, 3) root:SMAcameraFocusPoints/WAVE=focuspoints = 0
	Make/O/N=(numFocuspoints) root:SMAcameraPlanePeakMaximum/WAVE=focuspointsPval = 0
	for(i = 0; i < numFocuspoints; i += 1)
		pStart = i * round(numSpectra / 3)
		pEnd = (i + 1) * round(numSpectra / 3) - 1

		// quick fix: experimentally the first points are often garbage
		pStart += pOffset

		Duplicate/FREE/R=[pStart,pEnd] intensity_smooth singlePeakY
		Duplicate/FREE/R=[pStart,pEnd][2] coordinates singlePeakX
		Redimension/N=(-1, 0) singlePeakX

		WAVE guess = PeakFind(singlePeakY, wvXdata = singlePeakX, maxPeaks = 1)
		WAVE/WAVE coef = BuildCoefWv(singlePeakY, wvXdata = singlePeakX, peaks = guess)

		WAVE/WAVE/Z peakParam = fitGauss(singlePeakY, wvXdata = singlePeakX, wvCoef = coef)
		if(!WaveExists(peakParam))
			// revert to guess
			WAVE/WAVE coef = BuildCoefWv(singlePeakY, wvXdata = singlePeakX, peaks = guess)
			WAVE/WAVE peakParam = GaussCoefToPeakParam(coef)
		endif

		WAVE peakfind = peakParamToResult(peakParam)
		if(!WaveExists(peakfind))
			print "SMAgetFocuspoints(): Please correct manually and call again."
			Abort "Error in peakfind"
		endif

		numPeaks = DimSize(peakfind, 0)
		if(numPeaks == 1)
			pPeak = pStart - pOffset
			FindLevel singlePeakX, peakfind[0][%location]
			if(!V_flag)
				pPeak = V_LevelX
			endif
			focuspointsPval[i] = pPeak
			focuspoints[i][] = coordinates[pPeak][q]
			focuspoints[i][2] = peakfind[0][%location]
			printf "peak%d: \t x-Axis: \t%06.2f \ty-Axis: \t%06.2f \tz-Axis: \t%06.2f\r", i, focuspoints[i][0], focuspoints[i][1], focuspoints[i][2]
		endif
	endfor
	
	//output the results as graph
	if(graph)
		Make/O/T/N=(numFocuspoints) root:SMAcameraPlanePeakMaximumT = "(" + num2str(focuspoints[p][0]) + "," + num2str(focuspoints[p][1]) + ")"
		step = floor(DimSize(coordinates, 0) / 10 / 2) * 2
		Make/O/N=10 root:SMAcameraPlanePeakMaximumZ/WAVE=zWave = step / 2 + p * step
		Make/O/T/N=10 root:SMAcameraPlanePeakMaximumZT = num2str(round(coordinates[zWave[p]][2] * 10) / 10)
		Execute/Z "SMAcameraFocusPointsGraph()"
		SavePICT/O/P=home/E=-5/B=72
	endif

	return focuspoints
End

Function/WAVE SMAHessePlaneParameters(focuspoints)
	WAVE focuspoints

	printf "SMAHessePlaneParameters(%s)\r", GetWavesDataFolder(focuspoints, 2)

	Make/O/N=3 root:SMAcameraPlaneNormal/WAVE=normal
	Make/O/N=1 root:SMAcameraPlaneDistance/WAVE=distance

	MatrixOP/FREE row0 = row(focuspoints, 0)^t
	MatrixOP/FREE row1 = row(focuspoints, 1)^t
	MatrixOP/FREE row2 = row(focuspoints, 2)^t
	MatrixOP/FREE temp1 = row1 - row0
	MatrixOP/FREE temp2 = row2 - row0
	Cross/T/DEST=normal temp1, temp2

	MatrixOP/O distance = normal . averageCols(focuspoints)

	print "calculated plane in Hesse Normal form. Saving to home folder."
	Save/C/O/P=home distance, normal

	return focuspoints
End

Function SMAcameraGetTiltPlane(coordinateX, coordinateY, [zOffset])
	variable coordinateX, coordinateY, zOffset

	variable coordinateZ
	
	if(ParamIsDefault(zOffset))
		zOffset = 0
	else
		zOffset -= SMAcameraGetTiltPlane(0, 0)
	endif

	WAVE/Z normal = root:SMAcameraPlaneNormal
	WAVE/Z distance = root:SMAcameraPlaneDistance
	if(!WaveExists(normal) || !WaveExists(distance))
		SMAcameraGetTiltPlaneParameters()
		WAVE normal = root:SMAcameraPlaneNormal
		WAVE distance = root:SMAcameraPlaneDistance
	endif

	return zOffset + (distance[0] - normal[0] * coordinateX - normal[1] * coordinateY) / normal[2]
End

/// @brief Return a Wave of Waves with all matching PLEM indices for the supplied coordinates wave
Function/WAVE SMAfindCoordinatesInPLEM(coordinates, [verbose, accuracy])
	WAVE coordinates
	Variable verbose, accuracy

	Variable i, dim0

	verbose = ParamIsDefault(verbose) ? 0 : !!verbose

	WAVE PLEMcoordinates = PLEMd2getCoordinates()

	dim0 = DimSize(coordinates, 0)
	Make/FREE/U/I/WAVE/N=(dim0) indices
	if(ParamIsDefault(accuracy))
		indices[] = CoordinateFinderXYZ(PLEMcoordinates, coordinates[p][0], coordinates[p][1], coordinates[p][2], verbose = verbose)
	else
		indices[] = CoordinateFinderXYZ(PLEMcoordinates, coordinates[p][0], coordinates[p][1], coordinates[p][2], verbose = verbose, accuracy = accuracy)
	endif

	return indices
End

// @brief reduce the found indices from @c SMAfindCoordinatesInPLEM  to the first match
// @return unsigned integer wavv
Function/WAVE SMAreduceIndices(wv)
	WAVE/WAVE wv

	Variable i, numMaps = DimSize(wv, 0)
	Make/N=(numMaps)/U/I/FREE indices
	for(i = 0; i < numMaps; i += 1)
		WAVE matches = wv[i]
		indices[i] = matches[0]
	endfor

	return indices
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

// https://docs.byte-physics.de/json-xop/#download-and-installation
#include "json_functions"

static constant samplingAccuracy = 5e-2 // in nanometer

// Append the given spectra to a 2-dimensional wave.
//
// WARNING: Will alter the original data! (spikes, resample, interpolate)
//          Automatically resamples data only if it was not equally acquired
//          using different detectors, gratings, or excitation steps
//
// @param overwrite  if set to 1: recreate the wave if it already exists
// @param range      specify the spectra ids with a numeric, uint wave
// @param destName   Name of destination WAVE in root: data folder
// @param downsample set to force to a specific wavelength spacing
// @param graph      display a graph showing the source wave
Function/WAVE SMAgetSourceWave([overwrite, range, destName, downsample, graph])
	Variable overwrite
	WAVE/U/I range
	String destName
	Variable downsample, graph

	Variable i, j, dim0, dim1, dim2, numMarkers, scale, index, accuracy, err
	STRUCT PLEMd2Stats stats

	DFREF dfr = root:

	overwrite = ParamIsDefault(overwrite) ? 1 : !!overwrite
	graph = ParamIsDefault(graph) ? 1 : !!graph
	if(ParamIsDefault(range))
		Make/FREE/U/I/N=(PLEMd2getMapsAvailable()) range = p
	endif
	dim0 = DimSize(range, 0)
	if(ParamIsDefault(destName))
		destName = "source"
	endif
	WAVE/Z wv = dfr:$destName
	if(WaveExists(wv) && !overwrite)
		if(DimSize(wv, 0) == dim0)
			return wv
		endif
	endif

	if(dim0 > (2^32))
		Abort "SMAgetSourceWave: Too many spectra."
	endif

	STRUCT SMAsampling s
	SMAgetResamplingInformation(range, s, range = 1, verbose = 0)

	if(!ParamIsDefault(downsample))
		s.xDelta = max(s.xDelta, downsample)
		s.yDelta = max(s.yDelta, downsample)
	endif
	s.yDelta = numtype(s.yDelta) != 0 ? abs(s.yMax - s.yMin) : s.yDelta // undefined @c numExcitationStep in spectra

	dim1 = round(abs(s.xMax - s.xMin) / s.xDelta)
	dim2 = max(1, round(abs(s.yMax - s.yMin) / s.yDelta))
	if(dim0 * dim1 * dim2 > (2^32))
		if(dim0 * dim2 > (2^32))
			Abort "SMAgetSourceWave: Can not handle this many points."
		endif
		do // downsample x (x is always smaller than y due to setup constraints)
			s.xDelta *= 2
			dim1 = round(abs(s.xMax - s.xMin) / s.xDelta)
			if(s.xDelta > s.yDelta) // downsample y
				s.yDelta *= 2
				dim2 = max(1, round(abs(s.yMax - s.yMin) / s.yDelta))
			endif
		while(dim0 * dim1 * dim2 < (2^32))
	endif
	s.xSize = numtype(dim1) == 0 ? dim1 : 0
	s.ySize = numtype(dim2) == 0 ? dim2 : 0

	Make/FREE/N=(dim0, s.xSize * s.ySize) wv
	if(dim2 == 1)
		SetScale/I y, s.xMin, s.xMax, wv
	endif
	SMAsetSampling(wv, s)

	Make/FREE/N=(s.xSize, s.ySize) target
	Make/FREE/N=(s.xSize) targetX
	SetScale/I x, s.xMin, s.xMax, target, targetX
	SetScale/I y, s.yMin, s.yMax, target

	// fill source wave
	for(i = 0; i < dim0; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(range[i]))
		WAVE PLEM = stats.wavPLEM

		// resample along excitation Δλ > 1, size < 48
		if(DimSize(PLEM, 1) > 2)
			accuracy = min(samplingAccuracy, stats.numEmissionStep / s.yDelta)
			try
				RatioFromNumber/MERR=(accuracy) stats.numEmissionStep / s.yDelta; AbortOnRTE
				Resample/DIM=1/UP=(V_numerator)/DOWN=(V_denominator)/N=3 PLEM; AbortOnRTE
			catch
				err = GetRTError(1)
				printf "SMAgetSourceWave: Failed to Resample %s from %d to %d/%d Error: %s\r", stats.strPLEM, DimSize(PLEM, 1), V_numerator, V_denominator, GetRTErrMessage()
			endtry
		endif

		// interpolate along emission Δλ < 1, size > 768
		//
		// last spectrum is garbage in PLE acquisition.
		// Fixed with https://github.com/ukos-git/labview-plem/commit/87b38e6f03b345e5c9823fa79b9dc358dbe251be
		target = median(PLEM) // we should have enough noise to fill missing values with median.
		for(j = 0; j < max(1, DimSize(PLEM, 1) - 1); j += 1)
			Duplicate/FREE/R=[][j] PLEM dummy
			Redimension/N=(-1, 0) dummy
			// T=1: linear interpolation
			// T=3: smoothing spline interpolation
			try
				Interpolate2/T=1/I=3/Y=targetX stats.wavWavelength, dummy; AbortOnRTE
			catch
				err = GetRTError(1)
				printf "SMAgetSourceWave: Error in interpolate2 for %d in %s\r", j, stats.strPLEM
				targetX = NaN
			endtry
			scale = IndexToScale(PLEM, j, 1)
			index = min(dim2 - 1, max(0, ScaleToIndex(target, scale, 1)))
			target[][index] = targetX[p]
		endfor
		WAVE PLEM = removeSpikes(target)

		// resample not necessary due to the previous interpolation but more failsafe
		accuracy = min(samplingAccuracy, DimSize(targetX, 0) / s.xSize)
		RatioFromNumber/MERR=(accuracy) DimSize(targetX, 0) / s.xSize
		Resample/DIM=0/UP=(V_numerator)/DOWN=(V_denominator)/N=3 PLEM

		Multithread wv[i][] = PLEM[mod(q, dim1)][floor(q / dim1)]
	endfor
	Duplicate/O wv dfr:$destName/WAVE=wv

	if(!graph)
		return wv
	endif

	DoWindow SMAsourceGraph
	if(V_flag == 0)
		SMAcopyWavelengthToRoot()
		Display/N=SMAsourceGraph
		AppendImage wv
		ModifyImage ''#0  ctab= {*,*,YellowHot256,1}
	endif

	return wv
End

Function SMARedimensionToMap(wv)
	WAVE wv

	Variable dim0, dim1v1, dim1v2, dim1

	STRUCT PLEMd2Stats stats
	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))

	dim0 = DimSize(stats.wavPLEM, 0)
	dim1 = round(DimSize(wv, 0) / dim0)
	Redimension/E=1/N=(dim0, dim1) wv

	SetScale/P x, DimOffset(stats.wavPLEM, 0), DimDelta(stats.wavPLEM, 0), wv
	SetScale/P y, DimOffset(stats.wavPLEM, 1), DimDelta(stats.wavPLEM, 1), wv
End

// @brief Covariance for indices with different detectors
//
// @param indices Wave holding the ids of PLEM spectra
Function SMAgetCovarianceForDifferentSetups(indices)
	WAVE/U/I indices

	Variable i, numMaps
	STRUCT PLEMd2Stats stats

	WAVE allDetectors = PLEMd2getDetectors()

	numMaps = DimSize(indices, 0)
	if(numMaps == 0)
		return 1
	endif
	Make/U/B/N=(numMaps)/FREE detectors = allDetectors[indices[p]]

	Extract/FREE indices, indicesSilicon, (detectors[p] == PLEMd2detectorNewton)
	WAVE autocorrelation = SMAcovariance(range = indicesSilicon, graph = 0)
	Duplicate/O autocorrelation root:covariance_si_diag
	WAVE/Z covariance = root:covariance_sym
	if(WaveExists(covariance))
		Duplicate/O covariance root:covariance_si
	endif

	Extract/FREE indices, indicesInGaAs, (detectors[p] == PLEMd2detectorIdus)
	WAVE autocorrelation = SMAcovariance(range = indicesInGaAs, graph = 0)
	Duplicate/O autocorrelation root:covariance_ingaas_diag
	WAVE/Z covariance = root:covariance_sym
	if(WaveExists(covariance))
		Duplicate/O covariance root:covariance_ingaas
	endif

	KillWaves/Z covariance, autocorrelation
	return 0
End

/// @brief calculate symmetric correlation spectra
///
/// NOTE: acts on downsampled data of 25nm spacing
///
/// @param normalized  divide all spectra by its maximum
/// @param range       specify the spectra ids with a numeric, uint wave
/// @returns a wave reference to the symmetric autocorrelation
Function/WAVE SMAcovariance([normalized, range, graph])
	Variable normalized, graph
	WAVE/U/I range

	Variable replacement
	STRUCT SMAsampling s

	if(ParamIsDefault(normalized))
		normalized = 1
	endif
	if(ParamIsDefault(graph))
		graph = 1
	endif

	NVAR/Z downsample = root:downsample
	if(ParamIsDefault(range))
		if(NVAR_EXISTS(downsample))
			WAVE source = SMAgetSourceWave(overwrite = 1, graph = 0, downsample = downsample)
		else
			WAVE source = SMAgetSourceWave(overwrite = 1, graph = 0)
		endif
	else
		if(NVAR_EXISTS(downsample))
			WAVE source = SMAgetSourceWave(overwrite = 1, range = range, graph = 0, downsample = downsample)
		else
			WAVE source = SMAgetSourceWave(overwrite = 1, range = range, graph = 0)
		endif
	endif

	if(numpnts(source) == 0)
		Duplicate/O source root:covariance_sym_diag/WAVE=symdiag
		return symdiag
	endif

	// there should be a decent amount of equal noise to remove NaNs
	StatsQuantiles/Q source
	replacement = V_Q25
	MatrixOP/O source = ReplaceNaNs(source, replacement)

	// normalize
	if(normalized)
		MatrixoP/O source = normalizeRows(source)
	endif

	SMAgetSampling(source, s)
	if(s.ySize > 1)
		return SMAcovarianceMaps(source)
	endif

	MatrixOP/O root:covariance_sym/WAVE=sym = syncCorrelation(source)
	MatrixOP/O root:covariance_sym_diag/WAVE=symdiag = getDiag(sym, 0)

	SMAcopyWavelengthToRoot()

	SetScale/P x, s.xMin, s.xDelta, sym, symdiag
	SetScale/P y, s.xMin, s.xDelta, sym

	if(!graph)
		return symdiag
	endif

	DoWindow SMAcovarianceGraphDiagonal
	if(V_flag == 0)
		Display/N=SMAcovarianceGraphDiagonal symdiag/TN=diag_syncCorrelation
	endif

	DoWindow SMAcovarianceGraph
	if(V_flag == 0)
		Display/N=SMAcovarianceGraph
		AppendImage/W=SMAcovarianceGraph sym
	endif

	return symdiag
End

// @brief get the autocorrelation of a 2d maps wave
//
// @param source give source wave
// @return the symmetric autocorrelation
Function/WAVE SMAcovarianceMaps(source)
	WAVE source

	STRUCT SMAsampling s
	SMAgetSampling(source, s)

	if(DimSize(source, 0) > 1)
		//MatrixOP/O root:covariance_sym_diag/WAVE=symdiag = getDiag(syncCorrelation(source), 0)
		MatrixOP/O root:covariance_sym_diag/WAVE=symdiag = varCols(source)^t
	else
		Duplicate/O source root:covariance_sym_diag/WAVE=symdiag
	endif
	Redimension/N=(s.xSize, s.ySize)/E=1 symdiag
	SetScale/I x, s.xMin, s.xMax, symdiag
	SetScale/I y, s.yMin, s.yMax, symdiag

	DoWindow SMAcovarianceImage
	if(V_flag == 0)
		Display/N=SMAcovarianceImage
		AppendImage/W=SMAcovarianceImage symdiag
		ModifyImage covariance_sym_diag ctab= {*,*,Terrain256,0}
	endif

	return symdiag
End

// @brief structure with info for building source waves
//
// @see SMAgetResamplingInformation SMAgetSourceWave
static Structure SMAsampling
	Variable xMin, xDelta, xMax, xSize
	Variable yMin, yDelta, yMax, ySize
EndStructure

// @brief save the sampling information in the given wave.
//
// requires the JSON_XOP: http://docs.byte-physics.de/json-xop/
//
// @see SMAgetSamplin SMAsampling SMAcovarianceMaps SMAgetSourceWave
static Function SMAsetSampling(wv, s)
	WAVE wv
	STRUCT SMAsampling &s

	Variable jsonID
	String wavenote = Note(wv)

	if(strlen(wavenote) == 0)
		jsonID = JSON_New()
	else
		jsonID = JSON_Parse(wavenote, ignoreErr = 1)
	endif

	JSON_AddTreeObject(jsonID, "/scaling/x")
	JSON_SetVariable(jsonID,   "/scaling/x/min",   s.xMin)
	JSON_SetVariable(jsonID,   "/scaling/x/delta", s.xDelta)
	JSON_SetVariable(jsonID,   "/scaling/x/max",   s.xMax)
	JSON_SetVariable(jsonID,   "/scaling/x/size",  s.xSize)
	JSON_AddTreeObject(jsonID, "/scaling/y")
	JSON_SetVariable(jsonID,   "/scaling/y/min",   s.yMin)
	JSON_SetVariable(jsonID,   "/scaling/y/delta", s.yDelta)
	JSON_SetVariable(jsonID,   "/scaling/y/max",   s.yMax)
	JSON_SetVariable(jsonID,   "/scaling/y/size",  s.ySize)
	Note/K wv, JSON_Dump(jsonID)
	JSON_Release(jsonID)
End

// @brief get the sampling information back from the given wave.
//
// requires the JSON_XOP: http://docs.byte-physics.de/json-xop/
//
// @see SMAsetSampling SMAsampling SMAcovarianceMaps SMAgetSourceWave
static Function SMAgetSampling(wv, s)
	WAVE wv
	STRUCT SMAsampling &s

	Variable jsonID = JSON_Parse(Note(wv))
	s.xMin   = JSON_GetVariable(jsonID, "/scaling/x/min")
	s.xDelta = JSON_GetVariable(jsonID, "/scaling/x/delta")
	s.xMax   = JSON_GetVariable(jsonID, "/scaling/x/max")
	s.xSize  = JSON_GetVariable(jsonID, "/scaling/x/size")
	s.yMin   = JSON_GetVariable(jsonID, "/scaling/y/min")
	s.yDelta = JSON_GetVariable(jsonID, "/scaling/y/delta")
	s.yMax   = JSON_GetVariable(jsonID, "/scaling/y/max")
	s.ySize  = JSON_GetVariable(jsonID, "/scaling/y/size")
	JSON_Release(jsonID)
End

// @brief analyze a range of maps to get resampling information
//
// NOTE: for equally scaled wave sets taken with equal (detector, grating)
//       combination this function should yield the original wave scaling.
//       @todo: add unit test
//
// For covariance calculation and any averaging on maps or spectra taken with
// different detectors or gratings, the maps have to get aligned to get
// overlapping pixels. Note that this procedure assumes equally spaced input
// waves and is not accurate when acting i.e. on unequally spaced waves like
// the wavelength waves from the grating waves that are of quadratic or higher
// order distored along the wavelength scale.
//
// Assumes positive delta values
//
// @param[in]  indices  unsigned integer wave containing the ids of spectra
//                      to analyze
// @param[out] s        @see SMAsampling structure holding all acquired information
// @param[in]  range    respect fixed setup specific range (grating + detectors)
//                      @see PLEMd2NanotubeRangePLEM
// @param[in]  verbose  set output verbosity, default 1 (verbose)
Function SMAgetResamplingInformation(indices, s, [range, verbose])
	WAVE/U/I indices
	STRUCT SMAsampling &s
	Variable range, verbose

	Variable i, numMaps, dim0, newSamplingRate
	STRUCT PLEMd2Stats stats

	range = ParamIsDefault(range) ? 0 : !!range
	verbose = ParamIsDefault(verbose) ? 1 : !!verbose

	numMaps = DimSize(indices, 0)
	Make/D/N=(numMaps)/FREE xCRC, yCRC
	Make/N=(numMaps)/FREE excMin, excDelta, excMax, emiMin, emiDelta, emiMax
	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(indices[i]))

		// spectral emission intensity is called emi(ssion)
		emiMin[i] = stats.wavWavelength[0]
		emiMax[i] = stats.wavWavelength[DimSize(stats.wavWavelength, 0) - 1]
		emiDelta[i] = stats.numWLdelta // inaccurate (<0.5nm)
		if(range)
			emiMin[i] = max(emiMin[i], stats.numDetector == PLEMd2detectorNewton ? 820 : 950)
			emiMax[i] = min(emiMax[i], stats.numDetector == PLEMd2detectorNewton ? 1040 : 1250)
		endif
		xCRC[i] = StringCRC(0, num2str(stats.numGrating))
		xCRC[i] = StringCRC(xCRC[i], num2str(stats.numDetector))
		xCRC[i] = StringCRC(xCRC[i], num2str(stats.numWLcenter))

		// emission from light source is called exc(itation)
		excMin[i] = stats.numEmissionStart
		excMax[i] = stats.numEmissionEnd
		excDelta[i] = stats.numEmissionStep
		if(range)
			excMin[i] = max(excMin[i], 540)
			excMax[i] = min(excMax[i], 750)
		endif
		yCRC[i] = StringCRC(0, num2str(stats.numEmissionStep))
		yCRC[i] = StringCRC(yCRC[i], num2str(stats.numEmissionStart))
	endfor

	s.xMin    = WaveMin(emiMin)
	s.xMax    = WaveMax(emiMax)
	s.xDelta  = median(emiDelta)

	s.yMin    = WaveMin(excMin)
	s.yMax    = WaveMax(excMax)
	s.yDelta  = median(excDelta)

	if(numMaps < 2)
		return 0
	endif

	// output status information about experiment
	if(verbose)
		FindDuplicates/FREE/RN=grating xCRC
		dim0 = DimSize(grating, 0)
		if(dim0 > 1)
			printf "SMAgetResamplingInformation: %d different grating/detector setups\r", dim0
			for(i = 0; i < dim0; i += 1)
				Extract/FREE indices, sameXrange, xCRC[p] == grating[i]
				printf "%02d: %d maps\r", i, DimSize(sameXrange, 0)
			endfor
		endif
		FindDuplicates/FREE/RN=excitation yCRC
		dim0 = DimSize(excitation, 0)
		if(dim0 > 1)
			printf "SMAgetResamplingInformation: %d different excitation setups\r", dim0
			for(i = 0; i < dim0; i += 1)
				Extract/FREE excMin, sameYrange, yCRC[p] == excitation[i]
				printf "%02d: %d maps\r", i, DimSize(sameYrange, 0)
			endfor
		endif
	endif

	// find best sampling rate for different delta y
	//
	// do not do such things for xDelta as we can accept the error there but
	// not in y where spacing is allowed up to 50nm.
	excDelta[] = round(excDelta[p] / samplingAccuracy)
	s.yDelta  = median(excDelta)
	FindDuplicates/FREE/TOL=0/RN=excDeltaUnique excDelta
	dim0 = DimSize(excDeltaUnique, 0)
	for(i = 0; i < dim0; i += 1)
		newSamplingRate = excDeltaUnique[i]
		if(mod(newSamplingRate, s.yDelta) != 0)
			s.yDelta = gcd(s.yDelta, newSamplingRate )
		endif
	endfor
	FindDuplicates/FREE/TOL=0/RN=excOffsetUnique excMin
	dim0 = DimSize(excOffsetUnique, 0)
	for(i = 1; i < dim0; i += 1)
		newSamplingRate = abs(excOffsetUnique[i] - excOffsetUnique[i - 1])
		if(mod(newSamplingRate, s.yDelta) != 0)
			s.yDelta = gcd(s.yDelta, newSamplingRate )
		endif
	endfor
	s.yDelta *= samplingAccuracy
End

// copy the wavelength from PLEM
// this should be a PLEMd2 function
//
// @param numPLEM [optional] specify the id of the spectrum where wavelength comes from.
Function/WAVE SMAcopyWavelengthToRoot([numPLEM])
	variable numPLEM

	variable numPoints
	STRUCT PLEMd2Stats stats

	numPLEM = ParamIsDefault(numPLEM) ? 0 : numPLEM

	PLEMd2statsLoad(stats, PLEMd2strPLEM(numPLEM))

	Duplicate/O stats.wavWavelength root:wavelength/WAVE=wavelength

	// @todo: delete wavelengthImage here as it adds wrong assumtion. Instead use SetScale where 2D Waves need to be plotted.
	Duplicate/O stats.wavWavelength root:wavelengthImage/WAVE=wavelength_graph
	numPoints = DimSize(wavelength, 0)
	Redimension/N=(numPoints + 1) wavelength_graph
	wavelength_graph[numPoints] = wavelength_graph[numPoints - 1] + 1

	return wavelength
End

// copy the excitation wavelength from PLEM
// this should be a PLEMd2 function
//
// @param numPLEM [optional] specify the id of the spectrum where wavelength comes from.
Function/WAVE SMAcopyExcitationToRoot([numPLEM])
	variable numPLEM

	variable numPoints
	STRUCT PLEMd2Stats stats

	numPLEM = ParamIsDefault(numPLEM) ? 0 : numPLEM

	PLEMd2statsLoad(stats, PLEMd2strPLEM(numPLEM))
	if(DimSize(stats.wavPLEM, 1) < 2)
		Abort "Not a PLE map"
	endif

	Duplicate/O stats.wavWavelength root:excitation/WAVE=excitation

	// @todo: delete wavelengthImage here as it adds wrong assumtion. Instead use SetScale where 2D Waves need to be plotted.
	Duplicate/O stats.wavExcitation root:excitationImage/WAVE=excitation_image
	numPoints = DimSize(excitation, 0)
	Redimension/N=(numPoints + 1) excitation_image
	excitation_image[numPoints] = excitation_image[numPoints - 1] + 1

	return excitation
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

Function SMAdisplayOriginal([numPLEM])
	Variable numPLEM

	Variable xStart, xEnd, yStart, yEnd
	
	if(ParamIsDefault(numPLEM))
		numPLEM = SMAgetOriginalFromMarquee()
	endif

	// save graph axis settings
	GetAxis/Q bottom
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting bottom axis in top Graph"
		return -1
	endif
	yStart = V_min
	yEnd = V_max
	GetAxis/Q left
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting left axis in top Graph"
		return -1
	endif
	xStart = V_min
	xEnd = V_max

	Variable offsetX, offsetY
	SMAgetOffset(offsetX, offsetY)

	Plemd2displaybynum(numPLEM)

	// copy graph axis settings
	GetAxis/Q bottom
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting bottom axis in top Graph"
		return -1
	endif
	SetAxis bottom, yStart + offsetY, yEnd + offsetY
	GetAxis/Q left
	if(!!V_flag)
		print "SMAdisplayOriginal: Error getting left axis in top Graph"
		return -1
	endif
	SetAxis left, xStart + offsetX, xEnd + offsetX
End

// get center coordinate from marquee and display search the image that matches those coordinates closest
Function SMAgetOriginalFromMarquee()
	Variable centerX, centerY

	GetMarquee left, bottom //V_bottom, V_top, V_left and V_right
	if (V_flag == 0)
		return -1
	endif
	centerY = V_left + (V_left - V_right) / 2
	centerX = V_bottom + (V_bottom - V_top) / 2

	return SMAgetFirstImage(centerX, centerY, 150)
End

// returns the index of the image where the specified coordinates are located
Function SMAgetFirstImage(centerX, centerY, centerZ)
	Variable centerX, centerY, centerZ
	Variable accuracy = 0

	Variable index

	WAVE coordinates = PLEMd2getcoordinates(forceRenew=1)
	do
		accuracy += 10
		WAVE/Z indices = CoordinateFinderXYZ(coordinates, centerX, centerY, centerZ, accuracy = accuracy, verbose = 0)
		if(!WaveExists(indices))
			continue
		endif
		index = indices[0]
	while(!!numtype(index) && accuracy < 1e4)

	if(index > PLEMd2getmapsavailable())
		print "SMAgetFirstImage: Index out of range"
		return -1
	endif

	if(!!numtype(index))
		return -1
	endif

	return index
End

Function SMADuplicateRangeFromMarquee()
	String win

	WAVE wv = SMAduplicateRange(SMAgetOriginalFromMarquee())
	if(!WaveExists(wv))
		return 1
	endif

	win = "win_" + NameOfWave(wv)
	Display/N=$win as win
	AppendImage wv
	ModifyGraph width={Plan,1,bottom,left}
	ModifyImage ''#0 ctab= {*,*,RedWhiteBlue256,0}
End

Function SMADuplicateRangeFromCoordinates(coordinates)
	WAVE coordinates

	String name, win, strPath
	Variable i
	Variable numCoordinates = DimSize(coordinates, 0)
	String coordinatesName = NameOfWave(coordinates)
	String experimentName = IgorInfo(1)

	if(DimSize(coordinates, 1) < 3)
		Abort "Need at least 2 coordinates"
	endif

	PathInfo home
	strPath = S_Path + experimentName
	NewPath/C/O/Z images, strPath

	for(i = 0; i < numCoordinates; i += 1)
		sprintf name, "%s_image%03d", coordinatesName, i
		SMADisplayCoordinates(coordinates[i][0], coordinates[i][1], range = 3) // set axis for SMAduplicateRange
		DoUpdate/W=win_SMAimageStack
		WAVE wv = SMAduplicateRange(SMAgetFirstImage(coordinates[i][0], coordinates[i][1], coordinates[i][2]), outputName = name)
		Save/C/O/P=images wv

		// create image for standard range
		Redimension/U/I wv
		Display/N=temp
		win = S_name
		AppendImage/W=$win wv
		ModifyImage/W=$win $"#0" ctab= {0,*,YellowHot,0}
		ModifyGraph/W=$win nticks=0, axthick=0, margin=1, width={Plan,1,bottom,left}
		SetDrawLayer/W=$win UserFront
		SetDrawEnv/W=$win linethick= 5,linefgc= (56797,56797,56797)
		DrawLine/W=$win 0.822437513922712,0.1,0.944949852649121,0.1
		SetDrawEnv/W=$win fsize= 24,fstyle= 1,textrgb= (65535,65535,65535)
		DrawText/W=$win 0.82122905027933,0.25,"1µm"
		DoUpdate/W=$win
		saveWindow(win, customName = name, path = "images", saveJSON = 1, saveTiff = 1, saveImages = 0)

		// cleanup
		KillWindow/Z $win
		KillWaves/Z wv
	endfor
End

// read globally set offset variables and give instructions on how to update them.
// set the input variables to their global values.
Function SMAgetOffset(offsetX, offsetY)
	Variable &offsetX, &offsetY

	// manually set using: SMAtasksZeroToCursor()

	NVAR/Z gOffsetX = root:offsetX
	if(NVAR_Exists(gOffsetX))
		offsetX = gOffsetX
	endif

	NVAR/Z gOffsetY = root:offsetY
	if(NVAR_Exists(gOffsetY))
		offsetY = gOffsetY
	endif
End

Function SMAsetOffset(offsetX, offsetY)
	Variable offsetX, offsetY

	NVAR/Z gOffsetX = root:offsetX
	if(!NVAR_Exists(gOffsetX))
		Variable/G root:offsetX
		NVAR gOffsetX = root:offsetX
	endif

	NVAR/Z gOffsetY = root:offsetY
	if(!NVAR_Exists(gOffsetY))
		Variable/G root:offsetY
		NVAR gOffsetY = root:offsetY
	endif

	gOffsetX = offsetX
	gOffsetY = offsetY
End

Function SMAaddOffset(addX, addY)
	Variable addX, addY

	Variable offsetX, offsetY
	SMAgetOffset(offsetX, offsetY)

	SMAsetOffset(offsetX + addX, offsetY + addY)
End

Function SMADisplayCoordinates(xCoordinate, yCoordinate, [range])
	Variable xCoordinate, yCoordinate
	Variable range

	String win = "win_SMAimageStack" // use this image
	if(ParamIsDefault(range))
		range = 2 // extract 2µm in x- and 2*2µm in y-direction
	endif

	DoWindow/F $win
	if(!V_flag)
		Abort "Create SMAimageStack first"
	endif

	SetAxis/W=$win left, xCoordinate - range, xCoordinate + range
	SetAxis/W=$win bottom, yCoordinate - 2 * range, yCoordinate + 2 * range
	DoUpdate/W=$win
End

Function/WAVE SMAduplicateRange(FirstImage, [outputName])
	String outputName
	Variable FirstImage

	Variable i, numPLEM
	Variable offsetX, offsetY

	Variable dim2 = 1
	Variable StackSize = 24
	Variable zStep = 1

	if(ParamIsDefault(outputName))
		outputName = "imageRange"
		outputName = UniqueName(outputName, 1, 0)
	endif

	// get range from axis settings (not intuitive for marquee!)
	GetAxis/Q left
	if(!!V_flag)
		return $""
	endif
	Variable xstart = V_min
	Variable xend = V_max
	GetAxis/Q bottom
	if(!!V_flag)
		return $""
	endif
	Variable ystart = V_min
	Variable yend = V_max

	SMAorderAsc(xStart, xEnd)
	SMAorderAsc(yStart, yEnd)

	SMAgetOffset(offsetX, offsetY)

	Struct PLEMd2stats stats
	PLEMd2statsLoad(stats, PLEMd2strPLEM(FirstImage))

	WAVE imagestack = getTopWindowImage()
	if(WaveExists(imagestack))
		dim2 = DimSize(imagestack, 2)
	endif

	// prepare output wave container
	Duplicate/O/R=(yStart + offsetY, yEnd + offsetY)(xStart + offsetX, xEnd + offsetX) stats.wavPLEM $outputName/WAVE=wv
	Redimension/N=(-1, -1, dim2) wv
	// set z Axis
	WAVE zAxis = PLEMd2getCoordinates()
	if(FirstImage + StackSize < PLEMd2getMapsAvailable())
		zStep = zAxis[FirstImage][2] - zAxis[FirstImage + StackSize][2]
	endif
	SetScale/P z, zAxis[FirstImage][2], zStep, wv
	// set x,y Axis offset
	AddWaveScaleOffset(wv, offsetY, offsetX)

	// write output wave
	for(i = 0; i < dim2; i += 1)
		numPLEM = FirstImage + i * StackSize
		PLEMd2statsLoad(stats, PLEMd2strPLEM(numPLEM))
		Duplicate/FREE/R=(yStart + offsetY, yEnd + offsetY)(xStart + offsetX, xEnd + offsetX) stats.wavPLEM image
		wv[][][i] = image[p][q]
	endfor
	SMASetCoordinates(wv, (yEnd + yStart) / 2, (xEnd + xStart) / 2 )

	return wv
End

Function SMASetCoordinates(wv, yCoordinate, xCoordinate)
	WAVE wv
	Variable yCoordinate, xCoordinate

	Variable start, ende
	String wavenote = note(wv)

	start = strsearch(wavenote, "x-position", start)
	ende  = strsearch(wavenote, "\r", start)
	wavenote = wavenote[0, start - 1] + "x-position: " + num2str(xCoordinate) + wavenote[ende, inf]
	start = strsearch(wavenote, "y-position", start)
	ende  = strsearch(wavenote, "\r", start)
	wavenote = wavenote[0, start - 1] + "y-position: " + num2str(yCoordinate) + wavenote[ende, inf]
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

// append all Images to one big Image (fullimage)
Function/WAVE SMAmergeImages([createNew, indices])
	Variable createNew
	WAVE/U/I indices

	Variable pixelX, pixelY, resolution, imageborders
	Variable positionX, positionY
	Variable numMaps
	Variable i, j, k, dim0, dim1
	variable imagearea = 320
	STRUCT PLEMd2Stats stats

	Variable numMapsAvailable = PLEMd2getMapsAvailable()
	if(numMapsAvailable == 0)
		SMAread()
		numMapsAvailable = PLEMd2getMapsAvailable()
	endif

	NVAR/Z gquick = root:numFullCalcultions
	if(!NVAR_EXISTS(gquick))
		Variable/G root:numFullCalcultions = 0
		NVAR gquick = root:numFullCalcultions
	endif
	Variable quick = !gquick // quick fix for quick logic

	if(ParamIsDefault(indices))
		Make/FREE/N=(numMapsAvailable)/U/I indices = p
	endif

	createNew = ParamIsDefault(createNew) ? 1 : !!createNew
	if(!createNew)
		WAVE/Z fullimage = root:fullimage
		if(WaveExists(fullimage))
			return fullimage
		endif
		WaveClear fullimage
	endif

	Variable timerRefNum = StartMSTimer

	wave/Z background = root:backgroundImage
	if(!WaveExists(background))
		wave background = SMAestimateBackground()
		Duplicate/O background root:backgroundImage
	endif

	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))

	// create output image
	resolution = (abs(DimDelta(stats.wavPLEM, 0)) + abs(DimDelta(stats.wavPLEM, 1))) / 2
	WAVE imageRange = ImageDimensions(indices)
	dim0 = abs(imageRange[%x][%max] - imageRange[%x][%min]) / resolution
	dim1 = abs(imageRange[%y][%max] - imageRange[%y][%min]) / resolution
	Make/O/N=(dim0, dim1) root:fullimage/WAVE=fullimage = 0
	Make/FREE/B/U/N=(dim0, dim1) fullimagenorm = 0
	SetScale/I x, imageRange[%x][%min], imageRange[%x][%max], fullimage
	SetScale/I y, imageRange[%y][%min], imageRange[%y][%max], fullimage

	// create intermediate image
	dim0 = DimSize(stats.wavPLEM, 0)
	dim1 = DimSize(stats.wavPLEM, 1)
	Make/FREE/N=(dim0, dim1) currentImage

	// add current image to output image
	numMaps = DimSize(indices, 0)
	for(i = 0; i < numMaps; i += 1)
		if(numtype(indices[i]) != 0)
			continue
		endif
		PLEMd2statsLoad(stats, PLEMd2strPLEM(indices[i]))
		MultiThread currentImage[][] = stats.wavPLEM[p][q] - background[p][q]
		if(!quick)
			ImageFilter/O /N=5 median currentImage // remove spikes
		endif
		for(j = 0; j < dim0; j += 1)
			positionX = IndexToScale(stats.wavPLEM, j, 0)
			pixelX = ScaleToIndex(fullimage, positionX, 0)
			if((pixelX < 0) || (pixelX > DimSize(fullimage, 0) - 1))
				continue
			endif
			for(k = 0; k < dim1; k += 1)
				if(numtype(currentImage[j][k]) != 0)
					continue
				endif
				positionY = IndexToScale(stats.wavPLEM, k, 1)
				pixelY = ScaleToIndex(fullimage, positionY, 1)
				if((pixelY < 0) || (pixelY > DimSize(fullimage, 1) - 1))
					continue
				endif

				fullimage[pixelX][pixelY] += currentImage[j][k]
				fullimagenorm[pixelX][pixelY] += 1
			endfor
		endfor
	endfor
	MultiThread fullimage[][] = fullimagenorm[p][q] == 0 ? NaN : fullimage[p][q] / fullimagenorm[p][q]

	// interpolate values, that were not found directly
	if(!quick)
		MultiThread fullimagenorm[][] = numtype(fullimage[p][q]) == 2
		if(sum(fullimagenorm) / (dim0 * dim1) < 0.01)
			ImageFilter/O NanZapMedian fullimage
		endif
	endif

	SMAbuildGraphFullImage()
	lap(timerRefNum, "SMAmergeImages")

	return fullimage
End

// input a wave stackCoordinates and search for the coordinates included in it.
// the wave stackCoordinates is split to coordinate lists that have the size stackSize.
// the function can be called multiple times with varying stackNumber to merge differnt
// parts of the coordinate list.
Function/WAVE SMAmergeStack(stackCoordinates, stackNumber, stackSize, [createNew])
	WAVE stackCoordinates
	Variable stackNumber, stackSize
	Variable createNew

	Variable rangeStart, rangeEnd
	
	createNew = ParamIsDefault(createNew) ? 1 : !!createNew

	rangeStart = stackNumber * stackSize
	rangeEnd   = (stackNumber + 1) * stackSize - 1
	Duplicate/FREE/R=[rangeStart, rangeEnd][] stackCoordinates scan
	WAVE/U/I found = SMAreduceIndices(SMAfindCoordinatesInPLEM(scan))
	make/free/n=(stackSize) normalnumber = numType(found[p]) == 0
	if(sum(normalnumber) < stackSize / 4)
		return $""
	endif
	WAVE fullimage = SMAmergeImages(indices = found, createNew = createNew)

	SMAreduceRange(fullimage, bit = 8)
	SMAconvertWaveToUint(fullimage, bit = 8)

	return fullimage
End

Function/WAVE SMAprocessImageStack([coordinates, createNew])
	WAVE coordinates
	Variable createNew

	Variable i, numFullImages, numImages

	createNew = ParamIsDefault(createNew) ? 1 : !!createNew
	if(ParamIsDefault(coordinates))
		//WAVE coordinates = SMAcameraCoordinates(export = 0)
		WAVE coordinates = PLEMd2getCoordinates()
	endif

	numImages = PLEMd2getMapsAvailable()
	if(numImages == 0)
		SMAread()
		numImages = PLEMd2getMapsAvailable()
		WAVE coordinates = PLEMd2getCoordinates(forceRenew = 1)
	endif

	if(!WaveExists(coordinates))
		Abort
	endif

	numFullImages = floor(numImages / 24)
	Wave fullimage = SMAmergeStack(coordinates, 0, 24)
	WAVE/Z imagestack = root:SMAimagestack
	if(DimSize(fullimage, 0) != DimSize(imagestack, 0) && DimSize(fullimage, 1) != DimSize(imagestack, 1))
		Duplicate/O fullimage root:SMAimagestack/WAVE=imagestack
	else
		Multithread imagestack[][][0] = fullimage[p][q]
	endif
	Redimension/N=(-1, -1, numFullImages) imagestack

	for(i = 1; i < numFullImages; i += 1)
		Wave fullimage = SMAmergeStack(coordinates, i, 24, createNew = createNew)
		if(WaveExists(fullimage))
			MultiThread imagestack[][][i] = fullimage[p][q]
		endif
	endfor

	SMAimageStackopenWindow()
	
	return imagestack
End

// only valid for images
Function SMAmergeTimeSeries()
	Variable i
	Variable numImages = PLEMd2getMapsAvailable()

	STRUCT PLEMd2Stats stats

	if(numImages == 0)
		SMAload()
	endif

	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))
	Duplicate/O stats.wavPLEM root:SMAimagestack/WAVE=imagestack
	Redimension/N=(-1, -1, numImages) imagestack

	for(i = 1; i < numImages; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))
		MultiThread imagestack[][][i] = stats.wavPLEM[p][q]
	endfor

	SMAimageStackopenWindow()
End

// see ACW_EraseMarqueeArea.
Function SMA_ExtractSumMarqueeArea()
	variable pStart, pEnd, qStart, qEnd
	variable i, dim2, cursorExists
	string sourcewin, destwin
	variable normalization = 0
	string outputName = "timetrace"
	
	sourcewin = WinName(0, 1)
	destwin = outputName

	GetMarquee left, bottom //V_bottom, V_top, V_left and V_right
	if (V_flag == 0)
		return 0
	endif
	
	WAVE/Z image = getTopWindowImage()
	pStart = ScaleToIndex(image, V_left, 0)
	pEnd = ScaleToIndex(image, V_right, 0)
	qStart = ScaleToIndex(image, V_bottom, 1)
	qEnd = ScaleToIndex(image, V_top, 1)

	SMAorderAsc(pStart, pEnd)
	SMAorderAsc(qStart, qEnd)

	DoWindow $destwin
	if(!V_Flag)
		Display/N=$destwin
	endif

	outputName = UniqueName(outputName, 1, 0)
	dim2 = DimSize(image, 2)
	Make/N=(dim2) $outputName/WAVE=wv

	cursorExists = strlen(CsrInfo(A)) > 0 && strlen(CsrInfo(B)) > 0
	if(cursorExists)
		print "substracting area between cursors as background for marquee area"
	endif

	for(i = 0; i < dim2; i += 1)
		Duplicate/FREE/R=[pStart, pEnd][qStart, qEnd][i] image, marqueearea
		if(cursorExists)
			Duplicate/FREE/R=[pcsr(a, sourcewin), pcsr(b, sourcewin)][qcsr(a, sourcewin), qcsr(b, sourcewin)][i] image, reference
			// background for marquearea
			normalization = sum(reference) / (DimSize(reference, 0) * DimSize(reference, 1)) * (DimSize(marqueearea, 0) * DimSize(marqueearea, 1))
		endif
		wv[i] = sum(marqueearea) - normalization
	endfor

	AppendToGraph/W=$destwin wv
	print outputname
End

// changes input wv to the maximum value, specified in root:numMaxValue.
// The value numMaxValue is relative to the bits of the output wave.
// Information is lost
Function 	SMAreduceRange(wv, [bit])
	WAVE wv
	Variable bit

	Variable wMax, wMin, numSpace

	bit = ParamIsDefault(bit) ? 32 : bit
	numSpace = 2^bit - 1

	NVAR/Z numMaxValue = root:numMaxValue
	if(!NVAR_EXISTS(numMaxValue))
		Variable/G root:numMaxValue = numSpace
		NVAR numMaxValue = root:numMaxValue
	endif
	NVAR/Z numMinValue = root:numMinValue
	if(!NVAR_EXISTS(numMinValue))
		Variable/G root:numMinValue = 0
		NVAR numMinValue = root:numMinValue
	endif

	if(numMaxValue < numSpace)
		wMax = WaveMax(wv) / numSpace * numMaxValue
		MultiThread wv = wv[p][q] > wMax ? wMax : wv[p][q]
	endif

	if(numMinValue > 0)
		wMin = WaveMin(wv) / numSpace * numMinValue
		MultiThread wv = wv[p][q] < wMin ? wMin : wv[p][q]
	endif
End

// save storage by converting image to full uint
Function SMAconvertWaveToUint(wv, [bit])
	WAVE wv
	Variable bit

	Variable wMin, wMax
	Variable numSpace

	bit = ParamIsDefault(bit) ? 32 : bit
	numSpace = 2^bit - 1

	wMin = WaveMin(wv)
	wv -= wMin

	wMax = WaveMax(wv)
	wv[][] = round(wv[p][q] / wMax * numSpace)

	switch(bit)
		case 16:
			Redimension/W/U wv // 16bit
			break
		case 8:
			Redimension/B/U wv // 8bit
			break
		case 32:
		default:
			Redimension/I/U wv // 32bit
	endswitch
End

Function/S SMAbuildGraphPLEM()
	Variable i, range_min, range_max
	String ImageNames
	STRUCT PLEMd2Stats stats

	String graphName = "SMAgetCoordinatesGraph"
	NVAR gnumMapsAvailable = $(cstrPLEMd2root + ":gnumMapsAvailable")

	DoWindow $graphName
	if(V_flag != 0)
		return graphName
	endif

	Display/W=(400,40,1200,650)/N=$graphName
	graphName = S_name

	WAVE wavCoordinates = root:coordinates
	if(WaveExists(wavCoordinates))
		AppendToGraph/W=$graphName wavCoordinates[][0]/TN=coordinates vs wavCoordinates[][1]
		ModifyGraph/W=$graphName mode(coordinates)=3,marker(coordinates)=1,msize(coordinates)=2
	endif

	for(i = 0; i < gnumMapsAvailable; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))

		range_min = WaveMin(stats.wavPLEM)
		range_max = WaveMax(stats.wavPLEM)
		AppendImage/W=$graphName stats.wavPLEM
	endfor

	ImageNames = ImageNameList("", ";")
	for(i = 0; i < gnumMapsAvailable; i += 1)
		ModifyImage/W=$graphName $StringFromList(i, ImageNames) ctab= {range_min,range_max,Terrain,0}
	endfor

	return graphName
End

Function/S SMAbuildGraphFullImage()
	String graphName = "SMAgetCoordinatesfullImage"

	DoWindow $graphName
	if(V_flag == 0)
		Display/W=(400,40,1200,650)/N=$graphName
		graphName = S_name
		wave image = root:fullimage
		if(!WaveExists(image))
			Make/N=(2,2) root:fullimage/WAVE=image
		endif
		AppendImage/W=$graphName root:fullimage
		ModifyImage/W=$graphName fullimage ctab= {*,*,Blue,1}
		ModifyGraph/W=$graphName mirror=0
	endif

	return graphName
End

Function SMAtestSizeAdjustment()
	Variable i	
	variable dim4size = 10
	Variable dim4offset = 0.9760
	Variable dim4delta = 0.0001

	NVAR/Z numSizeAdjustment = root:numSizeAdjustment
	if(!NVAR_EXISTS(numSizeAdjustment))
		Variable/G root:numSizeAdjustment = 1
		NVAR/Z numSizeAdjustment = root:numSizeAdjustment
	endif

	// load for magnification
	STRUCT PLEMd2Stats stats
	PLEMd2statsLoad(stats, PLEMd2strPLEM(1))

	WAVE imagestack = SMAprocessImageStack(createNew = 0)

	Duplicate/O imagestack root:SMAsizeAdjustment/WAVE=wv
	Redimension/N=(-1, -1, -1, dim4size) wv
	SetScale/P t, dim4offset, dim4delta, wv

	Make/O/N=(dim4size) root:SMAnumSizeAdjustment/WAVE=dim4 = dim4offset + p * dim4delta

	for(i = 0; i < dim4size; i += 1)
		numSizeAdjustment = (dim4offset + i * dim4delta)
		dim4[i] = stats.numMagnification / numSizeAdjustment
		SMAreset(power = 0, photon = 0)
		WAVE imagestack = SMAprocessImageStack(createNew = 1)
		Multithread wv[][][][i] = imagestack[p][q][r]
	endfor

	DoWindow/F win_SMAimageStack
End

Function/WAVE SMAestimateBackground()
	Variable pVal, qVal
	Variable i
	Variable originalQ25, originalQ75, resultingQ25, resultingQ75

	Variable V_fitOptions=4 // used to suppress CurveFit dialog
	Variable V_FitQuitReason // stores the CurveFit Quit Reason
	Variable V_FitError // Curve Fit error

	WAVE medianImage = SMAgetMedian(overwrite = 1)

	Duplicate/FREE medianImage background
	ImageFilter NaNZapMedian background // rotation rotation induced borders
	ImageFilter/O/N=5 median background // remove spikes
	Smooth 5, background // smooth along the trenches (spikes)
	ImageFilter/O/N=101/P=3 avg background // reduce information

	// align wave so that it has somewhat equal boundaries as original
	StatsQuantiles/Q medianImage
	originalQ75 = V_Q75
	StatsQuantiles/Q background
	resultingQ75 = V_Q75
	background[][] = background[p][q] / resultingQ75 * originalQ75
	StatsQuantiles/Q medianImage
	originalQ25 = V_Q25
	StatsQuantiles/Q background
	resultingQ25 = V_Q25
	background[][] = background[p][q] - resultingQ25 + originalQ25

	// set to gaussian background from illumination if possible
	Make/O/T/N=3 T_Constraints = {"K1 > 0","K3 > 0","K5 > 0"}
	V_FitError = 0
	CurveFit/Q Gauss2D background /C=T_Constraints
	if(V_FitError == 0)
		Wave W_coef, W_sigma
		W_coef[] = abs(W_sigma[p] / W_coef[p]) > 0.3 ? NaN : W_coef[p]
		if(numType(sum(W_coef) == 0))
			background = Gauss2D(W_coef, x, y)
		endif
		WaveClear W_coef, W_sigma
	endif
	KillWaves/Z T_Constraints

	return background
End

Function/WAVE SMAgetMedian([overwrite])
	Variable overwrite

	Variable i, dim0, dim1
	Variable pVal, qVal
	Struct PLEMd2Stats stats
	NVAR gnumMapsAvailable = $(cstrPLEMd2root + ":gnumMapsAvailable")

	overwrite = ParamIsDefault(overwrite) ? 0 : !!overwrite

	WAVE/Z myMedian = root:SMAmedian
	if(WaveExists(myMedian) && !overwrite)
		return myMedian
	endif
	WaveClear myMedian

	PLEMd2statsLoad(stats, PLEMd2strPLEM(1))

	dim0 = DimSize(stats.wavPLEM, 0)
	dim1 = DimSize(stats.wavPLEM, 1)
	dim1 = dim1 != 0 ? dim1 : 1 // dim1 = 0 and dim1 = 1 is the same
	Make/O/N=(dim0, dim1) root:SMAmedianBackground/WAVE=myMedian
	SetScale/P x, 0, 1, myMedian
	SetScale/P y, 0, 1, myMedian

	// calculate median of all images
	//Make/O/N=(dim0, dim1, gnumMapsAvailable) root:SMAmedianMatrix/WAVE=bgMatrix
	Make/FREE/N=(dim0, dim1, gnumMapsAvailable) bgMatrix
	for(i = 0; i < gnumMapsAvailable; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))
		if(dim1 != DimSize(stats.wavPLEM, 1))
			continue
		endif
		if(dim1 == 1)
			bgMatrix[][0][i] = stats.wavPLEM[p]
		else
			bgMatrix[][][i] = stats.wavPLEM[p][q]
		endif
	endfor
	for(i = 0; i < dim0 * dim1; i += 1)
		pVal = mod(i, dim0)
		qVal = floor(i / dim0)
		Duplicate/FREE/R=[pVal][qVal][0,*] bgMatrix, currentPixel
		myMedian[pVal][qVal] = median(currentPixel)
		WaveClear currentPixel
	endfor
	Duplicate/o bgmatrix root:temp
	WaveClear bgMatrix

	if(dim1 == 1)
		Redimension/N=(dim0) myMedian
	endif

	return myMedian
End

Function/WAVE SMAWigner(numWigner, [transposed, forceReNew])
	Variable numWigner, transposed, forceReNew

	transposed = ParamIsDefault(transposed) ? 1 : !!transposed
	forceReNew = ParamIsDefault(forceReNew) ? 0 : !!forceReNew

	WAVE image = root:WignerSource
	if(!WaveExists(image))
		Abort "No Source defined for WignerTransform"
	endif

	if(transposed)
		WAVE/Z complete = root:WignerFullHor
	else
		WAVE/Z complete = root:WignerFullVert
	endif
	if(forceReNew | !WaveExists(complete))
		if(transposed)
			WAVE complete = CreateWignerHor()
		else
			WAVE complete = CreateWignerVert()
		endif
	endif

	if(transposed)
		DelayUpdate
		WAVE output = root:WignerImageHor
		Multithread output = complete[p][q][numWigner]
		MatrixOP/O root:WignerProfileSumHor/WAVE=WignerProfileSum = sqrt(sumcols((output*output)^t)^t))
	else
		//WIP
		MatrixOP/O root:WignerProfileVert/WAVE=profile = sumcols(output)^t)
	endif
	SetScale/P x, DimOffset(image, !transposed), DimDelta(image, !transposed), WignerProfileSum

	return output	
End

Function/WAVE CreateWignerVert()
	Variable i, j, dim0, dim1, dim3

	WAVE image = root:WignerSource

	dim0 = DimSize(image, 0) - 1
	dim1 = floor(DimSize(image, 1) / 2) * 2
	dim3 = dim1 / 2 + 1

	MatrixOP/O root:LineProfileVert/WAVE=LineProfileVert = sumcols(image)^t)
	SetScale/P x, DimOffset(image, 1), DimDelta(image, 1), LineProfileVert
	Redimension/N=(dim1) LineProfileVert
	Make/N=(dim1)/O root:LineProfileVertXdummy = DimOffset(image, 1) + p * DimDelta(image, 1)

	Make/N=(dim0, dim1)/O root:WignerProfileVert/WAVE=WignerProfileVert
	WignerTransform/DEST=WignerProfileVert LineProfileVert

	MatrixOP/O root:WignerProfileSumVert/WAVE=WignerProfileSumVert = sqrt(sumcols((WignerProfileVert*WignerProfileVert)^t)^t))
	SetScale/P x, DimOffset(image, 1), DimDelta(image, 1), WignerProfileSumVert
	Make/N=(dim1)/O root:WignerProfileSumVertXdummy = DimOffset(image, 1) + p * DimDelta(image, 1)

	// WIP
	Make/N=(dim0, dim1)/O root:WignerImageVert/WAVE=output
	Make/N=(dim0, dim1, dim3)/O root:WignerFullVert/WAVE=complete

	Make/N=(dim1)/O root:LineProfile/WAVE=current_line
	Make/N=(dim1)/O root:WignerProfile/WAVE=current_wigner
	for(i = 0; i < dim1; i += 1)
		current_line = image[i][p]
		WignerTransform/DEST=current_wigner current_line
		MultiThread complete[i][][] = current_wigner[q][r]
	endfor

	output = complete[p][q][0]
	return complete
End

Function/WAVE CreateWignerHor()
	Variable i, j, dim0, dim1, dim3

	WAVE image = root:WignerSource

	dim0 = floor(DimSize(image, 0) / 2) * 2
	dim1 = DimSize(image, 1) - 1
	dim3 = dim0 / 2 + 1

	MatrixOP/O root:LineProfileHor/WAVE=LineProfileHor = sumcols(image^t)^t)
	SetScale/P x, DimOffset(image, 0), DimDelta(image, 0), LineProfileHor
	Redimension/N=(dim0) LineProfileHor

	Make/N=(dim0, dim1)/O root:WignerProfileHor/WAVE=WignerProfileHor
	WignerTransform/DEST=WignerProfileHor LineProfileHor

	MatrixOP/O root:WignerProfileSumHor/WAVE=WignerProfileSumHor = sqrt(sumcols((WignerProfileHor*WignerProfileHor)^t)^t))
	SetScale/P x, DimOffset(image, 0), DimDelta(image, 0), WignerProfileSumHor

	Make/N=(dim0, dim1)/O root:WignerImageHor/WAVE=output
	CopyScales image, output
	Make/N=(dim0, dim1, dim3)/O root:WignerFullHor/WAVE=complete

	Make/N=(dim0)/O root:LineProfile/WAVE=current_line
	Make/N=(dim0)/O root:WignerProfile/WAVE=current_wigner
	for(i = 0; i < dim1; i += 1)
			current_line = image[p][i]
			WignerTransform/DEST=current_wigner current_line
			complete[][i][] = current_wigner[p][r]
	endfor

	output = complete[p][q][0]
	return complete
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

// structure idea taken from info (igor-file-loader)
// https://github.com/ukos-git/igor-file-loader
// released under MIT license by same author @ukos-git

static strConstant cstructure = "structure" // path for global vars in Package dfr
static strConstant cpeakfit   = "peakFit"   // path for temp peakfit waves
static Constant	cversion   = 0004

Structure SMAinfo
	Variable numVersion, numSpectra

	DFREF dfrStructure

	WAVE/WAVE wavSpectra, wavPeakFind
EndStructure

static Function SMAstructureInitGlobalVariables()
	DFREF dfrPeakFit   = SMApeakfitDF()

	DFREF dfrStructure = SMAstructureDF()
	createNVAR("numVersion", dfr = dfrStructure, set = cversion)
	createNVAR("numSpectra", dfr = dfrStructure, init = 0)
End

static Function SMAstructureInitWaves()
	DFREF dfrStructure = SMAstructureDF()

	WAVE/Z/WAVE/SDFR=dfrStructure wavSpectra = spectra
	if(!WaveExists(wavSpectra))
		Make/WAVE dfrStructure:spectra/WAVE=wavSpectra
	endif
	WAVE/Z/WAVE/SDFR=dfrStructure wavPeakFind = peakfind
	if(!WaveExists(wavPeakFind))
		Make/WAVE dfrStructure:peakfind/WAVE=wavPeakFind
	endif
End

Function SMAstructureLoad(info)
	STRUCT SMAinfo &info
	Variable SetDefault = 0

	if(!SMAstructureIsInit())
		SMApackageInitDF(info)
		SMAstructureUpdate(info)
	endif

	DFREF info.dfrStructure = SMAstructureDF()
	if(DataFolderRefStatus(info.dfrStructure) == 0)
		print "SMAstructureLoad: \tUnexpected Behaviour."
	endif
	if(DataFolderRefStatus(info.dfrStructure) == 0)
		print "SMAstructureLoad: \tUnexpected Behaviour."
	endif

	NVAR/Z/SDFR=info.dfrStructure numVersion

	if(numVersion < cversion)
		print "SMAstructureLoad: \tVersion Change detected."
		printf "current Version: \t%04d\r", numVersion
		SMAstructureUpdate(info)
		printf "new Version: \t%04d\r", numVersion
	endif
	info.numVersion = numVersion

	info.numSpectra = loadNVAR("numSpectra", dfr = info.dfrStructure)

	WAVE/WAVE/SDFR=info.dfrStructure info.wavSpectra = spectra
	WAVE/WAVE/SDFR=info.dfrStructure info.wavPeakFind = peakfind
End

Function SMAstructureSave(info)
	STRUCT SMAinfo &info

	DFREF dfrstructure = SMAstructureDF()

	saveNVAR("numVersion", info.numVersion, dfr = dfrStructure)
	saveNVAR("numSpectra", info.numSpectra, dfr = dfrStructure)
End

static Function/S SMApackageDF()
	return "root:Packages:" + PossiblyQuoteName(cSMApackage)
End

static Function/DF SMAstructureDF()
	string strDFR = SMApackageDF() + ":" + cstructure
	DFREF dfr = $strDFR
	return dfr
End

Function/DF SMApeakfitDF()
	string strDFR = SMApackageDF() + ":" + cpeakfit
	DFREF dfr = $strDFR
	return dfr
End

static Function SMAstructureIsInit()
	DFREF dfrStructure = SMAstructureDF()
	if(!DataFolderRefStatus(dfrStructure))
		return 0
	endif

	NVAR/Z/SDFR=dfrStructure numVersion
	if(!NVAR_EXISTS(numVersion))
		return 0
	endif

	return 1
End

static Function SMApackageInitDF(info)
	STRUCT SMAinfo &info
	DFREF dfrSave = GetDataFolderDFR()

	SetDataFolder root:
	NewDataFolder/O/S Packages
	NewDataFolder/O $cSMApackage

	SetDataFolder dfrSave
End

static Function SMAstructureInitDF(info)
	STRUCT SMAinfo &info

	DFREF dfr = $SMApackageDF()
	DFREF new = dfr:$cstructure
	if(DataFolderRefStatus(new) == 0)
		NewDataFolder dfr:$cstructure
	endif
End

static Function SMApeakfitInitDF(info)
	STRUCT SMAinfo &info

	DFREF dfr = $SMApackageDF()
	DFREF new = dfr:$cpeakfit
	if(DataFolderRefStatus(new) == 0)
		NewDataFolder dfr:$cpeakfit
	endif
End

static Function SMAstructureUpdate(info)
	STRUCT SMAinfo &info

	SMAstructureInitDF(info)
	SMApeakfitInitDF(info)
	SMAstructureInitGlobalVariables()
	SMAstructureInitWaves()
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
#include <ImageSlider>
#include <AxisSlider>
#include <Color Table Control Panel>

Window SMAintensityAnalysis() : Graph
	PauseUpdate; Silent 1 // building window...
	Display /W=(240,50,784.5,445.25) as "intensity_analysis"
	AppendImage nanotubes_transposed
	ModifyImage nanotubes_transposed ctab= {0,315,Red,1}
	ModifyGraph margin(right)=141,width={Aspect,1}
	ModifyGraph grid=1
	ModifyGraph mirror=0
	SetAxis left -5,105
	SetAxis bottom -5,105
	ColorScale/C/N=text0/F=0/A=MC/X=67.56/Y=3.78 image=nanotubes_transposed
	SetDrawLayer UserFront
EndMacro

Window SMAcameraFocusPointsGraph() : Graph
	PauseUpdate; Silent 1		// building window...
	Display /W=(825.75,299.75,1220.25,508.25) SMAcameraIntensity[160,*] as "zAxisIntensityv2"
	AppendToGraph/T SMAcameraIntensity
	AppendToGraph SMAcameraIntensity[0,79],SMAcameraIntensity[80,159]
	AppendToGraph SMAcameraIntensitySmth
	ModifyGraph userticks(bottom)={SMAcameraPlanePeakMaximum,SMAcameraPlanePeakMaximumT}
	ModifyGraph userticks(top)={SMAcameraPlanePeakMaximumZ,SMAcameraPlanePeakMaximumZT}
	ModifyGraph mode(SMAcameraIntensity#1)=3
	ModifyGraph marker(SMAcameraIntensity#1)=8
	ModifyGraph lSize(SMAcameraIntensity)=2,lSize(SMAcameraIntensity#2)=2,lSize(SMAcameraIntensity#3)=2
	ModifyGraph rgb(SMAcameraIntensity#2)=(0,0,0),rgb(SMAcameraIntensity#3)=(1,16019,65535)
	ModifyGraph msize(SMAcameraIntensity#1)=2,msize(SMAcameraIntensity#2)=2,msize(SMAcameraIntensity#3)=2
	ModifyGraph grid(bottom)=1
	SetAxis left 0,*
	Label left "laser spot intensity"
	Label bottom "(x,y) position"
	Label top "z position"
EndMacro

Function SMAimageStackopenWindow()
	NVAR/Z numSizeAdjustment = root:numSizeAdjustment
	if(!NVAR_EXISTS(numSizeAdjustment))
		Variable/G root:numSizeAdjustment = 1
	endif
	NVAR/Z numSizeAdjustmentSingleStack = root:numSizeAdjustmentSingleStack
	if(!NVAR_EXISTS(numSizeAdjustmentSingleStack))
		Variable/G root:numSizeAdjustmentSingleStack = 0
	endif

	DoWindow win_SMAimageStack
	if(!V_flag)
		Execute "win_SMAimageStack()"

		WMAppendAxisSlider()

		WAVE/Z imagestack = root:SMAimagestack
		if(WaveExists(imagestack) && (DimSize(imagestack, 2) > 1))
			WMAppend3DImageSlider()
		endif

		WMColorTableControlPanel#createColorTableControlPanel()
	endif
	DoWindow/F win_SMAimageStack
End

Window win_SMAimageStack() : Graph
	PauseUpdate; Silent 1		// building window...
	Display /W=(451.8,164.6,925.2,642.2)
	AppendImage SMAimagestack
	ModifyImage SMAimagestack ctab= {0,255,RedWhiteBlue256,0}
	ModifyGraph width={Plan,1,bottom,left},height=396.85
	ModifyGraph grid(left)=1
	ModifyGraph mirror(left)=2,mirror(bottom)=0
	ModifyGraph nticks=10
	ModifyGraph minor=1
	ModifyGraph fSize=8
	ModifyGraph standoff=0
	ModifyGraph axOffset(left)=0.428571
	ModifyGraph gridRGB(left)=(26205,52428,1)
	ModifyGraph tkLblRot(left)=90
	ModifyGraph btLen=3
	ModifyGraph tlOffset=-2
	ModifyGraph manTick(left)={0,20,0,0},manMinor(left)={4,5}
	ModifyGraph manTick(bottom)={0,20,0,0},manMinor(bottom)={4,0}
	ControlBar 45
	GroupBox CBSeparator0,pos={0.00,0.00},size={472.80,2.40}
	Slider WMAxSlSl,pos={49.80,9.00},size={402.60,6.00},proc=WMAxisSliderProc
	Slider WMAxSlSl,limits={0,1,0},value= 0.5,side= 0,vert= 0,ticks= 0
	PopupMenu WMAxSlPop,pos={9.60,4.80},size={15.60,15.60},proc=WMAxSlPopProc
	PopupMenu WMAxSlPop,mode=0,value= #"\"Instructions...;Set Axis...;Zoom Factor...;Resync position;Resize;Remove\""
	NewPanel/HOST=#/EXT=0/W=(0,0,216,438.6)  as "sizeAdjustment"
	ModifyPanel cbRGB=(65534,65534,65534), fixedSize=0
	SetDrawLayer UserBack
	SetDrawEnv dash= 6,fillfgc= (61166,61166,61166)
	DrawRect 15,87,186,145.8
	DrawText 37.2,109.8,"scaling"
	Button mergeImageStack,pos={54.00,297.00},size={99.00,18.00},proc=ButtonProcMergeImages,title="mergeImageStack"
	SetVariable cnumSizeAdjustment,pos={6.00,12.00},size={186.00,13.80}
	SetVariable cnumSizeAdjustment,limits={0.9,1.1,0.001},value= numSizeAdjustment
	CheckBox checkSizeAdjustment,pos={33.00,51.00},size={80.40,12.00},title="only current stack"
	CheckBox checkSizeAdjustment,variable= numSizeAdjustmentSingleStack
	Button save,pos={456.00,54.00},size={75.00,24.00},proc=ButtonProcSMAImageStackSave,title="simple save"
	Button save,labelBack=(65535,65535,65535)
	Button save1,pos={54.00,343.80},size={99.00,18.00},proc=ButtonProcSMAImageStackSave,title="simple save"
	Button save1,labelBack=(65535,65535,65535)
	SetVariable cnumRotationAdjustment,pos={6.00,27.00},size={186.00,13.80}
	SetVariable cnumRotationAdjustment,limits={-5,5,0.1},value= numRotationAdjustment
	CheckBox SMAimagestack_check_fullcalc,pos={30.00,66.00},size={42.00,12.00},title="full calc"
	CheckBox SMAimagestack_check_fullcalc,variable= numFullCalcultions
	Button sizeAdjustment,pos={54.00,321.00},size={99.00,18.00},proc=ButtonProcSizeAdjustment,title="sizeAdjustment"
	SetVariable cnumMin,pos={36.00,120.00},size={60.00,13.80},title="min"
	SetVariable cnumMin,limits={0,255,1},value= numMinValue
	SetVariable cnumMax,pos={102.00,120.00},size={60.00,13.80},title="max"
	SetVariable cnumMax,limits={0,255,1},value= numMaxValue
	RenameWindow #,P0
	SetActiveSubwindow ##
	NewPanel/HOST=#/EXT=1/W=(18.6,0,0,438.6)  as "controls"
	ModifyPanel cbRGB=(65534,65534,65534), fixedSize=0
	Slider WMAxSlY,pos={6.00,6.00},size={6.00,408.00},proc=SliderProcSMAimageStackY
	Slider WMAxSlY,limits={0,1,0},value= 0.644677661169415,side= 0,ticks= 0
	RenameWindow #,P1
	SetActiveSubwindow ##
EndMacro

Function SliderProcSMAimageStackY(sa) : SliderControl
	STRUCT WMSliderAction &sa

	switch( sa.eventCode )
		case -1: // control being killed
			break
		default:
			if( sa.eventCode & 1 ) // value set
				String grfName= WinName(0, 1)
				SVAR/Z axisName = root:Packages:WMAxisSlider:$(grfName):gAxisName
				if(!SVAR_EXISTS(axisName))
					break
				endif
				axisName = "left"
				WMAxisSliderProc(sa.ctrlName, sa.curval, sa.eventCode)
			endif
			break
	endswitch

	return 0
End

Function SliderProcSMAimageStackX(sa) : SliderControl
	STRUCT WMSliderAction &sa

	switch( sa.eventCode )
		case -1: // control being killed
			break
		default:
			if( sa.eventCode & 1 ) // value set
				String grfName= WinName(0, 1)
				SVAR axisName = root:Packages:WMAxisSlider:$(grfName):gAxisName
				axisName = "bottom"
				WMAxisSliderProc(sa.ctrlName, sa.curval, sa.eventCode)
			endif
			break
	endswitch

	return 0
End

Function ButtonProcSMAImageStackSave(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	
	NVAR/Z cnumLayer = root:Packages:WM3DImageSlider:win_SMAimageStack:gLayer
	if(!NVAR_EXISTS(cnumLayer))
		return 0
	endif
	
	variable i
	variable numLayers = 8 // hard coded
	variable zaxis

	switch( ba.eventCode )
		case 2: // mouse up

		for(i = 0; i < numLayers; i += 1)
			cnumLayer=i
			WM3DImageSliderProc("",0,0)
			zaxis = 150 - i * 0.5 // hard coded
			TextBox/C/N=zAxis "\\JL\\Z24z=" + num2str(round(zaxis * 10)/10) + "µm"
			SavePICT/O/P=home/E=-5/B=288 as "mkl12_focusscan_" + num2str(round(zaxis * 10)) + ".png"
		endfor
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End

Function ButtonProcMergeImages(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	variable numLayer = 0
	NVAR singleStack = root:numSizeAdjustmentSingleStack
	NVAR/Z cnumLayer = root:Packages:WM3DImageSlider:win_SMAimageStack:gLayer
	if(NVAR_EXISTS(cnumLayer))
		numLayer = cnumLayer
	endif

	switch( ba.eventCode )
		case 2: // mouse up
			smareset(power = 0, photon = 0)
			WAVE coordinates = PLEMd2getCoordinates(forceRenew = 0)

			if(singleStack)
				Wave fullimage = SMAmergeStack(coordinates, numLayer, 24)
				WAVE imagestack = root:SMAimagestack
				MultiThread imagestack[][][numLayer] = fullimage[p][q]
			else
				SMAprocessImageStack(coordinates = coordinates, createNew = 1)
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End

Function ButtonProcSizeAdjustment(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			SMAtestsizeAdjustment()
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End

// derives from WMColorTableControlPanel#SliderProc
Function SMAfullstackSliderProc(sa) : SliderControl
	STRUCT WMSliderAction &sa

	string target
	string colorPanel = "ColorTableControlPanel"
	string SMAfullimageGraph = "win_SMAimageStack"

	switch( sa.eventCode )
		case -1: // control being killed
			break
		case 9:  // mouse moved && mouse down
			DoWindow $colorPanel
			if(!V_flag)
				WMColorTableControlPanel#createColorTableControlPanel()
			endif
			Slider highSliderSC, win=$colorPanel, limits={0,255,1}
			Slider lowSliderSC, win=$colorPanel, limits={0,255,1}
			target = StringFromList(0, ImageNameList(SMAfullimageGraph, ";")) + "*"
			PopupMenu selectTracePU, win=$colorPanel, popmatch=target

			WMColorTableControlPanel#SliderProc(sa)

			break
	endswitch

	return 0
End

Function ListBoxProc_SMAselect(lba) : ListBoxControl
	STRUCT WMListboxAction &lba

	Variable row = lba.row
	Variable col = lba.col
	WAVE/T/Z listWave = lba.listWave
	WAVE/Z selWave = lba.selWave

	switch( lba.eventCode )
		case -1: // control being killed
			break
		case 1: // mouse down
			break
		case 3: // double click
			break
		case 4: // cell selection
		case 5: // cell selection plus shift key
			WAVE selectedMaps = PLEMd2getWaveMapsSelection()
			Extract listWave, selectedMaps, selWave[p] == 1
			break
		case 6: // begin edit
			break
		case 7: // finish edit
			break
		case 13: // checkbox clicked (Igor 6.2 or later)
			break
	endswitch

	return 0
End

Function ButtonProc_SMAselectPower(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			// click code here
			break
		case -1: // control being killed
			break
	endswitch

	return 0
End

Window SMAselectWaves() : Panel
	PauseUpdate; Silent 1		// building window...
	NewPanel /W=(306,137,806,237) as "select waves"
	ListBox mapsAvailable,pos={0.00,0.00},size={200.00,100.00},proc=ListBoxProc_SMAselect
	ListBox mapsAvailable,userdata(ResizeControlsInfo)= A"!!*'\"z!!#AW!!#@,z!!#](Aon\"Qzzzzzzzzzzzzzz!!#`-A7TLfzz"
	ListBox mapsAvailable,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzz!!#u:Du]k<zzzzzzzzzzz"
	ListBox mapsAvailable,userdata(ResizeControlsInfo) += A"zzz!!#?(FEDG<zzzzzzzzzzzzzz!!!"
	ListBox mapsAvailable,listWave=root:Packages:SMA:mapsavailable
	ListBox mapsAvailable,selWave=root:Packages:SMA:mapsselected,mode= 4
	ListBox mapsSelected,pos={206.00,0.00},size={200.00,100.00}
	ListBox mapsSelected,userdata(ResizeControlsInfo)= A"!!,G^z!!#AW!!#@,z!!#`-A7TLfzzzzzzzzzzzzzz!!#o2B4uAezz"
	ListBox mapsSelected,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzz!!#u:Du]k<zzzzzzzzzzz"
	ListBox mapsSelected,userdata(ResizeControlsInfo) += A"zzz!!#?(FEDG<zzzzzzzzzzzzzz!!!"
	ListBox mapsSelected,listWave=root:Packages:SMA:mapsselection
	Button extractpower,pos={414.00,2.00},size={81.00,25.00},proc=ButtonProc_SMAselectPower,title="extract power"
	Button extractpower,userdata(ResizeControlsInfo)= A"!!,I5!!#7a!!#?[!!#=+z!!#o2B4uAezzzzzzzzzzzzzz!!#o2B4uAezz"
	Button extractpower,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzz!!#u:Du]k<zzzzzzzzzzz"
	Button extractpower,userdata(ResizeControlsInfo) += A"zzz!!#u:Du]k<zzzzzzzzzzzzzz!!!"
	SetWindow kwTopWin,hook(ResizeControls)=ResizeControls#ResizeControlsHook
	SetWindow kwTopWin,userdata(ResizeControlsInfo)= A"!!*'\"z!!#C_!!#@,zzzzzzzzzzzzzzzzzzzzz"
	SetWindow kwTopWin,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzzzzzzzzzzzzzzz"
	SetWindow kwTopWin,userdata(ResizeControlsInfo) += A"zzzzzzzzzzzzzzzzzzz!!!"
	Execute/Q/Z "SetWindow kwTopWin sizeLimit={375,75,inf,inf}" // sizeLimit requires Igor 7 or later
EndMacro

Window SMAHistogram() : Graph
	PauseUpdate; Silent 1		// building window...
	Display /W=(256.5,314.75,653.25,522.5) fit_histResult,histResult,histResult as "SMAHistogram"
	ModifyGraph mode(histResult#1)=5
	ModifyGraph lSize(fit_histResult)=2,lSize(histResult)=2
	ModifyGraph rgb(fit_histResult)=(65535,0,0,32768),rgb(histResult)=(0,0,0)
	NewPanel/HOST=#/EXT=0/W=(0,0,78,278) 
	Slider slider0,pos={16.00,30.00},size={54.00,244.00},proc=SMAHistogramSliderProc
	Slider slider0,limits={0,25,1},value= 1
	CheckBox check_fit,pos={20.00,11.00},size={26.00,15.00},title="fit"
	CheckBox check_fit,variable= checkbox_fit
	RenameWindow #,P0
	SetActiveSubwindow ##
EndMacro

Function SMAHistogramSliderProc(sa) : SliderControl
	STRUCT WMSliderAction &sa
	
	variable resolution = 2.5
	variable width = 100
	SVAR/Z diffwave = root:diffwave
	if(!SVAR_EXists(diffwave))
		return 0
	endif
	NVAR checkbox_fit = root:checkbox_fit

	switch( sa.eventCode )
		case -1: // control being killed
			break
		default:
			if( sa.eventCode & 1 ) // value set
				Variable curval = sa.curval
				wave wv = $diffwave
				Duplicate/O wv diff
				wave histResult
				
				diff = p < curval ? 0 : wv[p] - wv[p - curval]
				Histogram/B={-(width - 1)/2,resolution,abs((width - 1)/resolution)} diff, histResult
				if(checkbox_fit)
					CurveFit/M=2/W=0 lor, histResult/D
				endif
			endif
			break
	endswitch

	return 0
End

Window SMAwignerHor() : Graph
	PauseUpdate; Silent 1		// building window...
	Display /W=(1803,147.2,2486.4,1053.2)/L=left_LineProfileHor LineProfileHor as "SMAwignerHor"
	AppendToGraph/L=left_WignerProfileSumHor WignerProfileSumHor
	AppendImage/L=left_WignerImageHor WignerImageHor
	ModifyImage WignerImageHor ctab= {-10000000000,10000000000,RedWhiteBlue,0}
	AppendImage WignerSource
	ModifyImage WignerSource ctab= {*,*,BlueHot,0}
	ModifyImage WignerSource minRGB=0,maxRGB=NaN
	AppendImage/L=left_WignerHor WignerProfileHor
	ModifyImage WignerProfileHor ctab= {-39810717055349.7,39810717055349.7,RedWhiteBlue,0}
	ModifyGraph margin(left)=42,margin(right)=127,width={Plan,1,bottom,left}
	ModifyGraph grid(bottom)=1,grid(left_WignerImageHor)=1,grid(left)=1
	ModifyGraph mirror(bottom)=0,mirror(left)=0
	ModifyGraph nticks(left_LineProfileHor)=0,nticks(bottom)=2,nticks(left_WignerProfileSumHor)=0
	ModifyGraph nticks(left_WignerImageHor)=2,nticks(left)=2,nticks(left_WignerHor)=0
	ModifyGraph minor(bottom)=1
	ModifyGraph noLabel(bottom)=2,noLabel(left_WignerImageHor)=2,noLabel(left)=2
	ModifyGraph axOffset(left)=2.61538
	ModifyGraph gridRGB(bottom)=(34952,34952,34952),gridRGB(left_WignerImageHor)=(34952,34952,34952)
	ModifyGraph gridRGB(left)=(34952,34952,34952)
	ModifyGraph axRGB(bottom)=(0,0,0,0),axRGB(left_WignerProfileSumHor)=(65535,65535,65535,0)
	ModifyGraph axRGB(left_WignerImageHor)=(0,0,0,0),axRGB(left)=(0,0,0,0)
	ModifyGraph lblPosMode(left_LineProfileHor)=1,lblPosMode(left_WignerHor)=1
	ModifyGraph lblPos(bottom)=49,lblPos(left)=67,lblPos(left_WignerHor)=34
	ModifyGraph lblLatPos(left_WignerHor)=13
	ModifyGraph freePos(left_LineProfileHor)={0,kwFraction}
	ModifyGraph freePos(left_WignerProfileSumHor)={0,kwFraction}
	ModifyGraph freePos(left_WignerImageHor)={0,kwFraction}
	ModifyGraph freePos(left_WignerHor)={0,kwFraction}
	ModifyGraph axisEnab(left_LineProfileHor)={0.25,0.4}
	ModifyGraph axisEnab(left_WignerProfileSumHor)={0.85,1}
	ModifyGraph axisEnab(left_WignerImageHor)={0.6,0.85}
	ModifyGraph axisEnab(left)={0,0.25}
	ModifyGraph axisEnab(left_WignerHor)={0.4,0.6}
	ModifyGraph manTick(left_LineProfileHor)={0,0,0,2},manMinor(left_LineProfileHor)={0,50}
	ModifyGraph manTick(bottom)={0,2,0,0},manMinor(bottom)={0,50}
	ModifyGraph manTick(left_WignerProfileSumHor)={0,0,0,2},manMinor(left_WignerProfileSumHor)={0,50}
	ModifyGraph manTick(left_WignerImageHor)={0,2,0,0},manMinor(left_WignerImageHor)={0,50}
	ModifyGraph manTick(left)={0,2,0,0},manMinor(left)={0,0}
	ModifyGraph manTick(left_WignerHor)={0,0,0,2},manMinor(left_WignerHor)={0,50}
	Label left_LineProfileHor "spacial emission\rintensity [a.u]"
	Label left_WignerHor "momentum [1/µm]\r(k-space)"
	Cursor/P/I/S=2/H=3/NUML=2 A WignerProfileHor -1194,8
	ColorScale/C/N=WignerProfileColorScale/F=0/A=LB/X=74.01/Y=38.01/E=2
	ColorScale/C/N=WignerProfileColorScale image=WignerProfileHor, heightPct=25
	ColorScale/C/N=WignerProfileColorScale nticks=1, minor=1, prescaleExp=-12
	ColorScale/C/N=WignerProfileColorScale tickUnit=1, ZisZ=1
	AppendText "Wigner Intensity"
	ColorScale/C/N=WignerImageScaleBar/F=0/A=LB/X=74.45/Y=61.16/E=2
	ColorScale/C/N=WignerImageScaleBar image=WignerImageHor, heightPct=25, nticks=3
	ColorScale/C/N=WignerImageScaleBar highTrip=10, notation=1, ZisZ=1
	AppendText "Wigner Intensity |8⟩"
	ColorScale/C/N=imageColorScale/F=0/A=LB/X=74.01/Y=3.88/E=2 image=WignerSource
	ColorScale/C/N=imageColorScale heightPct=25
	AppendText "intensity [a.u.]"
	SetDrawLayer UserFront
	SetDrawEnv linethick= 4,linefgc= (65535,65535,65535),fillfgc= (0,0,0),fsize= 16,textrgb= (65535,65535,65535)
	SetDrawEnv gstart,gname= scalebarBottom
	DrawLine 0.1,0.777306733167082,0.3,0.777306733167082
	SetDrawEnv fsize= 16,textrgb= (65535,65535,65535)
	DrawText 0.133442126514132,0.800504987531172,"0µm"
	SetDrawEnv linethick= 4,linefgc= (65535,65535,65535),fillfgc= (0,0,0),fsize= 16,textrgb= (65535,65535,65535)
	SetDrawEnv gstop
	SetDrawEnv gstart,gname= scalebarTop
	SetDrawEnv linefgc= (0,0,0)
	DrawLine 0.1,0.177556109725685,0.3,0.177556109725685
	SetDrawEnv fsize= 16
	DrawText 0.133442126514132,0.200754364089775,"0µm"
	SetDrawEnv linethick= 4,linefgc= (65535,65535,65535),fillfgc= (0,0,0),fsize= 16,textrgb= (65535,65535,65535)
	SetDrawEnv gstop
	SetDrawEnv gstart,gname= wigner_selection
	SetDrawEnv xcoord= prel,ycoord= left_WignerHor,fillfgc= (65535,65535,65535,32768)
	DrawRect 0,0.51098896382061,1,0.579120825663358
	SetDrawEnv gstop
	NewPanel/HOST=#/EXT=1/W=(30,0,0,567) 
	ModifyPanel fixedSize=0
	SetDrawLayer UserBack
	SetDrawEnv gstart,gname= wigner_selection
	DrawRect 0.05,1.19586670935928,0.95,1.29552226847255
	SetDrawEnv gstop
	Slider slider0,pos={0.00,18.00},size={38.40,498.00},proc=SMASliderProcWignerHor
	Slider slider0,limits={0,64,1},value= 8
	RenameWindow #,P0
	SetActiveSubwindow ##
EndMacro

Function SMASliderProcWignerHor(sa) : SliderControl
	STRUCT WMSliderAction &sa

	switch( sa.eventCode )
		case -1: // control being killed
			break
		default:
			if( sa.eventCode & 1 ) // value set
				Variable curval = sa.curval
				DelayUpdate
				SMAWigner(sa.curval)

				WAVE wv = root:WignerImageHor
				Variable scaleEven = getEvenScale(wv)
				ModifyImage/W=SMAwignerHor WignerImageHor ctab= {-1 * scaleEven, scaleEven,RedWhiteBlue,0}
				DoWindow WignerGizmo
				if(V_flag)
					ModifyGizmo/N=WignerGizmo ModifyObject=WignerImage,objectType=surface,property={surfaceMaxRGBA, scaleEven, 0, 0, 1, 1}
					ModifyGizmo/N=WignerGizmo ModifyObject=WignerImage,objectType=surface,property={surfaceMinRGBA, -1 * scaleEven, 1, 0, 0, 1}
				endif
				WAVE wv = root:WignerProfileHor
				scaleEven = getEvenScale(wv)
				ModifyImage/W=SMAwignerHor WignerProfileHor ctab= {-1 * scaleEven,scaleEven,RedWhiteBlue,0}

				WAVE wv = root:WignerProfileHor
				Cursor/W=SMAwignerHor/I A WignerProfileHor 115.42, IndexToScale(wv, sa.curval, 1)
				ColorScale/W=SMAwignerHor/C/N=WignerImageScaleBar "Wigner Intensity |" + num2str(sa.curval) + "⟩"

				SetDrawLayer/W=SMAwignerHor UserFront
				DrawAction/W=SMAwignerHor getgroup=wigner_selection, delete
				if(sa.curval > 26)
					break
				endif
				SetDrawEnv/W=SMAwignerHor gstart, gname=wigner_selection
					SetDrawEnv/W=SMAwignerHor xcoord= prel,ycoord= left_WignerHor
					SetDrawEnv/W=SMAwignerHor fillfgc= (65535,65535,65535,32768)
					Variable rectStart = DimOffset(wv, 1) + (sa.curval - 0.5) * DimDelta(wv, 1)
					Variable rectEnd   = DimOffset(wv, 1) + (sa.curval + 0.5) * DimDelta(wv, 1)
					DrawRect/W=SMAwignerHor 0, rectStart, 1, rectEnd
				SetDrawEnv/W=SMAwignerHor gstop
			endif
			break
	endswitch

	return 0
End


Window WignerGizmo() : GizmoPlot
	PauseUpdate; Silent 1		// building window...
	// Building Gizmo 7 window...
	NewGizmo/T="WignerGizmo"/W=(1119.6,235.4,1609.8,474.8)
	ModifyGizmo startRecMacro=700
	ModifyGizmo scalingOption=63
	AppendToGizmo Surface=root:WignerImageHor,name=WignerImage
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ lineColorType,1}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ fillMode,3}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ lineWidth,2}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ srcMode,0}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ lineColor,0.666667,0.666667,0.666667,1}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ surfaceCTab,RedWhiteBlue256}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ SurfaceCTABScaling,100}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ surfaceMinRGBA,-1e+10,1,0,0,1}
	ModifyGizmo ModifyObject=WignerImage,objectType=surface,property={ surfaceMaxRGBA,1e+10,0,0,1,1}
	ModifyGizmo modifyObject=WignerImage,objectType=Surface,property={calcNormals,1}
	AppendToGizmo Axes=boxAxes,name=axes0
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={-1,axisScalingMode,1}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={-1,axisColor,0,0,0,1}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={0,ticks,3}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={1,ticks,3}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={2,ticks,3}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={0,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={1,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={2,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={3,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={4,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={5,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={6,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={7,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={8,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={9,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={10,visible,0}
	ModifyGizmo ModifyObject=axes0,objectType=Axes,property={11,visible,0}
	ModifyGizmo modifyObject=axes0,objectType=Axes,property={-1,Clipped,0}
	ModifyGizmo setDisplayList=0, object=axes0
	ModifyGizmo setDisplayList=1, object=WignerImage
	ModifyGizmo autoscaling=1
	ModifyGizmo currentGroupObject=""
	ModifyGizmo showInfo
	ModifyGizmo infoWindow={1863,0,2695,313}
	ModifyGizmo endRecMacro
	ModifyGizmo SETQUATERNION={0.551374,-0.180123,-0.254025,0.773960}
EndMacro

Window SMAmapsSum() : Graph
	PauseUpdate; Silent 1		// building window...
	Display /W=(412.5,56.75,799.5,303.5) peakExcitation vs peakEmission as "SMAmapsSum"
	AppendImage mapsSum
	ModifyImage mapsSum ctab= {0,*,Spectrum,0}
	ModifyGraph mode=3
	ModifyGraph marker=19
	ModifyGraph rgb=(65535,65535,65535)
	ModifyGraph useMrkStrokeRGB=1
	ModifyGraph zmrkSize(peakExcitation)={peakHeight,0,*,1,10}
	ModifyGraph mirror=0
	Label left "excitation / nm"
	Label bottom "emission / nm"
	SetAxis left 525,*
EndMacro

Window SMApeakMaximum() : Graph
	PauseUpdate; Silent 1		// building window...
	Display /W=(39,178.25,425.25,425) peakExcitation vs peakEmission as "SMApeakMaximum"
	ModifyGraph mode=3
	ModifyGraph marker=19
	ModifyGraph zmrkSize(peakExcitation)={peakHeight,0,*,1,10}
	Label left "excitation / nm"
	Label bottom "emission / nm"
	SetAxis left 525,765
EndMacro

Window SMAexactscanImage() : Graph
	PauseUpdate; Silent 1		// building window...
	String fldrSav0= GetDataFolder(1)
	SetDataFolder root:PLEMd2:
	Display /W=(963,138.5,1316.25,413.75) coordinates[*][0]/TN=AcN vs coordinates[*][1] as "SMAexactscanImage"
	AppendImage ::borders
	ModifyImage borders explicit= 1
	ModifyImage borders eval={0,65535,65535,65535}
	ModifyImage borders eval={255,-1,-1,-1}
	ModifyImage borders eval={1,13107,13107,13107}
	AppendImage ::trenches
	ModifyImage trenches explicit= 1
	ModifyImage trenches eval={0,65535,65535,65535}
	ModifyImage trenches eval={255,-1,-1,-1}
	ModifyImage trenches eval={1,52428,52428,52428}
	SetDataFolder fldrSav0
	ModifyGraph margin(left)=7,margin(bottom)=7,margin(top)=7,margin(right)=85,expand=-1
	ModifyGraph width={Plan,1,bottom,left}
	ModifyGraph mode=3
	ModifyGraph marker=29
	ModifyGraph mrkThick=0.1
	ModifyGraph gaps=0
	ModifyGraph mrkStrokeRGB=(0,0,0,6554)
	ModifyGraph zmrkSize(AcN)={peakHeight,0,*,0,5}
	ModifyGraph zColor(AcN)={peakLocation,800,1300,dBZ14}
	ModifyGraph mirror=0
	ModifyGraph noLabel=2
	ModifyGraph axRGB(left)=(0,0,0,0),axRGB(bottom)=(65535,65535,65535,0)
	ModifyGraph manTick(left)={0,40,0,0},manMinor(left)={9,5}
	ModifyGraph manTick(bottom)={0,40,0,0},manMinor(bottom)={9,5}
	SetAxis left -5,300
	SetAxis bottom -5,300
	ColorScale/C/N=text0/F=0/A=LB/X=102.55/Y=4.55 trace=AcN
	AppendText "central emission wavelength [nm]"
	SetDrawLayer UserFront
	SetDrawEnv xcoord= bottom,ycoord= left,linethick= 5
	DrawLine 308.107553969184,287.5,358.107553969184,287.5
	SetDrawEnv xcoord= bottom,ycoord= left
	DrawText 317.7649537407,269.625,"50µm"
EndMacro
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

// requires IM FILO (igor-file-loader) version 0002
// https://github.com/ukos-git/igor-file-loader
#include "FILOmain"
#include "FILOprefs"
#include "FILOstructure"
#include "FILOtools"

// requires igor-common-utilites
// https://github.com/ukos-git/igor-common-utilities
#include "common-utilities"

// requires PLEM (igor-swnt-plem)
// https://github.com/ukos-git/igor-swnt-plem
#include "plem"

strConstant cSMApackage = "swnt-mass-analysis"
StrConstant cstrSMAroot = "root:Packages:SMA:"

Function SMAload()
	FILO#load(fileType = ".ibw", packageID = 1)
	SMAupdatePath()
End

Function SMAadd()
	FILO#load(fileType = ".ibw", packageID = 1, appendToList = 1)
	SMAupdatePath()
End

Function SMAfileInfo()
	STRUCT FILO#experiment filos
	FILO#structureLoad(filos)
	Struct SMAprefs prefs
	SMAloadPackagePrefs(prefs)

	printf "SMAfileInfo: reading %d files from\r", ItemsInList(filos.strFileList)
	printf "path:   %s\r", prefs.strBasePath
	printf "folder: %s\r", filos.strFolder
	printf "file0:  %s\r", StringFromList(0, filos.strFileList)
End

Function SMAread()
	String file, files
	Variable numFiles, i, error
	String strPath = ""

	Struct SMAprefs prefs
	STRUCT FILO#experiment filos

	SMAupdatePath() // update old paths

	SMAloadPackagePrefs(prefs)
	FILO#structureLoad(filos)

	if(ItemsInList(filos.strFileList) == 0)
		SMAload()
		FILO#structureLoad(filos)
	endif

	files = filos.strFileList
	file = StringFromList(0, filos.strFileList)
	if(!cmpstr(file[0], ":"))
		files = FILO#AddPrefixToListItems(prefs.strBasePath, files)
	endif

	// status
	SMAfileInfo()
	numFiles = ItemsInList(files)
	for(i = 0; i < numFiles; i += 1)
		file = StringFromList(i, files)
		GetFileFolderInfo/Q/Z=1 file
		if(!V_isFile)
			printf "SMAread: Could not find %s\r", file
			error += 1
		endif
	endfor
	if(error > 0)
		printf "SMAread: %d errors.", error
		Abort "SMAread: Errors in file list"
	endif

	// load files
	for(i = 0; i < numFiles; i += 1)
		file = StringFromList(i, files)
		printf "SMAread %03d: \t%s\r", i, file
		PLEMd2Open(strFile = file, display = 0)
	endfor

	// hotfix for file load
	file = StringFromList(0, files)
	PLEMd2Open(strFile = file, display = 0)
End

Function SMAupdatePath()
	String file, folder

	Struct SMAprefs prefs
	STRUCT FILO#experiment filos

	FILO#structureLoad(filos)
	SMAloadPackagePrefs(prefs)

	filos.strFolder = FILO#RemovePrefixFromListItems(prefs.strBasePath, filos.strFolder)
	filos.strFolder = RemoveEnding(filos.strFolder, ";")
	filos.strFileList = FILO#RemovePrefixFromListItems(prefs.strBasePath, filos.strFileList)

	// legacy format support
	file = StringFromList(0, filos.strFileList)
	if(!!cmpstr(file[0], ":"))
		if(!cmpstr(file[0], "D")) // measurements were saved on drive d
			filos.strFolder = FILO#RemovePrefixFromListItems("D", filos.strFolder)
			filos.strFileList = FILO#RemovePrefixFromListItems("D", filos.strFileList)
			filos.strFolder = ":data" + filos.strFolder
			filos.strFileList = FILO#AddPrefixToListItems(":data", filos.strFileList)
		elseif(!cmpstr(file[0], "Z"))
			filos.strFolder = FILO#RemovePrefixFromListItems("Z", filos.strFolder)
			filos.strFileList = FILO#RemovePrefixFromListItems("Z", filos.strFileList)
		elseif(!cmpstr(file[0], "W"))
			filos.strFolder = FILO#RemovePrefixFromListItems("W", filos.strFolder)
			filos.strFileList = FILO#RemovePrefixFromListItems("W", filos.strFileList)
		endif
	endif

	file = prefs.strBasePath + StringFromList(0, filos.strFileList)
	GetFileFolderInfo/Q/Z=1 file
	if(!V_isFile)
		print filos.strFileList
		Abort "File Not found"
	endif

	folder = prefs.strBasePath + filos.strFolder
	GetFileFolderInfo/Q/Z=1 folder
	if(!V_isFolder)
		print filos.strFolder
		Abort "Folder Not found"
	endif
	FILO#structureSave(filos)
End

Function SMAmapInfo()
	String strPLEM
	Variable i

	variable numSpectra = PLEMd2getMapsAvailable()
	STRUCT PLEMd2Stats stats
	STRUCT SMAinfo info

	SMAstructureLoad(info)
	info.numSpectra = numSpectra
	Redimension/N=(info.numSpectra) info.wavSpectra

	for(i = 0; i < numSpectra; i += 1)
		strPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, strPLEM)
		info.wavSpectra[i] = stats.wavPLEM
	endfor

	SMAstructureSave(info)
End

Function SMAgetBestSpectra(bestEnergy)
	variable bestEnergy

	variable i, j, numPeaks
	variable peakEnergy, peakHeight
	variable bestIntensity
	string secondBestPLEM, currentPLEM
	string bestPLEM = ""

	variable numSpec = PLEMd2getMapsAvailable()
	STRUCT PLEMd2Stats stats

	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))

	for(i = 0; i < numSpec; i += 1)
		currentPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, currentPLEM)
		WAVE peaks = SMApeakFind(stats.wavPLEM, createwaves = 0)
		numPeaks = DimSize(peaks, 0)
		for(j = 0; j < numPeaks; j += 1)
			peakEnergy = peaks[j][%location]
			peakHeight = peaks[j][%height]
			if(abs(peakEnergy - bestEnergy) < 5)
				print currentPLEM
				if(peakHeight > bestIntensity)
					bestIntensity = peakHeight
					secondBestPLEM = bestPLEM
					bestPLEM = currentPLEM
				endif
			endif
		endfor
	endfor
	PLEMd2Display(bestPLEM)
	PLEMd2Display(secondBestPLEM)
End

Function SMAgetMaximum(bestEnergy)
	variable bestEnergy

	variable i, j, numPeaks
	variable peakEnergy, peakHeight
	variable bestIntensity
	string secondBestPLEM, currentPLEM
	string bestPLEM = ""

	variable numSpec = PLEMd2getMapsAvailable()
	STRUCT PLEMd2Stats stats

	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))

	for(i = 0; i < numSpec; i += 1)
		currentPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, currentPLEM)
		WAVE nospikes = removeSpikes(stats.wavPLEM)
		WAVE guess = PeakFind(nospikes, maxPeaks = 10, minPeakPercent = 0.2, smoothingFactor = 1, verbose = 0)
		//WAVE peaks = SMApeakFind(stats.wavPLEM, createwaves = 0)
		numPeaks = DimSize(guess, 0)
		for(j = 0; j < numPeaks; j += 1)
			//peakEnergy = peaks[j][%location]
			//peakHeight = peaks[j][%height]
			peakEnergy = guess[j][%location]
			peakHeight = guess[j][%height]
			if(abs(peakEnergy - bestEnergy) < 5)
				print currentPLEM
				if(peakHeight > bestIntensity)
					bestIntensity = peakHeight
					secondBestPLEM = bestPLEM
					bestPLEM = currentPLEM
				endif
			endif
		endfor
	endfor
	PLEMd2Display(bestPLEM)
	PLEMd2Display(secondBestPLEM)
End

Function SMAreset([power, photon, background])
	variable power, photon, background

	String strPLEM
	Variable i

	variable numSpectra = PLEMd2getMapsAvailable()
	Struct PLEMd2Stats stats

	power = ParamIsDefault(power) ? 0 : !!power
	photon = ParamIsDefault(photon) ? 0 : !!photon
	background = ParamIsDefault(background) ? 1 : !!background

	for(i = 0; i < numSpectra; i += 1)
		strPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, strPLEM)
		stats.booBackground = background
		stats.booPhoton = photon
		stats.booPower = power
		stats.booGrating = 1
		stats.booQuantumEfficiency = 1
		stats.booTime = 1
		stats.booWavelengthPitch = 0
		PLEMd2statsSave(stats)
		PLEMd2BuildMaps(strPLEM)
	endfor
End

Function SMAbackgroundMedian([power])
	Variable power
	
	String strPLEM
	Variable i

	variable numSpectra = PLEMd2getMapsAvailable()
	Struct PLEMd2Stats stats
	
	power = ParamIsDefault(power) ? 1 : 0

	SMAreset(power = power)
	WAVE globalMedian = SMAgetMedian(overwrite = 1)

	for(i = 0; i < numSpectra; i += 1)
		strPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, strPLEM)
		stats.wavPLEM -= globalMedian
	endfor
End

Function SMABackgroundAncestor()
	String strPLEM, strPLEM2
	Variable i, previousmax

	Variable numSpectra = PLEMd2getMapsAvailable()
	Struct PLEMd2Stats stats
	Struct PLEMd2Stats stats2

	for(i = 0; i < numSpectra; i += 1)
		strPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, strPLEM)
		stats.wavPLEM = stats.wavmeasure - stats.wavbackground
	endfor

	for(i = 2; i < numSpectra; i += 1)
		strPLEM = PLEMd2strPLEM(i-1)
		PLEMd2statsLoad(stats, strPLEM)
		previousmax = WaveMax(stats.wavPLEM)

		strPLEM2= PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats2, strPLEM2)

		stats.wavPLEM = stats.wavPLEM - stats2.wavPLEM/WaveMax(stats.wavPLEM) * previousmax
	endfor
End

Function SMAanalyse(min, max)
	Variable min, max

	Struct PLEMd2Stats stats
	String strPLEM, caption
	Variable i
	Variable numSpectra = PLEMd2getMapsAvailable()

	for(i = 1; i < numSpectra; i += 1)
		strPLEM = PLEMd2strPLEM(i)
		PLEMd2statsLoad(stats, strPLEM)
		Wavestats/Q stats.wavPLEM
		if((V_max > min) && (V_max < max))
			PLEMd2DisplayByNum(i)
			caption = stats.strPLEM +" at\r(x,y) = " + num2str(stats.numPositionX) + ", " + num2str(stats.numPositionY) + ""
			TextBox/C/N=text0/F=0/B=1/A=RT/X=0.00/Y=0.00 caption
		endif
	endfor
End

Function SMAkillAllWindows()
	String myWindow
	Variable i

	for(i=0; i<400; i += 1)
		myWindow = "Graph" + num2str(i)
		myWindow = "win_spectra50_00_" + num2str(i)
		KillWindow/Z $myWindow
	endfor
End

Function/DF SMAgetPackageRoot()
	variable i, startFolder, numFolders
	string currentFolder = ""
	
	if(!DataFolderExists(cstrSMAroot))
		if(!cmpstr(cstrSMAroot[0,4], "root:"))
			startFolder = 1
			currentFolder = "root"
		endif
		numFolders = ItemsInList(cstrSMAroot, ":")
		for(i = startFolder; i < numFolders; i += 1)
			currentFolder += ":" + StringFromList(i, cstrSMAroot, ":")
			NewDataFolder/O $currentFolder
		endfor
	endif
	
	DFREF dfr = $cstrSMAroot

	return dfr
End

Function/WAVE SMAgetWaveMapsAvailable()
	variable numSpectra = PLEMd2getMapsAvailable()
	string strMaps = PLEMd2getStrMapsAvailable()
	
	DFREF dfr = SMAgetPackageRoot()

	if(numSpectra == 0)
		return $""
	endif

	WAVE/T/Z wv = dfr:mapsavailable
	if(WaveExists(wv))
		if(DimSize(wv, 0) == numSpectra)
			return wv
		endif
	endif
	
	Make/O/T/N=(numSpectra) dfr:mapsavailable/WAVE=wv = StringFromList(p, strMaps)

	return wv
End

Function/WAVE PLEMd2getWaveMapsSelected()
	variable numSpectra = PLEMd2getMapsAvailable()
	string strMaps = PLEMd2getStrMapsAvailable()

	DFREF dfr = SMAgetPackageRoot()

	if(numSpectra == 0)
		return $""
	endif

	WAVE/T/Z wv = dfr:mapsselected
	if(WaveExists(wv))
		if(DimSize(wv, 0) != numSpectra)
			Redimension/N=(numSpectra) dfr:mapsselected
		endif
		return wv
	endif

	Make/O/T/N=(numSpectra) dfr:mapsselection/WAVE=wv = StringFromList(p, strMaps)

	return wv
End

Function/WAVE PLEMd2getWaveMapsSelection()
	variable numSpectra = PLEMd2getMapsAvailable()
	string strMaps = PLEMd2getStrMapsAvailable()

	DFREF dfr = SMAgetPackageRoot()

	if(numSpectra == 0)
		return $""
	endif

	WAVE/Z wv = dfr:mapsselection
	if(WaveExists(wv))
		if(DimSize(wv, 0) != numSpectra)
			Redimension/N=(numSpectra) dfr:mapsselection 
		endif
		return wv
	endif

	Make/O/N=(numSpectra) dfr:mapsselection/WAVE=wv = 1

	return wv
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

Menu "CameraImage"
	// CTRL+1 is the keyboard shortcut
	"AddCoordinates/1", /Q, AddCoordinatesFromGraph()
	"Zero To Cursor", SMAtasksZeroToCursor()
	"Process xyz coordinates", SMAtasksProcessCoordinates()
	"Merge Coordinates", SMAtasksMergeCoordinates()
	"PeakFind for coordinates", /Q, GetCoordinates()
	"Correct Image overlap", /Q, SMAtestSizeAdjustment()
	"GetHeight/2", SMAtasksPrintZposition()
End

Menu "GraphMarquee"
	"Erase Points", SMA_EraseMarqueeArea()
	"Extract z-dimension", SMA_ExtractSumMarqueeArea()
	"Display Original", SMAdisplayOriginal()
	"Duplicate", SMADuplicateRangeFromMarquee()
	"Wigner", SMAtasksCreateWigner()
End

Menu "MassAnalysis"
	"Load (init file links)", SMAload()
	"Append (add file links)", SMAadd()
	"Read (call PLEMd2)", SMAread()
	"File Info", SMAfileInfo()

	"Load Spectra", SMAtasksLoadExactscan()
	"Load CameraScan", SMAtasksLoadCamerascan()

	"Load Tiltscan", /Q, SMAtasksGetTiltPlane()
	"Recalc TiltPlane", SMAcameraGetTiltPlaneParameters(createNew = 1)

	"Load PointZero", SMAtasksPointZero()
	"Load CameraScan (timeSeries to z)", SMAmergeTimeSeries()

	"Background: Median", SMABackgroundMedian(power = 0)

	"generate exactscan", SMAtasksGenerateExactscan()
	"convert excactscan to exactcoordinates", SMAtasksGenerateCoordinates()

	"Histogram", SMAtasksHistogram()

	"Peak Analysis", SMApeakAnalysis()
	"Simple Peak Analysis", SMAquickAnalysis()

	"Single Peak Analysis", SMAsinglePeakAction(hcsr(A), hcsr(B))
	"Analyse Exactscan", SMApeakAnalysisExactscan()

	"Select Spectra Panel", SMAopenPanelSelectWaves()
	"Set Base Path", SMASetBasePath()
End

Function SMAtasksZeroToCursor()
	SMAaddOffset(vCsr(a), hCsr(a))

	SetScaleToCursor()
End

Function SMAtasksLoadCamerascan()
	SMAread()
	SMAprocessImageStack()
	SaveWindow("win_SMAimageStack")
End

Function SMAtasksCreateWigner()
	WAVE wv = SMAduplicateRange(SMAgetOriginalFromMarquee())
	if(!WaveExists(wv))
		Abort "Could not Duplicate Image"
	endif
	DelayUpdate
	Duplicate/O wv root:WignerSource
	KillWaves/Z wv
	SMAWigner(0, forceReNew = 1)
	DoWindow WignerGizmo
	if(!V_flag)
		Execute "	WignerGizmo()"
	endif
	DoWindow/F SMAwignerHor
	if(!V_flag)
		Execute "	SMAwignerHor()"
	endif
End

Function SMAtasksPrintZposition()
	print "(x,y)=", hcsr(a), ",", vcsr(a)
	print "z =", SMAcameraGetTiltPlane(hcsr(a), vcsr(a))
End

Function SMAtasksHistogram()
	Make/O histResult = 0, fit_histResult = 0
	
	SVAR/Z diffwave = root:diffwave
	if(!SVAR_EXISTS(diffwave))
		String/G root:diffwave
		SVAR/Z diffwave = root:diffwave
	endif
	NVAR/Z checkbox_fit = root:checkbox_fit
	if(!NVAR_EXISTS(checkbox_fit))
		Variable/G root:checkbox_fit = 1
	endif	
	
	WAVE wv = SMA_PromptTrace()
	diffwave = GetWavesDataFolder(wv, 2)

	DoWindow/F SMAHistogram
	if(!V_flag)
		Execute "SMAHistogram()"
	endif
End

Function SMAtasksLoadExactscan()
	variable numMaps = PLEMd2getMapsAvailable()
	if(!numMaps)
		SMAread()
	endif
	SMAgetSourceWave(overwrite = 1)
	SMApeakAnalysisExactscan()
End

Function SMAtasksGenerateExactscan([wv])
	WAVE wv
	
	variable dim0, dim1
	variable resolution = 11
	variable stepsize = 0.5

	if(ParamIsDefault(wv))
		WAVE wv = root:coordinates
		print "SMAtasksGenerateExactscan(wv = root:coordinates)"
	endif
	if(!WaveExists(wv))
		print "SMAtasksGenerateExactscan: Can not find coordinates"
		return 1
	endif
	
	dim0 = DimSize(wv, 0)
	dim1 = DimSize(wv, 1)
	
	Make/O/N=(dim0 * resolution, dim1) root:exactscan/WAVE=exactscan
	exactscan[][] = wv[floor(p / resolution)][q]
	exactscan[][1] = wv[floor(p / resolution)][1] - (resolution - 1) / 2 * stepsize + mod(p, 11) * stepsize
	
	print "SMAcalcZcoordinateFromTiltPlane(wv = ", NameOfWave(exactscan), ", zOffset = ", SMAcameraGetTiltPlane(0,0), ")"
	SMAcalcZcoordinateFromTiltPlane(wv = exactscan)

	Save/J/O/DLIM=","/P=home exactscan as "exactscan.csv"

	return 0
End

Function SMAtasksPointZero()
	SMAgetFocuspoints(graph = 1)
	Duplicate/O/R=[][2] root:SMAcameraIntensityCoordinates root:SMAcameraIntensityCoordinateZ/wave=coordinateZ
	Redimension/N=(-1, 0) coordinateZ
	Display/K=0 root:SMAcameraIntensitySmth vs coordinateZ
	WaveStats/Q root:SMAcameraIntensitySmth
	print "maximum", coordinateZ[V_maxloc], "um"
End

Function SMAtasksProcessCoordinates()
	RoundCoordinates(accuracy = 4)
	print "rounded coordinates"
	SortCoordinates()
	print "sorted coordinates"
	DeleteCoordinates(-5, 305)
	print "deleted range from -5um to 305um"
	SMAcalcZcoordinateFromTiltPlane()
	print "SMAcalcZcoordinateFromTiltPlane(zOffset = ", SMAcameraGetTiltPlane(0,0), ")"
End

Function SMAtasksGetTiltPlane()
	Variable numMaps = PLEMd2getMapsAvailable()

	// Tilt Plane Parameters could have been loaded from ibw files.
	WAVE/Z normal = root:SMAcameraPlaneNormal
	WAVE/Z distance = root:SMAcameraPlaneDistance
	if(!WaveExists(normal) || !WaveExists(distance))
		if(numMaps == 0)
			Execute "SMAread()"
		endif
	endif
	Execute "SMAreset(power=0, background=0)"
	Execute "	SMAcameraGetTiltPlaneParameters(createNew = 1)"
	Execute "SMAcameraCoordinates()"
End

// panel for handling different wave-sets in one igor experiment
Function SMAopenPanelSelectWaves()

	// create waves
	SMAgetWaveMapsAvailable()
	PLEMd2getWaveMapsSelected()
	PLEMd2getWaveMapsSelection()

	DoWindow SMAselectWaves
	if(!V_flag)
		Execute/Q "SMAselectWaves"
	endif
	DoWindow/F SMAselectWaves
End

// for coordinates from two camerascans.
Function SMAtasksMergeCoordinates()
	WAVE coordinates0, coordinates1, coordinates
	SMAMergeCoordinates(coordinates0, coordinates1)
End

// for coordinates from two camerascans.
// do not input root:coordinates!
Function/WAVE SMAMergeCoordinates(coordinates0, coordinates1)
	WAVE coordinates0, coordinates1
	
	WAVE/Z coordinates = root:coordinates
	if(WAVEExists(coordinates))
		duplicate/o coordinates root:coordinates_backup
	endif

	Variable dim00, dim01
	dim00 = DimSize(coordinates0, 0)
	dim01 = DimSize(coordinates1, 0)
	make/O/N=(dim00 + dim01, 3) root:coordinates/WAVE=coordinates

	coordinates[0, dim00 - 1] = coordinates0[p][q]
	coordinates[dim00,*] = coordinates1[p - dim00][q]
End

// exactscan --> exactcoordinates
Function SMAtasksGenerateCoordinates()
	WAVE exactscan = root:coordinates
	
	Duplicate/O exactscan root:exactcoordinates/WAVE=wv

	WAVE coordinates = PLEMd2getCoordinates()
	wv[][] = coordinates[exactscan[p][1]][q]

	print "SMAcalcZcoordinateFromTiltPlane(wv = ", GetWavesDataFolder(wv, 2), ", zOffset = ", SMAcameraGetTiltPlane(0,0), ")"

	print "Save/J/O/DLIM=\",\"/P=home ", GetWavesDataFolder(wv, 2), " as \"exactcoordinates.csv\""
	Save/J/O/DLIM=","/P=home wv as "exactcoordinates.csv"
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

// use root:source and root:wavelength wave to search for peaks
Function SMAsinglePeakAction(startX, endX, [source])
	Variable startX, endX
	WAVE source

	variable i, dim0

	// input waves
	if(paramIsDefault(source))
		WAVE source = SMAgetSourceWave(overwrite = 0)
	endif
	WAVE wl = root:wavelength
	if(!WaveExists(wl))
		WAVE wl = SMAcopyWavelengthToRoot()
	endif

	// search x coordinate in wavelength wave
	FindValue/V=(startX)/T=1 wl
	variable start = V_Value
	FindValue/V=(endX)/T=1 wl
	variable ende = V_value
	if(start == -1 || ende == -1)
		Abort "start or end not found in wavelength wave"
	endif

	// sync correlation for specified range
	Duplicate/O/R=[0,*][start, ende] source root:source_extracted/wave=source2
	MatrixOP/O root:source_extracted_timecorrelation/WAVE=extracted = getdiag(synccorrelation(source2^t), 0)
	DoWindow source_extracted_timecorrelation_graph
	if(!V_flag)
		Display/N=source_extracted_timecorrelation_graph extracted
	endif

	dim0 = DimSize(source, 0)

	// create output waves
	Make/O/N=(DimSize(source, 0), 1024) root:source_extracted_fit/WAVE=myfitwave = NaN
	SetScale/I y, startX, EndX, myfitwave
	Make/O/N=(dim0) root:peakHeight/WAVE=wvHeight = NaN
	Make/O/N=(dim0) root:peakHeightErr/WAVE=wvHeightErr = NaN
	Make/O/N=(dim0) root:peakLocation/WAVE=wvPos = NaN
	Make/O/N=(dim0) root:peakLocationErr/WAVE=wvPosnErr = NaN
	Make/O/N=(dim0) root:peakFWHM/WAVE=wvFwhm = NaN
	Make/O/N=(dim0) root:peakFWHMErr/WAVE=wvFwhmErr = NaN

	// do fit in specified range
	Duplicate/FREE/R=[start, ende] wl wl_extracted
	for(i = 0; i < dim0; i += 1)
		Duplicate/FREE/R=[i][start, ende] source source_extracted
		Redimension/N=(abs(ende - start) + 1) source_extracted

		WAVE guess = PeakFind(source_extracted, wvXdata = wl_extracted, maxPeaks = 1, smoothingFactor = 3) // align smoothingFactor to your needs
		WAVE/WAVE coef = BuildCoefWv(source_extracted, peaks = guess)
		WAVE/WAVE peakParam = fitGauss(source_extracted, wvXdata = wl_extracted, wvCoef = coef, cleanup = 1)
		if(!WaveExists(peakParam))
			printf "SMAsinglePeakAction: error fitting %d.\r", i
			continue
		endif
		if(DimSize(peakParam, 0) != 1)
			Abort "Code Inconsitency: More than one peak found."
		endif
		WAVE result = peakParamToResult(peakParam)

		WAVE peakfit = CreateFitCurve(peakParam, startX, endX, 1024)
		myfitwave[i][] = peakfit[q]

		wvHeight[i]    = result[0][%height]
		wvHeightErr[i] = result[0][%height_err]
		wvPos[i]       = result[0][%location]
		wvPosnErr[i]   = result[0][%location_err]
		wvFwhm[i]      = result[0][%fwhm]
		wvFwhmErr[i]   = result[0][%fwhm_err]
	endfor

	DoWindow source_extracted_peak_height
	if(!V_flag)
		Display/N=source_extracted_peak_height wvHeight as "PLEM peak action"
	endif
end

Function/WAVE SMApeakFind(input, [wvXdata, verbose, createWaves, maxPeaks, minPeakPercent, smoothingFactor])
	WAVE input, wvXdata
	variable verbose, createWaves, maxPeaks, minPeakPercent, smoothingFactor

	variable numResults, i

	if(ParamIsDefault(verbose))
		verbose = 0
	endif
	if(ParamIsDefault(createWaves))
		createWaves = 1
	endif
	if(ParamIsDefault(maxPeaks))
		maxPeaks = 1
	endif
	if(ParamIsDefault(minPeakPercent))
		minPeakPercent = 5
	endif
	if(ParamIsDefault(smoothingFactor))
		smoothingFactor = 1
	endif

	Duplicate/FREE input, wv
	if(DimSize(wv, 1) == 1)
		Redimension/N=(-1,0) wv
	endif

	if(verbose)
		printf "SMApeakFind(%s, verbose=%d)\r", GetWavesDatafolder(input, 2), verbose
	endif

	WAVE nospikes = removeSpikes(wv)
	 //WAVE nobackground = RemoveBackground(nospikes)

	if(ParamIsDefault(wvXdata))
		WAVE guess = PeakFind(nospikes, maxPeaks = maxPeaks, minPeakPercent = minPeakPercent, smoothingFactor = smoothingFactor, verbose = verbose)
	else
		WAVE guess = PeakFind(nospikes, wvXdata = wvXdata, maxPeaks = maxPeaks, minPeakPercent = minPeakPercent, smoothingFactor = smoothingFactor, verbose = verbose)
	endif
	DFREF dfr = SMApeakfitDF()
	WAVE/WAVE coef = BuildCoefWv(nospikes, peaks = guess, dfr = dfr, verbose = verbose)
	if(ParamIsDefault(wvXdata))
		WAVE/WAVE/Z peakParam = fitGauss(nospikes, wvCoef = coef, verbose = verbose)
	else
		WAVE/WAVE/Z peakParam = fitGauss(nospikes, wvXdata = wvXdata, wvCoef = coef, verbose = verbose)
	endif

	if(verbose > 2)
		print "==COEF WAVE=="
		numResults = DimSize(coef, 0)
		for(i = 0; i < numResults; i += 1)
			WAVE output = coef[i]
			print output
		endfor
	endif
	KillWaveOfWaves(coef)

	if(createWaves)
		Duplicate/O nospikes root:nospikes
		Duplicate/O wv root:original
		//Duplicate/O nobackground root:nobackground
		Duplicate/O nospikes root:nospikes
		WAVE peakfit = CreateFitCurve(peakParam, DimOffset(input, 0), DimOffset(input, 0) + DimSize(input, 0) * DimDelta(input, 0), 1024)
		Duplicate/O peakfit root:peakfit
		WAVE peakfit = CreateFitCurve(peakParam, DimOffset(input, 0), DimOffset(input, 0) + DimSize(input, 0) * DimDelta(input, 0), DimSize(input, 0))
		Duplicate/O peakfit root:residuum/WAVE=res
		res = nospikes - peakfit
	endif

	if(!WaveExists(peakParam))
		print "SMApeakFind Error: no peakParam"
		return $""
	endif
	return peakParamToResult(peakParam)
End

Function SMApeakAnalysis()
	variable i, j, numPeaks, offset

	STRUCT PLEMd2Stats stats
	Variable dim0 = PLEMd2getMapsAvailable()

	SMAquickAnalysis()

	WAVE/Z loc = root:peakLocation
	if(!WaveExists(loc) || DimSize(loc, 0) != dim0)
		Make/O/N=(dim0) root:peakLocation/WAVE=loc = NaN
	endif
	WAVE/Z int = root:peakHeight
	if(!WaveExists(int) || DimSize(int, 0) != dim0)
		Make/O/N=(dim0) root:peakHeight/WAVE=int = NaN
	endif
	WAVE/Z fwhm = root:peakFWHM
	if(!WaveExists(fwhm) || DimSize(fwhm, 0) != dim0)
		Make/O/N=(dim0) root:peakFWHM/WAVE=fwhm = NaN
	endif
	WAVE/Z area = root:peakArea
	if(!WaveExists(area) || DimSize(area, 0) != dim0)
		Make/O/N=(dim0) root:peakArea/WAVE=area = NaN
	endif

	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))
	if(DimSize(stats.wavPLEM, 1) > 1)
		return SMApeakAnalysisMap()
	endif

	for(i = 0; i < dim0; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))

		// do peakfind on ranged wave
		WAVE PLEMrange = PLEMd2NanotubeRangePLEM(stats)
		WAVE corrected = removeSpikes(PLEMrange)
		WAVE/WAVE peakfind = SMApeakFind(corrected, maxPeaks = 3, verbose = 0)
		if(!WaveExists(peakfind))
			continue // fall back to SMAquickAnalysis()
		endif
		numPeaks = DimSize(peakfind, 0)
		for(j = 0; j < numPeaks; j += 1)
			if(peakfind[j][%height] < int[i])
				continue
			endif
			if((peakfind[j][%fwhm] < 5) || (peakfind[j][%fwhm] > 30))
				continue
			endif
			int[i] = peakfind[j][%height]
			loc[i]  = peakfind[j][%location]
			fwhm[i] = peakfind[j][%fwhm]
			area[i] = peakfind[j][%area]
		endfor
	endfor
End

Function SMAatlasFit(indices, init, initT, [verbose])
	WAVE/U/I indices
	WAVE init
	WAVE/T initT
	Variable verbose

	variable i, numPLEM, q25

	verbose = ParamIsDefault(verbose) ? 0 : !!verbose

	WAVE/T strPLEM = PLEMd2getAllstrPLEM()
	numPLEM = DimSize(indices, 0)
	for(i = 0; i < numPLEM; i += 1)
		if(verbose)
			printf "SMAatlasFit: %02d/%02d: index: %02d name: %s\r", i, numPLEM, indices[i], strPLEM[indices[i]]
		endif
		PLEMd2AtlasInit(strPLEM[indices[i]], init = init, initT = initT)
		PLEMd2AtlasFit3D(strPLEM[indices[i]])
	endfor

	q25 = DimSize(indices, 0) < 100 ? 0 : SMAgetLowerQuartileIntensity(indices)
	for(i = 0; i < numPLEM; i += 1)
		PLEMd2AtlasClean(strPLEM[indices[i]], threshold = q25)
		PLEMd2AtlasFit3D(strPLEM[indices[i]])
		PLEMd2AtlasFit3D(strPLEM[indices[i]])
		PLEMd2AtlasFit2D(strPLEM[indices[i]])
	endfor

	for(i = 0; i < numPLEM; i += 1)
		PLEMd2AtlasClean(strPLEM[indices[i]], threshold = 0)
	endfor
End

Function SMAgetLowerQuartileIntensity(indices)
	WAVE/U/I indices

	variable i, numPLEM
	STRUCT PLEMd2stats stats
	String strConcatenate = ""

	WAVE/T strPLEMs = PLEMd2getAllstrPLEM()
	numPLEM = DimSize(indices, 0)
	for(i = 0; i < numPLEM; i += 1)
		PLEMd2statsLoad(stats, strPLEMs[indices[i]])
		if(DimSize(stats.wav1Dfit, 0) == 0)
			continue
		endif
		strConcatenate = AddListItem(GetWavesDataFolder(stats.wav1Dfit, 2), strConcatenate)
	endfor
	if(ItemsInList(strConcatenate) == 0)
		return NaN
	endif
	Concatenate/FREE strConcatenate, concat
	MatrixOp/FREE intensity = replaceNaNs(concat, 0)
	StatsQuantiles/Q intensity

	return V_Q25
End

Function SMAdisplayAtlasFit(indices, color)
	WAVE/U/I indices
	WAVE/U/W color

	variable i, numPLEM
	String strPLEM, win
	Struct PLEMd2stats stats

	if(DimSize(color, 1) != 3)
		Abort "SMAdisplayAtlasFit: Need 3 colors"
	endif

	win = "SMAatlasFit_graph"
	DoWindow/F $win
	if(!V_flag)
		Display/N=$win as "SMAatlasFit"
		win = S_Name
	endif

	WAVE/T strPLEMs = PLEMd2getAllstrPLEM()
	WAVE/T traces = ListToTextWave(TraceNameList(win, ";", 0x1), ";")

	numPLEM = DimSize(indices, 0)
	for(i = 0; i < numPLEM; i += 1)
		strPLEM = strPLEMs[indices[i]]
		FindValue/TEXT=(strPLEM) traces
		if(V_value != -1 && V_startPos == 0)
			continue
		endif
		PLEMd2statsLoad(stats, strPLEM)
		AppendToGraph/Q/W=$win/C=(color[0][0], color[0][1], color[0][2]) stats.wavEnergyS2/TN=$strPLEM vs stats.wavEnergyS1
		ModifyGraph rgb($strPLEM)=(color[0][0],color[0][1],color[0][2],13107),useMrkStrokeRGB($strPLEM)=1
		//ModifyGraph zmrkSize($strPLEM)={stats.wav1Dfit,mini,maxi,0.25,2}
		ModifyGraph zmrkSize($strPLEM)={stats.wav1Dfit,0,*,0,2}
	endfor

	ModifyGraph/W=$win mode=4, marker=19, textMarker=0, useMrkStrokeRGB=1
End

Function SMApeakAnalysisMap()
	variable i, j, numPeaks, numAccuracy
	Variable fit_start, fit_end, numPoints
	Struct PLEMd2stats stats

	Variable numDelta = 100 / 2 // this is the fitting range around the initial guess

	Variable V_fitOptions = 4 // used to suppress CurveFit dialog
	Variable V_FitQuitReason  // stores the CurveFit Quit Reason
	Variable V_FitError   // Curve Fit error

	Variable dim0 = PLEMd2getMapsAvailable()

	WAVE wavelength = SMAcopyWavelengthToRoot()
	WAVE excitation = SMAcopyExcitationToRoot()

	// todo: integrate with SMAatlasFit()

	Make/O/N=(dim0) root:peakHeight/WAVE=peakHeight
	Make/O/N=(dim0) root:peakEmission/WAVE=peakEmission
	Make/O/N=(dim0) root:peakExcitation/WAVE=peakExcitation

	for(i = 0; i < dim0; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))
		WAVE corrected = RemoveSpikes(stats.wavPLEM)

		// fit Excitation
		// find p,q
		FindValue/T=2/V=(peakEmission[i] - numDelta) wavelength
		if(V_Value == -1)
			fit_start = 0
		else
			fit_start = V_Value
		endif
		FindValue/T=2/V=(peakEmission[i] + numDelta) wavelength
		if(V_Value == -1)
			fit_end = DimSize(corrected, 0) - 1
		else
			fit_end = V_Value
		endif
		numAccuracy = 0
		do
			numAccuracy += 1
			FindValue/T=(numAccuracy)/V=(peakExcitation[i]) excitation
		while(V_Value == -1)

		numPoints = abs(fit_end - fit_start) + 1
		Make/N=(numPoints)/FREE fitEmission = corrected[fit_start + p][V_Value]
		Make/N=(numPoints)/FREE fitEmissionX = wavelength[fit_start + p]

		WAVE/WAVE peakfind = SMApeakFind(fitEmission, wvXdata = fitEmissionX, maxPeaks = 3, verbose = 1)
		if(!WaveExists(peakfind))
			continue
		endif
		numPeaks = DimSize(peakfind, 0)
		for(j = 0; j < numPeaks; j += 1)
			if(peakfind[j][%height] < peakHeight[i])
				continue
			endif
			if((peakfind[j][%fwhm] < 5) || (peakfind[j][%fwhm] > 50))
				continue
			endif
			peakHeight[i] = peakfind[j][%height]
			peakEmission[i] = peakfind[j][%location]
		endfor
		WaveClear peakfind

		// fit Emission
		// find p,q
		FindValue/T=2/V=(peakExcitation[i] - numDelta) excitation
		if(V_Value == -1)
			fit_start = 0
		else
			fit_start = V_Value
		endif
		FindValue/T=2/V=(peakExcitation[i] + numDelta) excitation
		if(V_Value == -1)
			fit_end = DimSize(corrected, 1) - 1
		else
			fit_end = V_Value
		endif
		numAccuracy = 0
		do
			numAccuracy += 1
			FindValue/T=(numAccuracy)/V=(peakEmission[i]) wavelength
		while(V_Value == -1)

		numPoints = abs(fit_end - fit_start) + 1
		Make/N=(numPoints)/FREE fitExcitation = corrected[V_Value][fit_start + p]
		Make/N=(numPoints)/FREE fitExcitationX = excitation[fit_start + p]

		WAVE/WAVE peakfind = SMApeakFind(fitExcitation, wvXdata = fitExcitationX, maxPeaks = 3, verbose = 0)
		if(!WaveExists(peakfind))
			continue
		endif
		numPeaks = DimSize(peakfind, 0)
		for(j = 0; j < numPeaks; j += 1)
			if(peakfind[j][%height] < peakHeight[i])
				continue
			endif
			if((peakfind[j][%fwhm] < 5) || (peakfind[j][%fwhm] > 50))
				continue
			endif
			peakHeight[i] = peakfind[j][%height]
			peakExcitation[i] = peakfind[j][%location]
		endfor
		WaveClear peakfind
	endfor

	// create simple sum
	WAVE source = SMAgetSourceWave()
	MatrixOp/O root:mapsSum/WAVE=dest = sumCols(source)^t
	SMARedimensionToMap(dest)

	// display
	DoWindow/F SMAmapsSum
	if(!V_flag)
		Execute "SMAmapsSum()"
	endif
	DoWindow/F SMApeakMaximum
	if(!V_flag)
		Execute "SMApeakMaximum()"
	endif
End

Function SMAquickAnalysis()
	variable i, dim0
	Struct PLEMd2stats stats

	dim0 = Plemd2getMapsAvailable()
	PLEMd2statsLoad(stats, PLEMd2strPLEM(0))

	Make/O/N=(dim0) root:peakHeight/WAVE=int = NaN
	if(DimSize(stats.wavPLEM, 1) > 1)
		Make/O/N=(dim0) root:peakEmission/WAVE=emi = NaN
		Make/O/N=(dim0) root:peakExcitation/WAVE=exc = NaN
	else
		Make/O/N=(dim0) root:peakLocation/WAVE=loc = NaN
	endif

	for(i = 0; i < dim0; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(i))

		WAVE PLEMrange = PLEMd2NanotubeRangePLEM(stats)
		WAVE corrected = removeSpikes(PLEMrange)
		WaveStats/M=1/Q/P corrected // explicitly get min/max in points

		int[i] = V_max
		if(DimSize(stats.wavPLEM, 1) > 1)
			emi[i] = stats.wavWavelength[ScaleToIndex(stats.wavPLEM, IndexToScale(corrected, V_maxRowLoc, 0), 0)]
			exc[i] = stats.wavExcitation[ScaleToIndex(stats.wavPLEM, IndexToScale(corrected, V_maxColLoc, 1), 1)]
		else
			loc[i] = stats.wavWavelength[ScaleToIndex(stats.wavPLEM, IndexToScale(corrected, V_maxRowLoc, 0), 0)]
		endif
	endfor
End

// extract best Spectrum from exactscan to get only one spectrum per nanotube
Function SMApeakAnalysisExactscan()
	variable i, numItems

	WAVE index = SMApeakAnalysisGetBestIndex()
	numItems = DimSize(index, 0)

	WAVE loc = root:peakLocation
	WAVE int = root:peakHeight
	WAVE fwhm = root:peakFWHM
	WAVE area = root:peakArea

	Make/O/N=(numItems) root:peakLocationExact = numType(index[p]) == 0 ? loc[index[p]] : NaN
	Make/O/N=(numItems) root:peakHeightExact = numType(index[p]) == 0 ? int[index[p]] : NaN
	Make/O/N=(numItems) root:peakFWHMExact = numType(index[p]) == 0 ? fwhm[index[p]] : NaN
	Make/O/N=(numItems) root:peakAreaExact = numType(index[p]) == 0 ? area[index[p]] : NaN

	DoWindow/F SMAexactscanImage
	if(!V_flag)
		Plemd2getCoordinates()
		SetDataFolder root:
		SmaloadBasePath()
		LoadWave/H/O/P=SMAbasePath ":collection:suspended:methods:exactscan:template:trenches.ibw"
		LoadWave/H/O/P=SMAbasePath ":collection:suspended:methods:exactscan:template:borders.ibw"
		Execute "SMAexactscanImage()"
		Execute "saveWindow(\"SMAexactscanImage\", saveJSON = 0, saveImages = 1, saveSVG = 0)"
	endif
End

// @brief find best spectrum from exactscan
//
// fixed to 11 scans around central position
// does not respect 2 different peak locations in one exactscan (too close nanotubes or bundles)
Function/WAVE SMApeakAnalysisGetBestIndex()
	variable i, dim0, range, numPLEM, numIndex
	variable rangeStart, rangeEnd
	Struct PLEMd2stats stats
	
	dim0 = Plemd2getMapsAvailable()
	range = 11

	WAVE/Z int = root:peakHeight
	if(!WaveExists(int))
		SMApeakAnalysis()
		WAVE int = root:peakHeight
	endif
	numIndex = round(dim0 / range)
	Make/O/N=(numIndex) root:peakIndex/WAVE=index = NaN
	for(i = 0; i < numIndex; i += 1)
		rangeStart = i * range
		rangeEnd = rangeStart + range - 1
		Duplicate/FREE/R=[rangeStart, rangeEnd] int int_range
		SetScale/P x, 0, 1, int_range
		WAVE/WAVE/Z peakParam = SMApeakFind(int_range, maxPeaks = 1)
		numPLEM = NaN
		if(WaveExists(peakParam))
			numPLEM = peakParam[0][%location]
		endif
		if(numType(numPLEM) != 0) // fall back to maximum intensity spectrum
			Wavestats/Q/M=1/P int_range
			numPLEM = V_maxLoc
		endif
		numPLEM += rangeStart
		numPLEM = max(min(round(numPLEM), rangeEnd), rangeStart)
		index[i] = numPLEM
	endfor

	return index
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

#include <Peak AutoFind>

// prefs idea taken from WMs example and FILO (igor-file-loader)
// https://github.com/ukos-git/igor-file-loader
// released under MIT license by same author @ukos-git

static Constant cversion = 0002
static StrConstant cstrPackageName = "Spectra Mass Analysis"
static StrConstant cstrPreferencesFileName = "SMA.bin"
// The recordID is a unique number identifying a record within the preference file.
static Constant cPrefsRecordID = 0
static Constant reserved = 90

Structure SMAprefs
	double version
	char   strBasePath[40]

	// reserved forfuture use
	uchar  strReserved1[256]
	uchar  strReserved2[256]
	double dblReserved[100]
	uint32 intReserved[reserved]
EndStructure

static Function DefaultPackagePrefs(package)
	STRUCT SMAprefs &package

	Variable i

	package.version = 0
	package.strBasePath = ""

	// reserved forfuture use
	package.strReserved1 = ""
	package.strReserved2 = ""
	for(i = 0; i < reserved; i += 1)
		package.dblReserved[i] = 0
		package.intReserved[i] = 0
	endfor
End

static Function ResetPackagePrefs(package)
	STRUCT SMAprefs &package

	package.strReserved1 = ""
	package.strReserved2 = ""
End

static Function SyncPackagePrefs(package)
	STRUCT SMAprefs &package

	package.version = cversion
End

Function SMAloadPackagePrefs(package, [id])
	STRUCT SMAprefs &package
	Variable id

	if(ParamIsDefault(id))
		id = cPrefsRecordID
	endif

	LoadPackagePreferences cstrPackageName, cstrPreferencesFileName, id, package
	if(V_flag != 0 || V_bytesRead == 0)
		print "SMAloadPackagePrefs: \tPackage not initialized"
		DefaultPackagePrefs(package)
	endif

	if(package.version < cversion)
		print "SMALoadPackagePrefs: \tVersion change detected:"
		printf "\tcurrent Version: \t%04d\r", package.version
		ResetPackagePrefs(package)
		SMAsavePackagePrefs(package)
		printf "\tnew Version: \t%04d\r", cversion
	endif
End

Function SMAsavePackagePrefs(package, [id])
	STRUCT SMAprefs &package
	Variable id

	if(ParamIsDefault(id))
		id = cPrefsRecordID
	endif

	SyncPackagePrefs(package)
	SavePackagePreferences cstrPackageName, cstrPreferencesFileName, id, package
End

// Save the location of the base path.
// All files will be saved/updated relative to this path.
// This function originates at swnt-plem.
//
// DisplayHelpTopic "Symbolic Paths"
Function SMASetBasePath()
	String strBasePath

	Struct SMAprefs prefs
	SMAloadPackagePrefs(prefs)

	strBasePath = prefs.strBasePath
	NewPath/O/Q/Z SMAbasePath, strBasePath
	if(!V_flag)
		PathInfo/S path
	endif

	NewPath/O/Q/Z/M="Set SMA base path" SMAbasePath
	if(V_flag)
		return 0 // user canceled
	endif

	PathInfo SMAbasePath
	strBasePath = S_path
	if(!V_flag)
		return 0 // invalid path
	endif

	strBasePath = RemoveEnding(strBasePath, ":")
	GetFileFolderInfo/Q/Z=1 strBasePath
	if(!V_flag && V_isFolder)
		prefs.strBasePath = strBasePath
		SMAsavePackagePrefs(prefs)
	endif
End

Function SMAloadBasePath()
	String path
	PathInfo SMAbasePath
	if(V_flag)
		return 0
	endif

	Struct SMAprefs prefs
	SMAloadPackagePrefs(prefs)

	path = prefs.strBasePath
	NewPath/O/Q/Z SMAbasePath, path
	if(V_flag)
		printf "error setting base path to %s\r", path
		return 1
	endif

	return 0
End
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3

#include "utilities-peakfind"

Function SMAorderAsc(minimum, maximum)
	Variable &minimum, &maximum

	Variable temp

	if(minimum < maximum)
		return 0
	endif
	temp = minimum
	minimum = maximum
	maximum = temp

	return 0
End

Function/WAVE CoordinateFinderXYrange(coordinates, xmin, xmax, ymin, ymax, [verbose])
	WAVE coordinates
	Variable xmin, xmax, ymin, ymax, verbose

	verbose = ParamIsDefault(verbose) ? 0 : !!verbose

	Duplicate/FREE/R=[][0] coordinates, coordinateX
	Duplicate/FREE/R=[][1] coordinates, coordinateY

	Extract/INDX/FREE coordinateX, indicesX, coordinateX > xmin && coordinateX < xmax
	Extract/INDX/FREE coordinateY, indicesY, coordinateY > ymin && coordinateY < ymax

	Concatenate/FREE {indicesX, indicesY}, indicesXY
	FindDuplicates/FREE/DN=indices indicesXY
	if(DimSize(indices, 0) == 0 || numtype(indices[0]) != 0)
		return $""
	endif

	if(verbose)
		print indices
	endif
	return indices
End

/// @brief find x,y,z values in a 3-dimensional coordinates wave.
Function/WAVE CoordinateFinderXYZ(coordinates, xVal, yVal, zVal, [verbose, accuracy])
	WAVE coordinates
	Variable xVal, yVal, zVal, verbose
	Variable accuracy

	verbose = ParamIsDefault(verbose) ? 0 : !!verbose
	accuracy = ParamIsDefault(accuracy) ? 0.5 : abs(accuracy)

	Duplicate/FREE/R=[][0] coordinates, coordinateX
	Duplicate/FREE/R=[][1] coordinates, coordinateY
	Duplicate/FREE/R=[][2] coordinates, coordinateZ

	coordinateX = round(coordinateX[p] / accuracy) * accuracy
	coordinateY = round(coordinateY[p] / accuracy) * accuracy
	coordinateZ = round(coordinateZ[p] / accuracy) * accuracy
	xVal = round(xVal / accuracy) * accuracy
	yVal = round(yVal / accuracy) * accuracy
	zVal = round(zVal / accuracy) * accuracy

	Extract/INDX/FREE coordinateX, indicesX, (coordinateX[p] == xVal)
	Extract/INDX/FREE coordinateY, indicesY, (coordinateY[p] == yVal)
	Extract/INDX/FREE coordinateZ, indicesZ, (coordinateZ[p] == zVal)

	if(!DimSize(indicesX, 0) || !DimSize(indicesY, 0) || !DimSize(indicesZ, 0))
		return $""
	endif

	Make/FREE/N=0 indicesXYZ
	Concatenate {indicesX, indicesY, indicesZ}, indicesXYZ

	Sort indicesXYZ, indicesXYZ
	Redimension/N=(numpnts(indicesXYZ))/E=1 indicesXYZ

	Extract/FREE indicesXYZ, indices, (p > 1 && (indicesXYZ[p] == indicesXYZ[p - 1]) && (indicesXYZ[p] == indicesXYZ[p - 2]))
	if(!DimSize(indices, 0))
		return $""
	endif

	if(verbose)
		print "CoordinateFinderXYZ: found the following indices in the input wave:"
		print indices
	endif
	return indices
End

/// @param indices Wave holding numeric ids of PLEM waves.
Function/WAVE ImageDimensions(indices)
	WAVE indices

	variable i, numMaps
	variable xMin, xMax, yMin, yMax
	STRUCT PLEMd2Stats stats

	numMaps = DimSize(indices, 0)

	for(i = 0; i < numMaps; i += 1)
		PLEMd2statsLoad(stats, PLEMd2strPLEM(indices[i]))
		xMin = min(ScaleMin(stats.wavPLEM, 0), xMin)
		xMax = max(ScaleMax(stats.wavPLEM, 0), xMax)
		yMin = min(ScaleMin(stats.wavPLEM, 1), yMin)
		yMax = max(ScaleMax(stats.wavPLEM, 1)	, yMax)
	endfor

	Make/FREE/N=(2,2) wv
	SetDimLabel 0, 0, x, wv
	SetDimLabel 0, 1, y, wv
	SetDimLabel 1, 0, min, wv
	SetDimLabel 1, 1, max, wv
	wv[%x][%min] = xMin
	wv[%x][%max] = xMax
	wv[%y][%min] = yMin
	wv[%y][%max] = yMax

	return wv
End

Function ScaleMin(wv, dim)
	WAVE wv
	variable dim

	if(DimDelta(wv, dim) > 0)
		return DimOffset(wv, dim)
	else
		return DimOffset(wv, dim) + DimSize(wv, dim) * DimDelta(wv, dim)
	endif
End

Function ScaleMax(wv, dim)
	WAVE wv
	variable dim

	if(DimDelta(wv, dim) < 0)
		return DimOffset(wv, dim)
	else
		return DimOffset(wv, dim) + DimSize(wv, dim) * DimDelta(wv, dim)
	endif
End

// get a suitable value for ColorScales to have 0 always in the middle.
Function getEvenScale(wv)
	WAVE wv

	Variable scaleEven

	Wavestats/Q/M=0 wv
	scaleEven = max(abs(V_max), abs(V_min))
	scaleEven = 10^(ceil(log(scaleEven)*5)/5)

	return scaleEven
End

Function/S removePrefix(prefix, item)
	String prefix, item

	item = FILO#RemovePrefixFromListItems(prefix, item)
	return RemoveEnding(item, ";")
End

Data Export

Data export was performed on continuous integration installations using docker containers with an automated Igor Pro pipeline. All graphs are available online as Igor Experiment (pxp) files in their complete form to perform further analyses and to extract data.

The drone.io pipeline can be triggered locally using make drone or drone exec.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
---
kind: pipeline
name: build

platform:
  os: linux
  arch: amd64

clone:
  depth: 50

steps:
- name: submodules
  image: docker:git
  commands:
  - git submodule update --init --recursive
  - test -e /drone/src/programs/igor-utilities/app/utilities-images.ipf
  - test -e /drone/src/programs/igor-utilities/app/utilities-lists.ipf
  - test -e /drone/src/programs/igor-plotly/src/PlotlyPrefs.ipf
  - test -e /drone/src/programs/igor-plotly/src/PlotlyFunctions.ipf
  - test -e /drone/src/programs/igor-plotly/src/Plotly.ipf

- name: get_tracked
  image: drone/git
  commands:
  - mkdir -p log/
  - git ls-tree -r HEAD --name-only > log/tracked
  - stat log/tracked

- name: collect_changes
  image: drone/git
  commands:
  - if ! git rev-parse --quiet --verify master; then git fetch origin master; fi
  - git diff --name-only --diff-filter=MCRA master > log/changes
  when:
    branch:
      exclude:
        - master

- name: collect_all
  image: ukos/igorpro-minimal
  pull: if-not-exists
  commands:
  - git lfs version
  - git lfs ls-files --name-only > log/changes
  when:
    branch:
    - master
    - bugfix/*

- name: prepare_igorpro
  image: alpine
  commands:
  - grep '.pxp$' log/changes | tee log/pxp
  - if ! test $(cat log/pxp | wc -l) -gt 0; then exit 0;fi

- name: pull_files
  image: drone/git
  commands:
  - git lfs version
  - for file in $(awk "NR % 1 == 0" log/pxp); do if ! sha256sum -c "$file.sha256"; then git lfs pull --include="$file"; fi; done
  - git lfs pull --include="*.svg"

- name: pull_build_cache
  image: ukos/rsync
  commands:
  - if test -z "$DRONE_COMMIT"; then exit 0; fi
  - echo "be sure to mount that network share on the docker executor"
  - rsync -rlt --perms --chmod=o=rwX --ignore-existing --exclude build /phd/stable/ ./
  volumes:
  - name: phd
    path: /phd
  when:
    branch:
      exclude:
        - master

- name: igorpro
  image: ukos/igorpro-minimal
  pull: if-not-exists
  working_dir: /drone/src
  commands:
  - if ! test $(wc -l log/pxp | cut -c1) -gt 0; then exit 0;fi
  - ln -sv /drone/src/programs/igor-json-xop/output/win/x86/*.xop "/root/WaveMetrics/Igor Pro 8 User Files/Igor Extensions/"
  - ln -sv /drone/src/programs/igor-filo/app                      "/root/WaveMetrics/Igor Pro 8 User Files/User Procedures/filo"
  - ln -sv /drone/src/programs/igor-json-xop/procedures           "/root/WaveMetrics/Igor Pro 8 User Files/User Procedures/json"
  - ln -sv /drone/src/programs/igor-plem/app                      "/root/WaveMetrics/Igor Pro 8 User Files/User Procedures/plem"
  - ln -sv /drone/src/programs/igor-plotly/src                    "/root/WaveMetrics/Igor Pro 8 User Files/User Procedures/plotly"
  - ln -sv /drone/src/programs/igor-sma/app                       "/root/WaveMetrics/Igor Pro 8 User Files/User Procedures/sma"
  - ln -sv /drone/src/programs/igor-utilities/app                 "/root/WaveMetrics/Igor Pro 8 User Files/User Procedures/utilities"
  - readlink -e /root/WaveMetrics/Igor\ Pro\ 8\ User\ Files/User\ Procedures/*
  - readlink -e /root/WaveMetrics/Igor\ Pro\ 8\ User\ Files/Igor\ Procedures/*
  - for file in $(awk "NR % 1 == 0" log/pxp); do scripts/extract.sh "$file"; done

- name: verify
  image: ukos/plotly
  commands:
  - /bin/sh scripts/html_verify_json.sh

- name: images
  image: ukos/inkscape:latest
  commands:
  - /bin/sh scripts/latex_convert_svg.sh
  - /bin/sh scripts/latex_convert_gif.sh
  - /bin/sh scripts/html_convert_vector.sh
  - if test -z "$DRONE_COMMIT"; then exit 0; fi  # optional processing
  - /bin/sh scripts/html_convert_heic.sh

- name: pdf
  image: ukos/sphinx:latest
  commands:
  - /usr/bin/make latexpdf
  - du -h build/latex/thesis.pdf
  - mv -f build/latex/thesis.pdf _static/

- name: html
  image: ukos/sphinx:latest
  commands:
  - /usr/bin/make clean
  - /usr/bin/make html

- name: push_stable
  image: ukos/rsync
  commands:
  - if test -z "$DRONE_COMMIT"; then exit 0; fi
  - rsync -rlt --checksum --delete --delete-excluded --exclude .git --exclude-from=log/tracked ./ /phd/stable
  volumes:
  - name: phd
    path: /phd
  when:
    branch:
    - master


volumes:
- name: phd
  host:
    path: /mnt/phd

The GitLab runner pipeline allows parallelisation of the export step with different docker containers and deploys the website on the master branch.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - programs/

variables:
  GIT_LFS_SKIP_SMUDGE: "1"

stages:
  - init
  - igorpro
  - postprocess
  - build
  - deploy

collect:
  stage: init
  image: ${CI_REGISTRY}/ukos-git/docker-igorpro  # @todo use git lfs container with git lfs ls-files available
  script:
    - git lfs version
    - mkdir -p log
    - git ls-tree -r HEAD --name-only > log/tracked
    - git lfs ls-files --name-only > log/changes
    - grep '.pxp$' log/changes | tee log/pxp
    - if ! test $(cat log/pxp | wc -l) -gt 0; then exit 0;fi
  artifacts:
    paths:
      - log/
    expire_in: 1 day

igorpro:
  stage: igorpro
  image: ${CI_REGISTRY}/ukos-git/docker-igorpro
  variables:
    GIT_SUBMODULE_STRATEGY: recursive
  script:
    - if ! test $(wc -l log/pxp | cut -c1) -gt 0; then exit 0;fi
    - ln -sv $(pwd)/programs/igor-json-xop/output/win/x86/*.xop "${HOME}/WaveMetrics/Igor Pro 8 User Files/Igor Extensions/"
    - ln -sv $(pwd)/programs/igor-filo/app                      "${HOME}/WaveMetrics/Igor Pro 8 User Files/User Procedures/filo"
    - ln -sv $(pwd)/programs/igor-json-xop/procedures           "${HOME}/WaveMetrics/Igor Pro 8 User Files/User Procedures/json"
    - ln -sv $(pwd)/programs/igor-plem/app                      "${HOME}/WaveMetrics/Igor Pro 8 User Files/User Procedures/plem"
    - ln -sv $(pwd)/programs/igor-plotly/src                    "${HOME}/WaveMetrics/Igor Pro 8 User Files/User Procedures/plotly"
    - ln -sv $(pwd)/programs/igor-sma/app                       "${HOME}/WaveMetrics/Igor Pro 8 User Files/User Procedures/sma"
    - ln -sv $(pwd)/programs/igor-utilities/app                 "${HOME}/WaveMetrics/Igor Pro 8 User Files/User Procedures/utilities"
    - readlink -e ${HOME}/WaveMetrics/Igor\ Pro\ 8\ User\ Files/User\ Procedures/*
    - readlink -e ${HOME}/WaveMetrics/Igor\ Pro\ 8\ User\ Files/Igor\ Procedures/*
    - for file in $(awk "NR % ${CI_NODE_TOTAL} == ${CI_NODE_INDEX} - 1" log/pxp); do git lfs pull --include="$file" && scripts/extract.sh "$file"; done
  parallel: 16
  cache:
    key: igorpro
    untracked: true
  artifacts:
    untracked: true
    expire_in: 1 day
  timeout: 15 minutes
  retry:
    max: 1

verify:
  stage: postprocess
  image: ukos/plotly
  script:
    - /bin/sh scripts/html_verify_json.sh

images:
  stage: postprocess
  image: ukos/inkscape
  script:
    - /bin/sh scripts/latex_convert_svg.sh
    - /bin/sh scripts/latex_convert_gif.sh
    - /bin/sh scripts/html_convert_vector.sh
  artifacts:
    untracked: true
    expire_in: 1 day

sphinx:
  stage: build
  image: ukos/sphinx
  variables:
    GIT_SUBMODULE_STRATEGY: recursive
  script:
    - git lfs pull --exclude "*.pxp"
    - /usr/bin/make latexpdf
    - du -h build/latex/thesis.pdf
    - mv -f build/latex/thesis.pdf _static/
    - /usr/bin/make html
  artifacts:
    paths:
      - build/html/
    expire_in: 1 week
  retry:
    max: 2

pages:
  stage: deploy
  image: alpine
  script:
    - mv -f build/html public
  dependencies:
    - sphinx
  cache: {}
  artifacts:
    paths:
      - public
  only:
    - master

CVD Control

The CVD setup has a python GUI. The recording of CVD parameters was performed with an Arduino Uno and has already been decribed elsewhere [85].

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
#!/usr/bin/env python

import MySQLdb as mysqlconnector
from MySQLdb.constants import CLIENT
import os
import socket
import decimal
import struct
from time import sleep
import multiprocessing
import ConfigParser

from MKFlowMessage import FBconvertLong # converter for long numbers to float and percent
#cvd-client->rbBmSDP7fSKp87b5

class MKDatabase(object):
    sql = ""
    connected = False
    ready = False
    messageID = -1
    client = False
    recording = False
    recordingID = -1
    fileName = ""
    storage_description = 50
    storage_values = 30

    hostname = ""

    # settings.cfg (see loadConfig)
    dbUser = ""
    dbPass = ""
    dbHost = ""
    dbName = ""
    servername = ""

    def __init__(self, isClient = False):
        self.client = isClient
        self.loadConfig()
        self.test()
        decimal.getcontext().prec = 2

    def loadConfig(self):
        config = ConfigParser.ConfigParser()
        srcPath = os.path.dirname(os.path.realpath(__file__))
        settingsFile = srcPath + '/../settings.cfg'
        if not (os.path.exists(settingsFile)):
            print "settings.cfg not found"
            raise
        config.read(settingsFile)
        self.dbUser = config.get('Database', 'dbuser')
        self.dbPass = config.get('Database', 'dbpass')
        self.dbHost = config.get('Database', 'dbhost')
        self.dbName = config.get('Database', 'dbname')
        self.servername = config.get('Server', 'servername')

    def open(self):
        try:
            if not self.checkIP():
                print "server unavailable"
                raise
            self.db = mysqlconnector.connect(
                host = self.dbHost,
                user = self.dbUser,
                passwd = self.dbPass,
                db = self.dbName,
                client_flag = CLIENT.FOUND_ROWS,
                connect_timeout = 1
                )
        except:
            print "database open failed."
            self.close()
            return False
        else:
            print "connected as user: %s" % self.dbUser
            self.connected = True
            return True
    def close(self):
        try:
            self.db.close()
        except:
            if not self.checkIP():
                print "connection lost. Database could not be closed normal"
                self.connected = False
        else:
            self.connected = False

    def isOpen(self):
        #if not self.connected:
        #    return False
        #try:
        #    stats = self.db.stat()
        #    if stats == 'MySQL server has gone away':
        #        self.close()
        #except:
        #    self.connected = False
        return self.connected

    def write_without_timeout(self, db, sql, connection):
        try:
            cursor = db.cursor()
            cursor.execute(sql)
            affectedRows = cursor.rowcount
            cursor.close()
            db.commit()
        except:
            affectedRows = 0
            try:
                self.db.rollback()
            except:
                pass
        connection.send(affectedRows)
        connection.close()

    def read_without_timeout(self, db, sql, connection):
        affectedRows = 0
        try:
            cursor = db.cursor()
            cursor.execute(sql)
            data = cursor.fetchone()
            cursor.close()
        except:
            connection.send([])
        else:
            connection.send(data)
        connection.close()

    # from alex martelli on http://stackoverflow.com/questions/1507091/python-mysqldb-query-timeout
    def write(self, sql, update = False):
        if not self.isOpen():
            if not self.open():
                raise
        conn_parent, conn_child = multiprocessing.Pipe(False)
        subproc = multiprocessing.Process(target = self.write_without_timeout,
                                          args = (self.db, sql, conn_child))
        subproc.start()
        subproc.join(1)
        if conn_parent.poll():
            affectedRows = conn_parent.recv()
            # on update statements rise if no lines were affected
            if update and affectedRows == 0:
                raise UpdateError('UPDATE statement failed')
            else:
                return affectedRows
        subproc.terminate()
        raise TimeoutError("Query %r ran for >%r" % (sql, timeout))

    def read(self, sql):
        if not self.isOpen():
            if not self.open():
                raise
        conn_parent, conn_child = multiprocessing.Pipe(False)
        subproc = multiprocessing.Process(target = self.read_without_timeout,
                                          args = (self.db, sql, conn_child))
        subproc.start()
        subproc.join(1)
        if conn_parent.poll():
            data = conn_parent.recv()
            try:
                if len(data) == 0:
                    raise
            except:
                return []
            else:
                return data
        else:
            subproc.terminate()
            return []

    def writeArduino(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                print "writeArduino failed: create database and try again."
                self.createArduino()
                self.resetArduino()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeRecording(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                self.createRecording()
                self.resetRecording()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeFlowbus(self, sql):
        try:
            self.write(sql)
        except:
            try:
                self.createFlowbus()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeMessage(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                self.createMessage()
                self.resetMessage()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def test(self):
        print "-- starting self-test --"
        self.open()
        sql="SELECT VERSION()"
        data = self.read(sql)
        self.close()
        print "MySQL version : %s " % data
        print "-- self test complete --"

    def getHostname(self):
        if self.hostname == "":
            self.hostname = socket.gethostname()
        return self.hostname

    def isServer(self):
        if (self.getHostname() == self.servername):
            return True
        else:
            return False

    def getIP(self):
        if self.isServer():
            ip = 'localhost'
        else:
            ip = self.dbHost
        return ip

    def checkIP(self, ip = ""):
        if len(ip) == 0:
            ip = self.getIP()
        if ip == "localhost":
            return True
        command = "ping -c 1 -W 1 " + ip
        print "executing '" + command + "'"
        if os.system(command + " > /dev/null") == 0:
            return True
        else:
            print "ip not found. sleeping penalty."
            sleep(1)
            return False

    def createArduino(self):
        sql = """CREATE TABLE IF NOT EXISTS `runtime_arduino` (
                `temperature` decimal(6,2) NOT NULL DEFAULT '0',
                `pressure` decimal(6,2) NOT NULL DEFAULT '0',
                `argon` decimal(6,2) NOT NULL DEFAULT '0',
                `ethanol` decimal(6,2) NOT NULL DEFAULT '0',
                `spTemperature` int(11) NOT NULL DEFAULT '0',
                `spPressure` int(11) NOT NULL DEFAULT '1000',
                `spEthanol` int(11) NOT NULL DEFAULT '0',
                `spArgon` int(11) NOT NULL DEFAULT '0'
                ) ENGINE=MEMORY DEFAULT Charset=utf8;"""
        self.write(sql)

    def resetArduino(self):
        sql = """INSERT INTO `runtime_arduino`
                        (`temperature`, `pressure`, `argon`, `ethanol`, `spTemperature`, `spPressure`, `spEthanol`, `spArgon`)
                    VALUES
                        (0, 0, 0, 0, 0, 0, 0, 0);"""
        self.write(sql)

    def createFlowbus(self):
        sql = """
        CREATE TABLE IF NOT EXISTS `runtime_flowbus`
        (
            `instrument`    smallint(2) NOT NULL DEFAULT '0',
            `process`       smallint(2) NOT NULL,
            `flowBus`       smallint(2) NOT NULL,
            `dataType`      tinyint(1) NOT NULL DEFAULT '0',
            `parameter`     binary(%i) NOT NULL DEFAULT '0',
            `data`          binary(%i) NOT NULL DEFAULT '0',
            `time`          decimal(7,2) NOT NULL DEFAULT '0',
            UNIQUE KEY `instrument` (`instrument`,`process`,`flowBus`)
        )
        ENGINE=MEMORY
        DEFAULT CHARSET=utf8;""" % (self.storage_description, self.storage_values)
        self.write(sql)

    def createRecording(self):
        sql = """CREATE TABLE IF NOT EXISTS `runtime_recording` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `recording` tinyint(4) NOT NULL DEFAULT '0',
                `id_recording` int(11) DEFAULT '0',

                PRIMARY KEY (`id`)
                ) ENGINE=MEMORY DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

        sql = """CREATE TABLE IF NOT EXISTS `recording` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                `recording` tinyint(4) NOT NULL DEFAULT '0',
                `filename` text NOT NULL,
                PRIMARY KEY (`id`)
                ) ENGINE=InnoDB DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

    def resetRecording(self):
        sql = """INSERT INTO `runtime_recording`
                        (`recording`)
                    VALUES
                        (0)"""
        self.write(sql)

    def createMessage(self):
        sql = """CREATE TABLE IF NOT EXISTS `cvd`.`runtime_message` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `ready` tinyint(4) NOT NULL DEFAULT '0',
                `id_message` int(11) DEFAULT '0',

                PRIMARY KEY (`id`)
                ) ENGINE=MEMORY DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

        sql = """CREATE TABLE IF NOT EXISTS  `cvd`.`message` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                `processed` tinyint(4) NOT NULL DEFAULT '0',
                `text` text NOT NULL,
                PRIMARY KEY (`id`)
                ) ENGINE=InnoDB DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

    def resetMessage(self):
        sql = """INSERT INTO `cvd`.`runtime_message`
                        (`ready`)
                    VALUES
                        (0)"""
        self.write(sql)
        sql = """DELETE FROM `cvd`.`message`
                    WHERE `processed` = 1"""
        self.write(sql)

    def setData(self, data, setpoint):
        try:
            self.temperature = decimal.Decimal(data[0])
            self.pressure = decimal.Decimal(data[1])
            self.argon = decimal.Decimal(data[2])
            self.ethanol = decimal.Decimal(data[3])
        except:
            self.temperature = 0.00
            self.pressure = 0.00
            self.argon = 0.00
            self.ethanol = 0.00
        try:
            self.spTemperature = int(setpoint[0])
            self.spPressure = int(setpoint[1])
            self.spArgon = int(setpoint[2])
            self.spEthanol = int(setpoint[3])
        except:
            self.spTemperature = 0
            self.spPressure = 1000
            self.spEthanol = 0
            self.spArgon = 0
        sql = """UPDATE `cvd`.`runtime_arduino`
                SET	`temperature`	= %s,
                        `pressure`	= %s,
                        `ethanol`	= %s,
                        `argon`		= %s,
                        `spTemperature` = %s,
                        `spPressure`	= %s,
                        `spEthanol`	= %s,
                        `spArgon`	= %s;"""  % (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon)
        return self.writeArduino(sql)

    def setLogFile(self, fileName):
        id = self.getRecordingID()
        if id < 0:
            return False
        sql = """UPDATE `cvd`.`recording`
                    SET	`filename`  = '%s',
                        `recording` = 1
                    WHERE `id` = %i
                    LIMIT 1;""" % (fileName, id)
        if not self.writeRecording(sql):
            return False
        if self.getLogFile() == fileName:
            return True
        else:
            return False

    def isRecording(self):
        sql = """SELECT `recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 1:
            return False
        else:
            if data[0]:
                return True
            else:
                return False

    def stopRecording(self):
        sql = """UPDATE `cvd`.`runtime_recording`
                SET	`recording` = 0;"""
        if not self.writeRecording(sql):
            return False
        sql = """UPDATE `cvd`.`recording`
                SET	`recording` = 0
                WHERE `recording` = 1;"""
        if not self.writeRecording(sql):
            return False
        self.recordingID = -1
        return True

    def startRecording(self, filename = ''):
        self.stopRecording

        sql = """INSERT INTO `cvd`.`recording` (
                `id` ,`time` , `recording` , `filename` )
                VALUES (
                NULL , CURRENT_TIMESTAMP , 1, '%s')""" % filename
        if not self.writeRecording(sql):
            return False
        sql = """SELECT `id`
        FROM `cvd`.`recording`
                WHERE `recording` = 1
                LIMIT 1;"""
        data = self.read(sql)
        if not len(data) == 1:
            return False
        sql = """UPDATE `cvd`.`runtime_recording`
                SET `id_recording` = %i,
                    `recording` = 1
                LIMIT 1;""" % data
        if not self.writeRecording(sql):
            return False
        return True

    def getRecordingID(self):
        sql = """SELECT `id_recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            return int(data[0])
        else:
            return -1

    def getLogFile(self):
        # get id from memory table
        recordingID = self.getRecordingID()
        # update filename from disc table if not already saved in class
        if not (recordingID == self.recordingID) or len(self.fileName) == 0:
            print "querying filename from sql table"
            self.close()
            sql = """SELECT `filename`
                    FROM `cvd`.`recording`
                    WHERE `id` = %i;""" % recordingID
            data = self.read(sql)
            if len(data) == 1:
                self.fileName = data[0]
            else:
                self.fileName = ''
            self.recordingID = recordingID
        return self.fileName

    def setMessage(self, message):
        sql = """INSERT INTO `cvd`.`message`
                (`text`) VALUES ('%s');"""  % (message)
        if self.writeMessage(sql):
            return self.updateMessage()
        return False

    def updateMessage(self):
        sql = """SELECT `id` FROM `cvd`.`message`
                WHERE `processed` = 0
                LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            id_message = data[0]
            ready = 1
        else:
            ready = 0
            id_message = -1

        sql = """UPDATE `cvd`.`runtime_message`
                SET `ready` = %i,
                    `id_message` = %i
                LIMIT 1;""" % (ready, id_message)
        return self.writeMessage(sql)

    def isReady(self):
        if self.ready:
            return True
        sql = """SELECT `ready`, `id_message`
                FROM `cvd`.`runtime_message`;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 2:
                data = (0,-1)
        (self.ready, self.messageID) = data
        if self.ready:
            return True
        else:
            return False

    def getMessage(self):
        self.message = ""
        # read from runtime (memory table)
        if self.isReady():
            # ready flag did also read out messageID.
            # get message string from cvd.message
            sql = """SELECT `text`
                FROM `cvd`.`message`
                WHERE `id` = %i
                LIMIT 1;""" % self.messageID
            data = self.read(sql)
            if (len(data) == 1):
                self.message = data[0]
                # mark message in cvd.message as processed 
                sql = """UPDATE `cvd`.`message`
                    SET `processed` = 1
                    WHERE `id` = %i;""" % self.messageID
                self.writeMessage(sql)
            self.updateMessage()
        # reset readout
        self.ready = False
        return self.message

    def setFlowbus(self, instrument, process, flowBus, dataTypeString, dataInput, timeInput, parameterName):
        time = decimal.Decimal(timeInput)
        parameterName = parameterName.encode("hex")
        if (dataTypeString == "character"):
            dataType = 0
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "integer"):
            dataType = 1
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "long"):
            dataType = 2
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "string"):
            dataType = 3
            data = dataInput.encode("hex")
        else:
            raise ValueError("can not identify dataType at setFlowBus()")

        sql = """
        INSERT INTO `cvd`.`runtime_flowbus`
        (`instrument`,`process`,`flowBus`,`dataType`,`data`,`time`, `parameter`)
        VALUES
        (%i, %i, %i, %i, UNHEX(LPAD('%s',%i,'0')), %.2f, UNHEX(LPAD('%s',%i,'0')))""" % (instrument, process, flowBus, dataType, data, self.storage_values * 2, time, parameterName, self.storage_description * 2)
        sql += """
        ON DUPLICATE KEY UPDATE
        `data` = UNHEX(LPAD('%s',%i,'0')),
        `time` = %.2f;""" % (data, self.storage_values * 2, time)
        self.writeFlowbus(sql)

    def getFlowbus(self, instrument, process, flowBus):
        sql = """
        SELECT `dataType`,TRIM(LEADING '0' FROM HEX(`data`)),`time`,TRIM(LEADING '0' FROM HEX(`parameter`))
        FROM `cvd`.`runtime_flowbus`
        WHERE
        (   `instrument`    = %i
        AND `process`       = %i
        AND `flowBus`       = %i);
        """ % (instrument, process, flowBus)
        data = self.read(sql)
        if (len(data) == 4):
            (dataType, dataOut, timeOut, parameter) = data
        else:
            return (-1,-1,-1)

        parameter = parameter.decode("hex")
        time = decimal.Decimal(timeOut)
        if (dataType == 0):
            data = int(dataOut, 16)
        elif(dataType == 1):
            data = int(dataOut, 16)
        elif(dataType == 2):
            data = FBconvertLong(process, flowBus, int(dataOut,16))
        elif(dataType == 3):
            data = dataOut.decode("hex")
        else:
            raise ValueError("can not identify dataType at getFlowBus()")

        return (parameter, data, time)

    def getAll(self):
        sql = """SELECT temperature, pressure, ethanol, argon,
                             spTemperature, spPressure, spEthanol, spArgon
                      FROM `cvd`.`runtime_arduino`
                      LIMIT 1"""
        data = self.read(sql)
        if len(data) == 0:
            print "database readout failed for arduino!"
            data = (-1,-1,-1,-1, -1,-1,-1,-1)
        (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon) = data

class UpdateError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class TimeoutError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

        sql = """UPDATE `cvd`.`runtime_arduino`
                SET	`temperature`	= %s,
                        `pressure`	= %s,
                        `ethanol`	= %s,
                        `argon`		= %s
                        `spTemperature` = %s,
                        `spPressure`	= %s,
                        `spEthanol`	= %s,
                        `spArgon`	= %s
                LIMIT 1;"""  % (self.temperature, self.pressure, self.ethanol, self.argon, setpoint[0], setpoint[1], setpoint[2], setpoint[2])
        return self.writeArduino(sql)

    def setLogFile(self, fileName):
        id = self.getRecordingID()
        if id < 0:
            return False
        sql = """UPDATE `cvd`.`recording`
                    SET	`filename`  = '%s',
                        `recording` = 1
                    WHERE `id` = %i
                    LIMIT 1;""" % (fileName, id)
        if not self.writeRecording(sql):
            return False
        if self.getLogFile() == fileName:
            return True
        else:
            return False

    def isRecording(self):
        sql = """SELECT `recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 1:
            return False
        else:
            if data[0]:
                return True
            else:
                return False

    def stopRecording(self):
        sql = """UPDATE `cvd`.`runtime_recording`
                SET	`recording` = 0;"""
        if not self.writeRecording(sql):
            return False
        sql = """UPDATE `cvd`.`recording`
                SET	`recording` = 0
                WHERE `recording` = 1;"""
        if not self.writeRecording(sql):
            return False
        self.recordingID = -1
        return True

    def startRecording(self, filename = ''):
        self.stopRecording

        sql = """INSERT INTO `cvd`.`recording` (
                `id` ,`time` , `recording` , `filename` )
                VALUES (
                NULL , CURRENT_TIMESTAMP , 1, '%s')""" % filename
        if not self.writeRecording(sql):
            return False
        sql = """SELECT `id`
        FROM `cvd`.`recording`
                WHERE `recording` = 1
                LIMIT 1;"""
        data = self.read(sql)
        if not len(data) == 1:
            return False
        sql = """UPDATE `cvd`.`runtime_recording`
                SET `id_recording` = %i,
                    `recording` = 1
                LIMIT 1;""" % data
        if not self.writeRecording(sql):
            return False
        return True

    def getRecordingID(self):
        sql = """SELECT `id_recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            return int(data[0])
        else:
            return -1

    def getLogFile(self):
        # get id from memory table
        recordingID = self.getRecordingID()
        # update filename from disc table if not already saved in class
        if not (recordingID == self.recordingID) or len(self.fileName) == 0:
            print "querying filename from sql table"
            self.close()
            sql = """SELECT `filename`
                    FROM `cvd`.`recording`
                    WHERE `id` = %i;""" % recordingID
            data = self.read(sql)
            if len(data) == 1:
                self.fileName = data[0]
            else:
                self.fileName = ''
            self.recordingID = recordingID
        return self.fileName

    def setMessage(self, message):
        sql = """INSERT INTO `cvd`.`message`
                (`text`) VALUES ('%s');"""  % (message)
        if self.writeMessage(sql):
            return self.updateMessage()
        return False

    def updateMessage(self):
        sql = """SELECT `id` FROM `cvd`.`message`
                WHERE `processed` = 0
                LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            id_message = data[0]
            ready = 1
        else:
            ready = 0
            id_message = -1

        sql = """UPDATE `cvd`.`runtime_message`
                SET `ready` = %i,
                    `id_message` = %i
                LIMIT 1;""" % (ready, id_message)
        return self.writeMessage(sql)

    def isReady(self):
        if self.ready:
            return True
        sql = """SELECT `ready`, `id_message`
                FROM `cvd`.`runtime_message`;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 2:
                data = (0,-1)
        (self.ready, self.messageID) = data
        if self.ready:
            return True
        else:
            return False

    def getMessage(self):
        self.message = ""
        # read from runtime (memory table)
        if self.isReady():
            # ready flag did also read out messageID.
            # get message string from cvd.message
            sql = """SELECT `text`
                FROM `cvd`.`message`
                WHERE `id` = %i
                LIMIT 1;""" % self.messageID
            data = self.read(sql)
            if (len(data) == 1):
                self.message = data[0]
                # mark message in cvd.message as processed 
                sql = """UPDATE `cvd`.`message`
                    SET `processed` = 1
                    WHERE `id` = %i;""" % self.messageID
                self.writeMessage(sql)
            self.updateMessage()
        # reset readout
        self.ready = False
        return self.message

    def setFlowbus(self, instrument, process, flowBus, dataTypeString, dataInput, timeInput, parameterName):
        time = decimal.Decimal(timeInput)
        parameterName = parameterName.encode("hex")
        if (dataTypeString == "character"):
            dataType = 0
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "integer"):
            dataType = 1
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "long"):
            dataType = 2
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "string"):
            dataType = 3
            data = dataInput.encode("hex")
        else:
            raise ValueError("can not identify dataType at setFlowBus()")

        sql = """
        INSERT INTO `cvd`.`runtime_flowbus`
        (`instrument`,`process`,`flowBus`,`dataType`,`data`,`time`, `parameter`)
        VALUES
        (%i, %i, %i, %i, UNHEX(LPAD('%s',%i,'0')), %.2f, UNHEX(LPAD('%s',%i,'0')))""" % (instrument, process, flowBus, dataType, data, self.storage_values * 2, time, parameterName, self.storage_description * 2)
        sql += """
        ON DUPLICATE KEY UPDATE
        `data` = UNHEX(LPAD('%s',%i,'0')),
        `time` = %.2f;""" % (data, self.storage_values * 2, time)
        self.writeFlowbus(sql)

    def getFlowbus(self, instrument, process, flowBus):
        sql = """
        SELECT `dataType`,TRIM(LEADING '0' FROM HEX(`data`)),`time`,TRIM(LEADING '0' FROM HEX(`parameter`))
        FROM `cvd`.`runtime_flowbus`
        WHERE
        (   `instrument`    = %i
        AND `process`       = %i
        AND `flowBus`       = %i);
        """ % (instrument, process, flowBus)
        data = self.read(sql)
        if (len(data) == 4):
            (dataType, dataOut, timeOut, parameter) = data
        else:
            return (-1,-1,-1)

        parameter = parameter.decode("hex")
        time = decimal.Decimal(timeOut)
        if (dataType == 0):
            data = int(dataOut, 16)
        elif(dataType == 1):
            data = int(dataOut, 16)
        elif(dataType == 2):
            data = FBconvertLong(process, flowBus, int(dataOut,16))
        elif(dataType == 3):
            data = dataOut.decode("hex")
        else:
            raise ValueError("can not identify dataType at getFlowBus()")

        return (parameter, data, time)

    def getAll(self):
        sql = """SELECT temperature, pressure, ethanol, argon,
                             spTemperature, spPressure, spEthanol, spArgon
                      FROM `cvd`.`runtime_arduino`
                      LIMIT 1"""
        data = self.read(sql)
        if len(data) == 0:
            print "database readout failed for arduino!"
            data = (-1,-1,-1,-1, -1,-1,-1,-1)
        (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon) = data

class UpdateError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class TimeoutError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python
from __future__ import print_function
import threading
import time
import sys
from datetime import datetime

from MKDatabase import MKDatabase
from MKParser import MKParser
from MKLogFile import MKLogFileHandler
from MKSerial import MKSerial

class MKArduino():
    def __init__(self, port = '/dev/ttyACM0'):
        self.Serial = MKSerial('arduino', port, 9600)
        self.Parser = MKParser()
        self.Database = MKDatabase()
        self.newFile = True
        self.setFile = False
        self.alive = False
        self.sleeping = False
        self.verbose = 0

    def debug(self, verbose = 1):
        self.verbose = verbose

    def start(self):
        try:
            self.Serial.start()
            self.thread = threading.Thread(target=self.loop)
            self.thread.daemon = True
            self.thread.start()
        except:
            self.stop()
            raise
        else:
            self.alive = True

    def stop(self):
        self.alive = False
        self.Serial.stop()

    def join(self):
        self.thread.join()

    def isAlive(self):
        return self.alive

    def getPerformance(self):
        if self.verbose < 0:
            microsecond = datetime.now().microsecond
            print(self.perfCounter, "\t", int((microsecond - self.performance)/100)/10.0)
            self.performance = microsecond
            self.perfCounter += 1

    def resetPerformance(self):
        if self.verbose < 0:
            print('---')
            self.perfCounter = 0
            self.performance = datetime.now().microsecond
            self.getPerformance()

    def loop(self):
        if self.verbose > 1:
            print('entering loop ...')
        while self.isAlive():
            self.resetPerformance()
            serialReady = self.Serial.isReady()
            self.getPerformance()
            databaseReady = self.Database.isReady()
            self.getPerformance()
            if not serialReady and not databaseReady:
                if self.verbose > 0:
                    if not self.sleeping:
                        if self.verbose > 1:
                            print('sleeping .', end = '')
                        else:
                            print('.', end = '')
                        self.sleeping = True
                    else:
                        print('.', end = '')
                    sys.stdout.flush()
                self.getPerformance()
                time.sleep(0.1)
                self.getPerformance()
            else:
                if self.verbose > 0:
                    if self.sleeping:
                        print('.', end = '\n')
                        sys.stdout.flush()
                        self.sleeping = False
                    if serialReady:
                        print('serial ready with %i messages' % len(self.Serial.receiveBuffer))
                    if databaseReady:
                        print('database ready')
                self.getPerformance()
            if databaseReady:
                self.Serial.send(self.Database.getMessage())
            if serialReady:
                self.getPerformance()
                message = self.Serial.getMessage()
                self.getPerformance()
                self.Parser.input(message)
                self.getPerformance()
                oneline = self.Parser.oneline()
                self.getPerformance()
                if self.verbose > 2:
                    print(oneline)
                if self.Parser.getStatus():
                    self.getPerformance()
                    data = (self.Parser.get(2), self.Parser.get(5), self.Parser.get(8), self.Parser.get(11))
                    setpoint = (self.Parser.get(3), self.Parser.get(6), self.Parser.get(9), self.Parser.get(12))
                    setData = self.Database.setData(data, setpoint)
                    self.getPerformance()
                    if not setData:
                        if self.verbose > 2:
                            print("database write failed. add message to buffer again.")
                        self.Serial.receive(message)
                self.getPerformance()
                if self.Database.isRecording():
                    if self.newFile:
                        print('starting new LogFile.')
                        self.Logfile = MKLogFileHandler('mkmain','log',True)
                        self.Logfile.open()
                        self.newFile = False
                        self.setFile = True
                    if self.setFile:
                        print('set FileName in Database')
                        if self.Database.setLogFile(self.Logfile.getLogFile()):
                            self.setFile = False
                    self.Logfile.write(oneline)
                else:
                    self.newFile = True
                self.getPerformance()

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
#!/usr/bin/env python

import MySQLdb as mysqlconnector
from MySQLdb.constants import CLIENT
import os
import socket
import decimal
import struct
from time import sleep
import multiprocessing
import ConfigParser

from MKFlowMessage import FBconvertLong # converter for long numbers to float and percent
#cvd-client->rbBmSDP7fSKp87b5

class MKDatabase(object):
    sql = ""
    connected = False
    ready = False
    messageID = -1
    client = False
    recording = False
    recordingID = -1
    fileName = ""
    storage_description = 50
    storage_values = 30

    hostname = ""

    # settings.cfg (see loadConfig)
    dbUser = ""
    dbPass = ""
    dbHost = ""
    dbName = ""
    servername = ""

    def __init__(self, isClient = False):
        self.client = isClient
        self.loadConfig()
        self.test()
        decimal.getcontext().prec = 2

    def loadConfig(self):
        config = ConfigParser.ConfigParser()
        srcPath = os.path.dirname(os.path.realpath(__file__))
        settingsFile = srcPath + '/../settings.cfg'
        if not (os.path.exists(settingsFile)):
            print "settings.cfg not found"
            raise
        config.read(settingsFile)
        self.dbUser = config.get('Database', 'dbuser')
        self.dbPass = config.get('Database', 'dbpass')
        self.dbHost = config.get('Database', 'dbhost')
        self.dbName = config.get('Database', 'dbname')
        self.servername = config.get('Server', 'servername')

    def open(self):
        try:
            if not self.checkIP():
                print "server unavailable"
                raise
            self.db = mysqlconnector.connect(
                host = self.dbHost,
                user = self.dbUser,
                passwd = self.dbPass,
                db = self.dbName,
                client_flag = CLIENT.FOUND_ROWS,
                connect_timeout = 1
                )
        except:
            print "database open failed."
            self.close()
            return False
        else:
            print "connected as user: %s" % self.dbUser
            self.connected = True
            return True
    def close(self):
        try:
            self.db.close()
        except:
            if not self.checkIP():
                print "connection lost. Database could not be closed normal"
                self.connected = False
        else:
            self.connected = False

    def isOpen(self):
        #if not self.connected:
        #    return False
        #try:
        #    stats = self.db.stat()
        #    if stats == 'MySQL server has gone away':
        #        self.close()
        #except:
        #    self.connected = False
        return self.connected

    def write_without_timeout(self, db, sql, connection):
        try:
            cursor = db.cursor()
            cursor.execute(sql)
            affectedRows = cursor.rowcount
            cursor.close()
            db.commit()
        except:
            affectedRows = 0
            try:
                self.db.rollback()
            except:
                pass
        connection.send(affectedRows)
        connection.close()

    def read_without_timeout(self, db, sql, connection):
        affectedRows = 0
        try:
            cursor = db.cursor()
            cursor.execute(sql)
            data = cursor.fetchone()
            cursor.close()
        except:
            connection.send([])
        else:
            connection.send(data)
        connection.close()

    # from alex martelli on http://stackoverflow.com/questions/1507091/python-mysqldb-query-timeout
    def write(self, sql, update = False):
        if not self.isOpen():
            if not self.open():
                raise
        conn_parent, conn_child = multiprocessing.Pipe(False)
        subproc = multiprocessing.Process(target = self.write_without_timeout,
                                          args = (self.db, sql, conn_child))
        subproc.start()
        subproc.join(1)
        if conn_parent.poll():
            affectedRows = conn_parent.recv()
            # on update statements rise if no lines were affected
            if update and affectedRows == 0:
                raise UpdateError('UPDATE statement failed')
            else:
                return affectedRows
        subproc.terminate()
        raise TimeoutError("Query %r ran for >%r" % (sql, timeout))

    def read(self, sql):
        if not self.isOpen():
            if not self.open():
                raise
        conn_parent, conn_child = multiprocessing.Pipe(False)
        subproc = multiprocessing.Process(target = self.read_without_timeout,
                                          args = (self.db, sql, conn_child))
        subproc.start()
        subproc.join(1)
        if conn_parent.poll():
            data = conn_parent.recv()
            try:
                if len(data) == 0:
                    raise
            except:
                return []
            else:
                return data
        else:
            subproc.terminate()
            return []

    def writeArduino(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                print "writeArduino failed: create database and try again."
                self.createArduino()
                self.resetArduino()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeRecording(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                self.createRecording()
                self.resetRecording()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeFlowbus(self, sql):
        try:
            self.write(sql)
        except:
            try:
                self.createFlowbus()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeMessage(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                self.createMessage()
                self.resetMessage()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def test(self):
        print "-- starting self-test --"
        self.open()
        sql="SELECT VERSION()"
        data = self.read(sql)
        self.close()
        print "MySQL version : %s " % data
        print "-- self test complete --"

    def getHostname(self):
        if self.hostname == "":
            self.hostname = socket.gethostname()
        return self.hostname

    def isServer(self):
        if (self.getHostname() == self.servername):
            return True
        else:
            return False

    def getIP(self):
        if self.isServer():
            ip = 'localhost'
        else:
            ip = self.dbHost
        return ip

    def checkIP(self, ip = ""):
        if len(ip) == 0:
            ip = self.getIP()
        if ip == "localhost":
            return True
        command = "ping -c 1 -W 1 " + ip
        print "executing '" + command + "'"
        if os.system(command + " > /dev/null") == 0:
            return True
        else:
            print "ip not found. sleeping penalty."
            sleep(1)
            return False

    def createArduino(self):
        sql = """CREATE TABLE IF NOT EXISTS `runtime_arduino` (
                `temperature` decimal(6,2) NOT NULL DEFAULT '0',
                `pressure` decimal(6,2) NOT NULL DEFAULT '0',
                `argon` decimal(6,2) NOT NULL DEFAULT '0',
                `ethanol` decimal(6,2) NOT NULL DEFAULT '0',
                `spTemperature` int(11) NOT NULL DEFAULT '0',
                `spPressure` int(11) NOT NULL DEFAULT '1000',
                `spEthanol` int(11) NOT NULL DEFAULT '0',
                `spArgon` int(11) NOT NULL DEFAULT '0'
                ) ENGINE=MEMORY DEFAULT Charset=utf8;"""
        self.write(sql)

    def resetArduino(self):
        sql = """INSERT INTO `runtime_arduino`
                        (`temperature`, `pressure`, `argon`, `ethanol`, `spTemperature`, `spPressure`, `spEthanol`, `spArgon`)
                    VALUES
                        (0, 0, 0, 0, 0, 0, 0, 0);"""
        self.write(sql)

    def createFlowbus(self):
        sql = """
        CREATE TABLE IF NOT EXISTS `runtime_flowbus`
        (
            `instrument`    smallint(2) NOT NULL DEFAULT '0',
            `process`       smallint(2) NOT NULL,
            `flowBus`       smallint(2) NOT NULL,
            `dataType`      tinyint(1) NOT NULL DEFAULT '0',
            `parameter`     binary(%i) NOT NULL DEFAULT '0',
            `data`          binary(%i) NOT NULL DEFAULT '0',
            `time`          decimal(7,2) NOT NULL DEFAULT '0',
            UNIQUE KEY `instrument` (`instrument`,`process`,`flowBus`)
        )
        ENGINE=MEMORY
        DEFAULT CHARSET=utf8;""" % (self.storage_description, self.storage_values)
        self.write(sql)

    def createRecording(self):
        sql = """CREATE TABLE IF NOT EXISTS `runtime_recording` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `recording` tinyint(4) NOT NULL DEFAULT '0',
                `id_recording` int(11) DEFAULT '0',

                PRIMARY KEY (`id`)
                ) ENGINE=MEMORY DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

        sql = """CREATE TABLE IF NOT EXISTS `recording` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                `recording` tinyint(4) NOT NULL DEFAULT '0',
                `filename` text NOT NULL,
                PRIMARY KEY (`id`)
                ) ENGINE=InnoDB DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

    def resetRecording(self):
        sql = """INSERT INTO `runtime_recording`
                        (`recording`)
                    VALUES
                        (0)"""
        self.write(sql)

    def createMessage(self):
        sql = """CREATE TABLE IF NOT EXISTS `cvd`.`runtime_message` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `ready` tinyint(4) NOT NULL DEFAULT '0',
                `id_message` int(11) DEFAULT '0',

                PRIMARY KEY (`id`)
                ) ENGINE=MEMORY DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

        sql = """CREATE TABLE IF NOT EXISTS  `cvd`.`message` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                `processed` tinyint(4) NOT NULL DEFAULT '0',
                `text` text NOT NULL,
                PRIMARY KEY (`id`)
                ) ENGINE=InnoDB DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

    def resetMessage(self):
        sql = """INSERT INTO `cvd`.`runtime_message`
                        (`ready`)
                    VALUES
                        (0)"""
        self.write(sql)
        sql = """DELETE FROM `cvd`.`message`
                    WHERE `processed` = 1"""
        self.write(sql)

    def setData(self, data, setpoint):
        try:
            self.temperature = decimal.Decimal(data[0])
            self.pressure = decimal.Decimal(data[1])
            self.argon = decimal.Decimal(data[2])
            self.ethanol = decimal.Decimal(data[3])
        except:
            self.temperature = 0.00
            self.pressure = 0.00
            self.argon = 0.00
            self.ethanol = 0.00
        try:
            self.spTemperature = int(setpoint[0])
            self.spPressure = int(setpoint[1])
            self.spArgon = int(setpoint[2])
            self.spEthanol = int(setpoint[3])
        except:
            self.spTemperature = 0
            self.spPressure = 1000
            self.spEthanol = 0
            self.spArgon = 0
        sql = """UPDATE `cvd`.`runtime_arduino`
                SET	`temperature`	= %s,
                        `pressure`	= %s,
                        `ethanol`	= %s,
                        `argon`		= %s,
                        `spTemperature` = %s,
                        `spPressure`	= %s,
                        `spEthanol`	= %s,
                        `spArgon`	= %s;"""  % (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon)
        return self.writeArduino(sql)

    def setLogFile(self, fileName):
        id = self.getRecordingID()
        if id < 0:
            return False
        sql = """UPDATE `cvd`.`recording`
                    SET	`filename`  = '%s',
                        `recording` = 1
                    WHERE `id` = %i
                    LIMIT 1;""" % (fileName, id)
        if not self.writeRecording(sql):
            return False
        if self.getLogFile() == fileName:
            return True
        else:
            return False

    def isRecording(self):
        sql = """SELECT `recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 1:
            return False
        else:
            if data[0]:
                return True
            else:
                return False

    def stopRecording(self):
        sql = """UPDATE `cvd`.`runtime_recording`
                SET	`recording` = 0;"""
        if not self.writeRecording(sql):
            return False
        sql = """UPDATE `cvd`.`recording`
                SET	`recording` = 0
                WHERE `recording` = 1;"""
        if not self.writeRecording(sql):
            return False
        self.recordingID = -1
        return True

    def startRecording(self, filename = ''):
        self.stopRecording

        sql = """INSERT INTO `cvd`.`recording` (
                `id` ,`time` , `recording` , `filename` )
                VALUES (
                NULL , CURRENT_TIMESTAMP , 1, '%s')""" % filename
        if not self.writeRecording(sql):
            return False
        sql = """SELECT `id`
        FROM `cvd`.`recording`
                WHERE `recording` = 1
                LIMIT 1;"""
        data = self.read(sql)
        if not len(data) == 1:
            return False
        sql = """UPDATE `cvd`.`runtime_recording`
                SET `id_recording` = %i,
                    `recording` = 1
                LIMIT 1;""" % data
        if not self.writeRecording(sql):
            return False
        return True

    def getRecordingID(self):
        sql = """SELECT `id_recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            return int(data[0])
        else:
            return -1

    def getLogFile(self):
        # get id from memory table
        recordingID = self.getRecordingID()
        # update filename from disc table if not already saved in class
        if not (recordingID == self.recordingID) or len(self.fileName) == 0:
            print "querying filename from sql table"
            self.close()
            sql = """SELECT `filename`
                    FROM `cvd`.`recording`
                    WHERE `id` = %i;""" % recordingID
            data = self.read(sql)
            if len(data) == 1:
                self.fileName = data[0]
            else:
                self.fileName = ''
            self.recordingID = recordingID
        return self.fileName

    def setMessage(self, message):
        sql = """INSERT INTO `cvd`.`message`
                (`text`) VALUES ('%s');"""  % (message)
        if self.writeMessage(sql):
            return self.updateMessage()
        return False

    def updateMessage(self):
        sql = """SELECT `id` FROM `cvd`.`message`
                WHERE `processed` = 0
                LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            id_message = data[0]
            ready = 1
        else:
            ready = 0
            id_message = -1

        sql = """UPDATE `cvd`.`runtime_message`
                SET `ready` = %i,
                    `id_message` = %i
                LIMIT 1;""" % (ready, id_message)
        return self.writeMessage(sql)

    def isReady(self):
        if self.ready:
            return True
        sql = """SELECT `ready`, `id_message`
                FROM `cvd`.`runtime_message`;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 2:
                data = (0,-1)
        (self.ready, self.messageID) = data
        if self.ready:
            return True
        else:
            return False

    def getMessage(self):
        self.message = ""
        # read from runtime (memory table)
        if self.isReady():
            # ready flag did also read out messageID.
            # get message string from cvd.message
            sql = """SELECT `text`
                FROM `cvd`.`message`
                WHERE `id` = %i
                LIMIT 1;""" % self.messageID
            data = self.read(sql)
            if (len(data) == 1):
                self.message = data[0]
                # mark message in cvd.message as processed 
                sql = """UPDATE `cvd`.`message`
                    SET `processed` = 1
                    WHERE `id` = %i;""" % self.messageID
                self.writeMessage(sql)
            self.updateMessage()
        # reset readout
        self.ready = False
        return self.message

    def setFlowbus(self, instrument, process, flowBus, dataTypeString, dataInput, timeInput, parameterName):
        time = decimal.Decimal(timeInput)
        parameterName = parameterName.encode("hex")
        if (dataTypeString == "character"):
            dataType = 0
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "integer"):
            dataType = 1
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "long"):
            dataType = 2
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "string"):
            dataType = 3
            data = dataInput.encode("hex")
        else:
            raise ValueError("can not identify dataType at setFlowBus()")

        sql = """
        INSERT INTO `cvd`.`runtime_flowbus`
        (`instrument`,`process`,`flowBus`,`dataType`,`data`,`time`, `parameter`)
        VALUES
        (%i, %i, %i, %i, UNHEX(LPAD('%s',%i,'0')), %.2f, UNHEX(LPAD('%s',%i,'0')))""" % (instrument, process, flowBus, dataType, data, self.storage_values * 2, time, parameterName, self.storage_description * 2)
        sql += """
        ON DUPLICATE KEY UPDATE
        `data` = UNHEX(LPAD('%s',%i,'0')),
        `time` = %.2f;""" % (data, self.storage_values * 2, time)
        self.writeFlowbus(sql)

    def getFlowbus(self, instrument, process, flowBus):
        sql = """
        SELECT `dataType`,TRIM(LEADING '0' FROM HEX(`data`)),`time`,TRIM(LEADING '0' FROM HEX(`parameter`))
        FROM `cvd`.`runtime_flowbus`
        WHERE
        (   `instrument`    = %i
        AND `process`       = %i
        AND `flowBus`       = %i);
        """ % (instrument, process, flowBus)
        data = self.read(sql)
        if (len(data) == 4):
            (dataType, dataOut, timeOut, parameter) = data
        else:
            return (-1,-1,-1)

        parameter = parameter.decode("hex")
        time = decimal.Decimal(timeOut)
        if (dataType == 0):
            data = int(dataOut, 16)
        elif(dataType == 1):
            data = int(dataOut, 16)
        elif(dataType == 2):
            data = FBconvertLong(process, flowBus, int(dataOut,16))
        elif(dataType == 3):
            data = dataOut.decode("hex")
        else:
            raise ValueError("can not identify dataType at getFlowBus()")

        return (parameter, data, time)

    def getAll(self):
        sql = """SELECT temperature, pressure, ethanol, argon,
                             spTemperature, spPressure, spEthanol, spArgon
                      FROM `cvd`.`runtime_arduino`
                      LIMIT 1"""
        data = self.read(sql)
        if len(data) == 0:
            print "database readout failed for arduino!"
            data = (-1,-1,-1,-1, -1,-1,-1,-1)
        (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon) = data

class UpdateError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class TimeoutError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

        sql = """UPDATE `cvd`.`runtime_arduino`
                SET	`temperature`	= %s,
                        `pressure`	= %s,
                        `ethanol`	= %s,
                        `argon`		= %s
                        `spTemperature` = %s,
                        `spPressure`	= %s,
                        `spEthanol`	= %s,
                        `spArgon`	= %s
                LIMIT 1;"""  % (self.temperature, self.pressure, self.ethanol, self.argon, setpoint[0], setpoint[1], setpoint[2], setpoint[2])
        return self.writeArduino(sql)

    def setLogFile(self, fileName):
        id = self.getRecordingID()
        if id < 0:
            return False
        sql = """UPDATE `cvd`.`recording`
                    SET	`filename`  = '%s',
                        `recording` = 1
                    WHERE `id` = %i
                    LIMIT 1;""" % (fileName, id)
        if not self.writeRecording(sql):
            return False
        if self.getLogFile() == fileName:
            return True
        else:
            return False

    def isRecording(self):
        sql = """SELECT `recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 1:
            return False
        else:
            if data[0]:
                return True
            else:
                return False

    def stopRecording(self):
        sql = """UPDATE `cvd`.`runtime_recording`
                SET	`recording` = 0;"""
        if not self.writeRecording(sql):
            return False
        sql = """UPDATE `cvd`.`recording`
                SET	`recording` = 0
                WHERE `recording` = 1;"""
        if not self.writeRecording(sql):
            return False
        self.recordingID = -1
        return True

    def startRecording(self, filename = ''):
        self.stopRecording

        sql = """INSERT INTO `cvd`.`recording` (
                `id` ,`time` , `recording` , `filename` )
                VALUES (
                NULL , CURRENT_TIMESTAMP , 1, '%s')""" % filename
        if not self.writeRecording(sql):
            return False
        sql = """SELECT `id`
        FROM `cvd`.`recording`
                WHERE `recording` = 1
                LIMIT 1;"""
        data = self.read(sql)
        if not len(data) == 1:
            return False
        sql = """UPDATE `cvd`.`runtime_recording`
                SET `id_recording` = %i,
                    `recording` = 1
                LIMIT 1;""" % data
        if not self.writeRecording(sql):
            return False
        return True

    def getRecordingID(self):
        sql = """SELECT `id_recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            return int(data[0])
        else:
            return -1

    def getLogFile(self):
        # get id from memory table
        recordingID = self.getRecordingID()
        # update filename from disc table if not already saved in class
        if not (recordingID == self.recordingID) or len(self.fileName) == 0:
            print "querying filename from sql table"
            self.close()
            sql = """SELECT `filename`
                    FROM `cvd`.`recording`
                    WHERE `id` = %i;""" % recordingID
            data = self.read(sql)
            if len(data) == 1:
                self.fileName = data[0]
            else:
                self.fileName = ''
            self.recordingID = recordingID
        return self.fileName

    def setMessage(self, message):
        sql = """INSERT INTO `cvd`.`message`
                (`text`) VALUES ('%s');"""  % (message)
        if self.writeMessage(sql):
            return self.updateMessage()
        return False

    def updateMessage(self):
        sql = """SELECT `id` FROM `cvd`.`message`
                WHERE `processed` = 0
                LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            id_message = data[0]
            ready = 1
        else:
            ready = 0
            id_message = -1

        sql = """UPDATE `cvd`.`runtime_message`
                SET `ready` = %i,
                    `id_message` = %i
                LIMIT 1;""" % (ready, id_message)
        return self.writeMessage(sql)

    def isReady(self):
        if self.ready:
            return True
        sql = """SELECT `ready`, `id_message`
                FROM `cvd`.`runtime_message`;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 2:
                data = (0,-1)
        (self.ready, self.messageID) = data
        if self.ready:
            return True
        else:
            return False

    def getMessage(self):
        self.message = ""
        # read from runtime (memory table)
        if self.isReady():
            # ready flag did also read out messageID.
            # get message string from cvd.message
            sql = """SELECT `text`
                FROM `cvd`.`message`
                WHERE `id` = %i
                LIMIT 1;""" % self.messageID
            data = self.read(sql)
            if (len(data) == 1):
                self.message = data[0]
                # mark message in cvd.message as processed 
                sql = """UPDATE `cvd`.`message`
                    SET `processed` = 1
                    WHERE `id` = %i;""" % self.messageID
                self.writeMessage(sql)
            self.updateMessage()
        # reset readout
        self.ready = False
        return self.message

    def setFlowbus(self, instrument, process, flowBus, dataTypeString, dataInput, timeInput, parameterName):
        time = decimal.Decimal(timeInput)
        parameterName = parameterName.encode("hex")
        if (dataTypeString == "character"):
            dataType = 0
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "integer"):
            dataType = 1
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "long"):
            dataType = 2
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "string"):
            dataType = 3
            data = dataInput.encode("hex")
        else:
            raise ValueError("can not identify dataType at setFlowBus()")

        sql = """
        INSERT INTO `cvd`.`runtime_flowbus`
        (`instrument`,`process`,`flowBus`,`dataType`,`data`,`time`, `parameter`)
        VALUES
        (%i, %i, %i, %i, UNHEX(LPAD('%s',%i,'0')), %.2f, UNHEX(LPAD('%s',%i,'0')))""" % (instrument, process, flowBus, dataType, data, self.storage_values * 2, time, parameterName, self.storage_description * 2)
        sql += """
        ON DUPLICATE KEY UPDATE
        `data` = UNHEX(LPAD('%s',%i,'0')),
        `time` = %.2f;""" % (data, self.storage_values * 2, time)
        self.writeFlowbus(sql)

    def getFlowbus(self, instrument, process, flowBus):
        sql = """
        SELECT `dataType`,TRIM(LEADING '0' FROM HEX(`data`)),`time`,TRIM(LEADING '0' FROM HEX(`parameter`))
        FROM `cvd`.`runtime_flowbus`
        WHERE
        (   `instrument`    = %i
        AND `process`       = %i
        AND `flowBus`       = %i);
        """ % (instrument, process, flowBus)
        data = self.read(sql)
        if (len(data) == 4):
            (dataType, dataOut, timeOut, parameter) = data
        else:
            return (-1,-1,-1)

        parameter = parameter.decode("hex")
        time = decimal.Decimal(timeOut)
        if (dataType == 0):
            data = int(dataOut, 16)
        elif(dataType == 1):
            data = int(dataOut, 16)
        elif(dataType == 2):
            data = FBconvertLong(process, flowBus, int(dataOut,16))
        elif(dataType == 3):
            data = dataOut.decode("hex")
        else:
            raise ValueError("can not identify dataType at getFlowBus()")

        return (parameter, data, time)

    def getAll(self):
        sql = """SELECT temperature, pressure, ethanol, argon,
                             spTemperature, spPressure, spEthanol, spArgon
                      FROM `cvd`.`runtime_arduino`
                      LIMIT 1"""
        data = self.read(sql)
        if len(data) == 0:
            print "database readout failed for arduino!"
            data = (-1,-1,-1,-1, -1,-1,-1,-1)
        (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon) = data

class UpdateError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class TimeoutError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#!/usr/bin/env python

import MKDatabase
from MKFlowMessage import FBconvertLong
# Main Class
class MKFlowCommunication():
    def __init__(self):
        self.nullNode = MKFlowNode(-1)
        self.node_numbers = []
        self.nodes = []

    def addNode(self, number):
        if not self.isNode(number):
            self.node_numbers += [number]
            node = MKFlowNode(number)
            self.nodes += [node]

    def isNode(self, number):
        return (number in self.node_numbers)

    def getNode(self, number):
        if self.isNode(number):
            id = self.node_numbers.index(number)
            node = self.nodes[id]
            return node
        else:
            return self.nullNode

    def Node(self, number):
        if not self.isNode(number):
            self.addNode(number)
        return self.getNode(number)

class MKFlowNode():
    def __init__(self, node):
        self.node = node
        self.nullSequence = MKFlowSequence(-1)
        self.sequence_numbers = []
        self.sequences = []

    def getNumber(self):
        return self.node

    def addSequence(self, number):
        if not self.isSequence(number):
            self.sequence_numbers += [number]
            sequence = MKFlowSequence(number)
            self.sequences += [sequence]

    def isSequence(self, number):
        return (number in self.sequence_numbers)

    def getSequence(self, number):
        if self.isSequence(number):
            id = self.sequence_numbers.index(number)
            sequence = self.sequences[id]
            return sequence
        else:
            return self.nullSequence

    def Sequence(self, number):
        if not self.isSequence(number):
            self.addSequence(number)
        return self.getSequence(number)

class MKFlowSequence():
    def __init__(self, sequence):
        self.sequence = sequence
        self.nullChild = MKFlowModbus(-1)
        self.reset()

    def reset(self):
        self.parameter_ids = []
        self.parameters = []

        self.hasAnswer = False
        self.hasRequest = False
        self.RequestHasValue = False
        self.isAnalysed = False
        self.isStatus = False
        self.isError = False
        self.isValid = False

    def setReadRequest(self, Message):
        self.Request = Message.getSubType()
        self.hasRequest = True
        self.hasAnswer = False
        self.RequestHasValue = False
        self.timeRequest = Message.getSeconds()

    def setWriteRequest(self, Message):
        self.setReadRequest(Message)
        self.RequestHasValue = True

    def setStatus(self, Message):
        self.setAnswer(Message)
        self.isStatus = True

    def setError(self, Message):
        self.setAnswer(Message)
        self.isError = True

    def setAnswer(self, Message):
        self.Answer = Message.getSubType()
        self.timeAnswer = Message.getSeconds()
        self.hasAnswer = True
        self.isStatus = False
        self.isError = False

    def check(self):
        if self.hasAnswer and self.hasRequest:
            if abs(self.timeAnswer - self.timeRequest) > 10:
                return False
            else:
                return True
        else:
            return False

    def addParameter(self, index):
        if not self.isParameter(index):
            self.parameter_ids += [index]
            Parameter = MKFlowModbus(index)
            self.parameters += [Parameter]

    def isParameter(self, index):
        return index in self.parameter_ids

    def getParameter(self, index):
        if self.isParameter(index):
            id = self.parameter_ids.index(index)
            Parameter = self.parameters[id]
            return Parameter
        else:
            return self.nullChild

    def Parameter(self, index):
        if not self.isParameter(index):
            self.addParameter(index)
        return self.getParameter(index)

    def analyse(self):
        if self.check():
            # Process Request
            for process in self.Request.process:
                for parameter in process.Parameter:
                    self.Parameter(parameter.getIndex()).setNumber(parameter.getNumber())
                    self.Parameter(parameter.getIndex()).setProcess(parameter.getProcess())
                    self.Parameter(parameter.getIndex()).setName(parameter.getHuman())
                    self.Parameter(parameter.getIndex()).setLength(parameter.getLength())
                    if self.RequestHasValue:
                        self.Parameter(parameter.getIndex()).setValue(parameter.getValue())
                        self.Parameter(parameter.getIndex()).setDataType(parameter.getDataType())

            # Process Answer
            if not self.RequestHasValue and not self.isStatus and not self.isError:
                for process in self.Answer.process:
                    for parameter in process.Parameter:
                        self.Parameter(parameter.getIndex()).setValue(parameter.getValue())
                        self.Parameter(parameter.getIndex()).setDataType(parameter.getDataType())

            # Answer with Status or Error and set valid
            self.valid = True
            self.analyseStatus()
            self.analyseError()
            self.isAnalysed = True

    def analyseStatus(self):
        if self.isStatus:
            if self.Answer.getStatus() == 0:
                # no error
                self.valid = True
            elif self.Answer.getStatus() > 3 and self.Answer.getStatus() < 8:
                # Parameter Error
                where = self.Answer.getIndex()
                count = 4
                for index in self.parameter_ids:
                    Parameter = self.getParameter(index)
                    if not self.RequestHasValue:
                        Parameter.setInvalid()
                    if where == count:
                        self.error = "Status: %s\t Parameter: %s" % (self.Answer.getHuman(), Parameter.getName())
                        Parameter.setError(self.Answer.getHuman())
                    count += int(Parameter.getLength())
            else:
                self.error = self.Answer.getHuman()
                self.valid = False

    def analyseError(self):
        if self.isError:
            self.error = self.Answer.getText()
            self.valid = False
        if not self.valid:
            for index in self.parameter_ids:
                Parameter = self.getParameter(index)
                Parameter.setError(self.error)

    def output(self):
        if self.check():
            if not self.isAnalysed:
                self.analyse()
            for index in self.parameter_ids:
                Parameter = self.getParameter(index)
                try:
                    Parameter.stdout()
                except:
                    self.stdout()
                    raise ValueError("error in MKFlowCommunication ModbusClass stdout")

    def save(self, Database, instrument = 0):
        if self.check():
            reset = True
            if not self.isAnalysed:
                self.analyse()
            for index in self.parameter_ids:
                Parameter = self.getParameter(index)
                try:
                    if not Parameter.isInvalid():
                        valid = True
                        proc = Parameter.getProcess()
                        fbnr = Parameter.getNumber()
                        name = Parameter.getName()
                        value = Parameter.getValue()
                        dataType = Parameter.getDataType()
                        time = self.timeAnswer
                        parameter = Parameter.getName()
                        reset = Database.setFlowbus(instrument, proc, fbnr, dataType, value, time, parameter)
                except:
                    self.stdout()
                    print "error storing parameter."
                    reset = False
            if reset:
                self.reset()
            else:
                print "Sequence not cleared."

    def stdout(self):
        print "--- sequence: %i ---" % self.sequence
        print "---- parameters: %s ----" % self.parameter_ids
        if self.hasRequest:
            print "---- request ----"
            self.Request.stdout()
        if self.hasAnswer:
            print "---- answer ----"
            self.Answer.stdout()


class MKFlowModbus():
    def __init__(self, index):
        self.index = index
        self.invalid = False
        self.error = ''
        self.value = None
        self.human = ''
        self.dataType = 'invalid' # readybility. store as string
        self.length = 0

    def setProcess(self, process):
        self.process = process

    def getProcess(self):
        return self.process

    def setNumber(self, number):
        self.number = number

    def getNumber(self):
        return self.number

    def setValue(self, value):
        self.value = value

    def getValue(self):
        return self.value

    def setDataType(self, dataType):
        self.dataType = dataType

    def getDataType(self):
        return self.dataType

    def setName(self, string):
        self.human = string

    def getName(self):
        return self.human

    def setInvalid(self):
        self.invalid = True

    def setLength(self, length):
        self.length = length

    def getLength(self):
        return self.length

    def setError(self, error):
        self.error = error
        self.setInvalid()

    def isInvalid(self):
        if self.invalid:
            return True
        else:
            return False

    def stdout(self):
        returnarray = [self.isInvalid(), self.getProcess(), self.getNumber(), self.getName()]
        if not self.invalid:
            returnarray += [FBconvertLong(self.getProcess(), self.getNumber(), self.getValue())]
        else:
            returnarray += [self.error]
        print '\t'.join(str(i) for i in returnarray)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python
#import struct                  # imports struct API ???
import os                       # imports file system functions like open()
import time                     # sleep
import subprocess               # socat
import MKFlowMessage            # Message analyis class
from MKFlowSocat import MKFlowSocat
from MKFlowLogFile import MKFlowLogFile
# Main Class
class MKFlowInput():
    def __init__(self):
        self.reset()

    def reset(self):
        self.message1 = ''
        self.message2 = ''
        self.socat = False
        self.log = False
        self.port1 = ''
        self.port2 = ''

    def setLogFile(self,filename):
        self.input = MKFlowLogFile(filename)
        self.start()

    def setBridge(self, port1, port2):
        self.input = MKFlowSocat(port1, port2)
        self.start()

    def start(self):
        self.input.open()
        self.input.start()

    def stop(self):
        self.input.stop()

    def readOut(self):
        while not self.input.isReady():
            time.sleep(0.1)
        self.message1, self.message2 = self.input.read()

    def getMessage(self):
        try:
            Message = self.Message()
            self.readOut()
            Message.process(self.message2, self.message1)
            if Message.isInvalid:
                # try next message. maybe they belong together
                message_buffer = self.message2
                self.readOut()
                Message.process(self.message2, self.message1)
                if Message.isInvalid:
                    message_buffer += self.message2
                    Message.process(message_buffer, self.message1)
        except:
            self.input.stop()
            raise
        else:
            return Message

    def isAlive(self):
        return self.input.isAlive()

    # shortcut
    class Message(MKFlowMessage.MKFlowMessage):
        class Invalid(MKFlowMessage.MKFlowInvalid):
            pass
        class Error(MKFlowMessage.MKFlowError):
            pass
        class Status(MKFlowMessage.MKFlowStatus):
            pass
        class Request(MKFlowMessage.MKFlowRequest):
            pass
        class Sent(MKFlowMessage.MKFlowSent):
            pass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class MKFlowLogFile():
    def __init__(self, logfile):
        self.reset()
        self.logfile = logfile

    def reset(self):
        self.logfile = ''
        self.openmode = 'r'
        self.fsoOpen = False

    def open(self):
        if not self.fsoOpen:
            try:
                self.fso = open(self.logfile, self.openmode)
            except:
                self.close()
                raise ValueError('cannot open log file at ' + self.logfile)
            else:
                self.fsoOpen = True

    def close(self):
        if self.fsoOpen:
            try:
                self.fso.close()
            except:
                raise ValueError('cannot close log file at ' + self.logfile)
            else:
                self.fsoOpen = False

    def read(self):
        try:
            self.open()
            message1 = self.fso.readline()
            message2 = self.fso.readline()
            if (len(message2) == 0):
                raise EOF("end of file reached")
        except EOF:
            self.close()
            raise EOF
        except:
            message1 = ''
            message2 = ''
        return message1, message2

    def start(self):
        self.alive = True

    def stop(self):
        try:
            self.close()
            self.reset()
        except:
            print 'error closing logfile'
        else:
            self.alive = False

    def isReady(self):
        # ready while alive for log file
        return self.isAlive()

    def isAlive(self):
        return self.alive

class EOF(Exception):
    pass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python
import threading

from MKFlowInput import MKFlowInput
from MKFlowCommunication import MKFlowCommunication
from MKDatabase import MKDatabase

class MKFlow():
    def __init__(self, port1 = '/dev/ttyUSB2', port2 = '/dev/ttyUSB3', instrument = 0):
        self.Input = MKFlowInput()
        self.Storage = MKFlowCommunication()
        self.Database = MKDatabase()
        self.Input.setBridge(port1, port2)
        #self.Input.setLogFile('/home/matthias/Documents/programs/python/swnt-reactor/data/log/bridge/testing/log1.log')
        self.instrument = instrument
        self.debugging = False

    def debug(self):
        self.debugging = True

    def start(self):
        try:
            self.thread = threading.Thread(target=self.loop)
            self.thread.daemon = True
            self.thread.start()
        except:
            self.stop()
            raise

    def stop(self):
        self.Input.stop()

    def join(self):
        self.thread.join()

    def loop(self):
        while self.Input.isAlive():
            try:
                Message = self.Input.getMessage()
                SubMessage = Message.getSubType()
            except:
                break
            else:
                if not Message.isInvalid:
                    node = Message.getNode()
                    sequence = Message.getSequence()
                    Entity = self.Storage.Node(node).Sequence(sequence)
                    try:
                        if Message.isError:
                            Entity.setError(Message)
                        elif Message.isStatus:
                            Entity.setStatus(Message)
                        elif Message.isSent:
                            Entity.setAnswer(Message)
                        elif Message.isSentStatus:
                            Entity.setWriteRequest(Message)
                        elif Message.isRequest:
                            Entity.setReadRequest(Message)
                        else:
                            raise
                    except:
                        print "--- something happened ---"
                        try:
                            Message.stdout()
                            Submessage.stdout()
                            Entity.reset()
                        except:
                            raise
                    else:
                        # store if two messages are present in current Entity
                        # reset Entity afterwards.
                        try:
                            if self.debugging:
                                bufferSize = self.Input.input.bufferSize()
                                if bufferSize > 0:
                                    print "Buffer: %i" % bufferSize
                                Entity.output()
                            Entity.save(self.Database, self.instrument)
                            pass
                        except ValueError:
                            raise
                            pass
                        except:
                            raise
                else:
                    print "--- invalid ---"
                    SubMessage.stdoutShort(Message.stdoutShort())
                    pass

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
#!/usr/bin/env python
from datetime import datetime       # datetime supports milliseconds. time doesn't
import sys                          # used to get line number of error print sys.exc_traceback.tb_lineno
import struct                       # used to convert bin-->float

def FBconvertLong(process, fbnr, value):
    if process == 33:
        if fbnr == 0 or fbnr == 3:
            return toFloat(value)
    elif process == 114:
        if fbnr == 1:
            return float(value) / 16777215.0 * 100.0
    return value

def toFloat(integer):
    # convert integer to IEE float
    return struct.unpack(">f", struct.pack(">I", integer))[0]

def fromFloat(floatingpoint):
    # convert IEE float to integer
    return struct.unpack('<I', struct.pack('<f', float(floatingpoint)))[0]

class MKFlowMessage():
    def __init__(self):
        self.clear()

    # input binary message and socat headline
    def process(self, message, socat = ''):
        self.message1 = socat
        self.message2 = message
        self.resetSubType()
        self.analyse()

    def resetSubType(self):
        # message type identifier
        self.commandByte= -3
        self.isInvalid  = False # -2
        self.isError    = False # -1
        self.isStatus   = False #  0
        self.isSent     = False #  2
        self.isSentStatus = False # 1
        self.isRequest  = False #  4
        # clear message objects
        self.Invalid    = MKFlowInvalid()
        self.Error      = MKFlowError()
        self.Status     = MKFlowStatus()
        self.Sent       = MKFlowSent()
        #self.SentStatus = MKFlowSentStatus()
        self.Request    = MKFlowRequest()

    def getSubType(self):
        if self.isInvalid:
            return self.Invalid
        if self.isError:
            return self.Error
        if self.isStatus:
            return self.Status
        if self.isSent or self.isSentStatus:
            return self.Sent
        if self.isRequest:
            return self.Request

    def clear(self):
        self.data = []
        self.node       = -1
        self.sequence   = -1
        self.length     = -1
        self.dataLength = -1
        self.commandByte= -1
        self.commandByteHuman = ''
        self.commandByteHumanShort = ''
        self.direction      = ''
        self.time           = datetime
        self.time_human     = "%Y/%m/%d;%H:%M:%S.%f"
        self.time_second    = 0.00

    def getNode(self):
        return self.node

    def getSequence(self):
        return self.sequence

    def getLength(self):
        return self.length

    def setLength(self, length):
        self.length = length

    def getDirection(self):
        return self.direction

    def getTime(self):
        return self.time_human

    def getSeconds(self):
        return self.time_second

    def getCommandByte(self):
        return self.commandByte

    def getCommandByteShort(self):
        return self.commandByteHumanShort

    def trim(self):
        self.message1 = self.message1.replace('\n','')
        self.message2 = self.message2.replace('\n','')
        while self.message2[0:1] == " ":
            self.message2 = self.message2[1:]
            if len(self.message2) == 0:
                break

    def split(self):
        self.message1 = self.message1.split(" ")
        self.message2 = self.message2.split(" ")

    def analyseDirection(self):
        if self.message1[0] == '>':
            self.direction = 'right'
        elif self.message1[0] == '<':
            self.direction = 'left'
        else:
            self.direction = 'none'

    def analyseTime(self):
        if len(self.message1) > 1:
            self.time = datetime.strptime(self.message1[1] + ';' + self.message1[2], "%Y/%m/%d;%H:%M:%S.%f")
        else:
            self.time = datetime.now()
        self.time_human = self.time.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        self.time_second = self.time.hour*60*60 + self.time.minute * 60 + self.time.second + self.time.microsecond/1e6

    def parseMessage(self):
        try:
            self.trim()
            self.split()
        except:
            self.Invalid.add('MKFlowMessage:parseMessage:\tError while parsing')
            raise

    def parseData(self):
        #replace two DLE (escaped) sequences (10 10) by one databyte (10)
        try:
            start = 0
            foundOne = False
            while len(self.data) -1 > start:
                found = self.data[start:].index(16)
                if self.data[(found+start+1)] == 16:
                    self.data.pop(found+start)
                    start = start + found
                    foundOne = True
                else:
                    start +=1
        except ValueError:
            # 10 not found. Ignore
            pass
        except:
            # other errors --> report
            self.Invalid.add('parseData Error at found: %i start: %i len: %i' % (found, start, len(self.data)))
            raise

    def checkMessage2(self):
        # dataLength is length-Byte
        if not (self.dataLength==len(self.data)-3) and not (self.dataLength == 0):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tError in socat output: length-Byte in message does not match socat length')
        if (len(self.message2)<6):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tMessage shorter than 6 bytes: DLE STX SEQUENCE NODE ... DLE ETX')
            # DLE STX SEQUENCE NODE LEN DATA DLE ETX
        if not ((self.message2[0] == "10") and (self.message2[1] == "02")):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tno valid message beginning:\tDLE (0x10) STX (0x02) missing')
        if not ((self.message2[-2] == "10") and (self.message2[-1] == "03")):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tno valid message ending:\tDLE (0x10) ETX (0x03) missing')
        if self.Invalid.isActive():
            raise ValueError('MKFlowMessage:parseMessage2:\tError while processing Message2')

    def analyse(self):
        try:
            self.parseMessage()
            self.analyseMessage()
            self.analyseMessageType()
            self.analyseCommandByte()
            if self.Invalid.isActive():
                raise ValueError('MKFlowMessage:analyse:\tMessage invalid')

        except Exception as e:
            self.Invalid.add('MKFlowMessage:analyse:\t'+str(e))
            self.Invalid.add('MKFlowMessage:analyse:\tLine number:\t' + str(sys.exc_traceback.tb_lineno))
            self.Invalid.add('MKFlowMessage:analyse:\tcommandByte:\t' + str(self.commandByte))
            self.commandByte = -2
            self.isInvalid = True
            #raise

    def analyseMessage(self):
        # DLE STX SEQUENCE NODE LEN DATA DLE ETX
        try:
            self.analyseMessage1()
            self.analyseMessage2()
        except:
            self.Invalid.add('MKFlowMessage:analyseMessage:\tError while extracting data.')
            raise

    def analyseMessage1(self):
        try:
            self.analyseDirection()
            self.analyseTime()
            self.setLength(len(self.message2))
        except:
            self.Invalid.add('MKFlowMessage:analyseMessage1:\tError while processing message1')
            raise

    def analyseMessage2(self):
        # DLE STX SEQUENCE NODE LEN DATA DLE ETX
        try:
            self.data = [int(i,16) for i in self.message2[2:-2]]
            self.parseData()
            self.sequence   = self.data[0]
            self.node       = self.data[1]
            self.dataLength = self.data[2]
            self.checkMessage2()
        except:
            self.Invalid.add('MKFlowMessage:analyseMessage2:\tError while extracting data.')
            if len(self.data)<3:
                self.Invalid.add('MKFlowMessage:analyseMessage2:\tlen(data)='+str(len(self.data)))
            raise

    def analyseMessageType(self):
        # check if the message contains information
        try:
            # length 0 means.Error.Status in message. next byte after 00 contains.Error code.
            if self.data[2] == 0:
                self.commandByte = -1
            else:
                self.commandByte = self.data[3]
        except:
            self.Invalid.add('MKFlowMessage:analyseMessageType Error while retrieving Message')
            raise

    def analyseCommandByte(self):
        try:
            self.translateCommandByte()
            if self.commandByte == -2:
                self.isInvalid = True
            elif self.commandByte == -1:
                self.isError = True
                self.Error.set(self.data[3:])
                self.Error.setSequence(self.sequence)
                self.Error.setNode(self.node)
                self.Error.analyse()
            elif self.commandByte == 0:
                self.Status.set(self.data[4:])
                self.Status.analyse()
                self.isStatus = True
            elif self.commandByte == 4:
                self.Request.set(self.data[4:])
                self.isRequest = True
            elif (self.commandByte == 2):
                self.Sent.set(self.data[4:])
                self.isSent = True
            elif (self.commandByte == 1):
                self.Sent.set(self.data[4:])
                self.isSentStatus = True
            else:
                myError = 'MKFlowMessage:analyseCommandByte commandByte ' + str(self.commandbyte) + ' not handled'
                self.Invalid.add(myError)
                self.Invalid.add(self.commandByteHuman)
                raise ValueError(myError)
        except:
            raise

    def translateCommandByte(self):
        try:
            if self.commandByte == -2:
                self.commandByteHuman = "Invalid Message: Error in Program"
            elif self.commandByte == -1:
                self.commandByteHuman = "Valid Message: Error Message from Device"
            elif self.commandByte == 0:
                self.commandByteHuman = "Status message"
            elif self.commandByte == 1:
                self.commandByteHuman = "Send Parameter with destination address, will be answered with type 00 command"
            elif self.commandByte == 2:
                self.commandByteHuman = "Send Parameter with destination address, no.Status.Requested"
            elif self.commandByte == 3:
                self.commandByteHuman = "Send Parameter with source address, no.Status.Requested"
            elif self.commandByte == 4:
                self.commandByteHuman = "Request Parameter, will be answered with type 02 or 00 command"
            elif self.commandByte == 6:
                self.commandByteHuman = "Stop Process"
            elif self.commandByte == 7:
                self.commandByteHuman = "Start Process"
            elif self.commandByte == 8:
                self.commandByteHuman = "Claim Process"
            elif self.commandByte == 9:
                self.commandByteHuman = "Unclaim Process"
            else:
                self.commandByteHuman = "unhandled commandByte: " + str(self.commandByte)
                raise ValueError(self.commandByteHuman)
            if self.commandByte == 4:
                self.commandByteHumanShort = 'REQ'
            elif (self.commandByte >= 1) and (self.commandByte <= 3):
                self.commandByteHumanShort = 'ST' + str(self.commandByte)
            if self.commandByte == -2:
                self.commandByteHumanShort = 'INV'
            if self.commandByte == -1:
                self.commandByteHumanShort = 'ERR'
            if self.commandByte == 0:
                self.commandByteHumanShort = 'INF'
        except:
            raise

    def stdout(self):
        print '-- message Class Output begin --'
        print self.message1
        print self.message2
        print 'direction: \t' , self.getDirection()
        print 'length: \t'    , self.getLength()
        print 'time: \t'      , self.getTime()
        print 'seconds: \t'   , self.getSeconds()
        print 'sequence: \t'  , self.getSequence()
        print 'node: \t'      , self.getNode()
        print 'command: \t'   , self.getCommandByte()
        print '-- message end --'

    def stdoutShort(self, indent=''):
        return '\t'.join(str(i) for i in [indent, self.getNode(), self.getSequence(), self.getCommandByteShort()])

# Message Type Classes
class MKFlowInvalid():
    def __init__(self):
        self.value = ''
        self.active = False

    def add(self, newValue):
        self.active = True
        self.value += str(newValue) + '\n'

    def get(self):
        if self.active:
            return self.value
        else:
            return

    def isActive(self):
        return self.active

    def stdout(self, leading = '\t'):
        print leading, "-- MKFlowRequest Class Output Begin --"
        print leading, "Value:\t", self.value
        print leading, "-- MKFlowRequest Class Output End --"

    def stdoutShort(self, indent=''):
        return '\t'.join(str(i) for i in [indent, self.isActive()])

class MKFlowError():
    def __init__(self):
        self.value = 0
        self.node = -1
        self.sequence = -1
        self.text = ''
        self.active = False

    def set(self, data):
        self.active = True
        self.data = data
        self.value = data[0]

    def setNode(self, node):
        self.node = node

    def setSequence(self,sequence):
        self.sequence = sequence

    def analyse(self):
        if not len(self.data) == 1:
            raise ValueError('Error Class has received too many input bytes')
        self.translate()

    def getText(self):
        return self.text

    def getHuman(self):
        return self.getText()

    def getValue(self):
        return self.value

    def getData(self):
        return self.data

    def translate(self):
        if self.active:
            self.text = 'device returned error code ' + str(self.value) + ': '
            if (self.value==3):
                self.text += 'propar protocol error'
            elif (self.value==4):
                self.text += 'propar protocol error (or CRC error)'
            elif (self.value==5):
                if self.node >= 0:
                    self.text += 'destination node address ' + str(self.node) + ' rejected'
                else:
                    self.text += 'destination node address rejected'
                if self.sequence >= 0:
                    self.text += ' for sequence ' + str(self.sequence)
            elif (self.value==9):
                self.text += 'response message timeout'

    def stdout(self, indent = '\t'):
        print indent, '-- Error Class Output Begin--'
        print indent, 'Error Data:\t', self.getData()
        print indent, 'Error Number:\t', self.getValue()
        print indent, 'Error Message:\t', self.getText()
        print indent, '-- Error Class Output Begin--'

    def stdoutShort(self, indent=''):
        return '\t'.join(str(i) for i in [indent, self.getValue(),None,None,self.getHuman()])

class MKFlowStatus():
    def __init__(self):
        self.data = []
        self.status         = -1
        self.status_human   = ''
        self.index          = -1
        self.active = False

    def set(self, data):
        self.data   = data
        self.active = True

    def analyse(self):
        if len(self.data)>0:
            self.status = self.data[0]
            self.status_hex = hex(self.data[0])[2:]
        if len(self.data)>1:
            self.index  = self.data[1]
        self.humanize()

    def isActive(self):
        return self.active

    def getStatus(self):
        return self.status

    def getStatusByte(self):
        return self.status

    def getIndex(self):
        return self.index

    def getHuman(self):
        if self.status_human == '':
            self.humanize()
        return self.status_human

    def humanize(self):
        # dict([('no Status', -1), ('no Error', 0), ...])
        if self.status == -1:
            self.status_human = 'no Status'
        elif self.status_hex == '0':
            self.status_human = 'no Error'
        elif self.status_hex == '1':
            self.status_human = 'Process claimed'
        elif self.status_hex == '2':
            self.status_human = 'Command Error'
        elif self.status_hex == '3':
            self.status_human = 'Process Error'
        elif self.status_hex == '4':
            self.status_human = 'Parameter Error'
        elif self.status_hex == '5':
            self.status_human = 'Parameter type Error'
        elif self.status_hex == '6':
            self.status_human = 'Parameter value Error'
        elif self.status_hex == '7':
            self.status_human = 'Network not active'
        elif self.status_hex == '8':
            self.status_human = 'Time-out start character'
        elif self.status_hex == '9':
            self.status_human = 'Time-out serial line'
        elif self.status_hex == 'a':
            self.status_human = 'Hardware memory Error'
        elif self.status_hex == 'b':
            self.status_human = 'Node number Error'
        elif self.status_hex == 'c':
            self.status_human = 'General communication Error'
        elif self.status_hex == 'd':
            self.status_human = 'Read only Parameter.'
        elif self.status_hex == 'e':
            self.status_human = 'Error PC-communication'
        elif self.status_hex == 'f':
            self.status_human = 'No RS232 connection'
        elif self.status_hex == '10':
            self.status_human = 'PC out of memory'
        elif self.status_hex == '11':
            self.status_human = 'Write only Parameter'
        elif self.status_hex == '12':
            self.status_human = 'System configuration unknown'
        elif self.status_hex == '13':
            self.status_human = 'No free node address'
        elif self.status_hex == '14':
            self.status_human = 'Wrong interface type'
        elif self.status_hex == '15':
            self.status_human = 'Error serial port connection'
        elif self.status_hex == '16':
            self.status_human = 'Error opening communication'
        elif self.status_hex == '17':
            self.status_human = 'Communication Error'
        elif self.status_hex == '18':
            self.status_human = 'Error interface bus master'
        elif self.status_hex == '19':
            self.status_human = 'Timeout answer'
        elif self.status_hex == '1a':
            self.status_human = 'No start character'
        elif self.status_hex == '1b':
            self.status_human = 'Error first digit'
        elif self.status_hex == '1c':
            self.status_human = 'Buffer overflow in host'
        elif self.status_hex == '1d':
            self.status_human = 'Buffer overflow'
        elif self.status_hex == '1e':
            self.status_human = 'No answer found'
        elif self.status_hex == '1f':
            self.status_human = 'Error closing communication'
        elif self.status_hex == '20':
            self.status_human = 'Synchronisation Error'
        elif self.status_hex == '21':
            self.status_human = 'Send Error'
        elif self.status_hex == '22':
            self.status_human = 'Protocol Error'
        elif self.status_hex == '23':
            self.status_human = 'Buffer overflow in module'

    def stdout(self, indent = '\t'):
        print indent, "MKFlowStatus Class Output Begin"
        print indent, "Data Array:\t", self.data
        print indent, "Status :   \t", self.getStatus()
        print indent, "Status :   \t", self.getHuman()
        print indent, "Index Byte:\t", self.getIndex()
        print indent, "MKFlowStatus Class Output End"

    def stdoutShort(self,indent=''):
        print indent, 'INFO\t', self.getStatus(), '\t\t', self.getHuman()

# subclass for MKFlowProcess
class MKFlowParameter():
    def __init__(self):
        self.dataType   = 'undefined'

        self.index      = -1
        self.process    = -1
        self.number     = -1

        self.dataLength = 0
        self.dataStart = 0

        self.data       = []
        self.human      = ''

    def set(self, data):
        self.data = data

    def setLength(self,length=0):
        self.length = length

    def setProcess(self, process):
        self.process = process

    def analyse(self):
        pass

    def analyseData(self):
        self.index = self.data[0]
        self.number= self.data[0]

    def analyseDataType(self, number = -3):
        if number == -3:
            number = self.number
        identifier = format(number, '08b')[1:3]
        if identifier == '00':
            self.dataType = 'character'
        elif identifier == '01':
            self.dataType = 'integer'
        elif identifier == '10':
            self.dataType = 'long'
        elif identifier == '11':
            self.dataType = 'string'

    def substractDataType(self, number = -3):
        if number == -3:
            number = self.number
        if self.dataType == 'string':
            number -= int('60',16)
        elif self.dataType == 'long':
            number -= int('40',16)
        elif self.dataType == 'integer':
            number -= int('20',16)
        elif self.dataType == 'character':
            pass
        return number

    def isChained(self):
        if self.index >= 128:
            return True
        else:
            return False

    def getData(self):
        return self.data[0:self.length]

    def getIndex(self):
        index = self.index
        if self.isChained():
            index -= 128
        if index >= 32:
            index = self.substractDataType(index)
        return index

    def getProcess(self):
        return self.process

    def getNumber(self):
        return self.number

    def getDataType(self):
        return self.dataType

    def getLength(self):
        return self.length

    def getHuman(self):
        if self.human == '':
            self.humanize()
        return self.human

    def humanize(self):
        getIdent = dict([('0:0', 0), ('0:1', 1), ('0:2', 2), ('0:3', 3), ('0:4', 4), ('0:5', 5), ('0:10', 6), ('1:0', 7), ('1:1', 8), ('1:2', 9), ('1:3', 10), ('1:4', 11), ('1:5', 12), ('1:6', 13), ('1:7', 14), ('1:8', 15), ('1:9', 16), ('1:10', 17), ('1:11', 18), ('1:12', 19), ('1:13', 20), ('1:14', 21), ('1:15', 22), ('1:16', 23), ('1:17', 24), ('1:18', 25), ('1:19', 26), ('1:20', 27), ('0:12', 28), ('0:13', 29), ('0:14', 30), ('9:1', 31), ('10:0', 32), ('10:1', 33), ('10:2', 34), ('114:12', 51), ('115:3', 52), ('116:6', 53), ('114:1', 54), ('117:1', 55), ('117:2', 56), ('115:1', 57), ('116:7', 58), ('115:2', 59), ('114:2', 60), ('114:3', 61), ('116:1', 62), ('116:2', 63), ('116:3', 64), ('116:4', 65), ('114:4', 66), ('116:5', 67), ('115:4', 68), ('115:5', 69), ('115:6', 70), ('114:5', 71), ('117:3', 72), ('117:4', 73), ('115:7', 78), ('114:6', 79), ('0:19', 80), ('114:7', 81), ('114:8', 82), ('114:9', 83), ('114:10', 84), ('114:11', 85), ('114:13', 86), ('114:14', 87), ('114:15', 88), ('113:1', 89), ('113:2', 90), ('113:3', 91), ('113:4', 92), ('118:1', 93), ('118:2', 94), ('118:3', 95), ('118:4', 96), ('118:5', 97), ('118:6', 98), ('118:7', 99), ('118:8', 100), ('118:9', 101), ('118:10', 102), ('114:16', 103), ('113:5', 104), ('115:9', 105), ('116:8', 106), ('115:8', 113), ('113:6', 114), ('97:1', 115), ('97:2', 116), ('97:3', 117), ('97:4', 118), ('97:5', 119), ('97:6', 120), ('104:1', 121), ('104:2', 122), ('104:3', 123), ('104:4', 124), ('104:5', 125), ('104:6', 126), ('104:7', 127), ('1:31', 128), ('104:8', 129), ('113:7', 130), ('33:1', 138), ('33:2', 139), ('114:17', 140), ('33:7', 141), ('33:8', 142), ('33:9', 143), ('33:10', 144), ('115:10', 146), ('33:9', 148), ('33:10', 149), ('33:5', 150), ('33:6', 151), ('33:11', 152), ('33:13', 153), ('97:9', 155), ('104:9', 156), ('33:14', 157), ('33:15', 158), ('33:16', 159), ('33:17', 160), ('33:18', 161), ('33:20', 162), ('115:11', 163), ('114:18', 164), ('114:20', 165), ('114:21', 166), ('114:22', 167), ('114:23', 168), ('33:21', 169), ('113:8', 170), ('113:9', 171), ('113:10', 172), ('113:11', 173), ('113:12', 174), ('118:11', 175), ('115:12', 176), ('113:13', 177), ('113:14', 178), ('113:15', 179), ('113:16', 180), ('97:7', 181), ('33:22', 182), ('0:18', 183), ('0:20', 184), ('123:1', 185), ('123:3', 186), ('123:4', 187), ('123:10', 188), ('114:24', 189), ('115:13', 190), ('115:14', 191), ('116:9', 192), ('115:15', 193), ('115:16', 194), ('115:17', 195), ('115:18', 196), ('33:4', 197), ('125:10', 198), ('125:3', 199), ('125:9', 200), ('125:20', 201), ('115:22', 202), ('125:21', 203), ('33:0', 204), ('33:3', 205), ('33:23', 206), ('119:1', 207), ('119:2', 208), ('119:3', 209), ('119:4', 210), ('119:5', 211), ('119:6', 212), ('116:21', 213), ('116:22', 214), ('116:23', 215), ('116:24', 216), ('116:25', 217), ('116:26', 218), ('116:27', 219), ('116:28', 220), ('117:5', 221), ('33:24', 222), ('117:6', 223), ('33:25', 224), ('33:26', 225), ('33:27', 226), ('33:28', 227), ('33:29', 228), ('33:30', 229), ('114:25', 230), ('114:26', 231), ('114:27', 232), ('114:28', 233), ('114:29', 234), ('0:21', 235), ('115:20', 236), ('33:31', 237), ('33:12', 238), ('33:13', 239), ('33:16', 240), ('33:17', 241), ('33:10', 244), ('33:11', 245), ('113:17', 248), ('113:18', 249), ('113:20', 250), ('113:21', 251), ('113:22', 252), ('114:30', 253), ('113:23', 254), ('113:24', 255), ('113:25', 256), ('113:26', 257), ('113:27', 258), ('113:28', 259), ('113:29', 260), ('113:30', 261), ('113:31', 262), ('116:10', 263), ('116:11', 264), ('116:12', 265), ('116:13', 266), ('116:14', 267), ('65:15', 268), ('116:15', 269), ('116:18', 270), ('116:8', 271), ('116:9', 272), ('104:10', 273), ('104:11', 274), ('65:1', 275), ('116:17', 276), ('116:29', 277), ('116:30', 278), ('116:30', 279), ('116:31', 280), ('121:0', 281), ('121:1', 282), ('121:2', 283), ('121:3', 284), ('121:4', 285), ('121:5', 286), ('114:31', 287), ('65:21', 288), ('65:22', 289), ('65:23', 290), ('65:24', 291), ('65:25', 292), ('116:20', 293), ('115:31', 294), ('104:12', 295), ('104:13', 296), ('104:14', 297), ('125:8', 298), ('125:11', 299), ('124:7', 300), ('124:8', 301), ('124:10', 302), ('124:9', 303), ('124:11', 304), ('124:20', 305), ('124:21', 306), ('120:0', 307), ('120:2', 308), ('120:6', 309), ('120:7', 310), ('120:3', 311), ('120:1', 312), ('120:4', 313), ('120:5', 314), ('120:8', 315), ('120:9', 316), ('120:10', 317), ('120:11', 318), ('0:6', 319), ('0:7', 320), ('124:31', 321), ('115:23', 322), ('118:12', 323), ('65:26', 324), ('116:16', 325), ('119:31', 326), ('115:24', 327), ('125:12', 328), ('124:12', 329), ('0:8', 330)])
        getWord = dict([(0, 'Identification string'), (1, 'Primary node address'), (2, 'Secondary node address'), (3, 'Next node address'), (4, 'Last node address'), (5, 'Arbitrage'), (6, 'Initreset'), (7, 'Measure'), (8, 'Setpoint'), (9, 'Setpoint slope'), (10, 'Analog input'), (11, 'Control mode'), (12, 'Polynomial constant A'), (13, 'Polynomial constant B'), (14, 'Polynomial constant C'), (15, 'Polynomial constant D'), (16, 'Polynomial constant E'), (17, 'Polynomial constant F'), (18, 'Polynomial constant G'), (19, 'Polynomial constant H'), (20, 'Capacity'), (21, 'Sensor type'), (22, 'Capacity unit index'), (23, 'Fluid number'), (24, 'Fluid name'), (25, 'Claim node'), (26, 'Modify'), (27, 'Alarm info'), (28, 'Channel amount'), (29, 'First channel'), (30, 'Last channel'), (31, '<hostcontrl>'), (32, 'Alarm message unit type'), (33, 'Alarm message number'), (34, 'Relay status'), (51, 'Cycle time'), (52, 'Analog mode'), (53, 'Reference voltage'), (54, 'Valve output'), (55, 'Dynamic display factor'), (56, 'Static display factor'), (57, 'Calibration mode'), (58, 'Valve offset'), (59, 'Monitor mode'), (60, 'Alarm register1'), (61, 'Alarm register2'), (62, '<CalRegZS1>'), (63, '<CalRegFS1>'), (64, '<CalRegZS2>'), (65, '<CalRegFS2>'), (66, 'ADC control register'), (67, 'Bridge potmeter'), (68, '<AlarmEnble>'), (69, 'Test mode'), (70, '<ADC channel select>'), (71, 'Normal step controller response'), (72, 'Setpoint exponential smoothing filter'), (73, 'Sensor exponential smoothing filter'), (78, 'Tuning mode'), (79, 'Valve default'), (80, 'Global modify'), (81, 'Valve span correction factor'), (82, 'Valve curve correction'), (83, '<MemShipNor>'), (84, '<MemShipOpn>'), (85, 'IO status'), (86, '<FuzzStNeNo>'), (87, '<FuzzStPoNo>'), (88, '<FuzzStOpen>'), (89, 'Device type'), (90, 'BHTModel number'), (91, 'Serial number'), (92, 'Customer model'), (93, 'BHT1'), (94, 'BHT2'), (95, 'BHT3'), (96, 'BHT4'), (97, 'BHT5'), (98, 'BHT6'), (99, 'BHT7'), (100, 'BHT8'), (101, 'BHT9'), (102, 'BHT10'), (103, 'Broadcast repeating time'), (104, 'Firmware version'), (105, 'Pressure sensor type'), (106, 'Barometer pressure'), (113, 'Reset'), (114, 'User tag'), (115, 'Alarm limit maximum'), (116, 'Alarm limit minimum'), (117, 'Alarm mode'), (118, 'Alarm output mode'), (119, 'Alarm setpoint mode'), (120, 'Alarm new setpoint'), (121, 'Counter value'), (122, 'Counter unit index'), (123, 'Counter limit'), (124, 'Counter output mode'), (125, 'Counter setpoint mode'), (126, 'Counter new setpoint'), (127, 'Counter unit'), (128, 'Capacity unit'), (129, 'Counter mode'), (130, 'Minimum hardware revision'), (138, 'Slave factor'), (139, 'Reference voltage input'), (140, 'Stable situation controller response'), (141, 'Temperature'), (142, 'Pressure'), (143, 'Time'), (144, 'Calibrated volume'), (146, 'Range select'), (148, 'Frequency'), (149, 'Impulses/m3'), (150, 'Normal volume flow'), (151, 'Volume flow'), (152, 'Delta-p'), (153, '<scalefact>'), (155, 'Reset alarm enable'), (156, 'Reset counter enable'), (157, 'Master node'), (158, 'Master process'), (159, 'Remote instrument node'), (160, 'Remote instrument process'), (161, 'Minimum custom range'), (162, 'Maximum custom range'), (163, 'Relay/TTL output'), (164, 'Open from zero controller response'), (165, 'Controller features'), (166, 'PID-Kp'), (167, 'PID-Ti'), (168, 'PID-Td'), (169, 'Density'), (170, 'Calibration certificate'), (171, 'Calibration date'), (172, 'Service number'), (173, 'Service date'), (174, 'Identification number'), (175, 'BHT11'), (176, 'Power mode'), (177, 'Pressure inlet'), (178, 'Pressure outlet'), (179, 'Orifice'), (180, 'Fluid temperature'), (181, 'Alarm delay'), (182, 'Capacity 0%'), (183, 'Number of channels'), (184, 'Device function'), (185, 'Scan channel'), (186, 'Scan parameter'), (187, 'Scan time'), (188, 'Scan data'), (189, 'Valve open'), (190, 'Number of runs'), (191, 'Minimum process time'), (192, 'Leak rate'), (193, 'Mode info request'), (194, 'Mode info option list'), (195, 'Mode info option description'), (196, 'Calibrations options'), (197, 'Mass flow'), (198, 'Bus address'), (199, 'Interface configuration'), (200, 'Baudrate'), (201, 'Bus diagnostic string'), (202, 'Number of vanes'), (203, 'Fieldbus'), (204, 'fMeasure'), (205, 'fSetpoint'), (206, 'Mass'), (207, 'Manufacturer status register'), (208, 'Manufacturer warning register'), (209, 'Manufacturer error register'), (210, 'Diagnostic history string'), (211, 'Diagnostic mode'), (212, 'Manufacturer status enable'), (213, 'Analog output zero adjust'), (214, 'Analog output span adjust'), (215, 'Analog input zero adjust'), (216, 'Analog input span adjust'), (217, 'Sensor input zero adjust'), (218, 'Sensor input span adjust'), (219, 'Temperature input zero adjust'), (220, 'Temperature input span adjust'), (221, 'Adaptive smoothing factor'), (222, 'Slope setpoint step'), (223, 'Filter length'), (224, 'Absolute accuracy'), (225, 'Lookup table index'), (226, 'Lookup table X'), (227, 'Lookup table Y'), (228, 'Lookup table temperature index'), (229, 'Lookup table temperature'), (230, 'Valve maximum'), (231, 'Valve mode'), (232, 'Valve open correction'), (233, 'Valve zero hold'), (234, 'Valve slope'), (235, 'IFI data'), (236, 'Range used'), (237, 'Fluidset properties'), (238, 'Lookup table unit type index'), (239, 'Lookup table unit type'), (240, 'Lookup table unit index'), (241, 'Lookup table unit'), (244, 'Capacity unit type temperature'), (245, 'Capacity unit pressure'), (248, 'Formula type'), (249, 'Heat capacity'), (250, 'Thermal conductivity'), (251, 'Viscosity'), (252, 'Standard flow'), (253, 'Controller speed'), (254, 'Sensor code'), (255, 'Sensor configuration code'), (256, 'Restriction code'), (257, 'Restriction configurator code'), (258, 'Restriction NxP'), (259, 'Seals information'), (260, 'Valve code'), (261, 'Valve configuration code'), (262, 'Instrument properties'), (263, 'Lookup table frequency index'), (264, 'Lookup table frequency frequency'), (265, 'Lookup table frequency temperature'), (266, 'Lookup table frequency density'), (267, 'Lookup table frequency span adjust'), (268, 'Capacity unit index (ext)'), (269, 'Density actual'), (270, 'Measured restriction'), (271, 'Temperature potmeter'), (272, 'Temperature potmeter gain'), (273, 'Counter controller overrun correction'), (274, 'Counter controller gain'), (275, 'Sub fluid number'), (276, 'Temperature compensation factor'), (277, 'DSP register address'), (278, 'DSP register long'), (279, 'DSP register floating point'), (280, 'DSP register integer'), (281, 'Standard deviation'), (282, 'Measurement status'), (283, 'Measurement stop criteria'), (284, 'Measurement time out'), (285, 'Maximum number of runs'), (286, 'Minimum standard deviation'), (287, 'IO switch status'), (288, 'Sensor bridge settings'), (289, 'Sensor bridge current'), (290, 'Sensor resistance'), (291, 'Sensor bridge voltage'), (292, 'Sensor group name'), (293, 'Sensor calibration temperature'), (294, 'Valve safe state'), (295, 'Counter unit type index'), (296, 'Counter unit type'), (297, 'Counter unit index (ext)'), (298, 'Bus1 selection'), (299, 'Bus1 medium'), (300, 'Bus2 mode'), (301, 'Bus2 selection'), (302, 'Bus2 address'), (303, 'Bus2 baudrate'), (304, 'Bus2 medium'), (305, 'Bus2 diagnostics'), (306, 'Bus2 name'), (307, 'PIO channel selection'), (308, 'PIO parameter'), (309, 'PIO input/output filter'), (310, 'PIO parameter capacity 0%'), (311, 'PIO parameter capacity 100%'), (312, 'PIO configuration selection'), (313, 'PIO analog zero adjust'), (314, 'PIO analog span adjust'), (315, 'PIO hardware capacity max'), (316, 'PIO capacity set selection'), (317, 'PIO hardware capacity 0%'), (318, 'PIO hardware capacity 100%'), (319, 'Hardware platform id'), (320, 'Hardware platform sub id'), (321, 'Temporary baudrate'), (322, 'Setpoint monitor mode'), (323, 'BHT12'), (324, 'Nominal sensor voltage'), (325, 'Sensor voltage compensation factor'), (326, 'PCB serial number'), (327, 'Minimum measure time'), (328, 'Bus1 parity'), (329, 'Bus2 parity'), (330, 'Firmware id')])
        myIdent = str(self.getProcess()) + ":" + str(self.getNumber())

        if myIdent in getIdent.keys():
            if getIdent[myIdent] in getWord.keys():
                self.human = getWord[getIdent[myIdent]]

# subclass for MKFlowData
class MKFlowProcess():
    class MKFlowParameter(MKFlowParameter):
        pass

    def __init__(self):
        self.number = -1
        self.length = 0
        self.data = []
        self.Parameter = []

    def set(self, data):
        self.data   = data
        self.number = data[0]

    def analyse(self):
        index = 0
        position = 1
        chained = True
        while chained:
            index = len(self.Parameter)
            self.Parameter.append(self.MKFlowParameter())
            self.Parameter[index].set(self.data[position:])
            self.Parameter[index].setProcess(self.getProcess())
            self.Parameter[index].analyse()
            position += self.Parameter[index].getLength()
            chained = self.Parameter[index].isChained()
        self.length = position

    def isChained(self):
        if self.number >= 128:
            return True
        else:
            return False

    def getNumber(self):
        return self.number

    def getProcess(self):
        if self.isChained():
            return (self.number - 128)
        else:
            return self.number

    def getLength(self):
        return self.length

    def stdout(self, leading = '\t\t'):
        print leading, "-- MKFlowProcess Class Output Begin --"
        print leading, 'Data:\t', self.data
        print leading, 'Data:\t', self.data[0:self.getLength()]
        print leading, '1st byte:\t', format(self.data[0], '08b')[0:4] + ' ' + format(self.data[0], '08b')[4:8]
        print leading, 'chained:\t', self.isChained()
        print leading, 'Process:\t', self.getProcess()
        print leading, 'Total Parameters: ', len(self.Parameter)
        for parameter in self.Parameter:
            parameter.stdout(leading + '\t')
        print leading, "-- MKFlowProcess Class Output End --"

class MKFlowData():
    class MKFlowProcess(MKFlowProcess):
        pass

    def __init__(self):
        self.data = []
        self.active = False
        self.process = []
        self.length = 0

    def set(self,data):
        self.active = True
        self.data = data
        self.analyse()

    def analyse(self):
        self.analyseProcess()
        if not self.check():
            raise ValueError('Data was not fully processed. Some information might be missing. Declare whole as invalid')
            pass

    def check(self):
        return (len(self.data) == self.length)

    def analyseProcess(self):
        index = 0
        position = 0
        chained = True
        while chained:
            index = len(self.process)
            self.process.append(self.MKFlowProcess())
            self.process[index].set(self.data[position:])
            self.process[index].analyse()
            position += self.process[index].getLength()
            chained = self.process[index].isChained()
        self.length = position

    def stdout(self, leading = '\t'):
        print leading, "-- MKFlowData Class Output Begin --"
        print leading, "Data Array: \t", self.data
        print leading, "Data Array: \t", self.data[0:self.length]
        print leading, 'Total Processes:\t', len(self.process)
        print leading, 'Data' + (" not " if not self.check() else " ") +'fully processed'
        for process in self.process:
            process.stdout('\t\t')
        print leading, "-- MKFlowData Class Output End --"

class MKFlowRequest(MKFlowData):
    def stdoutShort(self, indent=''):
        for process in self.process:
            for parameter in process.Parameter:
                return '\t'.join(str(i) for i in [indent , parameter.getProcess(), parameter.getIndex(), parameter.getNumber(), parameter.getHuman()])

    class MKFlowProcess(MKFlowProcess):
        class MKFlowParameter(MKFlowParameter):
            def analyse(self):
                self.analyseData()
                self.analyseDataType(self.number)
                self.setLength(3)

            def analyseData(self):
                try:
                    if len(self.data)<3:
                        raise ValueError('MKFlowRequest:analyseData data array too short')
                    self.index   = self.data[0]
                    self.process = self.data[1] # process should be filled by parent
                    self.number  = self.data[2]
                except:
                    raise

            def setLength(self,length=0):
                self.length = length
                if self.dataType == 'string':
                    self.length += 1
                    self.DataLength = self.data[3]

            def getNumber(self):
                # FB Number ( no chaining parameter present)
                return self.substractDataType(self.number)

            def stdout(self, leading = '\t\t\t'):
                print leading, "-- MKFlowRequest Class Output Begin --"
                print leading, 'Data:    \t', self.getData()
                print leading, '1st byte:\t', format(self.data[0], '08b')[0:4] + ' ' + format(self.data[0], '08b')[4:8]
                print leading, 'Chained: \t', self.isChained()
                print leading, 'Index:   \t', self.getIndex()
                print leading, '2nd byte:\t', format(self.data[1], '08b')[0:4] + ' ' + format(self.data[1], '08b')[4:8]
                print leading, 'Process: \t', self.getProcess()
                print leading, '3rd byte:\t', format(self.data[2], '08b')[0:4] + ' ' + format(self.data[2], '08b')[4:8]
                print leading, 'DataType:\t', self.getDataType()
                print leading, 'FbNr:    \t', self.getNumber()
                print leading, 'Length:  \t', self.getLength()
                print leading, 'Human Ind:\t', self.getHuman()
                print leading, "-- MKFlowRequest Class Output End --"

class MKFlowSent(MKFlowData):
    def stdoutShort(self, indent=''):
        for process in self.process:
            for parameter in process.Parameter:
                return '\t'.join(str(i) for i in [indent, process.getProcess(), parameter.getIndex(), None, parameter.getValue()])

    class MKFlowProcess(MKFlowProcess):
        class MKFlowParameter(MKFlowParameter):
            def analyse(self):
                self.analyseData()
                self.analyseDataType()
                self.analyseValue()

            def analyseValue(self):
                self.dataStart = 1
                self.dataValueFloat = float(0)
                if self.dataType == 'string':
                    self.dataLength = self.data[self.dataStart]
                    self.dataStart += 1
                    self.setLength()
                    self.dataValue  = ''.join(chr(i) for i in self.data[self.dataStart:self.length])
                elif self.dataType == 'long':
                    # can also be IEE floating point notation
                    self.dataLength = 4
                    self.setLength()
                    self.dataValue  = int(''.join(hex(i)[2:] for i in self.data[self.dataStart:self.length]),16)
                elif self.dataType == 'integer':
                    self.dataLength = 2
                    self.setLength()
                    self.dataValue  = int(''.join(hex(i)[2:] for i in self.data[self.dataStart:self.length]),16)
                elif self.dataType == 'character':
                    self.dataLength = 1
                    self.setLength()
                    self.dataValue = self.data[self.dataStart]
                else:
                    self.dataLength = 0
                    self.setLength()
                    self.dataValue = None
                    raise ValueError('MKFlowSent:analyseValue datatype not found: ' + str(self.dataType))
                if (len(self.data)) < self.length:
                    pass
                    #raise ValueError('length of data does not match message size.')
                elif (len(self.data)) > self.length:
                    pass
                    #raise ValueError('length of data too long. Check for chaining!')

            def setLength(self):
                if self.dataLength == 0:
                    # undefined length. use whole
                    self.length = len(self.data)
                else:
                    self.length = self.dataLength + self.dataStart

            def getValue(self):
                return self.dataValue

            def stdout(self, leading = '\t\t\t'):
                print leading, "-- MKFlowSent:MKFlowProcess:MKFlowParameter Class Output Begin --"
                print leading, 'Data:    \t', self.getData()
                print leading, '1st byte:\t', format(self.data[0], '08b')[0:4] + ' ' + format(self.data[0], '08b')[4:8]
                print leading, 'Chained: \t', self.isChained()
                print leading, 'DataType:\t', self.getDataType()
                print leading, 'Index:    \t', self.getIndex()
                print leading, '2nd byte:\t', format(self.data[1], '08b')[0:4] + ' ' + format(self.data[1], '08b')[4:8]
                print leading, 'Length:  \t', self.getLength()
                print leading, 'Value:   \t', self.getValue()
                print leading, "-- MKFlowSent:MKFlowProcess:MKFlowParameter Class Output End --"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/env python

import subprocess
import threading

class MKFlowSocat:
    def __init__(self, port1, port2):
        self.buffer = []
        self.port1 = port1
        self.port2 = port2

    def start(self):
        try:
            self.alive = True
            self.thread = threading.Thread(target=self.loop)
            self.thread.daemon = True # never care about it anymore
            self.thread.start()
        except:
            print 'error stopping thread'

    def stop(self):
        try:
            self.close()
        except:
            print 'error stopping thread'
        else:
            self.alive = False

    def join(self):
        self.thread.join()

    def open(self):
        exe = 'socat -x %s,raw,echo=0,b38400,crnl %s,raw,echo=0,b38400,crnl' % (self.port1, self.port2)
        self.popen = subprocess.Popen(exe.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def close(self):
        try:
            self.popen.terminate()
            self.popen.wait()
        except:
            print "error closing socat"
            raise

    def loop(self):
        for line in iter(self.popen.stdout.readline, b''):
            self.buffer.append(line)

    def read(self):
        if self.buffer[0][0] == "<" or self.buffer[0][0] == ">":
            return self.buffer.pop(0), self.buffer.pop(0)
        elif len(self.buffer[0]) == 0:
            # message 1 is empty. pop two messages
            return self.buffer.pop(0), self.buffer.pop(0)
        else:
            # message 1 is missing in rpi's socat
            return '', self.buffer.pop(0)

    def isReady(self):
        size = self.bufferSize()
        if size > 30:
            self.buffer = [self.buffer[-1]]
            print "buffer overflow"
            return True
        return size > 0

    def bufferSize(self):
        return len(self.buffer)

    def isAlive(self):
        return self.alive
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!/usr/bin/env python

import struct						# imports struct API ???
import os							# imports file system functions like open()
import threading					# multithreading
from time import time
from datetime import datetime

class MKLogFileHandler:
	#path_general = os.getcwd() + '/'	#use current working dir path
	path_general	= '/var/local'
	path_log	= '/log'
	path_run	= '/run'
	path		= '/'

	file_run	= '_runtime.log'
	file_log	= '_dummyTIME.log'
	file_error	= '_error.log'

	ready = False
	separator	= '\t'
	newline		= ''
	timestamp_short = False

	error_message = ''

	def __init__(self,path = '',log_type = 'error', fulldate = False):
		self.path = '/' + path
		# update date
		if fulldate:
			self.file_log	= '_' + datetime.now().strftime("%Y%m%d-%H%M%S") + '.log'
		else:
			self.file_log	= '_' + datetime.now().strftime("%Y%m%d") + '.log'

		if  log_type == 'run':
			self.logfile = self.path_general + self.path_run + self.path + self.file_run
			self.openmode='w' #open run-time-log file, delete content first.
			self.timestamp_short = True
		elif log_type ==  'log':
			self.logfile = self.path_general + self.path_log + self.path + self.file_log
			self.openmode='w'
			self.timestamp_short = True
		elif log_type ==  'error':
			self.logfile    = self.path_general + self.path_run + self.path + self.file_error
			self.openmode	= 'w' #make new log file
			self.separator	= ':\t'
			self.newline	= '\n'


	def open(self):
		try:
			self.fso = open(self.logfile, self.openmode)
		except:
			self.error_message = 'cannot open log file at ' + self.logfile
			raise
		try:
			self.fso.close()
		except:
			self.error_message = 'cannot close log file at ' + self.logfile
			raise
		else:
			self.ready=True


	def timestamp(self):
		if self.timestamp_short:
			return str(time())
		else:
			return datetime.now().strftime("%Y-%m-%d %H:%M:%S") #use %f for microseconds

	def write(self, strWrite):
		with open(self.logfile, 'a') as fso:
			fso.write(self.timestamp())
			fso.write(self.separator)
			fso.write(strWrite)
			fso.write(self.newline)
			fso.close
	def setNewline(self, newline='\n'):
		self.newline = newline
	def getError(self):
		return self.error_message
	def getLogFile(self):
		return self.logfile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python

import MKTerminal,MKSerial,MKParser,MKDatabase,MKFlowMain
from MKLogFile import MKLogFileHandler
import threading	# multithreading
import time			# sleep

#def transmitter(mySerial arduino, myTerminal terminal):

def main():
    def myTransmitter(arduino, terminal):
        #while arduino.isAlive() and terminal.isAlive():
        parser = MKParser.MKParser()
        database = MKDatabase.MKDatabase()
        newFile = True
        while True:
            if terminal.isReady():
                arduino.send(terminal.getMessage())
            if arduino.isReady():
                parser.input(arduino.getMessage())
                oneline = parser.oneline()
                if parser.getStatus():
                    terminal.display(oneline)
                    #terminal.display("")
                if not parser.isHeadline() and parser.getStatus():
                    database.setData(parser.get(2), parser.get(5), parser.get(8), parser.get(11))
                    database.setSetpoint(parser.get(3), parser.get(6), parser.get(9), parser.get(12))
                if database.isRecording():
                    if newFile:
                        logfile = MKLogFileHandler('mkmain','log',True)
                        logfile.open()
                        database.setLogFile(logfile.getLogFile())
                        newFile = False
                    logfile.write(oneline)
                else:
                    newFile = True
            if database.isReady():
                arduino.send(database.getMessage())
            time.sleep(0.1)
    threads = []

    # Create new threads
    arduino = MKSerial.MKSerial('arduino','/dev/ttyACM0',9600)
    terminal = MKTerminal.MKTerminal()
    ethanol = MKFlowMain.MKFlow('/dev/ttyUSB2', '/dev/ttyUSB3', 0)
    argon = MKFlowMain.MKFlow('/dev/ttyUSB0', '/dev/ttyUSB1', 1)
    transmitter = threading.Thread(target=myTransmitter, args=(arduino,terminal))
    transmitter.setDaemon(True) # never care about it anymore

    # Add threads to thread list
    threads.append(arduino)
    threads.append(terminal)
    threads.append(transmitter)#append after arduino and terminal!
    threads.append(ethanol)
    threads.append(argon)

    # Start new Threads
    for t in threads:
        t.start()

    terminal.join()
main()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python
from MKLogFile import MKLogFileHandler
import sys

class MKParser:
	name = 'parser'
	message = ['time','Temperature','temp','spTemp','Pressure','press','spPress','Argon','argon','spArgon','EtOH','etoh','spEtoh']
	length = 0
	status = False
	def __init__(self):
		try:
			self.error = MKLogFileHandler(self.name,'error')
			self.error.open()
		except:
			print 'Error creating log-file: error.log'
			print self.error.getError()
			sys.exit(1)
		try:
			self.log = MKLogFileHandler(self.name,'run')
			self.log.open()
			self.log.setNewLine('\n')
		except:
			self.error.write('error opening log file')
	def input(self,strInput):
		#self.log.write('input received')
		if self.parse(strInput):
			# process content and save as array
			self.status = True
		else:
			self.error.write('length missmatch in string. Counting ' + str(self.length) + ' items')
			self.status = False
		return self.status
	def parse(self,strInput,intArduinoVersion=None):
		if intArduinoVersion is None:
			intArduinoVersion = 2
			# Default Version (from master thesis) is 2
		# strInput is the string that gets extracted
		# intArduinoVersion is the version
		success = False
		strInput = strInput.replace('\n','')
		extractMe = strInput.split("\t")
		self.length = len(extractMe)
		if intArduinoVersion == 2:
			if self.length == 23:
				self.message[0]=extractMe[0]
				self.message[2]=extractMe[2] # temp
				self.message[3]=extractMe[5] # sptemp
				self.message[5]=extractMe[9] # press
				self.message[6]=extractMe[12]# sppress
				self.message[8]=extractMe[14]# argon
				self.message[9]=extractMe[17]# spargon
				self.message[11]=extractMe[19]# etoh
				self.message[12]=extractMe[22]# spetoh
				success = True
		elif intArduinoVersion == 3:
			if self.length == 18:
				self.message[0]=extractMe[0]
				# self.message[1] = Temperature
				self.message[2]=extractMe[1] # temp
				self.message[3]=extractMe[4] # sptemp
				# self.message[4] = Pressure
				self.message[5]=extractMe[7] # press
				self.message[6]=extractMe[10]# sppress
				# self.message[7] = Argon
				self.message[8]=extractMe[11]# argon
				self.message[9]=extractMe[14]# spargon
				# self.message[10] = Argon
				self.message[11]=extractMe[15]# etoh
				self.message[12]=extractMe[18]# spetoh
				success = True
		return success
	def oneline(self):
		strReturn = ''
		for i in self.message:
			strReturn += i + '\t'
		strReturn += '\n'
		return strReturn
	def isHeadline(self):
		if self.get(2) == "temp":
			return True
		else:
			return False
	def getStatus(self):
		return self.status
	def get(self,position):
		return self.message[position]
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python

import serial				# imports pyserial API
import struct				# imports struct API
import sys				# sys.exit()
import threading			# multithreading
import time				# sleep
from MKLogFile import MKLogFileHandler

LF = serial.to_bytes([10])
CR = serial.to_bytes([13])
CRLF = serial.to_bytes([13, 10])

class MKSerial:
    sendBuffer = ''
    receiveBuffer = []
    receiveBufferReady = False
    alive=False

    serialName='unnamed'
    serialPort=''
    serialBaudrate = 9600
    newline=CRLF
    def __init__(self,serialName='arduino',serialPort='/dev/ttyACM0',serialBaudrate=9600):
        self.serialName = serialName
        self.serialPort = serialPort
        self.serialBaudrate = serialBaudrate

        self.openLogfiles()
        self.openSerial()
        #self.start() # automatically start threading

    def openLogfiles(self):
        try:
            self.error = MKLogFileHandler(self.serialName,'error')
            self.error.open()
        except:
            print 'Error creating file: error.log'
            sys.exit(1)
        try:
            self.run = MKLogFileHandler(self.serialName,'run')
            self.run.open()
        except:
            self.error.write('error opening log files')

    def openSerial(self):
        self.error.write('opening RS232 for ' + self.serialName + ' on Port ' + self.serialPort + ' with ' + str(self.serialBaudrate) + ' baud')
        try:
            self.serial = serial.Serial()
            self.serial.port     = self.serialPort
            self.serial.baudrate = self.serialBaudrate
            self.serial.timeout  = 3     # required so that the readline can exit.
        except self.serial.SerialException, e:
            self.error.write('error initializing serial class')
            sys.exit(1)
        try:
            self.serial.open()
            self.serial.setDTR(True)
            self.serial.setRTS(True)
        except self.serial.SerialException, e:
            self.error.write('Could not open serial port')
            sys.exit(1)
        while not self.serial.isOpen():
            self.error.write('opening port ...')
            time.sleep(0.1)
        self.error.write('port open.')

    def start(self):
        self.error.write('starting thread')
        try:
            self.alive = True
            self.thread = threading.Thread(target=self.infiniteloop)
            self.thread.daemon = True # never care about it anymore
            self.thread.start()
        except:
            self.error.write('error stopping thread')

    def join(self):
        self.thread.join()

    def stop(self):
        self.error.write('stopping thread ...')
        try:
            self.alive = False
            #self.join()
        except:
            self.error.write('error stopping thread')
        else:
            self.error.write('thread stopped.')
        self.close()

    def close(self):
        self.error.write('closing serial connection ...')
        try:
            self.serial.setDTR(False)
            self.serial.setRTS(False)
            self.serial.close()
        except:
            self.error.write('error closing serial connection')
        else:
            self.error.write('serial connection closed.')

    def send(self,message):
        self.sendBuffer+=message

    def receive(self, message):
        self.receiveBuffer.append(message)
        self.receiveBufferReady = True

    def read(self):
        val = self.serial.readline();   #read line by line data from the serial file
        self.receive(val)              #clear from time to time!
        self.run.write(val)

    def write(self):
        if len(self.sendBuffer) > 0 and self.isAlive():
            self.error.write('sending message ' + self.sendBuffer)
            try:
                self.serial.write(self.sendBuffer)
                self.serial.write(self.newline)
            except:
                self.error.write('error sending message')
            else:
                self.sendBuffer=''

    def infiniteloop(self):
        self.error.write('starting infinite Loop on ' + self.serial.port)
        while (self.serial.isOpen() and self.alive):
            try:
                self.read()
                self.write()
            except:
                self.error.write('Exception in infinite Loop')
                self.stop()
            time.sleep(0.1)
        self.error.write('stoped infinite loop.')

    def isAlive(self):
        return self.alive

    def isReady(self):
        return self.receiveBufferReady

    def getMessage(self):
        text = self.receiveBuffer.pop(0)
        self.receiveBufferReady = len(self.receiveBuffer) > 0
        return text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python

import sys, os, threading, time

if sys.version_info >= (3, 0):
    def character(b):
        return b.decode('latin1')
else:
    def character(b):
        return b

class MKConsole(object):
    def __init__(self):
        self.fd = sys.stdin.fileno()

    def getkey(self):
        c = os.read(self.fd, 1)
        return c

class MKTerminal(object):
    EXITCHARCTER = 'q'
    message = ''
    alive=False
    readyToSend=False
    readyToDisplay=False

    def __init__(self,echo=False):
        self.echo = echo
        self.Console = MKConsole()

    def start(self):
        self.alive = True
        self.readerthread = threading.Thread(target=self.read)
        self.readerthread.setDaemon(True) # never care about it anymore
        self.readerthread.start()
        self.writerthread = threading.Thread(target=self.write)
        self.writerthread.setDaemon(True) # never care about it anymore
        self.writerthread.start()

    def join(self):
        self.readerthread.join()

    def stop(self):
        self.alive = False

    def send(self,myChar):
        if myChar == '\n':
            self.readyToSend=True
        else:
            self.message+=myChar

    def read(self):
        while self.alive:
            try:
                b = self.Console.getkey()
                c = character(b)
                if c == self.EXITCHARCTER:
                    raise KeyboardInterrupt
                self.send(c)
                if self.echo == True:
                    sys.stdout.write(c)
                    sys.stdout.flush()
                time.sleep(0.1)
            except KeyboardInterrupt:
                self.stop()

    def write(self):
        while self.alive:
            if self.readyToDisplay:
                sys.stdout.write(self.printme)
                sys.stdout.flush()
                self.readyToDisplay=False
            time.sleep(0.1)

    def isAlive(self):
        return self.alive

    def isReady(self):
        return self.readyToSend

    def getMessage(self):
        message=self.message
        self.message=''
        self.readyToSend=False
        return message

    def display(self,text):
        self.readyToDisplay=True
        self.printme=text
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python
import Tkinter as tk
import os
import MKDatabase

class swntReactorGUI(object):
    def __init__(self, master, **kwargs):
        # database connection as client
        self.db = MKDatabase.MKDatabase(True)
        # fullscreen
        self.master=master
        pad=30
        self.geometry_toggle='200x200+0+0'
        self.master.geometry("{0}x{1}+0+0".format(master.winfo_screenwidth(), master.winfo_screenheight()-pad))
        # keyboard capture events.
        self.master.bind('<F4>',self.geometryToggle)
        self.master.bind('<Escape>',self.buttonShutdownFocus)
        self.master.bind("<Key>", self.key)
        self.master.bind("<Return>", self.newline)
        self.master.bind("<KP_Enter>", self.newline)
        # GUI basics
        self.GUI(**kwargs)
        # init GUI Variables
        self.Buffer = ''
        self.bufferDisplay.set("press key")
        self.timerDisplay.set("")
        self.pressureDisplay.set("")
        self.temperatureDisplay.set("")
        self.argonDisplay.set("")
        self.ethanolDisplay.set("")
        self.timer = 0

    def key(self, event):
        self.LabelBufferDisplay.configure(fg='blue')
        self.BufferAdd(event.char)

    def newline(self, event):
        self.LabelBufferDisplay.configure(fg='red')
        self.BufferSend()

    def BufferAdd(self,char):
        if char == '/':
            char = 't'
        elif char == '+':
            char = 'e'
        elif char == '-':
            char = 'a'
        elif char == '*':
            char = 'p'
        self.Buffer = self.Buffer + char
        self.bufferDisplay.set(self.Buffer)

    def BufferSend(self):
        if self.db.setMessage(self.Buffer):
            self.LabelBufferDisplay.configure(fg='green')
            self.BufferClear()

    def BufferClear(self):
        self.Buffer = ''

    def update(self):
        # update database
        self.db.getAll()
        self.timer += 0.1
        # update labels
        self.timerDisplay.set("{0:.1f}".format(self.timer))
        self.pressureDisplay.set("{0:.2f} ({1:.0f})".format(self.db.pressure, self.db.spPressure))
        self.temperatureDisplay.set("{0:.0f} ({1:.0f})".format(self.db.temperature, self.db.spTemperature))
        self.argonDisplay.set("{0:.1f} ({1:.0f})".format(self.db.argon, self.db.spArgon))
        self.ethanolDisplay.set("{0:.1f} ({1:.0f})".format(self.db.ethanol, self.db.spEthanol))
        self.ip.set(self.db.getIP())
        if self.db.isRecording(): # needs to call database every time
            self.filename.set(self.db.getLogFile())
        else:
            self.filename.set('')
        # schedule next call
        self.master.after(100, self.update)

    def GUI(self, **kwargs):
        # buttons
        self.frameQuit = tk.Frame(self.master, **kwargs)
        self.frameQuit.pack(side=tk.BOTTOM)
        self.filename = tk.StringVar()
        tk.Label(self.frameQuit, textvariable=self.filename, font=("Helvetica", 14)).pack(side=tk.LEFT)
        tk.Button(self.frameQuit, text="record", command=self.buttonRecord).pack(side=tk.LEFT)
        tk.Button(self.frameQuit, text="quit", command=self.master.quit).pack(side=tk.LEFT)
        self.buttonShutdown = tk.Button(
                self.frameQuit,
                text="shutdown", fg="red",
                command=self.buttonShutdownExec)
        self.buttonShutdown.pack(side=tk.LEFT)
        self.buttonShutdown.bind('<Return>', self.buttonShutdownExec)

        # label timer
        self.timerDisplay = tk.StringVar()
        tk.Label(self.frameQuit, textvariable=self.timerDisplay, font=("Helvetica", 14)).pack(side=tk.LEFT)

        # label ip
        self.ip = tk.StringVar()
        tk.Label(self.frameQuit, textvariable=self.ip, font=("Helvetica", 14)).pack(side=tk.LEFT)

        # input
        self.frameInput = tk.Frame(self.master, **kwargs)
        self.frameInput.pack(side=tk.TOP)
        self.bufferDisplay = tk.StringVar()
        tk.Label(self.frameInput, text="Input: ", font=("Helvetica", 64)).pack(side=tk.LEFT)
        self.LabelBufferDisplay = tk.Label(self.frameInput, textvariable=self.bufferDisplay, font=("Helvetica", 64))
        self.LabelBufferDisplay.pack(side=tk.RIGHT)

        # temperature
        self.frameTemperature = tk.Frame(self.master, **kwargs)
        self.frameTemperature.pack(side=tk.TOP)
        self.temperatureDisplay = tk.StringVar()
        tk.Label(self.frameTemperature, text="Temperature: ", font=("Helvetica", 64)).pack(padx=5,side=tk.LEFT)
        tk.Label(self.frameTemperature, textvariable=self.temperatureDisplay, font=("Helvetica", 64)).pack(side=tk.RIGHT)

        # pressure
        self.framePressure = tk.Frame(self.master, **kwargs)
        self.framePressure.pack(side=tk.TOP)
        self.pressureDisplay = tk.StringVar()
        tk.Label(self.framePressure, text="Pressure: ", font=("Helvetica", 64)).pack(padx=5,side=tk.LEFT)
        tk.Label(self.framePressure, textvariable=self.pressureDisplay, font=("Helvetica", 64)).pack(side=tk.RIGHT)

        # Argon
        self.frameArgon = tk.Frame(self.master, **kwargs)
        self.frameArgon.pack(side=tk.TOP)
        self.argonDisplay = tk.StringVar()
        tk.Label(self.frameArgon, text="Argon: ", font=("Helvetica", 64)).pack(padx=5,side=tk.LEFT)
        tk.Label(self.frameArgon, textvariable=self.argonDisplay, font=("Helvetica", 64)).pack(side=tk.RIGHT)

        # Ethanol
        self.frameEthanol = tk.Frame(self.master, **kwargs)
        self.frameEthanol.pack(side=tk.TOP)
        self.ethanolDisplay = tk.StringVar()
        tk.Label(self.frameEthanol, text="Ethanol: ", font=("Helvetica", 64)).pack(padx=5,side=tk.LEFT)
        tk.Label(self.frameEthanol, textvariable=self.ethanolDisplay, font=("Helvetica", 64)).pack(side=tk.RIGHT)

    def buttonShutdownExec(self,event=None):
        command1 = "/home/matthias/Documents/programs/python/swnt-reactor/script/shutdown"
        command2 = "/home/pi/programs/swnt-reactor/script/shutdown"
        if os.path.isfile(command1):
            print "shutdown was pressed in test environment"
        if os.path.isfile(command2):
            self.filename.set("shutdown in progress ...")
            os.system(command2)
        self.master.quit

    def buttonShutdownFocus(self,event=None):
        self.buttonShutdown.focus()

    def buttonRecord(self, event=None):
        if self.db.isRecording():
            success = self.db.stopRecording()
        else:
            success = self.db.startRecording()
        if success:
            self.timer = 0

    def geometryToggle(self,event=None):
        # event will be filled with Tkinter.Event when Key is Pressed. 
        # save old geom.
        geometry_old=self.master.winfo_geometry()
        # toggle to new
        self.master.geometry(self.geometry_toggle)
        self.geometry_toggle=geometry_old

root=tk.Tk()
app=swntReactorGUI(root)
root.after(100, app.update)
root.mainloop()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
import time

from MKArduino import MKArduino

arduino = MKArduino()

threads = []
threads.append(arduino)

for t in threads:
    print "starting ..."
    t.debug(1)
    t.start()

try:
    while True:
        time.sleep(10)
except KeyboardInterrupt:
    for t in threads:
        print "stoping ..."
        t.stop()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
import time

from MKFlowMain import MKFlow

ethanol = MKFlow('/dev/ttyUSB2', '/dev/ttyUSB3', 0)
argon   = MKFlow('/dev/ttyUSB0', '/dev/ttyUSB1', 1)

threads = []
threads.append(ethanol)
threads.append(argon)

for t in threads:
    print "starting ..."
    t.start()
    t.debug()

try:
    while True:
        time.sleep(10)
except KeyboardInterrupt:
    for t in threads:
        print "stoping ..."
        t.stop()

The flow for various carbon sources is digitally read with the Bronkhorst flow meters using a custom python program

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#!/usr/bin/env python
import threading

from MKFlowInput import MKFlowInput
from MKFlowCommunication import MKFlowCommunication
from MKDatabase import MKDatabase

class MKFlow():
    def __init__(self, port1 = '/dev/ttyUSB2', port2 = '/dev/ttyUSB3', instrument = 0):
        self.Input = MKFlowInput()
        self.Storage = MKFlowCommunication()
        self.Database = MKDatabase()
        self.Input.setBridge(port1, port2)
        #self.Input.setLogFile('/home/matthias/Documents/programs/python/swnt-reactor/data/log/bridge/testing/log1.log')
        self.instrument = instrument
        self.debugging = False

    def debug(self):
        self.debugging = True

    def start(self):
        try:
            self.thread = threading.Thread(target=self.loop)
            self.thread.daemon = True
            self.thread.start()
        except:
            self.stop()
            raise

    def stop(self):
        self.Input.stop()

    def join(self):
        self.thread.join()

    def loop(self):
        while self.Input.isAlive():
            try:
                Message = self.Input.getMessage()
                SubMessage = Message.getSubType()
            except:
                break
            else:
                if not Message.isInvalid:
                    node = Message.getNode()
                    sequence = Message.getSequence()
                    Entity = self.Storage.Node(node).Sequence(sequence)
                    try:
                        if Message.isError:
                            Entity.setError(Message)
                        elif Message.isStatus:
                            Entity.setStatus(Message)
                        elif Message.isSent:
                            Entity.setAnswer(Message)
                        elif Message.isSentStatus:
                            Entity.setWriteRequest(Message)
                        elif Message.isRequest:
                            Entity.setReadRequest(Message)
                        else:
                            raise
                    except:
                        print "--- something happened ---"
                        try:
                            Message.stdout()
                            Submessage.stdout()
                            Entity.reset()
                        except:
                            raise
                    else:
                        # store if two messages are present in current Entity
                        # reset Entity afterwards.
                        try:
                            if self.debugging:
                                bufferSize = self.Input.input.bufferSize()
                                if bufferSize > 0:
                                    print "Buffer: %i" % bufferSize
                                Entity.output()
                            Entity.save(self.Database, self.instrument)
                            pass
                        except ValueError:
                            raise
                            pass
                        except:
                            raise
                else:
                    print "--- invalid ---"
                    SubMessage.stdoutShort(Message.stdoutShort())
                    pass

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
#!/usr/bin/env python

import MySQLdb as mysqlconnector
from MySQLdb.constants import CLIENT
import os
import socket
import decimal
import struct
from time import sleep
import multiprocessing

from MKFlowMessage import FBconvertLong # converter for long numbers to float and percent
#cvd-client->rbBmSDP7fSKp87b5

class MKDatabase(object):
    sql = ""
    connected = False
    ready = False
    messageID = -1
    hostname = ""
    client = False
    recording = False
    recordingID = -1
    fileName = ""
    storage_description = 50
    storage_values = 30

    def __init__(self, isClient = False):
        self.client = isClient
        self.test()
        decimal.getcontext().prec = 2

    def open(self):
        try:
            if not self.checkIP():
                print "server unavailable"
                raise
            dbHost = self.getIP()
            dbName = "cvd"
            if self.client:
                dbUser = "cvd-client"
                dbPass = "rbBmSDP7fSKp87b5"
            else:
                if self.getHostname() == "lab117":
                    dbUser = "cvd-server"
                    dbPass = "uhNYLSHRn2f3LhmS"
                elif self.getHostname() == "uk-work":
                    dbUser = "cvd-uk-work"
                    dbPass = "ARHFpNwB5ZbZQdqh"
                else:
                    dbUser = "cvd-other"
                    dbPass = "bmF94vVXAB5yf7Mx"
            self.db = mysqlconnector.connect(
                host = dbHost,
                user = dbUser,
                passwd = dbPass,
                db = dbName,
                client_flag = CLIENT.FOUND_ROWS,
                connect_timeout = 1
                )
        except:
            print "database open failed."
            self.close()
            return False
        else:
            print "connected as user: %s" % dbUser
            self.connected = True
            return True
    def close(self):
        try:
            self.db.close()
        except:
            if not self.checkIP():
                print "connection lost. Database could not be closed normal"
                self.connected = False
        else:
            self.connected = False

    def isOpen(self):
        #if not self.connected:
        #    return False
        #try:
        #    stats = self.db.stat()
        #    if stats == 'MySQL server has gone away':
        #        self.close()
        #except:
        #    self.connected = False
        return self.connected

    def write_without_timeout(self, db, sql, connection):
        try:
            cursor = db.cursor()
            cursor.execute(sql)
            affectedRows = cursor.rowcount
            cursor.close()
            db.commit()
        except:
            affectedRows = 0
            try:
                self.db.rollback()
            except:
                pass
        connection.send(affectedRows)
        connection.close()

    def read_without_timeout(self, db, sql, connection):
        affectedRows = 0
        try:
            cursor = db.cursor()
            cursor.execute(sql)
            data = cursor.fetchone()
            cursor.close()
        except:
            connection.send([])
        else:
            connection.send(data)
        connection.close()

    # from alex martelli on http://stackoverflow.com/questions/1507091/python-mysqldb-query-timeout
    def write(self, sql, update = False):
        if not self.isOpen():
            if not self.open():
                raise
        conn_parent, conn_child = multiprocessing.Pipe(False)
        subproc = multiprocessing.Process(target = self.write_without_timeout,
                                          args = (self.db, sql, conn_child))
        subproc.start()
        subproc.join(1)
        if conn_parent.poll():
            affectedRows = conn_parent.recv()
            # on update statements rise if no lines were affected
            if update and affectedRows == 0:
                raise UpdateError('UPDATE statement failed')
            else:
                return affectedRows
        subproc.terminate()
        raise TimeoutError("Query %r ran for >%r" % (sql, timeout))

    def read(self, sql):
        if not self.isOpen():
            if not self.open():
                raise
        conn_parent, conn_child = multiprocessing.Pipe(False)
        subproc = multiprocessing.Process(target = self.read_without_timeout,
                                          args = (self.db, sql, conn_child))
        subproc.start()
        subproc.join(1)
        if conn_parent.poll():
            data = conn_parent.recv()
            try:
                if len(data) == 0:
                    raise
            except:
                return []
            else:
                return data
        else:
            subproc.terminate()
            return []

    def writeArduino(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                print "writeArduino failed: create database and try again."
                self.createArduino()
                self.resetArduino()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeRecording(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                self.createRecording()
                self.resetRecording()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeFlowbus(self, sql):
        try:
            self.write(sql)
        except:
            try:
                self.createFlowbus()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def writeMessage(self, sql):
        try:
            self.write(sql, True)
        except:
            try:
                self.createMessage()
                self.resetMessage()
                self.write(sql)
            except:
                self.close()
                return False
            else:
                return True
        else:
            return True

    def test(self):
        print "-- starting self-test --"
        self.open()
        sql="SELECT VERSION()"
        data = self.read(sql)
        self.close()
        print "MySQL version : %s " % data
        print "-- self test complete --"

    def getHostname(self):
        if self.hostname == "":
            self.hostname = socket.gethostname()
        return self.hostname

    def isServer(self):
        if (self.getHostname() == "lab117"):
            return True
        else:
            return False

    def getIP(self):
        if self.isServer():
            ip = 'localhost'
        else:
            ip = "132.187.77.71"
        return ip

    def checkIP(self, ip = ""):
        if len(ip) == 0:
            ip = self.getIP()
        if ip == "localhost":
            return True
        command = "ping -c 1 -W 1 " + ip
        print "executing '" + command + "'"
        if os.system(command + " > /dev/null") == 0:
            return True
        else:
            print "ip not found. sleeping penalty."
            sleep(1)
            return False

    def createArduino(self):
        sql = """CREATE TABLE IF NOT EXISTS `runtime_arduino` (
                `temperature` decimal(6,2) NOT NULL DEFAULT '0',
                `pressure` decimal(6,2) NOT NULL DEFAULT '0',
                `argon` decimal(6,2) NOT NULL DEFAULT '0',
                `ethanol` decimal(6,2) NOT NULL DEFAULT '0',
                `spTemperature` int(11) NOT NULL DEFAULT '0',
                `spPressure` int(11) NOT NULL DEFAULT '1000',
                `spEthanol` int(11) NOT NULL DEFAULT '0',
                `spArgon` int(11) NOT NULL DEFAULT '0'
                ) ENGINE=MEMORY DEFAULT Charset=utf8;"""
        self.write(sql)

    def resetArduino(self):
        sql = """INSERT INTO `runtime_arduino`
                        (`temperature`, `pressure`, `argon`, `ethanol`, `spTemperature`, `spPressure`, `spEthanol`, `spArgon`)
                    VALUES
                        (0, 0, 0, 0, 0, 0, 0, 0);"""
        self.write(sql)

    def createFlowbus(self):
        sql = """
        CREATE TABLE IF NOT EXISTS `runtime_flowbus`
        (
            `instrument`    smallint(2) NOT NULL DEFAULT '0',
            `process`       smallint(2) NOT NULL,
            `flowBus`       smallint(2) NOT NULL,
            `dataType`      tinyint(1) NOT NULL DEFAULT '0',
            `parameter`     binary(%i) NOT NULL DEFAULT '0',
            `data`          binary(%i) NOT NULL DEFAULT '0',
            `time`          decimal(7,2) NOT NULL DEFAULT '0',
            UNIQUE KEY `instrument` (`instrument`,`process`,`flowBus`)
        )
        ENGINE=MEMORY
        DEFAULT CHARSET=utf8;""" % (self.storage_description, self.storage_values)
        self.write(sql)

    def createRecording(self):
        sql = """CREATE TABLE IF NOT EXISTS `runtime_recording` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `recording` tinyint(4) NOT NULL DEFAULT '0',
                `id_recording` int(11) DEFAULT '0',

                PRIMARY KEY (`id`)
                ) ENGINE=MEMORY DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

        sql = """CREATE TABLE IF NOT EXISTS `recording` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                `recording` tinyint(4) NOT NULL DEFAULT '0',
                `filename` text NOT NULL,
                PRIMARY KEY (`id`)
                ) ENGINE=InnoDB DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

    def resetRecording(self):
        sql = """INSERT INTO `runtime_recording`
                        (`recording`)
                    VALUES
                        (0)"""
        self.write(sql)

    def createMessage(self):
        sql = """CREATE TABLE IF NOT EXISTS `cvd`.`runtime_message` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `ready` tinyint(4) NOT NULL DEFAULT '0',
                `id_message` int(11) DEFAULT '0',

                PRIMARY KEY (`id`)
                ) ENGINE=MEMORY DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

        sql = """CREATE TABLE IF NOT EXISTS  `cvd`.`message` (
                `id` int(11) NOT NULL AUTO_INCREMENT,
                `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                `processed` tinyint(4) NOT NULL DEFAULT '0',
                `text` text NOT NULL,
                PRIMARY KEY (`id`)
                ) ENGINE=InnoDB DEFAULT Charset=utf8 AUTO_INCREMENT=40;"""
        self.write(sql)

    def resetMessage(self):
        sql = """INSERT INTO `cvd`.`runtime_message`
                        (`ready`)
                    VALUES
                        (0)"""
        self.write(sql)
        sql = """DELETE FROM `cvd`.`message`
                    WHERE `processed` = 1"""
        self.write(sql)

    def setData(self, data, setpoint):
        try:
            self.temperature = decimal.Decimal(data[0])
            self.pressure = decimal.Decimal(data[1])
            self.argon = decimal.Decimal(data[2])
            self.ethanol = decimal.Decimal(data[3])
        except:
            self.temperature = 0.00
            self.pressure = 0.00
            self.argon = 0.00
            self.ethanol = 0.00
        try:
            self.spTemperature = int(setpoint[0])
            self.spPressure = int(setpoint[1])
            self.spArgon = int(setpoint[2])
            self.spEthanol = int(setpoint[3])
        except:
            self.spTemperature = 0
            self.spPressure = 1000
            self.spEthanol = 0
            self.spArgon = 0
        sql = """UPDATE `cvd`.`runtime_arduino`
                SET	`temperature`	= %s,
                        `pressure`	= %s,
                        `ethanol`	= %s,
                        `argon`		= %s,
                        `spTemperature` = %s,
                        `spPressure`	= %s,
                        `spEthanol`	= %s,
                        `spArgon`	= %s;"""  % (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon)
        return self.writeArduino(sql)

    def setLogFile(self, fileName):
        id = self.getRecordingID()
        if id < 0:
            return False
        sql = """UPDATE `cvd`.`recording`
                    SET	`filename`  = '%s',
                        `recording` = 1
                    WHERE `id` = %i
                    LIMIT 1;""" % (fileName, id)
        if not self.writeRecording(sql):
            return False
        if self.getLogFile() == fileName:
            return True
        else:
            return False

    def isRecording(self):
        sql = """SELECT `recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 1:
            return False
        else:
            if data[0]:
                return True
            else:
                return False

    def stopRecording(self):
        sql = """UPDATE `cvd`.`runtime_recording`
                SET	`recording` = 0;"""
        if not self.writeRecording(sql):
            return False
        sql = """UPDATE `cvd`.`recording`
                SET	`recording` = 0
                WHERE `recording` = 1;"""
        if not self.writeRecording(sql):
            return False
        self.recordingID = -1
        return True

    def startRecording(self, filename = ''):
        self.stopRecording

        sql = """INSERT INTO `cvd`.`recording` (
                `id` ,`time` , `recording` , `filename` )
                VALUES (
                NULL , CURRENT_TIMESTAMP , 1, '%s')""" % filename
        if not self.writeRecording(sql):
            return False
        sql = """SELECT `id`
        FROM `cvd`.`recording`
                WHERE `recording` = 1
                LIMIT 1;"""
        data = self.read(sql)
        if not len(data) == 1:
            return False
        sql = """UPDATE `cvd`.`runtime_recording`
                SET `id_recording` = %i,
                    `recording` = 1
                LIMIT 1;""" % data
        if not self.writeRecording(sql):
            return False
        return True

    def getRecordingID(self):
        sql = """SELECT `id_recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            return int(data[0])
        else:
            return -1

    def getLogFile(self):
        # get id from memory table
        recordingID = self.getRecordingID()
        # update filename from disc table if not already saved in class
        if not (recordingID == self.recordingID) or len(self.fileName) == 0:
            print "querying filename from sql table"
            self.close()
            sql = """SELECT `filename`
                    FROM `cvd`.`recording`
                    WHERE `id` = %i;""" % recordingID
            data = self.read(sql)
            if len(data) == 1:
                self.fileName = data[0]
            else:
                self.fileName = ''
            self.recordingID = recordingID
        return self.fileName

    def setMessage(self, message):
        sql = """INSERT INTO `cvd`.`message`
                (`text`) VALUES ('%s');"""  % (message)
        if self.writeMessage(sql):
            return self.updateMessage()
        return False

    def updateMessage(self):
        sql = """SELECT `id` FROM `cvd`.`message`
                WHERE `processed` = 0
                LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            id_message = data[0]
            ready = 1
        else:
            ready = 0
            id_message = -1

        sql = """UPDATE `cvd`.`runtime_message`
                SET `ready` = %i,
                    `id_message` = %i
                LIMIT 1;""" % (ready, id_message)
        return self.writeMessage(sql)

    def isReady(self):
        if self.ready:
            return True
        sql = """SELECT `ready`, `id_message`
                FROM `cvd`.`runtime_message`;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 2:
                data = (0,-1)
        (self.ready, self.messageID) = data
        if self.ready:
            return True
        else:
            return False

    def getMessage(self):
        self.message = ""
        # read from runtime (memory table)
        if self.isReady():
            # ready flag did also read out messageID.
            # get message string from cvd.message
            sql = """SELECT `text`
                FROM `cvd`.`message`
                WHERE `id` = %i
                LIMIT 1;""" % self.messageID
            data = self.read(sql)
            if (len(data) == 1):
                self.message = data[0]
                # mark message in cvd.message as processed 
                sql = """UPDATE `cvd`.`message`
                    SET `processed` = 1
                    WHERE `id` = %i;""" % self.messageID
                self.writeMessage(sql)
            self.updateMessage()
        # reset readout
        self.ready = False
        return self.message

    def setFlowbus(self, instrument, process, flowBus, dataTypeString, dataInput, timeInput, parameterName):
        time = decimal.Decimal(timeInput)
        parameterName = parameterName.encode("hex")
        if (dataTypeString == "character"):
            dataType = 0
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "integer"):
            dataType = 1
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "long"):
            dataType = 2
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "string"):
            dataType = 3
            data = dataInput.encode("hex")
        else:
            raise ValueError("can not identify dataType at setFlowBus()")

        sql = """
        INSERT INTO `cvd`.`runtime_flowbus`
        (`instrument`,`process`,`flowBus`,`dataType`,`data`,`time`, `parameter`)
        VALUES
        (%i, %i, %i, %i, UNHEX(LPAD('%s',%i,'0')), %.2f, UNHEX(LPAD('%s',%i,'0')))""" % (instrument, process, flowBus, dataType, data, self.storage_values * 2, time, parameterName, self.storage_description * 2)
        sql += """
        ON DUPLICATE KEY UPDATE
        `data` = UNHEX(LPAD('%s',%i,'0')),
        `time` = %.2f;""" % (data, self.storage_values * 2, time)
        self.writeFlowbus(sql)

    def getFlowbus(self, instrument, process, flowBus):
        sql = """
        SELECT `dataType`,TRIM(LEADING '0' FROM HEX(`data`)),`time`,TRIM(LEADING '0' FROM HEX(`parameter`))
        FROM `cvd`.`runtime_flowbus`
        WHERE
        (   `instrument`    = %i
        AND `process`       = %i
        AND `flowBus`       = %i);
        """ % (instrument, process, flowBus)
        data = self.read(sql)
        if (len(data) == 4):
            (dataType, dataOut, timeOut, parameter) = data
        else:
            return (-1,-1,-1)

        parameter = parameter.decode("hex")
        time = decimal.Decimal(timeOut)
        if (dataType == 0):
            data = int(dataOut, 16)
        elif(dataType == 1):
            data = int(dataOut, 16)
        elif(dataType == 2):
            data = FBconvertLong(process, flowBus, int(dataOut,16))
        elif(dataType == 3):
            data = dataOut.decode("hex")
        else:
            raise ValueError("can not identify dataType at getFlowBus()")

        return (parameter, data, time)

    def getAll(self):
        sql = """SELECT temperature, pressure, ethanol, argon,
                             spTemperature, spPressure, spEthanol, spArgon
                      FROM `cvd`.`runtime_arduino`
                      LIMIT 1"""
        data = self.read(sql)
        if len(data) == 0:
            print "database readout failed for arduino!"
            data = (-1,-1,-1,-1, -1,-1,-1,-1)
        (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon) = data

class UpdateError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class TimeoutError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

        sql = """UPDATE `cvd`.`runtime_arduino`
                SET	`temperature`	= %s,
                        `pressure`	= %s,
                        `ethanol`	= %s,
                        `argon`		= %s
                        `spTemperature` = %s,
                        `spPressure`	= %s,
                        `spEthanol`	= %s,
                        `spArgon`	= %s
                LIMIT 1;"""  % (self.temperature, self.pressure, self.ethanol, self.argon, setpoint[0], setpoint[1], setpoint[2], setpoint[2])
        return self.writeArduino(sql)

    def setLogFile(self, fileName):
        id = self.getRecordingID()
        if id < 0:
            return False
        sql = """UPDATE `cvd`.`recording`
                    SET	`filename`  = '%s',
                        `recording` = 1
                    WHERE `id` = %i
                    LIMIT 1;""" % (fileName, id)
        if not self.writeRecording(sql):
            return False
        if self.getLogFile() == fileName:
            return True
        else:
            return False

    def isRecording(self):
        sql = """SELECT `recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 1:
            return False
        else:
            if data[0]:
                return True
            else:
                return False

    def stopRecording(self):
        sql = """UPDATE `cvd`.`runtime_recording`
                SET	`recording` = 0;"""
        if not self.writeRecording(sql):
            return False
        sql = """UPDATE `cvd`.`recording`
                SET	`recording` = 0
                WHERE `recording` = 1;"""
        if not self.writeRecording(sql):
            return False
        self.recordingID = -1
        return True

    def startRecording(self, filename = ''):
        self.stopRecording

        sql = """INSERT INTO `cvd`.`recording` (
                `id` ,`time` , `recording` , `filename` )
                VALUES (
                NULL , CURRENT_TIMESTAMP , 1, '%s')""" % filename
        if not self.writeRecording(sql):
            return False
        sql = """SELECT `id`
        FROM `cvd`.`recording`
                WHERE `recording` = 1
                LIMIT 1;"""
        data = self.read(sql)
        if not len(data) == 1:
            return False
        sql = """UPDATE `cvd`.`runtime_recording`
                SET `id_recording` = %i,
                    `recording` = 1
                LIMIT 1;""" % data
        if not self.writeRecording(sql):
            return False
        return True

    def getRecordingID(self):
        sql = """SELECT `id_recording`
                    FROM `cvd`.`runtime_recording`
                    LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            return int(data[0])
        else:
            return -1

    def getLogFile(self):
        # get id from memory table
        recordingID = self.getRecordingID()
        # update filename from disc table if not already saved in class
        if not (recordingID == self.recordingID) or len(self.fileName) == 0:
            print "querying filename from sql table"
            self.close()
            sql = """SELECT `filename`
                    FROM `cvd`.`recording`
                    WHERE `id` = %i;""" % recordingID
            data = self.read(sql)
            if len(data) == 1:
                self.fileName = data[0]
            else:
                self.fileName = ''
            self.recordingID = recordingID
        return self.fileName

    def setMessage(self, message):
        sql = """INSERT INTO `cvd`.`message`
                (`text`) VALUES ('%s');"""  % (message)
        if self.writeMessage(sql):
            return self.updateMessage()
        return False

    def updateMessage(self):
        sql = """SELECT `id` FROM `cvd`.`message`
                WHERE `processed` = 0
                LIMIT 1;"""
        data = self.read(sql)
        if (len(data) == 1):
            id_message = data[0]
            ready = 1
        else:
            ready = 0
            id_message = -1

        sql = """UPDATE `cvd`.`runtime_message`
                SET `ready` = %i,
                    `id_message` = %i
                LIMIT 1;""" % (ready, id_message)
        return self.writeMessage(sql)

    def isReady(self):
        if self.ready:
            return True
        sql = """SELECT `ready`, `id_message`
                FROM `cvd`.`runtime_message`;"""
        try:
            data = self.read(sql)
        except:
            return False
        if not len(data) == 2:
                data = (0,-1)
        (self.ready, self.messageID) = data
        if self.ready:
            return True
        else:
            return False

    def getMessage(self):
        self.message = ""
        # read from runtime (memory table)
        if self.isReady():
            # ready flag did also read out messageID.
            # get message string from cvd.message
            sql = """SELECT `text`
                FROM `cvd`.`message`
                WHERE `id` = %i
                LIMIT 1;""" % self.messageID
            data = self.read(sql)
            if (len(data) == 1):
                self.message = data[0]
                # mark message in cvd.message as processed 
                sql = """UPDATE `cvd`.`message`
                    SET `processed` = 1
                    WHERE `id` = %i;""" % self.messageID
                self.writeMessage(sql)
            self.updateMessage()
        # reset readout
        self.ready = False
        return self.message

    def setFlowbus(self, instrument, process, flowBus, dataTypeString, dataInput, timeInput, parameterName):
        time = decimal.Decimal(timeInput)
        parameterName = parameterName.encode("hex")
        if (dataTypeString == "character"):
            dataType = 0
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "integer"):
            dataType = 1
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "long"):
            dataType = 2
            data = format(int(dataInput), 'x')
        elif(dataTypeString == "string"):
            dataType = 3
            data = dataInput.encode("hex")
        else:
            raise ValueError("can not identify dataType at setFlowBus()")

        sql = """
        INSERT INTO `cvd`.`runtime_flowbus`
        (`instrument`,`process`,`flowBus`,`dataType`,`data`,`time`, `parameter`)
        VALUES
        (%i, %i, %i, %i, UNHEX(LPAD('%s',%i,'0')), %.2f, UNHEX(LPAD('%s',%i,'0')))""" % (instrument, process, flowBus, dataType, data, self.storage_values * 2, time, parameterName, self.storage_description * 2)
        sql += """
        ON DUPLICATE KEY UPDATE
        `data` = UNHEX(LPAD('%s',%i,'0')),
        `time` = %.2f;""" % (data, self.storage_values * 2, time)
        self.writeFlowbus(sql)

    def getFlowbus(self, instrument, process, flowBus):
        sql = """
        SELECT `dataType`,TRIM(LEADING '0' FROM HEX(`data`)),`time`,TRIM(LEADING '0' FROM HEX(`parameter`))
        FROM `cvd`.`runtime_flowbus`
        WHERE
        (   `instrument`    = %i
        AND `process`       = %i
        AND `flowBus`       = %i);
        """ % (instrument, process, flowBus)
        data = self.read(sql)
        if (len(data) == 4):
            (dataType, dataOut, timeOut, parameter) = data
        else:
            return (-1,-1,-1)

        parameter = parameter.decode("hex")
        time = decimal.Decimal(timeOut)
        if (dataType == 0):
            data = int(dataOut, 16)
        elif(dataType == 1):
            data = int(dataOut, 16)
        elif(dataType == 2):
            data = FBconvertLong(process, flowBus, int(dataOut,16))
        elif(dataType == 3):
            data = dataOut.decode("hex")
        else:
            raise ValueError("can not identify dataType at getFlowBus()")

        return (parameter, data, time)

    def getAll(self):
        sql = """SELECT temperature, pressure, ethanol, argon,
                             spTemperature, spPressure, spEthanol, spArgon
                      FROM `cvd`.`runtime_arduino`
                      LIMIT 1"""
        data = self.read(sql)
        if len(data) == 0:
            print "database readout failed for arduino!"
            data = (-1,-1,-1,-1, -1,-1,-1,-1)
        (self.temperature, self.pressure, self.ethanol, self.argon, self.spTemperature, self.spPressure, self.spEthanol, self.spArgon) = data

class UpdateError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class TimeoutError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python
#import struct                  # imports struct API ???
import os                       # imports file system functions like open()
import time                     # sleep
import subprocess               # socat
import MKFlowMessage            # Message analyis class
from MKFlowSocat import MKFlowSocat
from MKFlowLogFile import MKFlowLogFile
# Main Class
class MKFlowInput():
    def __init__(self):
        self.reset()

    def reset(self):
        self.message1 = ''
        self.message2 = ''
        self.socat = False
        self.log = False
        self.port1 = ''
        self.port2 = ''

    def setLogFile(self,filename):
        self.input = MKFlowLogFile(filename)
        self.start()

    def setBridge(self, port1, port2):
        self.input = MKFlowSocat(port1, port2)
        self.start()

    def start(self):
        self.input.open()
        self.input.start()

    def stop(self):
        self.input.stop()

    def readOut(self):
        while not self.input.isReady():
            time.sleep(0.1)
        self.message1, self.message2 = self.input.read()

    def getMessage(self):
        try:
            Message = self.Message()
            self.readOut()
            Message.process(self.message2, self.message1)
            if Message.isInvalid:
                # try next message. maybe they belong together
                message_buffer = self.message2
                self.readOut()
                Message.process(self.message2, self.message1)
                if Message.isInvalid:
                    message_buffer += self.message2
                    Message.process(message_buffer, self.message1)
        except:
            self.input.stop()
            raise
        else:
            return Message

    def isAlive(self):
        return self.input.isAlive()

    # shortcut
    class Message(MKFlowMessage.MKFlowMessage):
        class Invalid(MKFlowMessage.MKFlowInvalid):
            pass
        class Error(MKFlowMessage.MKFlowError):
            pass
        class Status(MKFlowMessage.MKFlowStatus):
            pass
        class Request(MKFlowMessage.MKFlowRequest):
            pass
        class Sent(MKFlowMessage.MKFlowSent):
            pass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/usr/bin/env python

import subprocess
import threading

class MKFlowSocat:
    def __init__(self, port1, port2):
        self.buffer = []
        self.port1 = port1
        self.port2 = port2

    def start(self):
        try:
            self.alive = True
            self.thread = threading.Thread(target=self.loop)
            self.thread.daemon = True # never care about it anymore
            self.thread.start()
        except:
            print 'error stopping thread'

    def stop(self):
        try:
            self.close()
        except:
            print 'error stopping thread'
        else:
            self.alive = False

    def join(self):
        self.thread.join()

    def open(self):
        exe = 'socat -x %s,raw,echo=0,b38400,crnl %s,raw,echo=0,b38400,crnl' % (self.port1, self.port2)
        self.popen = subprocess.Popen(exe.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    def close(self):
        try:
            self.popen.terminate()
            self.popen.wait()
        except:
            print "error closing socat"
            raise

    def loop(self):
        for line in iter(self.popen.stdout.readline, b''):
            self.buffer.append(line)

    def read(self):
        if self.buffer[0][0] == "<" or self.buffer[0][0] == ">":
            return self.buffer.pop(0), self.buffer.pop(0)
        elif len(self.buffer[0]) == 0:
            # message 1 is empty. pop two messages
            return self.buffer.pop(0), self.buffer.pop(0)
        else:
            # message 1 is missing in rpi's socat
            return '', self.buffer.pop(0)

    def isReady(self):
        size = self.bufferSize()
        if size > 30:
            self.buffer = [self.buffer[-1]]
            print "buffer overflow"
            return True
        return size > 0

    def bufferSize(self):
        return len(self.buffer)

    def isAlive(self):
        return self.alive
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
#!/usr/bin/env python

import MKDatabase
from MKFlowMessage import FBconvertLong
# Main Class
class MKFlowCommunication():
    def __init__(self):
        self.nullNode = MKFlowNode(-1)
        self.node_numbers = []
        self.nodes = []

    def addNode(self, number):
        if not self.isNode(number):
            self.node_numbers += [number]
            node = MKFlowNode(number)
            self.nodes += [node]

    def isNode(self, number):
        return (number in self.node_numbers)

    def getNode(self, number):
        if self.isNode(number):
            id = self.node_numbers.index(number)
            node = self.nodes[id]
            return node
        else:
            return self.nullNode

    def Node(self, number):
        if not self.isNode(number):
            self.addNode(number)
        return self.getNode(number)

class MKFlowNode():
    def __init__(self, node):
        self.node = node
        self.nullSequence = MKFlowSequence(-1)
        self.sequence_numbers = []
        self.sequences = []

    def getNumber(self):
        return self.node

    def addSequence(self, number):
        if not self.isSequence(number):
            self.sequence_numbers += [number]
            sequence = MKFlowSequence(number)
            self.sequences += [sequence]

    def isSequence(self, number):
        return (number in self.sequence_numbers)

    def getSequence(self, number):
        if self.isSequence(number):
            id = self.sequence_numbers.index(number)
            sequence = self.sequences[id]
            return sequence
        else:
            return self.nullSequence

    def Sequence(self, number):
        if not self.isSequence(number):
            self.addSequence(number)
        return self.getSequence(number)

class MKFlowSequence():
    def __init__(self, sequence):
        self.sequence = sequence
        self.nullChild = MKFlowModbus(-1)
        self.reset()

    def reset(self):
        self.parameter_ids = []
        self.parameters = []

        self.hasAnswer = False
        self.hasRequest = False
        self.RequestHasValue = False
        self.isAnalysed = False
        self.isStatus = False
        self.isError = False
        self.isValid = False

    def setReadRequest(self, Message):
        self.Request = Message.getSubType()
        self.hasRequest = True
        self.hasAnswer = False
        self.RequestHasValue = False
        self.timeRequest = Message.getSeconds()

    def setWriteRequest(self, Message):
        self.setReadRequest(Message)
        self.RequestHasValue = True

    def setStatus(self, Message):
        self.setAnswer(Message)
        self.isStatus = True

    def setError(self, Message):
        self.setAnswer(Message)
        self.isError = True

    def setAnswer(self, Message):
        self.Answer = Message.getSubType()
        self.timeAnswer = Message.getSeconds()
        self.hasAnswer = True
        self.isStatus = False
        self.isError = False

    def check(self):
        if self.hasAnswer and self.hasRequest:
            if abs(self.timeAnswer - self.timeRequest) > 10:
                return False
            else:
                return True
        else:
            return False

    def addParameter(self, index):
        if not self.isParameter(index):
            self.parameter_ids += [index]
            Parameter = MKFlowModbus(index)
            self.parameters += [Parameter]

    def isParameter(self, index):
        return index in self.parameter_ids

    def getParameter(self, index):
        if self.isParameter(index):
            id = self.parameter_ids.index(index)
            Parameter = self.parameters[id]
            return Parameter
        else:
            return self.nullChild

    def Parameter(self, index):
        if not self.isParameter(index):
            self.addParameter(index)
        return self.getParameter(index)

    def analyse(self):
        if self.check():
            # Process Request
            for process in self.Request.process:
                for parameter in process.Parameter:
                    self.Parameter(parameter.getIndex()).setNumber(parameter.getNumber())
                    self.Parameter(parameter.getIndex()).setProcess(parameter.getProcess())
                    self.Parameter(parameter.getIndex()).setName(parameter.getHuman())
                    self.Parameter(parameter.getIndex()).setLength(parameter.getLength())
                    if self.RequestHasValue:
                        self.Parameter(parameter.getIndex()).setValue(parameter.getValue())
                        self.Parameter(parameter.getIndex()).setDataType(parameter.getDataType())

            # Process Answer
            if not self.RequestHasValue and not self.isStatus and not self.isError:
                for process in self.Answer.process:
                    for parameter in process.Parameter:
                        self.Parameter(parameter.getIndex()).setValue(parameter.getValue())
                        self.Parameter(parameter.getIndex()).setDataType(parameter.getDataType())

            # Answer with Status or Error and set valid
            self.valid = True
            self.analyseStatus()
            self.analyseError()
            self.isAnalysed = True

    def analyseStatus(self):
        if self.isStatus:
            if self.Answer.getStatus() == 0:
                # no error
                self.valid = True
            elif self.Answer.getStatus() > 3 and self.Answer.getStatus() < 8:
                # Parameter Error
                where = self.Answer.getIndex()
                count = 4
                for index in self.parameter_ids:
                    Parameter = self.getParameter(index)
                    if not self.RequestHasValue:
                        Parameter.setInvalid()
                    if where == count:
                        self.error = "Status: %s\t Parameter: %s" % (self.Answer.getHuman(), Parameter.getName())
                        Parameter.setError(self.Answer.getHuman())
                    count += int(Parameter.getLength())
            else:
                self.error = self.Answer.getHuman()
                self.valid = False

    def analyseError(self):
        if self.isError:
            self.error = self.Answer.getText()
            self.valid = False
        if not self.valid:
            for index in self.parameter_ids:
                Parameter = self.getParameter(index)
                Parameter.setError(self.error)

    def output(self):
        if self.check():
            if not self.isAnalysed:
                self.analyse()
            for index in self.parameter_ids:
                Parameter = self.getParameter(index)
                try:
                    Parameter.stdout()
                except:
                    self.stdout()
                    raise ValueError("error in MKFlowCommunication ModbusClass stdout")

    def save(self, Database, instrument = 0):
        if self.check():
            reset = True
            if not self.isAnalysed:
                self.analyse()
            for index in self.parameter_ids:
                Parameter = self.getParameter(index)
                try:
                    if not Parameter.isInvalid():
                        valid = True
                        proc = Parameter.getProcess()
                        fbnr = Parameter.getNumber()
                        name = Parameter.getName()
                        value = Parameter.getValue()
                        dataType = Parameter.getDataType()
                        time = self.timeAnswer
                        parameter = Parameter.getName()
                        reset = Database.setFlowbus(instrument, proc, fbnr, dataType, value, time, parameter)
                except:
                    self.stdout()
                    print "error storing parameter."
                    reset = False
            if reset:
                self.reset()
            else:
                print "Sequence not cleared."

    def stdout(self):
        print "--- sequence: %i ---" % self.sequence
        print "---- parameters: %s ----" % self.parameter_ids
        if self.hasRequest:
            print "---- request ----"
            self.Request.stdout()
        if self.hasAnswer:
            print "---- answer ----"
            self.Answer.stdout()


class MKFlowModbus():
    def __init__(self, index):
        self.index = index
        self.invalid = False
        self.error = ''
        self.value = None
        self.human = ''
        self.dataType = 'invalid' # readybility. store as string
        self.length = 0

    def setProcess(self, process):
        self.process = process

    def getProcess(self):
        return self.process

    def setNumber(self, number):
        self.number = number

    def getNumber(self):
        return self.number

    def setValue(self, value):
        self.value = value

    def getValue(self):
        return self.value

    def setDataType(self, dataType):
        self.dataType = dataType

    def getDataType(self):
        return self.dataType

    def setName(self, string):
        self.human = string

    def getName(self):
        return self.human

    def setInvalid(self):
        self.invalid = True

    def setLength(self, length):
        self.length = length

    def getLength(self):
        return self.length

    def setError(self, error):
        self.error = error
        self.setInvalid()

    def isInvalid(self):
        if self.invalid:
            return True
        else:
            return False

    def stdout(self):
        returnarray = [self.isInvalid(), self.getProcess(), self.getNumber(), self.getName()]
        if not self.invalid:
            returnarray += [FBconvertLong(self.getProcess(), self.getNumber(), self.getValue())]
        else:
            returnarray += [self.error]
        print '\t'.join(str(i) for i in returnarray)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class MKFlowLogFile():
    def __init__(self, logfile):
        self.reset()
        self.logfile = logfile

    def reset(self):
        self.logfile = ''
        self.openmode = 'r'
        self.fsoOpen = False

    def open(self):
        if not self.fsoOpen:
            try:
                self.fso = open(self.logfile, self.openmode)
            except:
                self.close()
                raise ValueError('cannot open log file at ' + self.logfile)
            else:
                self.fsoOpen = True

    def close(self):
        if self.fsoOpen:
            try:
                self.fso.close()
            except:
                raise ValueError('cannot close log file at ' + self.logfile)
            else:
                self.fsoOpen = False

    def read(self):
        try:
            self.open()
            message1 = self.fso.readline()
            message2 = self.fso.readline()
            if (len(message2) == 0):
                raise EOF("end of file reached")
        except EOF:
            self.close()
            raise EOF
        except:
            message1 = ''
            message2 = ''
        return message1, message2

    def start(self):
        self.alive = True

    def stop(self):
        try:
            self.close()
            self.reset()
        except:
            print 'error closing logfile'
        else:
            self.alive = False

    def isReady(self):
        # ready while alive for log file
        return self.isAlive()

    def isAlive(self):
        return self.alive

class EOF(Exception):
    pass

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
#!/usr/bin/env python
from datetime import datetime       # datetime supports milliseconds. time doesn't
import sys                          # used to get line number of error print sys.exc_traceback.tb_lineno
import struct                       # used to convert bin-->float

def FBconvertLong(process, fbnr, value):
    if process == 33:
        if fbnr == 0 or fbnr == 3:
            return toFloat(value)
    elif process == 114:
        if fbnr == 1:
            return float(value) / 16777215.0 * 100.0
    return value

def toFloat(integer):
    # convert integer to IEE float
    return struct.unpack(">f", struct.pack(">I", integer))[0]

def fromFloat(floatingpoint):
    # convert IEE float to integer
    return struct.unpack('<I', struct.pack('<f', float(floatingpoint)))[0]

class MKFlowMessage():
    def __init__(self):
        self.clear()

    # input binary message and socat headline
    def process(self, message, socat = ''):
        self.message1 = socat
        self.message2 = message
        self.resetSubType()
        self.analyse()

    def resetSubType(self):
        # message type identifier
        self.commandByte= -3
        self.isInvalid  = False # -2
        self.isError    = False # -1
        self.isStatus   = False #  0
        self.isSent     = False #  2
        self.isSentStatus = False # 1
        self.isRequest  = False #  4
        # clear message objects
        self.Invalid    = MKFlowInvalid()
        self.Error      = MKFlowError()
        self.Status     = MKFlowStatus()
        self.Sent       = MKFlowSent()
        #self.SentStatus = MKFlowSentStatus()
        self.Request    = MKFlowRequest()

    def getSubType(self):
        if self.isInvalid:
            return self.Invalid
        if self.isError:
            return self.Error
        if self.isStatus:
            return self.Status
        if self.isSent or self.isSentStatus:
            return self.Sent
        if self.isRequest:
            return self.Request

    def clear(self):
        self.data = []
        self.node       = -1
        self.sequence   = -1
        self.length     = -1
        self.dataLength = -1
        self.commandByte= -1
        self.commandByteHuman = ''
        self.commandByteHumanShort = ''
        self.direction      = ''
        self.time           = datetime
        self.time_human     = "%Y/%m/%d;%H:%M:%S.%f"
        self.time_second    = 0.00

    def getNode(self):
        return self.node

    def getSequence(self):
        return self.sequence

    def getLength(self):
        return self.length

    def setLength(self, length):
        self.length = length

    def getDirection(self):
        return self.direction

    def getTime(self):
        return self.time_human

    def getSeconds(self):
        return self.time_second

    def getCommandByte(self):
        return self.commandByte

    def getCommandByteShort(self):
        return self.commandByteHumanShort

    def trim(self):
        self.message1 = self.message1.replace('\n','')
        self.message2 = self.message2.replace('\n','')
        while self.message2[0:1] == " ":
            self.message2 = self.message2[1:]
            if len(self.message2) == 0:
                break

    def split(self):
        self.message1 = self.message1.split(" ")
        self.message2 = self.message2.split(" ")

    def analyseDirection(self):
        if self.message1[0] == '>':
            self.direction = 'right'
        elif self.message1[0] == '<':
            self.direction = 'left'
        else:
            self.direction = 'none'

    def analyseTime(self):
        if len(self.message1) > 1:
            self.time = datetime.strptime(self.message1[1] + ';' + self.message1[2], "%Y/%m/%d;%H:%M:%S.%f")
        else:
            self.time = datetime.now()
        self.time_human = self.time.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
        self.time_second = self.time.hour*60*60 + self.time.minute * 60 + self.time.second + self.time.microsecond/1e6

    def parseMessage(self):
        try:
            self.trim()
            self.split()
        except:
            self.Invalid.add('MKFlowMessage:parseMessage:\tError while parsing')
            raise

    def parseData(self):
        #replace two DLE (escaped) sequences (10 10) by one databyte (10)
        try:
            start = 0
            foundOne = False
            while len(self.data) -1 > start:
                found = self.data[start:].index(16)
                if self.data[(found+start+1)] == 16:
                    self.data.pop(found+start)
                    start = start + found
                    foundOne = True
                else:
                    start +=1
        except ValueError:
            # 10 not found. Ignore
            pass
        except:
            # other errors --> report
            self.Invalid.add('parseData Error at found: %i start: %i len: %i' % (found, start, len(self.data)))
            raise

    def checkMessage2(self):
        # dataLength is length-Byte
        if not (self.dataLength==len(self.data)-3) and not (self.dataLength == 0):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tError in socat output: length-Byte in message does not match socat length')
        if (len(self.message2)<6):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tMessage shorter than 6 bytes: DLE STX SEQUENCE NODE ... DLE ETX')
            # DLE STX SEQUENCE NODE LEN DATA DLE ETX
        if not ((self.message2[0] == "10") and (self.message2[1] == "02")):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tno valid message beginning:\tDLE (0x10) STX (0x02) missing')
        if not ((self.message2[-2] == "10") and (self.message2[-1] == "03")):
            self.Invalid.add('MKFlowMessage:parseMessage2:\tno valid message ending:\tDLE (0x10) ETX (0x03) missing')
        if self.Invalid.isActive():
            raise ValueError('MKFlowMessage:parseMessage2:\tError while processing Message2')

    def analyse(self):
        try:
            self.parseMessage()
            self.analyseMessage()
            self.analyseMessageType()
            self.analyseCommandByte()
            if self.Invalid.isActive():
                raise ValueError('MKFlowMessage:analyse:\tMessage invalid')

        except Exception as e:
            self.Invalid.add('MKFlowMessage:analyse:\t'+str(e))
            self.Invalid.add('MKFlowMessage:analyse:\tLine number:\t' + str(sys.exc_traceback.tb_lineno))
            self.Invalid.add('MKFlowMessage:analyse:\tcommandByte:\t' + str(self.commandByte))
            self.commandByte = -2
            self.isInvalid = True
            #raise

    def analyseMessage(self):
        # DLE STX SEQUENCE NODE LEN DATA DLE ETX
        try:
            self.analyseMessage1()
            self.analyseMessage2()
        except:
            self.Invalid.add('MKFlowMessage:analyseMessage:\tError while extracting data.')
            raise

    def analyseMessage1(self):
        try:
            self.analyseDirection()
            self.analyseTime()
            self.setLength(len(self.message2))
        except:
            self.Invalid.add('MKFlowMessage:analyseMessage1:\tError while processing message1')
            raise

    def analyseMessage2(self):
        # DLE STX SEQUENCE NODE LEN DATA DLE ETX
        try:
            self.data = [int(i,16) for i in self.message2[2:-2]]
            self.parseData()
            self.sequence   = self.data[0]
            self.node       = self.data[1]
            self.dataLength = self.data[2]
            self.checkMessage2()
        except:
            self.Invalid.add('MKFlowMessage:analyseMessage2:\tError while extracting data.')
            if len(self.data)<3:
                self.Invalid.add('MKFlowMessage:analyseMessage2:\tlen(data)='+str(len(self.data)))
            raise

    def analyseMessageType(self):
        # check if the message contains information
        try:
            # length 0 means.Error.Status in message. next byte after 00 contains.Error code.
            if self.data[2] == 0:
                self.commandByte = -1
            else:
                self.commandByte = self.data[3]
        except:
            self.Invalid.add('MKFlowMessage:analyseMessageType Error while retrieving Message')
            raise

    def analyseCommandByte(self):
        try:
            self.translateCommandByte()
            if self.commandByte == -2:
                self.isInvalid = True
            elif self.commandByte == -1:
                self.isError = True
                self.Error.set(self.data[3:])
                self.Error.setSequence(self.sequence)
                self.Error.setNode(self.node)
                self.Error.analyse()
            elif self.commandByte == 0:
                self.Status.set(self.data[4:])
                self.Status.analyse()
                self.isStatus = True
            elif self.commandByte == 4:
                self.Request.set(self.data[4:])
                self.isRequest = True
            elif (self.commandByte == 2):
                self.Sent.set(self.data[4:])
                self.isSent = True
            elif (self.commandByte == 1):
                self.Sent.set(self.data[4:])
                self.isSentStatus = True
            else:
                myError = 'MKFlowMessage:analyseCommandByte commandByte ' + str(self.commandbyte) + ' not handled'
                self.Invalid.add(myError)
                self.Invalid.add(self.commandByteHuman)
                raise ValueError(myError)
        except:
            raise

    def translateCommandByte(self):
        try:
            if self.commandByte == -2:
                self.commandByteHuman = "Invalid Message: Error in Program"
            elif self.commandByte == -1:
                self.commandByteHuman = "Valid Message: Error Message from Device"
            elif self.commandByte == 0:
                self.commandByteHuman = "Status message"
            elif self.commandByte == 1:
                self.commandByteHuman = "Send Parameter with destination address, will be answered with type 00 command"
            elif self.commandByte == 2:
                self.commandByteHuman = "Send Parameter with destination address, no.Status.Requested"
            elif self.commandByte == 3:
                self.commandByteHuman = "Send Parameter with source address, no.Status.Requested"
            elif self.commandByte == 4:
                self.commandByteHuman = "Request Parameter, will be answered with type 02 or 00 command"
            elif self.commandByte == 6:
                self.commandByteHuman = "Stop Process"
            elif self.commandByte == 7:
                self.commandByteHuman = "Start Process"
            elif self.commandByte == 8:
                self.commandByteHuman = "Claim Process"
            elif self.commandByte == 9:
                self.commandByteHuman = "Unclaim Process"
            else:
                self.commandByteHuman = "unhandled commandByte: " + str(self.commandByte)
                raise ValueError(self.commandByteHuman)
            if self.commandByte == 4:
                self.commandByteHumanShort = 'REQ'
            elif (self.commandByte >= 1) and (self.commandByte <= 3):
                self.commandByteHumanShort = 'ST' + str(self.commandByte)
            if self.commandByte == -2:
                self.commandByteHumanShort = 'INV'
            if self.commandByte == -1:
                self.commandByteHumanShort = 'ERR'
            if self.commandByte == 0:
                self.commandByteHumanShort = 'INF'
        except:
            raise

    def stdout(self):
        print '-- message Class Output begin --'
        print self.message1
        print self.message2
        print 'direction: \t' , self.getDirection()
        print 'length: \t'    , self.getLength()
        print 'time: \t'      , self.getTime()
        print 'seconds: \t'   , self.getSeconds()
        print 'sequence: \t'  , self.getSequence()
        print 'node: \t'      , self.getNode()
        print 'command: \t'   , self.getCommandByte()
        print '-- message end --'

    def stdoutShort(self, indent=''):
        return '\t'.join(str(i) for i in [indent, self.getNode(), self.getSequence(), self.getCommandByteShort()])

# Message Type Classes
class MKFlowInvalid():
    def __init__(self):
        self.value = ''
        self.active = False

    def add(self, newValue):
        self.active = True
        self.value += str(newValue) + '\n'

    def get(self):
        if self.active:
            return self.value
        else:
            return

    def isActive(self):
        return self.active

    def stdout(self, leading = '\t'):
        print leading, "-- MKFlowRequest Class Output Begin --"
        print leading, "Value:\t", self.value
        print leading, "-- MKFlowRequest Class Output End --"

    def stdoutShort(self, indent=''):
        return '\t'.join(str(i) for i in [indent, self.isActive()])

class MKFlowError():
    def __init__(self):
        self.value = 0
        self.node = -1
        self.sequence = -1
        self.text = ''
        self.active = False

    def set(self, data):
        self.active = True
        self.data = data
        self.value = data[0]

    def setNode(self, node):
        self.node = node

    def setSequence(self,sequence):
        self.sequence = sequence

    def analyse(self):
        if not len(self.data) == 1:
            raise ValueError('Error Class has received too many input bytes')
        self.translate()

    def getText(self):
        return self.text

    def getHuman(self):
        return self.getText()

    def getValue(self):
        return self.value

    def getData(self):
        return self.data

    def translate(self):
        if self.active:
            self.text = 'device returned error code ' + str(self.value) + ': '
            if (self.value==3):
                self.text += 'propar protocol error'
            elif (self.value==4):
                self.text += 'propar protocol error (or CRC error)'
            elif (self.value==5):
                if self.node >= 0:
                    self.text += 'destination node address ' + str(self.node) + ' rejected'
                else:
                    self.text += 'destination node address rejected'
                if self.sequence >= 0:
                    self.text += ' for sequence ' + str(self.sequence)
            elif (self.value==9):
                self.text += 'response message timeout'

    def stdout(self, indent = '\t'):
        print indent, '-- Error Class Output Begin--'
        print indent, 'Error Data:\t', self.getData()
        print indent, 'Error Number:\t', self.getValue()
        print indent, 'Error Message:\t', self.getText()
        print indent, '-- Error Class Output Begin--'

    def stdoutShort(self, indent=''):
        return '\t'.join(str(i) for i in [indent, self.getValue(),None,None,self.getHuman()])

class MKFlowStatus():
    def __init__(self):
        self.data = []
        self.status         = -1
        self.status_human   = ''
        self.index          = -1
        self.active = False

    def set(self, data):
        self.data   = data
        self.active = True

    def analyse(self):
        if len(self.data)>0:
            self.status = self.data[0]
            self.status_hex = hex(self.data[0])[2:]
        if len(self.data)>1:
            self.index  = self.data[1]
        self.humanize()

    def isActive(self):
        return self.active

    def getStatus(self):
        return self.status

    def getStatusByte(self):
        return self.status

    def getIndex(self):
        return self.index

    def getHuman(self):
        if self.status_human == '':
            self.humanize()
        return self.status_human

    def humanize(self):
        # dict([('no Status', -1), ('no Error', 0), ...])
        if self.status == -1:
            self.status_human = 'no Status'
        elif self.status_hex == '0':
            self.status_human = 'no Error'
        elif self.status_hex == '1':
            self.status_human = 'Process claimed'
        elif self.status_hex == '2':
            self.status_human = 'Command Error'
        elif self.status_hex == '3':
            self.status_human = 'Process Error'
        elif self.status_hex == '4':
            self.status_human = 'Parameter Error'
        elif self.status_hex == '5':
            self.status_human = 'Parameter type Error'
        elif self.status_hex == '6':
            self.status_human = 'Parameter value Error'
        elif self.status_hex == '7':
            self.status_human = 'Network not active'
        elif self.status_hex == '8':
            self.status_human = 'Time-out start character'
        elif self.status_hex == '9':
            self.status_human = 'Time-out serial line'
        elif self.status_hex == 'a':
            self.status_human = 'Hardware memory Error'
        elif self.status_hex == 'b':
            self.status_human = 'Node number Error'
        elif self.status_hex == 'c':
            self.status_human = 'General communication Error'
        elif self.status_hex == 'd':
            self.status_human = 'Read only Parameter.'
        elif self.status_hex == 'e':
            self.status_human = 'Error PC-communication'
        elif self.status_hex == 'f':
            self.status_human = 'No RS232 connection'
        elif self.status_hex == '10':
            self.status_human = 'PC out of memory'
        elif self.status_hex == '11':
            self.status_human = 'Write only Parameter'
        elif self.status_hex == '12':
            self.status_human = 'System configuration unknown'
        elif self.status_hex == '13':
            self.status_human = 'No free node address'
        elif self.status_hex == '14':
            self.status_human = 'Wrong interface type'
        elif self.status_hex == '15':
            self.status_human = 'Error serial port connection'
        elif self.status_hex == '16':
            self.status_human = 'Error opening communication'
        elif self.status_hex == '17':
            self.status_human = 'Communication Error'
        elif self.status_hex == '18':
            self.status_human = 'Error interface bus master'
        elif self.status_hex == '19':
            self.status_human = 'Timeout answer'
        elif self.status_hex == '1a':
            self.status_human = 'No start character'
        elif self.status_hex == '1b':
            self.status_human = 'Error first digit'
        elif self.status_hex == '1c':
            self.status_human = 'Buffer overflow in host'
        elif self.status_hex == '1d':
            self.status_human = 'Buffer overflow'
        elif self.status_hex == '1e':
            self.status_human = 'No answer found'
        elif self.status_hex == '1f':
            self.status_human = 'Error closing communication'
        elif self.status_hex == '20':
            self.status_human = 'Synchronisation Error'
        elif self.status_hex == '21':
            self.status_human = 'Send Error'
        elif self.status_hex == '22':
            self.status_human = 'Protocol Error'
        elif self.status_hex == '23':
            self.status_human = 'Buffer overflow in module'

    def stdout(self, indent = '\t'):
        print indent, "MKFlowStatus Class Output Begin"
        print indent, "Data Array:\t", self.data
        print indent, "Status :   \t", self.getStatus()
        print indent, "Status :   \t", self.getHuman()
        print indent, "Index Byte:\t", self.getIndex()
        print indent, "MKFlowStatus Class Output End"

    def stdoutShort(self,indent=''):
        print indent, 'INFO\t', self.getStatus(), '\t\t', self.getHuman()

# subclass for MKFlowProcess
class MKFlowParameter():
    def __init__(self):
        self.dataType   = 'undefined'

        self.index      = -1
        self.process    = -1
        self.number     = -1

        self.dataLength = 0
        self.dataStart = 0

        self.data       = []
        self.human      = ''

    def set(self, data):
        self.data = data

    def setLength(self,length=0):
        self.length = length

    def setProcess(self, process):
        self.process = process

    def analyse(self):
        pass

    def analyseData(self):
        self.index = self.data[0]
        self.number= self.data[0]

    def analyseDataType(self, number = -3):
        if number == -3:
            number = self.number
        identifier = format(number, '08b')[1:3]
        if identifier == '00':
            self.dataType = 'character'
        elif identifier == '01':
            self.dataType = 'integer'
        elif identifier == '10':
            self.dataType = 'long'
        elif identifier == '11':
            self.dataType = 'string'

    def substractDataType(self, number = -3):
        if number == -3:
            number = self.number
        if self.dataType == 'string':
            number -= int('60',16)
        elif self.dataType == 'long':
            number -= int('40',16)
        elif self.dataType == 'integer':
            number -= int('20',16)
        elif self.dataType == 'character':
            pass
        return number

    def isChained(self):
        if self.index >= 128:
            return True
        else:
            return False

    def getData(self):
        return self.data[0:self.length]

    def getIndex(self):
        index = self.index
        if self.isChained():
            index -= 128
        if index >= 32:
            index = self.substractDataType(index)
        return index

    def getProcess(self):
        return self.process

    def getNumber(self):
        return self.number

    def getDataType(self):
        return self.dataType

    def getLength(self):
        return self.length

    def getHuman(self):
        if self.human == '':
            self.humanize()
        return self.human

    def humanize(self):
        getIdent = dict([('0:0', 0), ('0:1', 1), ('0:2', 2), ('0:3', 3), ('0:4', 4), ('0:5', 5), ('0:10', 6), ('1:0', 7), ('1:1', 8), ('1:2', 9), ('1:3', 10), ('1:4', 11), ('1:5', 12), ('1:6', 13), ('1:7', 14), ('1:8', 15), ('1:9', 16), ('1:10', 17), ('1:11', 18), ('1:12', 19), ('1:13', 20), ('1:14', 21), ('1:15', 22), ('1:16', 23), ('1:17', 24), ('1:18', 25), ('1:19', 26), ('1:20', 27), ('0:12', 28), ('0:13', 29), ('0:14', 30), ('9:1', 31), ('10:0', 32), ('10:1', 33), ('10:2', 34), ('114:12', 51), ('115:3', 52), ('116:6', 53), ('114:1', 54), ('117:1', 55), ('117:2', 56), ('115:1', 57), ('116:7', 58), ('115:2', 59), ('114:2', 60), ('114:3', 61), ('116:1', 62), ('116:2', 63), ('116:3', 64), ('116:4', 65), ('114:4', 66), ('116:5', 67), ('115:4', 68), ('115:5', 69), ('115:6', 70), ('114:5', 71), ('117:3', 72), ('117:4', 73), ('115:7', 78), ('114:6', 79), ('0:19', 80), ('114:7', 81), ('114:8', 82), ('114:9', 83), ('114:10', 84), ('114:11', 85), ('114:13', 86), ('114:14', 87), ('114:15', 88), ('113:1', 89), ('113:2', 90), ('113:3', 91), ('113:4', 92), ('118:1', 93), ('118:2', 94), ('118:3', 95), ('118:4', 96), ('118:5', 97), ('118:6', 98), ('118:7', 99), ('118:8', 100), ('118:9', 101), ('118:10', 102), ('114:16', 103), ('113:5', 104), ('115:9', 105), ('116:8', 106), ('115:8', 113), ('113:6', 114), ('97:1', 115), ('97:2', 116), ('97:3', 117), ('97:4', 118), ('97:5', 119), ('97:6', 120), ('104:1', 121), ('104:2', 122), ('104:3', 123), ('104:4', 124), ('104:5', 125), ('104:6', 126), ('104:7', 127), ('1:31', 128), ('104:8', 129), ('113:7', 130), ('33:1', 138), ('33:2', 139), ('114:17', 140), ('33:7', 141), ('33:8', 142), ('33:9', 143), ('33:10', 144), ('115:10', 146), ('33:9', 148), ('33:10', 149), ('33:5', 150), ('33:6', 151), ('33:11', 152), ('33:13', 153), ('97:9', 155), ('104:9', 156), ('33:14', 157), ('33:15', 158), ('33:16', 159), ('33:17', 160), ('33:18', 161), ('33:20', 162), ('115:11', 163), ('114:18', 164), ('114:20', 165), ('114:21', 166), ('114:22', 167), ('114:23', 168), ('33:21', 169), ('113:8', 170), ('113:9', 171), ('113:10', 172), ('113:11', 173), ('113:12', 174), ('118:11', 175), ('115:12', 176), ('113:13', 177), ('113:14', 178), ('113:15', 179), ('113:16', 180), ('97:7', 181), ('33:22', 182), ('0:18', 183), ('0:20', 184), ('123:1', 185), ('123:3', 186), ('123:4', 187), ('123:10', 188), ('114:24', 189), ('115:13', 190), ('115:14', 191), ('116:9', 192), ('115:15', 193), ('115:16', 194), ('115:17', 195), ('115:18', 196), ('33:4', 197), ('125:10', 198), ('125:3', 199), ('125:9', 200), ('125:20', 201), ('115:22', 202), ('125:21', 203), ('33:0', 204), ('33:3', 205), ('33:23', 206), ('119:1', 207), ('119:2', 208), ('119:3', 209), ('119:4', 210), ('119:5', 211), ('119:6', 212), ('116:21', 213), ('116:22', 214), ('116:23', 215), ('116:24', 216), ('116:25', 217), ('116:26', 218), ('116:27', 219), ('116:28', 220), ('117:5', 221), ('33:24', 222), ('117:6', 223), ('33:25', 224), ('33:26', 225), ('33:27', 226), ('33:28', 227), ('33:29', 228), ('33:30', 229), ('114:25', 230), ('114:26', 231), ('114:27', 232), ('114:28', 233), ('114:29', 234), ('0:21', 235), ('115:20', 236), ('33:31', 237), ('33:12', 238), ('33:13', 239), ('33:16', 240), ('33:17', 241), ('33:10', 244), ('33:11', 245), ('113:17', 248), ('113:18', 249), ('113:20', 250), ('113:21', 251), ('113:22', 252), ('114:30', 253), ('113:23', 254), ('113:24', 255), ('113:25', 256), ('113:26', 257), ('113:27', 258), ('113:28', 259), ('113:29', 260), ('113:30', 261), ('113:31', 262), ('116:10', 263), ('116:11', 264), ('116:12', 265), ('116:13', 266), ('116:14', 267), ('65:15', 268), ('116:15', 269), ('116:18', 270), ('116:8', 271), ('116:9', 272), ('104:10', 273), ('104:11', 274), ('65:1', 275), ('116:17', 276), ('116:29', 277), ('116:30', 278), ('116:30', 279), ('116:31', 280), ('121:0', 281), ('121:1', 282), ('121:2', 283), ('121:3', 284), ('121:4', 285), ('121:5', 286), ('114:31', 287), ('65:21', 288), ('65:22', 289), ('65:23', 290), ('65:24', 291), ('65:25', 292), ('116:20', 293), ('115:31', 294), ('104:12', 295), ('104:13', 296), ('104:14', 297), ('125:8', 298), ('125:11', 299), ('124:7', 300), ('124:8', 301), ('124:10', 302), ('124:9', 303), ('124:11', 304), ('124:20', 305), ('124:21', 306), ('120:0', 307), ('120:2', 308), ('120:6', 309), ('120:7', 310), ('120:3', 311), ('120:1', 312), ('120:4', 313), ('120:5', 314), ('120:8', 315), ('120:9', 316), ('120:10', 317), ('120:11', 318), ('0:6', 319), ('0:7', 320), ('124:31', 321), ('115:23', 322), ('118:12', 323), ('65:26', 324), ('116:16', 325), ('119:31', 326), ('115:24', 327), ('125:12', 328), ('124:12', 329), ('0:8', 330)])
        getWord = dict([(0, 'Identification string'), (1, 'Primary node address'), (2, 'Secondary node address'), (3, 'Next node address'), (4, 'Last node address'), (5, 'Arbitrage'), (6, 'Initreset'), (7, 'Measure'), (8, 'Setpoint'), (9, 'Setpoint slope'), (10, 'Analog input'), (11, 'Control mode'), (12, 'Polynomial constant A'), (13, 'Polynomial constant B'), (14, 'Polynomial constant C'), (15, 'Polynomial constant D'), (16, 'Polynomial constant E'), (17, 'Polynomial constant F'), (18, 'Polynomial constant G'), (19, 'Polynomial constant H'), (20, 'Capacity'), (21, 'Sensor type'), (22, 'Capacity unit index'), (23, 'Fluid number'), (24, 'Fluid name'), (25, 'Claim node'), (26, 'Modify'), (27, 'Alarm info'), (28, 'Channel amount'), (29, 'First channel'), (30, 'Last channel'), (31, '<hostcontrl>'), (32, 'Alarm message unit type'), (33, 'Alarm message number'), (34, 'Relay status'), (51, 'Cycle time'), (52, 'Analog mode'), (53, 'Reference voltage'), (54, 'Valve output'), (55, 'Dynamic display factor'), (56, 'Static display factor'), (57, 'Calibration mode'), (58, 'Valve offset'), (59, 'Monitor mode'), (60, 'Alarm register1'), (61, 'Alarm register2'), (62, '<CalRegZS1>'), (63, '<CalRegFS1>'), (64, '<CalRegZS2>'), (65, '<CalRegFS2>'), (66, 'ADC control register'), (67, 'Bridge potmeter'), (68, '<AlarmEnble>'), (69, 'Test mode'), (70, '<ADC channel select>'), (71, 'Normal step controller response'), (72, 'Setpoint exponential smoothing filter'), (73, 'Sensor exponential smoothing filter'), (78, 'Tuning mode'), (79, 'Valve default'), (80, 'Global modify'), (81, 'Valve span correction factor'), (82, 'Valve curve correction'), (83, '<MemShipNor>'), (84, '<MemShipOpn>'), (85, 'IO status'), (86, '<FuzzStNeNo>'), (87, '<FuzzStPoNo>'), (88, '<FuzzStOpen>'), (89, 'Device type'), (90, 'BHTModel number'), (91, 'Serial number'), (92, 'Customer model'), (93, 'BHT1'), (94, 'BHT2'), (95, 'BHT3'), (96, 'BHT4'), (97, 'BHT5'), (98, 'BHT6'), (99, 'BHT7'), (100, 'BHT8'), (101, 'BHT9'), (102, 'BHT10'), (103, 'Broadcast repeating time'), (104, 'Firmware version'), (105, 'Pressure sensor type'), (106, 'Barometer pressure'), (113, 'Reset'), (114, 'User tag'), (115, 'Alarm limit maximum'), (116, 'Alarm limit minimum'), (117, 'Alarm mode'), (118, 'Alarm output mode'), (119, 'Alarm setpoint mode'), (120, 'Alarm new setpoint'), (121, 'Counter value'), (122, 'Counter unit index'), (123, 'Counter limit'), (124, 'Counter output mode'), (125, 'Counter setpoint mode'), (126, 'Counter new setpoint'), (127, 'Counter unit'), (128, 'Capacity unit'), (129, 'Counter mode'), (130, 'Minimum hardware revision'), (138, 'Slave factor'), (139, 'Reference voltage input'), (140, 'Stable situation controller response'), (141, 'Temperature'), (142, 'Pressure'), (143, 'Time'), (144, 'Calibrated volume'), (146, 'Range select'), (148, 'Frequency'), (149, 'Impulses/m3'), (150, 'Normal volume flow'), (151, 'Volume flow'), (152, 'Delta-p'), (153, '<scalefact>'), (155, 'Reset alarm enable'), (156, 'Reset counter enable'), (157, 'Master node'), (158, 'Master process'), (159, 'Remote instrument node'), (160, 'Remote instrument process'), (161, 'Minimum custom range'), (162, 'Maximum custom range'), (163, 'Relay/TTL output'), (164, 'Open from zero controller response'), (165, 'Controller features'), (166, 'PID-Kp'), (167, 'PID-Ti'), (168, 'PID-Td'), (169, 'Density'), (170, 'Calibration certificate'), (171, 'Calibration date'), (172, 'Service number'), (173, 'Service date'), (174, 'Identification number'), (175, 'BHT11'), (176, 'Power mode'), (177, 'Pressure inlet'), (178, 'Pressure outlet'), (179, 'Orifice'), (180, 'Fluid temperature'), (181, 'Alarm delay'), (182, 'Capacity 0%'), (183, 'Number of channels'), (184, 'Device function'), (185, 'Scan channel'), (186, 'Scan parameter'), (187, 'Scan time'), (188, 'Scan data'), (189, 'Valve open'), (190, 'Number of runs'), (191, 'Minimum process time'), (192, 'Leak rate'), (193, 'Mode info request'), (194, 'Mode info option list'), (195, 'Mode info option description'), (196, 'Calibrations options'), (197, 'Mass flow'), (198, 'Bus address'), (199, 'Interface configuration'), (200, 'Baudrate'), (201, 'Bus diagnostic string'), (202, 'Number of vanes'), (203, 'Fieldbus'), (204, 'fMeasure'), (205, 'fSetpoint'), (206, 'Mass'), (207, 'Manufacturer status register'), (208, 'Manufacturer warning register'), (209, 'Manufacturer error register'), (210, 'Diagnostic history string'), (211, 'Diagnostic mode'), (212, 'Manufacturer status enable'), (213, 'Analog output zero adjust'), (214, 'Analog output span adjust'), (215, 'Analog input zero adjust'), (216, 'Analog input span adjust'), (217, 'Sensor input zero adjust'), (218, 'Sensor input span adjust'), (219, 'Temperature input zero adjust'), (220, 'Temperature input span adjust'), (221, 'Adaptive smoothing factor'), (222, 'Slope setpoint step'), (223, 'Filter length'), (224, 'Absolute accuracy'), (225, 'Lookup table index'), (226, 'Lookup table X'), (227, 'Lookup table Y'), (228, 'Lookup table temperature index'), (229, 'Lookup table temperature'), (230, 'Valve maximum'), (231, 'Valve mode'), (232, 'Valve open correction'), (233, 'Valve zero hold'), (234, 'Valve slope'), (235, 'IFI data'), (236, 'Range used'), (237, 'Fluidset properties'), (238, 'Lookup table unit type index'), (239, 'Lookup table unit type'), (240, 'Lookup table unit index'), (241, 'Lookup table unit'), (244, 'Capacity unit type temperature'), (245, 'Capacity unit pressure'), (248, 'Formula type'), (249, 'Heat capacity'), (250, 'Thermal conductivity'), (251, 'Viscosity'), (252, 'Standard flow'), (253, 'Controller speed'), (254, 'Sensor code'), (255, 'Sensor configuration code'), (256, 'Restriction code'), (257, 'Restriction configurator code'), (258, 'Restriction NxP'), (259, 'Seals information'), (260, 'Valve code'), (261, 'Valve configuration code'), (262, 'Instrument properties'), (263, 'Lookup table frequency index'), (264, 'Lookup table frequency frequency'), (265, 'Lookup table frequency temperature'), (266, 'Lookup table frequency density'), (267, 'Lookup table frequency span adjust'), (268, 'Capacity unit index (ext)'), (269, 'Density actual'), (270, 'Measured restriction'), (271, 'Temperature potmeter'), (272, 'Temperature potmeter gain'), (273, 'Counter controller overrun correction'), (274, 'Counter controller gain'), (275, 'Sub fluid number'), (276, 'Temperature compensation factor'), (277, 'DSP register address'), (278, 'DSP register long'), (279, 'DSP register floating point'), (280, 'DSP register integer'), (281, 'Standard deviation'), (282, 'Measurement status'), (283, 'Measurement stop criteria'), (284, 'Measurement time out'), (285, 'Maximum number of runs'), (286, 'Minimum standard deviation'), (287, 'IO switch status'), (288, 'Sensor bridge settings'), (289, 'Sensor bridge current'), (290, 'Sensor resistance'), (291, 'Sensor bridge voltage'), (292, 'Sensor group name'), (293, 'Sensor calibration temperature'), (294, 'Valve safe state'), (295, 'Counter unit type index'), (296, 'Counter unit type'), (297, 'Counter unit index (ext)'), (298, 'Bus1 selection'), (299, 'Bus1 medium'), (300, 'Bus2 mode'), (301, 'Bus2 selection'), (302, 'Bus2 address'), (303, 'Bus2 baudrate'), (304, 'Bus2 medium'), (305, 'Bus2 diagnostics'), (306, 'Bus2 name'), (307, 'PIO channel selection'), (308, 'PIO parameter'), (309, 'PIO input/output filter'), (310, 'PIO parameter capacity 0%'), (311, 'PIO parameter capacity 100%'), (312, 'PIO configuration selection'), (313, 'PIO analog zero adjust'), (314, 'PIO analog span adjust'), (315, 'PIO hardware capacity max'), (316, 'PIO capacity set selection'), (317, 'PIO hardware capacity 0%'), (318, 'PIO hardware capacity 100%'), (319, 'Hardware platform id'), (320, 'Hardware platform sub id'), (321, 'Temporary baudrate'), (322, 'Setpoint monitor mode'), (323, 'BHT12'), (324, 'Nominal sensor voltage'), (325, 'Sensor voltage compensation factor'), (326, 'PCB serial number'), (327, 'Minimum measure time'), (328, 'Bus1 parity'), (329, 'Bus2 parity'), (330, 'Firmware id')])
        myIdent = str(self.getProcess()) + ":" + str(self.getNumber())

        if myIdent in getIdent.keys():
            if getIdent[myIdent] in getWord.keys():
                self.human = getWord[getIdent[myIdent]]

# subclass for MKFlowData
class MKFlowProcess():
    class MKFlowParameter(MKFlowParameter):
        pass

    def __init__(self):
        self.number = -1
        self.length = 0
        self.data = []
        self.Parameter = []

    def set(self, data):
        self.data   = data
        self.number = data[0]

    def analyse(self):
        index = 0
        position = 1
        chained = True
        while chained:
            index = len(self.Parameter)
            self.Parameter.append(self.MKFlowParameter())
            self.Parameter[index].set(self.data[position:])
            self.Parameter[index].setProcess(self.getProcess())
            self.Parameter[index].analyse()
            position += self.Parameter[index].getLength()
            chained = self.Parameter[index].isChained()
        self.length = position

    def isChained(self):
        if self.number >= 128:
            return True
        else:
            return False

    def getNumber(self):
        return self.number

    def getProcess(self):
        if self.isChained():
            return (self.number - 128)
        else:
            return self.number

    def getLength(self):
        return self.length

    def stdout(self, leading = '\t\t'):
        print leading, "-- MKFlowProcess Class Output Begin --"
        print leading, 'Data:\t', self.data
        print leading, 'Data:\t', self.data[0:self.getLength()]
        print leading, '1st byte:\t', format(self.data[0], '08b')[0:4] + ' ' + format(self.data[0], '08b')[4:8]
        print leading, 'chained:\t', self.isChained()
        print leading, 'Process:\t', self.getProcess()
        print leading, 'Total Parameters: ', len(self.Parameter)
        for parameter in self.Parameter:
            parameter.stdout(leading + '\t')
        print leading, "-- MKFlowProcess Class Output End --"

class MKFlowData():
    class MKFlowProcess(MKFlowProcess):
        pass

    def __init__(self):
        self.data = []
        self.active = False
        self.process = []
        self.length = 0

    def set(self,data):
        self.active = True
        self.data = data
        self.analyse()

    def analyse(self):
        self.analyseProcess()
        if not self.check():
            raise ValueError('Data was not fully processed. Some information might be missing. Declare whole as invalid')
            pass

    def check(self):
        return (len(self.data) == self.length)

    def analyseProcess(self):
        index = 0
        position = 0
        chained = True
        while chained:
            index = len(self.process)
            self.process.append(self.MKFlowProcess())
            self.process[index].set(self.data[position:])
            self.process[index].analyse()
            position += self.process[index].getLength()
            chained = self.process[index].isChained()
        self.length = position

    def stdout(self, leading = '\t'):
        print leading, "-- MKFlowData Class Output Begin --"
        print leading, "Data Array: \t", self.data
        print leading, "Data Array: \t", self.data[0:self.length]
        print leading, 'Total Processes:\t', len(self.process)
        print leading, 'Data' + (" not " if not self.check() else " ") +'fully processed'
        for process in self.process:
            process.stdout('\t\t')
        print leading, "-- MKFlowData Class Output End --"

class MKFlowRequest(MKFlowData):
    def stdoutShort(self, indent=''):
        for process in self.process:
            for parameter in process.Parameter:
                return '\t'.join(str(i) for i in [indent , parameter.getProcess(), parameter.getIndex(), parameter.getNumber(), parameter.getHuman()])

    class MKFlowProcess(MKFlowProcess):
        class MKFlowParameter(MKFlowParameter):
            def analyse(self):
                self.analyseData()
                self.analyseDataType(self.number)
                self.setLength(3)

            def analyseData(self):
                try:
                    if len(self.data)<3:
                        raise ValueError('MKFlowRequest:analyseData data array too short')
                    self.index   = self.data[0]
                    self.process = self.data[1] # process should be filled by parent
                    self.number  = self.data[2]
                except:
                    raise

            def setLength(self,length=0):
                self.length = length
                if self.dataType == 'string':
                    self.length += 1
                    self.DataLength = self.data[3]

            def getNumber(self):
                # FB Number ( no chaining parameter present)
                return self.substractDataType(self.number)

            def stdout(self, leading = '\t\t\t'):
                print leading, "-- MKFlowRequest Class Output Begin --"
                print leading, 'Data:    \t', self.getData()
                print leading, '1st byte:\t', format(self.data[0], '08b')[0:4] + ' ' + format(self.data[0], '08b')[4:8]
                print leading, 'Chained: \t', self.isChained()
                print leading, 'Index:   \t', self.getIndex()
                print leading, '2nd byte:\t', format(self.data[1], '08b')[0:4] + ' ' + format(self.data[1], '08b')[4:8]
                print leading, 'Process: \t', self.getProcess()
                print leading, '3rd byte:\t', format(self.data[2], '08b')[0:4] + ' ' + format(self.data[2], '08b')[4:8]
                print leading, 'DataType:\t', self.getDataType()
                print leading, 'FbNr:    \t', self.getNumber()
                print leading, 'Length:  \t', self.getLength()
                print leading, 'Human Ind:\t', self.getHuman()
                print leading, "-- MKFlowRequest Class Output End --"

class MKFlowSent(MKFlowData):
    def stdoutShort(self, indent=''):
        for process in self.process:
            for parameter in process.Parameter:
                return '\t'.join(str(i) for i in [indent, process.getProcess(), parameter.getIndex(), None, parameter.getValue()])

    class MKFlowProcess(MKFlowProcess):
        class MKFlowParameter(MKFlowParameter):
            def analyse(self):
                self.analyseData()
                self.analyseDataType()
                self.analyseValue()

            def analyseValue(self):
                self.dataStart = 1
                self.dataValueFloat = float(0)
                if self.dataType == 'string':
                    self.dataLength = self.data[self.dataStart]
                    self.dataStart += 1
                    self.setLength()
                    self.dataValue  = ''.join(chr(i) for i in self.data[self.dataStart:self.length])
                elif self.dataType == 'long':
                    # can also be IEE floating point notation
                    self.dataLength = 4
                    self.setLength()
                    self.dataValue  = int(''.join(hex(i)[2:] for i in self.data[self.dataStart:self.length]),16)
                elif self.dataType == 'integer':
                    self.dataLength = 2
                    self.setLength()
                    self.dataValue  = int(''.join(hex(i)[2:] for i in self.data[self.dataStart:self.length]),16)
                elif self.dataType == 'character':
                    self.dataLength = 1
                    self.setLength()
                    self.dataValue = self.data[self.dataStart]
                else:
                    self.dataLength = 0
                    self.setLength()
                    self.dataValue = None
                    raise ValueError('MKFlowSent:analyseValue datatype not found: ' + str(self.dataType))
                if (len(self.data)) < self.length:
                    pass
                    #raise ValueError('length of data does not match message size.')
                elif (len(self.data)) > self.length:
                    pass
                    #raise ValueError('length of data too long. Check for chaining!')

            def setLength(self):
                if self.dataLength == 0:
                    # undefined length. use whole
                    self.length = len(self.data)
                else:
                    self.length = self.dataLength + self.dataStart

            def getValue(self):
                return self.dataValue

            def stdout(self, leading = '\t\t\t'):
                print leading, "-- MKFlowSent:MKFlowProcess:MKFlowParameter Class Output Begin --"
                print leading, 'Data:    \t', self.getData()
                print leading, '1st byte:\t', format(self.data[0], '08b')[0:4] + ' ' + format(self.data[0], '08b')[4:8]
                print leading, 'Chained: \t', self.isChained()
                print leading, 'DataType:\t', self.getDataType()
                print leading, 'Index:    \t', self.getIndex()
                print leading, '2nd byte:\t', format(self.data[1], '08b')[0:4] + ' ' + format(self.data[1], '08b')[4:8]
                print leading, 'Length:  \t', self.getLength()
                print leading, 'Value:   \t', self.getValue()
                print leading, "-- MKFlowSent:MKFlowProcess:MKFlowParameter Class Output End --"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
import time

from MKFlowMain import MKFlow

ethanol = MKFlow('/dev/ttyUSB2', '/dev/ttyUSB3', 0)
argon   = MKFlow('/dev/ttyUSB0', '/dev/ttyUSB1', 1)

threads = []
threads.append(ethanol)
threads.append(argon)

for t in threads:
    print "starting ..."
    t.start()
    t.debug()

try:
    while True:
        time.sleep(10)
except KeyboardInterrupt:
    for t in threads:
        print "stoping ..."
        t.stop()